Part 2. 레퍼런스

Dockerfile 인스트럭션 레퍼런스

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}-jre

RUN — 명령어 실행

이미지 빌드 시점에 실행된다.

# 사용문법
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 로 감싼다. 그 결과 두 가지가 무너진다.

  1. CMD 와 docker run 뒤의 인자를 전부 무시한다. 위에서 설명한 「인자로 전달」 이 일어나지 않는다.

  2. 시그널이 앱까지 닿지 않는다. docker stop 의 SIGTERM 은 PID 1 에게만 간다. shell form 에서는 그 자리를 sh 가 차지하고 앱은 자식이 되므로, 앱은 아무것도 받지 못한 채 타임아웃 뒤 SIGKILL 로 강제 종료된다. 정상 종료 처리를 할 기회가 없다.

ENTRYPOINT java -jar app.jar          # 하지 말 것
ENTRYPOINT ["java", "-jar", "app.jar"]  # 이렇게 쓴다

CMD vs ENTRYPOINT 요약

docker run myimage

docker run myimage bash

CMD ["python3","app.py"]

python3 app.py

bash (CMD 전체 교체)

ENTRYPOINT ["java","-jar"] + CMD ["app.jar"]

java -jar app.jar

java -jar bash (인자만 교체)

🔬 직접 해 보기 — 인자가 교체되는지 확인한다

위 표를 손으로 확인한다. 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-both

EXPOSE — 컨테이너에서 사용할 포트 정보

실제로 포트를 여는 것은 아니며, docker run 실행 시 -p 옵션을 사용하여 포트를 열어주어야 한다.

# 사용문법
EXPOSE <port> [<port>/<protocol>...]

# 예시
EXPOSE 9000

ENV — 환경 변수를 설정

빌드 시점과 컨테이너 실행 시점 모두에서 사용 가능하다.

# 사용문법
ENV <key>=<value> ...

# 예시
ENV MY_NAME="John Doe"
ENV MY_DOG=Rex\ The\ Dog
ENV MY_CAT=fluffy

ARG — 빌드 시 환경변수 설정

빌드 시점에만 존재한다. 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/x
docker image build --build-arg DB_PASSWORD=hunter2 -t arg-demo .
docker image history --no-trunc arg-demo | grep DB_PASSWORD
RUN |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-demo

ADD — 외부 파일 또는 디렉토리 추가

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.jar

VOLUME — 마운트 지점을 생성

FROM ubuntu
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol

USER — 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/build

RUN 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-demo

LABEL — 이미지에 메타데이터 붙이기

# 사용문법
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

옵션

뜻

기본값

--interval

검사 주기

30s

--timeout

이 시간을 넘기면 실패로 본다

30s

--start-period

기동 유예 시간. 이 동안의 실패는 재시도 횟수에 넣지 않는다

0s

--retries

연속 몇 번 실패하면 unhealthy 로 볼 것인가

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 가 되어 여기서 끊긴다.

실습은 어디에 있나

이 문서 안의 🔬 직접 해 보기 는 인스트럭션 하나의 동작을 확인하는 짧은 것들이다.

붙어 있는 절

확인하는 것

ENTRYPOINT

docker run 뒤의 인자가 CMD 를 어떻게 밀어내는가

ARG

--build-arg 로 넘긴 비밀값이 이미지에 남는가

WORKDIR

상대 경로가 어디에 붙는가, RUN cd 는 왜 안 남는가

이미지를 처음부터 설계하고 빌드해 배포하는 본 실습은 이미지 생성 및 관리에 있다. 멀티 스테이지 빌드로 visit-counter 이미지를 만들고 Docker Hub 에 올린다.

마지막 수정 2026년 10월 22일