Dockerfile은 이미지를 만드는 순서를 적은 파일입니다. 이 글에서는 명령마다 이미지에 무엇이 남는지, 빌드 캐시를 잘 타도록 순서를 정하는 방법, CMD와 ENTRYPOINT의 차이, 그리고 빌드 도구를 최종 이미지에서 빼는 멀티 스테이지 빌드를 Go 웹 서버 예제로 정리합니다.
컨테이너부터 Kubernetes까지 시리즈 3편입니다. 이전 글은 [Docker] 이미지와 레이어 정리이고, 전체 순서는 시리즈 목차에 있습니다. 명령어 하나하나의 설명은 [Docker] Dockerfile 명령어 정리에 있습니다.
1. Dockerfile이 이미지가 되는 과정
docker build를 실행하면 현재 폴더(빌드 컨텍스트)를 BuildKit이라는 빌드 엔진에 보내고, Dockerfile을 위에서부터 한 줄씩 실행해 이미지를 만듭니다. 파일을 바꾸는 명령은 레이어를 남기고, 실행 설정만 바꾸는 명령은 이미지 정보(메타데이터)에만 기록됩니다.
| 항목 | 동작 | 남는 것 |
| FROM | 베이스 이미지 지정 | 베이스의 레이어 |
| RUN | 빌드 중에 명령 실행(패키지 설치 등) | 레이어 |
| COPY | 빌드 컨텍스트의 파일을 이미지로 복사 | 레이어 |
| WORKDIR | 이후 명령이 실행될 폴더 | 설정 |
| ENV | 환경 변수 | 설정 |
| USER | 이후 명령과 컨테이너를 실행할 사용자 | 설정 |
| EXPOSE | 앱이 쓰는 포트를 문서로 표시 실제로 포트를 열지는 않음 |
설정 |
| CMD · ENTRYPOINT | 컨테이너가 시작할 때 실행할 명령 | 설정 |
2. 빌드 캐시와 명령 순서
BuildKit은 명령과 입력 파일이 지난 빌드와 같으면 그 단계를 다시 실행하지 않고 캐시를 씁니다. 다만 한 단계가 바뀌면 그 아래 단계는 모두 다시 실행됩니다. 그래서 자주 바뀌지 않는 것을 위에, 자주 바뀌는 소스 코드를 아래에 둡니다.

의존성 목록을 먼저 복사해 설치하면, 소스만 바뀌었을 때 의존성 설치 단계는 캐시로 넘어갑니다
빌드 컨텍스트에 필요 없는 파일이 섞이면 COPY . . 단계의 캐시가 자주 깨지고 전송 시간도 늘어납니다. 프로젝트 루트에 .dockerignore 파일을 두고 .git, 로그, 로컬 설정(.env) 같은 파일을 빼 둡니다.
3. CMD와 ENTRYPOINT
둘 다 컨테이너가 시작할 때 실행할 명령이지만, docker run 뒤에 붙인 인자를 다루는 방식이 다릅니다. CMD는 "기본값"이라 인자를 주면 통째로 바뀌고, ENTRYPOINT는 "고정된 실행 파일"이라 인자가 뒤에 덧붙습니다.
| 구분 | 동작 | docker run 이미지 --help |
| CMD만 | 인자가 없으면 CMD 실행 | CMD 대신 --help를 명령으로 실행 |
| ENTRYPOINT만 | 항상 ENTRYPOINT 실행 | ENTRYPOINT --help로 실행 |
| 둘 다 | ENTRYPOINT + CMD(기본 인자) | CMD 부분만 --help로 바뀜 |
작성 형식도 중요합니다. ["app", "--port", "8080"]처럼 JSON 배열로 쓰는 exec 형식은 앱이 PID 1로 바로 실행되고, 따옴표 없이 쓰는 shell 형식은 /bin/sh -c가 PID 1이 되어 앱을 자식으로 띄웁니다. docker stop이나 Kubernetes가 보내는 종료 신호(SIGTERM)는 PID 1이 받으므로, shell 형식에서는 앱이 신호를 못 받아 정상 종료 처리를 하지 못하고 제한 시간 뒤 강제로 종료됩니다. 그래서 exec 형식으로 씁니다.
4. 멀티 스테이지 빌드
Go나 Java처럼 컴파일하는 언어는 빌드할 때만 컴파일러가 필요하고, 실행할 때는 결과 파일 하나면 충분합니다. 멀티 스테이지 빌드는 Dockerfile 안에 FROM을 여러 번 써서 빌드 단계와 실행 단계를 나누고, 마지막 단계에는 필요한 결과물만 복사합니다. 공장에서 조립은 작업장에서 하고, 고객에게는 완성품만 상자에 담아 보내는 것과 같습니다.

