지난 글에서 컨테이너가 뭔지, 이미지가 붕어빵 틀이라는 것까지 잡았다. 그때는 nginx나 hello-world처럼 남이 만들어둔 이미지를 가져다 썼다. 오늘은 한 걸음 더 나간다. 내 앱을 담은 이미지를 직접 만드는 법, 바로 Dockerfile이다.
이걸 할 줄 알아야 "내가 만든 프로그램을 어디서든 똑같이 돌린다"는 Docker의 진짜 목적에 도달한다.
Dockerfile = 이미지 만드는 레시피
Dockerfile은 "이 이미지를 이렇게 만들어라"라고 적어둔 설명서(레시피)다. 붕어빵 틀(이미지)을 찍어내기 위한 설계도라고 보면 된다.
베이스 환경은 뭘 쓸지, 내 코드는 어디에 복사할지, 어떤 명령으로 실행할지를 위에서 아래로 순서대로 적는다. 그러면 Docker가 그 순서대로 실행해서 이미지를 만들어준다.
흐름은 이렇다.
Dockerfile (레시피) → docker build → 이미지 → docker run → 컨테이너예제: 간단한 Node.js 앱을 이미지로
간단한 Node.js 웹 앱이 있다고 하자. 프로젝트 구조는 이렇다.
my-app/
├── package.json
├── app.js
└── Dockerfile ← 이걸 만든다이 앱을 이미지로 만드는 Dockerfile은 이렇게 생겼다.
# 1. 베이스 이미지 선택
FROM node:20
# 2. 컨테이너 안 작업 폴더 지정
WORKDIR /app
# 3. 의존성 파일 먼저 복사
COPY package*.json ./
# 4. 의존성 설치
RUN npm install
# 5. 나머지 소스코드 전체 복사
COPY . .
# 6. 앱이 사용할 포트 알려주기
EXPOSE 3000
# 7. 컨테이너 시작 시 실행할 명령
CMD ["node", "app.js"]
한 줄씩 뜯어보자.
명령어 하나씩 뜯어보기
FROM node:20 — 베이스 이미지다. 맨땅부터 시작하는 게 아니라, Node 20이 이미 깔린 이미지 위에서 시작한다. 모든 Dockerfile은 FROM으로 시작한다.
WORKDIR /app — 컨테이너 안에서 작업할 디렉토리를 정한다. 이후 명령들은 다 이 폴더 기준으로 돌아간다. 없으면 자동으로 만들어준다.
COPY package*.json ./ — 내 컴퓨터의 package.json을 컨테이너 안으로 복사한다. 소스 전체가 아니라 의존성 파일만 먼저 복사하는 게 포인트다. (이유는 아래 캐시 설명에서.)
RUN npm install — 이미지를 만드는 시점에 실행되는 명령이다. 여기서 의존성을 설치한다. RUN은 이미지 빌드 중에 실행된다는 걸 기억하자.
COPY . . — 이제 나머지 소스코드 전체를 복사한다.
EXPOSE 3000 — 이 컨테이너가 3000번 포트를 쓴다고 알려주는 문서 역할이다. (실제 연결은 run 할 때 -p로 한다.)
CMD ["node", "app.js"] — 컨테이너가 시작될 때 실행할 명령이다. RUN이 빌드 시점이라면 CMD는 실행 시점이다. 이 차이가 처음엔 헷갈리니 꼭 구분하자.
빌드하고 실행하기
Dockerfile을 다 썼으면 이미지를 만든다(build).
docker build -t my-app .
-t my-app: 이미지에my-app이라는 이름(태그)을 붙인다..: Dockerfile이 있는 현재 폴더를 빌드 대상으로 삼는다.
만들어진 이미지를 확인하고,
docker images
컨테이너로 실행한다.
docker run -d -p 3000:3000 my-app
이제 localhost:3000으로 들어가면 내가 만든 앱이 컨테이너 안에서 돌아간다. 내 앱이 드디어 "어디서든 똑같이 도는" 이미지가 된 거다.
RUN vs CMD — 헷갈리는 포인트 정리
입문자가 가장 많이 헷갈리는 게 RUN과 CMD다. 표로 정리한다.
| 구분 | 실행 시점 | 용도 | 예시 |
|---|---|---|---|
RUN |
이미지 빌드 중 | 설치, 준비 작업 | RUN npm install |
CMD |
컨테이너 실행 시 | 앱 시작 명령 | CMD ["node", "app.js"] |
RUN은 붕어빵 틀을 만들 때(재료 넣고 반죽) 하는 일, CMD는 그 틀로 붕어빵을 구울 때(실제로 굽기) 하는 일이라고 생각하면 편하다.
왜 package.json을 먼저 복사했을까? (레이어 캐시)
아까 소스 전체(COPY . .)보다 package.json을 먼저 복사하고 npm install을 했다. 이건 실수가 아니라 빌드 캐시 때문이다.
Docker는 Dockerfile의 각 줄을 레이어로 만들어 캐싱한다. 바뀌지 않은 레이어는 다시 빌드하지 않고 재사용한다. 만약 소스코드만 바꾸고 의존성은 그대로일 때,
package.json을 먼저 복사해두면 →npm install레이어는 캐시가 살아있어서 재설치 안 함 (빠름)- 소스랑 같이 복사했으면 → 코드 한 줄만 바꿔도 캐시가 깨져서
npm install을 매번 다시 (느림)
그래서 "잘 안 바뀌는 것(의존성)을 위에, 자주 바뀌는 것(소스)을 아래에" 두는 게 Dockerfile 작성의 기본 요령이다.
정리
오늘은 내 앱을 직접 이미지로 만드는 법을 배웠다.
- Dockerfile은 이미지를 만드는 레시피다.
FROM(베이스) →WORKDIR→COPY→RUN(빌드 시점 작업) →CMD(실행 시점 명령) 순서가 기본 뼈대다.docker build로 이미지를 만들고,docker run으로 컨테이너를 띄운다.RUN은 빌드 시점,CMD는 실행 시점이라는 차이를 꼭 기억하자.- 잘 안 바뀌는 걸 위에 두면 레이어 캐시 덕에 빌드가 빨라진다.
이제 내 앱 하나를 이미지로 만들 수 있게 됐다. 그런데 실무 앱은 보통 혼자 안 돈다. 웹 서버 + 데이터베이스 + 캐시(Redis)처럼 여러 컨테이너가 같이 떠야 한다. 다음 글에서는 이런 여러 컨테이너를 한 번에 관리하는 Docker Compose를 다뤄보자.
'프로그래밍 > DevOps' 카테고리의 다른 글
| [DevOps] GitHub Actions 첫걸음 — 푸시하면 자동으로 테스트 돌리기 (0) | 2026.07.11 |
|---|---|
| [DevOps] Docker 이미지 최적화 — 멀티스테이지 빌드로 용량 확 줄이기 (0) | 2026.07.11 |
| [DevOps] Docker Compose — 여러 컨테이너를 한 번에 관리하기 (0) | 2026.07.09 |
| [DevOps] Docker 기초 — 컨테이너가 대체 뭔데? "내 컴퓨터에선 되는데요"의 종말 (0) | 2026.07.06 |