Dockerfile 스크립트를 작성하고 이 스크립트를 빌드해 이미지를 만들 수 있다.
이 문서는 인스트럭션별 문법 레퍼런스다. 실제 이미지 설계·최적화 방법(멀티 스테이지 빌드, 레이어 캐시, .dockerignore)은 이미지 생성 및 관리 챕터에서 다룬다.
읽기만 해서는 잘 안 믿기는 대목 세 곳에는 🔬 직접 해 보기 를 넣어 두었다. 셋 다 Dockerfile 한 개를 만들어 띄워 보는 정도로, 각각 3분이면 끝난다.
빌드 명령어 형식
docker build -t [생성할 이미지 이름] [빌드 컨텍스트 경로]
# 신형(관리 명령어) 방식
docker image build -t [생성할 이미지 이름] [빌드 컨텍스트 경로]
# 예시 — 현재 디렉터리를 컨텍스트로, 그 안의 Dockerfile 을 쓴다
docker build -t myapp .마지막 인자는 Dockerfile 이 아니라 「빌드 컨텍스트」 다.
Docker 에게 넘기는 것은 디렉터리다. 그 디렉터리 전체가 빌드 엔진으로 전송되고, Dockerfile 은 그 안의 Dockerfile 이 기본으로 선택된다.
docker build -t myapp ./Dockerfile # 하지 말 것
# unable to prepare context: path "./Dockerfile" not found파일 이름이 Dockerfile 이 아니거나 다른 위치에 있으면 -f 로 따로 지정한다. 그래도 마지막 인자는 여전히 컨텍스트 디렉터리다.
docker build -t myapp -f docker/Dockerfile-prod .컨텍스트가 크면 빌드가 느려진다. 빼고 싶은 것은 .dockerignore 에 적는다 — 이미지 생성 및 관리 챕터에서 다룬다.
FROM — 기본 이미지를 지정
# 사용문법
FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
# 예시
FROM ubuntu:24.04태그를 생략하면 latest 가 붙고, 그것은 고정된 이름일 뿐 "최신"이 아니다. 같은 Dockerfile 이 반년 뒤에 다른 베이스 위에서 빌드될 수 있다. 재현 가능한 빌드를 원하면 nginx:1.30 처럼 버전을 적는다.
FROM 이 파일의 첫 줄이어야 하는 것은 아니다. 정확히는 첫 번째 인스트럭션이고, 그 앞에 두 가지가 올 수 있다.
# syntax=docker/dockerfile:1같은 파서 디렉티브. 주석 형태이지만 BuildKit 이 쓸 Dockerfile 문법 버전을 지정하는 지시문이라 반드시 파일 맨 위에 와야 한다.ARG. 베이스 이미지 태그를 빌드 인자로 받고 싶을 때 쓴다. 스테이지 안에서 다시 쓰려면 재선언이 필요하다 — 아래ARG절을 본다.
# syntax=docker/dockerfile:1
ARG JAVA_VERSION=21
FROM eclipse-temurin:${JAVA_VERSION}-jreRUN — 명령어 실행
이미지 빌드 시점에 실행된다.
# 사용문법
RUN <command>
RUN ["executable", "param1", "param2"]
# 예시
RUN apt-get update
RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'
RUN ["/bin/bash", "-c", "echo hello"]RUN 한 줄이 레이어 하나다. 패키지 설치는 한 RUN 으로 묶고 끝에 캐시를 지운다. 나눠 쓰면 레이어가 늘고, 캐시를 남기면 이미지가 수십 MB 커진다.
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*apt-get update 를 별도 레이어로 두면 더 나쁘다. 다음 빌드에서 update 만 캐시로 재사용되어 낡은 패키지 목록으로 설치하게 된다.
apt-get upgrade 를 쓰지 않는다. 언제 빌드하느냐에 따라 결과가 달라져 재현이 되지 않는다. 베이스 이미지에 보안 패치가 필요하면 베이스 태그를 올린다.
컨테이너에 SSH 데몬을 넣지 않는다. openssh-server 를 설치하는 예제를 흔히 보지만, 컨테이너는 프로세스 하나를 돌리는 것이 원칙이고 안으로 들어가는 일은 docker exec 가 한다. SSH 데몬은 공격 표면과 관리 대상만 늘린다.
CMD — 컨테이너 생성 시 실행할 명령어
Dockerfile 내에 여러 개가 있어도 마지막 CMD 하나만 유효하다.
# 사용문법
CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2
# 예시
CMD echo "This is a test." | wc -
CMD ["/usr/bin/wc","--help"]CMD는 컨테이너 실행 시 단일 기본 명령어를 지정하는 용도로 사용되며, 필요 시 ENTRYPOINT와 조합하여 사용할 수 있다.
FROM ubuntu:24.04
ENTRYPOINT ["echo"]
CMD ["Hello from CMD"]ENTRYPOINT — 컨테이너 구성 시 기본 실행 명령어
docker run 뒤에 인자를 붙여도 교체되지 않고, 붙인 내용이 ENTRYPOINT의 인자로 전달된다. 단, 이것은 exec form(JSON 배열)일 때만 그렇다.
# 사용문법
ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2
# 예시
ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]shell form 은 쓰지 않는다.
ENTRYPOINT command param1 param2 처럼 배열 없이 쓰면 Docker 가 /bin/sh -c 로 감싼다. 그 결과 두 가지가 무너진다.
CMD와docker run뒤의 인자를 전부 무시한다. 위에서 설명한 「인자로 전달」 이 일어나지 않는다.시그널이 앱까지 닿지 않는다.
docker stop의 SIGTERM 은 PID 1 에게만 간다. shell form 에서는 그 자리를sh가 차지하고 앱은 자식이 되므로, 앱은 아무것도 받지 못한 채 타임아웃 뒤 SIGKILL 로 강제 종료된다. 정상 종료 처리를 할 기회가 없다.
ENTRYPOINT java -jar app.jar # 하지 말 것
ENTRYPOINT ["java", "-jar", "app.jar"] # 이렇게 쓴다CMD vs ENTRYPOINT 요약
|
| |
|---|---|---|
|
|
|
|
|
|
🔬 직접 해 보기 — 인자가 교체되는지 확인한다
위 표를 손으로 확인한다. Dockerfile 두 개를 만든다.
# Dockerfile.cmd — CMD 만 있다
FROM alpine:3.20
CMD ["echo", "[CMD 만 있음]"]# Dockerfile.both — ENTRYPOINT + CMD
FROM alpine:3.20
ENTRYPOINT ["echo", "[ENTRYPOINT]"]
CMD ["[CMD]"]docker image build -f Dockerfile.cmd -t demo-cmd .
docker image build -f Dockerfile.both -t demo-both .인자를 붙이지 않았을 때와 붙였을 때를 비교한다.
docker container run --rm demo-cmd
docker container run --rm demo-cmd whoami
docker container run --rm demo-both
docker container run --rm demo-both 안녕[CMD 만 있음]
root
[ENTRYPOINT] [CMD]
[ENTRYPOINT] 안녕두 번째 줄이 핵심이다. demo-cmd whoami 가 [CMD 만 있음] whoami 가 아니라 root 를 출력한다. CMD 는 인자를 받는 것이 아니라 통째로 밀려난다. 반면 demo-both 안녕 은 [ENTRYPOINT] 를 그대로 둔 채 CMD 자리만 바꾼다.
docker image rm demo-cmd demo-bothEXPOSE — 컨테이너에서 사용할 포트 정보
실제로 포트를 여는 것은 아니며, docker run 실행 시 -p 옵션을 사용하여 포트를 열어주어야 한다.
# 사용문법
EXPOSE <port> [<port>/<protocol>...]
# 예시
EXPOSE 9000ENV — 환경 변수를 설정
빌드 시점과 컨테이너 실행 시점 모두에서 사용 가능하다.
# 사용문법
ENV <key>=<value> ...
# 예시
ENV MY_NAME="John Doe"
ENV MY_DOG=Rex\ The\ Dog
ENV MY_CAT=fluffyARG — 빌드 시 환경변수 설정
빌드 시점에만 존재한다. docker build --build-arg 로 값을 전달한다.
첫 FROM 앞에 선언한 ARG 는 빌드 스테이지 바깥에 있다. FROM 줄에서는 쓸 수 있지만 스테이지 안에서 쓰려면 FROM 뒤에 다시 선언해야 한다. 값은 그대로 이어받는다.
ARG JDK=21
FROM eclipse-temurin:${JDK}-jre # 여기서는 쓸 수 있다
ARG JDK # 다시 선언해야 아래에서 쓸 수 있다
RUN echo "JDK=${JDK}"# 사용문법
ARG <name>[=<default value>]
# 예시
FROM busybox
ARG user1=someuser
ARG buildno=1비밀값은 ENV는 물론 ARG로도 전달하지 않는다. 이미지 히스토리에 남을 수 있다. Docker Secrets 또는 BuildKit의 --mount=type=secret을 사용한다.
🔬 직접 해 보기 — ARG 로 넘긴 값은 이미지에 남는다
"이미지 히스토리에 남을 수 있다" 를 눈으로 확인한다.
FROM alpine:3.20
ARG DB_PASSWORD
RUN echo "빌드 중 비밀번호 사용: $DB_PASSWORD" > /tmp/xdocker image build --build-arg DB_PASSWORD=hunter2 -t arg-demo .
docker image history --no-trunc arg-demo | grep DB_PASSWORDRUN |1 DB_PASSWORD=hunter2 /bin/sh -c echo "빌드 중 비밀번호 사용: $DB_PASSWORD" > /tmp/x 12.3kB
ARG DB_PASSWORD=hunter2 0B평문으로, 그것도 두 줄에 남는다. RUN 에서 그 값을 쓰지 않았더라도 ARG 줄은 남는다. 이미지를 받은 사람은 누구나 볼 수 있고, 레지스트리에 한 번 올렸다면 이미지를 지워도 안전해지지 않는다 — 값 자체를 폐기하고 새로 발급해야 한다.
docker image rm arg-demoADD — 외부 파일 또는 디렉토리 추가
COPY와 유사하지만 URL 다운로드·압축 해제 기능이 추가되어 있다.
자동 압축 해제는 「로컬 아카이브」 에만 적용된다.
로컬
tar/tar.gz를ADD하면 대상 경로에 풀려서 들어간다.URL 로 받은 아카이브는 풀리지 않고 파일 그대로 들어간다. 풀려면
--unpack을 붙이거나RUN tar로 직접 푼다.
원격 파일은 ADD 보다 RUN curl + 체크섬 검증이 낫다. 어디서 무엇을 받았는지 명령에 남기 때문이다.
# 사용문법
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
# 예시
ADD test.txt relativeDir/
ADD test.txt /absoluteDir/COPY — 외부 파일 또는 디렉터리를 복사
동작이 명시적이라 대부분의 경우 ADD보다 COPY를 권장한다.
# 사용문법
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
# 예시
COPY test.txt relativeDir/
COPY test.txt /absoluteDir/
# 멀티 스테이지 빌드에서 다른 단계의 결과물 복사
COPY --from=builder /build/app.jar app.jarVOLUME — 마운트 지점을 생성
FROM ubuntu
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvolUSER — RUN, CMD, ENTRYPOINT를 실행하는 사용자
# 사용문법
USER <user>[:<group>]
USER <UID>[:<GID>]
# 예시
FROM ubuntu:24.04
# 컨테이너 안에 사용자를 만들고
RUN useradd --create-home patrick
# 이후 명령을 그 사용자로 실행한다
USER patrick왜 굳이 사용자를 바꾸는가. 지정하지 않으면 컨테이너 안에서 root 로 돈다. 컨테이너가 뚫렸을 때 할 수 있는 일이 그만큼 많아진다. 애플리케이션을 돌리는 데 관리자 권한이 필요 없다면 일반 사용자로 내려두는 것이 원칙이다.
WORKDIR — 작업 디렉토리 설정
# 예시
WORKDIR /path/to/workdir
# 디렉토리가 없으면 생성한다.
# CMD, COPY, RUN 등의 명령어를 실행하기 전에 작업 디렉토리를 변경할 때 사용상대 경로를 주면 「직전 WORKDIR 기준」 으로 붙는다. 절대 경로가 아니다.
WORKDIR /app
WORKDIR build # 결과는 /app/buildRUN cd /app 은 그 RUN 안에서만 유효하고 다음 줄에 이어지지 않는다. 디렉터리를 옮길 때는 cd 가 아니라 WORKDIR 를 쓴다.
🔬 직접 해 보기 — WORKDIR 는 쌓이고, RUN cd 는 안 남는다
위 두 가지를 한 번에 확인한다.
FROM alpine:3.20
WORKDIR /app
WORKDIR build
RUN cd /tmp
RUN pwd > /where.txt
CMD ["cat", "/where.txt"]docker image build -t workdir-demo .
docker container run --rm workdir-demo/app/build두 가지가 동시에 드러난다.
WORKDIR build가/build가 아니라/app/build다. 상대 경로는 직전WORKDIR에 붙는다.바로 위에
RUN cd /tmp가 있는데도/tmp가 아니다.cd는 그RUN안에서 끝난다.
docker image rm workdir-demoLABEL — 이미지에 메타데이터 붙이기
# 사용문법
LABEL <key>=<value> <key>=<value> ...
# 예시
LABEL org.opencontainers.image.source="https://github.com/kaeback/visit-counter"
LABEL org.opencontainers.image.description="방문자 카운터"docker image inspect 로 확인한다. GHCR 은 org.opencontainers.image.source 를 보고 패키지를 저장소에 연결하므로, 이 한 줄이 있으면 패키지 페이지에서 소스로 바로 넘어간다.
HEALTHCHECK — 컨테이너가 살아 있는지 판정
# 사용문법
HEALTHCHECK [OPTIONS] CMD <command>
HEALTHCHECK NONE
# 예시
HEALTHCHECK --interval=10s --timeout=3s --start-period=40s --retries=3 CMD curl -fsS http://localhost:8080/actuator/health || exit 1옵션 | 뜻 | 기본값 |
|---|---|---|
| 검사 주기 | 30s |
| 이 시간을 넘기면 실패로 본다 | 30s |
| 기동 유예 시간. 이 동안의 실패는 재시도 횟수에 넣지 않는다 | 0s |
| 연속 몇 번 실패하면 | 3 |
명령의 종료 코드로 판정한다 — 0 이면 정상, 1 이면 비정상. 결과는 docker ps 의 STATUS 에 (healthy) / (unhealthy) 로 나온다.
unhealthy 가 돼도 Docker 가 컨테이너를 알아서 재시작하지는 않는다. 상태를 표시할 뿐이다. 그래서 이 교안의 배포 스크립트는 HEALTHCHECK 에 기대지 않고 배포 쪽에서 직접 헬스 체크를 돌려 통과했을 때만 트래픽을 넘긴다 — 무중단 배포 챕터에서 다룬다.
검사 명령은 이미지 안에 있어야 한다. curl 이 없는 슬림 이미지에 curl 로 검사를 걸면 항상 실패한다. 없으면 wget --spider 나 언어 런타임으로 대체한다.
STOPSIGNAL — 종료 시 보낼 시그널
# 사용문법
STOPSIGNAL <signal>
# 예시
STOPSIGNAL SIGTERM기본값은 SIGTERM 이다. docker stop 은 이 시그널을 보내고 기본 10초를 기다린 뒤 응답이 없으면 SIGKILL 로 강제 종료한다. 기다리는 시간은 docker stop -t 30 으로 늘린다.
시그널이 애플리케이션까지 닿으려면 PID 1 이 애플리케이션이어야 한다. ENTRYPOINT 를 shell form 으로 쓰면 PID 1 이 sh 가 되어 여기서 끊긴다.
실습은 어디에 있나
이 문서 안의 🔬 직접 해 보기 는 인스트럭션 하나의 동작을 확인하는 짧은 것들이다.
붙어 있는 절 | 확인하는 것 |
|---|---|
|
|
|
|
| 상대 경로가 어디에 붙는가, |
이미지를 처음부터 설계하고 빌드해 배포하는 본 실습은 이미지 생성 및 관리에 있다. 멀티 스테이지 빌드로 visit-counter 이미지를 만들고 Docker Hub 에 올린다.