컴파일러와 소스, 캐시는 1단계에 남고, 최종 이미지에는 바이너리 하나만 들어갑니다
| 항목 | 단일 스테이지 | 멀티 스테이지 |
| 최종 이미지 내용 | 컴파일러, 소스, 캐시, 바이너리 | 실행에 필요한 파일과 바이너리 |
| 크기 | 빌드 도구만큼 큼 | 작음 |
| 보안 | 컴파일러, 소스 코드가 운영 이미지에 남음 | 공격에 쓸 수 있는 도구가 적음 |
| 배포 속도 | 받을 레이어가 커서 느림 | 빠름 |
5. 직접 해 보기
빈 폴더에 파일 4개(main.go, go.mod, Dockerfile, .dockerignore)를 만듭니다. Go가 로컬에 없어도 컴파일은 컨테이너 안에서 하므로 Docker만 있으면 됩니다. 먼저 8080 포트로 응답하는 웹 서버입니다.
// main.go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello from container")
})
http.ListenAndServe(":8080", nil)
}
go.mod에는 "module demo"와 "go 1.27" 두 줄을 적고, .dockerignore에는 .git을 적습니다. Dockerfile은 두 단계로 나눕니다.
# 1단계: 빌드
FROM golang:1.27-alpine AS build
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app .
# 2단계: 실행
FROM alpine:3.24
RUN adduser -D -u 10001 app
USER app
COPY --from=build /out/app /usr/local/bin/app
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/app"]
CGO_ENABLED=0은 C 라이브러리에 의존하지 않는 바이너리를 만들어 어떤 리눅스 이미지에서도 실행되게 합니다. 외부 패키지를 쓰면 go.sum도 COPY go.mod go.sum ./ 처럼 함께 복사합니다. 빌드하고 실행합니다.
docker build -t hello-go:1.0 .
docker run -d --name hello -p 8080:8080 hello-go:1.0
curl localhost:8080
docker image ls hello-go
curl에 hello from container가 돌아오면 성공입니다. docker image ls로 크기를 보면 golang:1.27-alpine보다 훨씬 작은데, alpine 기본 파일에 바이너리 하나만 더해졌기 때문입니다. main.go의 문구만 바꿔 다시 빌드하면 빌드 로그에서 go mod download 단계가 CACHED로 넘어가는 것도 확인할 수 있습니다.
6. 운영 시 주의점
| 항목 | 조치 |
| 비밀 정보 | ARG, ENV, COPY로 넣은 값은 이미지에 남음 빌드에 필요하면 RUN --mount=type=secret 사용 |
| 실행 사용자 | USER로 일반 사용자를 지정하고 root로 실행하지 않음 |
| 베이스 이미지 태그 | latest 대신 golang:1.27-alpine처럼 버전을 고정 |
| 패키지 설치 | 설치와 캐시 정리를 같은 RUN에서 처리 예: apt-get install 뒤에 rm -rf /var/lib/apt/lists/* |
| EXPOSE | 포트를 열지 않으므로 실행할 때 -p로 매핑 |
| 종료 신호 | ENTRYPOINT, CMD는 exec 형식(JSON 배열)으로 작성 |
| HEALTHCHECK | Docker와 Compose에서만 쓰이고 Kubernetes는 무시함 Kubernetes에서는 Probe로 따로 설정 |
7. 정리
- RUN과 COPY는 레이어를 남기고 나머지 명령은 설정만 기록하며, 자주 바뀌는 것을 아래에 둬야 빌드 캐시를 잘 탑니다.
- ENTRYPOINT와 CMD는 exec 형식으로 써야 앱이 PID 1로 종료 신호를 받습니다.
- 멀티 스테이지 빌드로 빌드 도구는 1단계에 두고, 최종 이미지에는 실행에 필요한 파일만 담습니다.
다음 글 4편에서는 컨테이너끼리 통신하는 네트워크와 데이터를 남기는 볼륨을 다룹니다.
참고
'DevOps > Docker' 카테고리의 다른 글
| [Docker] Docker Compose로 여러 컨테이너 실행 정리 (0) | 2026.09.28 |
|---|---|
| [Docker] 컨테이너 네트워크와 볼륨 정리 (0) | 2026.09.28 |
| [Docker] 이미지와 레이어 정리 (0) | 2026.09.28 |
| [Docker] 컨테이너 개념 정리 (0) | 2026.09.28 |
| [Docker] Dockerfile 명령어 정리 (0) | 2021.09.03 |