컨테이너 학습 노트 목차

Dockerfile — 명령어와 그 함정

명령어는 18개뿐인데, 그중 절반이 비슷해 보이는 것끼리 짝을 이루고 있다. COPY vs ADD, CMD vs ENTRYPOINT, ENV vs ARG — 이 짝이 사고의 근원이다.


1. 전체 목록

FROM · RUN · CMD · LABEL · EXPOSE · ENV · ADD · COPY · ENTRYPOINT VOLUME · USER · WORKDIR · ARG · ONBUILD · STOPSIGNAL · HEALTHCHECK · SHELL MAINTAINER (폐기 — LABEL maintainer 를 쓴다)

  • 레이어를 만드는 것 — RUN · COPY · ADD
  • 메타데이터만 바꾸는 것 — CMD · ENTRYPOINT · ENV · LABEL · EXPOSE · USER · WORKDIR …

2. 문법 버전을 고정한다

# syntax=docker/dockerfile:1
FROM alpine:3.20

이 한 줄이 파서 지시자다 — BuildKit 이 어떤 Dockerfile 문법을 쓸지 정한다

기능마다 요구 버전이 다르다
  1.2   RUN --mount=type=cache
  1.4   COPY --link · 히어독
  1.8   # check= 지시자 (빌드 검사)
  1.14  최신 기능들

:1 로 두면 1.x 최신을 따라간다 — 대부분 이렇게 쓴다 없으면 데몬에 내장된 낡은 문법으로 파싱돼 "문법 오류" 가 난다

3. COPY vs ADD

COPY app.jar /app/          # 파일·디렉터리 복사. 이게 기본이다
ADD  https://x/y.tar.gz /   # 원격 URL 다운로드
ADD  archive.tar.gz /app/   # tar 자동 압축 해제

공식 권장 — 기본은 COPY 를 쓴다

ADD 가 위험한 이유

  • tar 를 '자동으로 푼다' — 의도치 않게 파일이 흩어진다
  • 원격 URL 은 캐시 판정이 애매하고 검증이 없다
  • 무엇을 할지 파일 확장자에 따라 달라진다 (예측 불가)

원격 파일이 필요하면 RUN 안에서 명시적으로 받고 체크섬을 검증한다

  • RUN curl -fsSL -o x.tgz https://... \
    • && echo "abc123 x.tgz" | sha256sum -c -

빌드 컨텍스트와 .dockerignore

docker build . 의 '.' 이 빌드 컨텍스트다 이 디렉터리 전체가 빌더로 전송된다

.git · node_modules · 빌드 산출물이 통째로 넘어가면

  • 전송이 느려진다
  • COPY . . 로 이미지에 딸려 들어간다 (비밀 유출 위험)

.dockerignore 를 .gitignore 처럼 쓴다 .git node_modules *.log .env

.dockerignore 가 없는 프로젝트가 의외로 많다. 이미지에 .git 이 통째로 들어가 있는 경우도 흔하다 — 커밋 히스토리에 남은 비밀까지 함께 나간다.

4. CMD vs ENTRYPOINT

ENTRYPOINT ["java", "-jar", "/app.jar"]   # 항상 실행되는 것
CMD        ["--spring.profiles.active=prod"]   # 기본 인자 (덮어쓸 수 있다)
docker run myapp                    → java -jar /app.jar --spring...=prod
docker run myapp --debug            → java -jar /app.jar --debug
                                       (CMD 만 교체된다)
docker run --entrypoint sh myapp    → sh  (ENTRYPOINT 를 바꾼다)
  • ENTRYPOINT — 이 컨테이너가 '무엇인가' 를 정한다

  • CMD — 기본 인자. 사용자가 덮어쓸 것을 전제로 한다

  • CMD 만 쓰면 — docker run myapp bash 로 통째로 교체된다 (디버깅엔 편하다)

  • ENTRYPOINT 만 — 인자를 덧붙이는 형태가 된다

exec form 과 shell form — 시그널이 갈린다

CMD java -jar app.jar               # shell form → /bin/sh -c "java -jar app.jar"
CMD ["java", "-jar", "app.jar"]     # exec form  → 직접 실행

shell form 이면 PID 1 이 sh 가 된다

  • 컨테이너 정지 시 SIGTERM 을 sh 가 받는다
  • sh 는 그것을 자식에게 전달하지 않는다
  • 애플리케이션이 graceful shutdown 을 못 한다 → 강제 종료

배포마다 요청이 잘리는 원인 1순위다 반드시 exec form(대괄호 표기)을 쓴다

5. ENV vs ARG

ARG NODE_VERSION=22          # 빌드 시점에만 존재
ENV NODE_ENV=production      # 이미지에 남아 런타임에도 보인다
  • ARG — 빌드 인자. --build-arg 로 넘긴다. 실행 중에는 없다

  • ENV — 환경변수. 이미지에 박히고 컨테이너에서 보인다

  • 둘 다 비밀을 담으면 안 된다

    • ARG 로 넘긴 값도 docker history 에 남는다
    • ENV 는 docker inspect 로 그대로 보인다

비밀은 시크릿 마운트로

# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci
docker build --secret id=npmrc,src=$HOME/.npmrc .

빌드 중에만 파일로 존재하고 레이어에는 남지 않는다 토큰·키가 필요한 빌드는 전부 이 방식으로 간다

ARG 로 토큰을 넘기는 코드를 보면 그 자리에서 고친다

6. 캐시 마운트로 빌드를 줄인다

# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.m2 \
    ./mvnw -B package -DskipTests

의존성 캐시를 레이어에 굽지 않고 '빌드 사이에 유지되는 별도 저장소' 에 둔다

  • 이미지가 커지지 않는다
  • COPY 순서로 캐시를 지켜야 하는 부담이 줄어든다
  • CI 에서는 이 캐시를 노드 간에 공유할 수 있어야 효과가 난다

지원 target 예 — /root/.m2 · /root/.gradle · ~/.npm · /go/pkg/mod · ~/.cache/pip

7. 멀티스테이지 빌드

# syntax=docker/dockerfile:1

FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /src
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn -B dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 mvn -B package -DskipTests

FROM eclipse-temurin:21-jre-alpine AS runtime
WORKDIR /app
COPY --from=build /src/target/app.jar ./app.jar
USER 10001:10001
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

빌드 도구(JDK · maven · 컴파일러)는 최종 이미지에 필요 없다

효과

  • 이미지 크기가 수백 MB → 수십 MB
  • 공격 표면이 줄어든다 (컴파일러 · 셸 · 패키지 매니저가 없다)
  • 빌드 도구의 취약점이 운영 이미지에 안 딸려 온다

특정 스테이지만 빌드할 수도 있다

  • docker build --target build .

8. HEALTHCHECK

HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
  CMD curl -fsS http://localhost:8080/healthz || exit 1
옵션기본값
--interval30s
--timeout30s
--start-period0s
--start-interval5s
--retries3
종료 코드
  0  healthy
  1  unhealthy
  2  예약됨 — 쓰지 않는다

start-period 동안의 실패는 재시도 횟수에 세지 않는다

  • 기동이 오래 걸리는 애플리케이션에 필수다
  • 이 값이 없으면 기동 중에 unhealthy 로 판정된다

쿠버네티스에서는 이 HEALTHCHECK 를 쓰지 않는다

  • 파드 스펙의 liveness/readiness probe 가 대신한다
  • 둘 다 정의하면 혼란만 는다. 어느 쪽을 쓸지 정한다

9. 그 밖에 자주 틀리는 것

EXPOSE 8080

EXPOSE 는 '문서' 다 — 포트를 실제로 여는 것이 아니다 실제 공개는 docker run -p 8080:8080 이 한다 "EXPOSE 했는데 접속이 안 된다" 는 여기서 나온다

WORKDIR /app          # 없으면 만든다. cd 대신 이것을 쓴다
RUN cd /app && ...    # ✗ 다음 RUN 에서는 원래 위치로 돌아간다
VOLUME /data
  • VOLUME 을 선언하면 그 경로에 익명 볼륨이 자동 생성된다
    • 이후 레이어에서 그 경로에 쓴 내용이 반영되지 않는다
    • 정리되지 않은 익명 볼륨이 쌓인다
  • 이미지에 VOLUME 을 넣기보다 실행 시 -v 로 지정하는 편이 통제하기 쉽다
STOPSIGNAL SIGQUIT      # nginx 처럼 SIGTERM 이 '즉시 종료' 인 경우

10. 빌드 검사

# syntax=docker/dockerfile:1
# check=error=true

BuildKit 이 Dockerfile 의 흔한 실수를 잡아 준다

  • 정의하지 않은 ARG 사용
  • JSON 이 아닌 CMD/ENTRYPOINT (shell form 경고)
  • 대소문자 불일치 (from → FROM)
  • 중복 스테이지 이름

Dockerfile v1.8.0+ 에서 쓸 수 있다 CI 에 넣으면 리뷰에서 반복 지적하던 것들이 자동으로 걸린다


한눈에 정리

  • syntax 지시자 — # syntax=docker/dockerfile:1 — 없으면 낡은 문법으로 파싱된다
  • COPY vs ADD — 기본은 COPY. ADD 는 자동 압축 해제·원격 URL 이라 예측이 어렵다
  • .dockerignore — 빌드 컨텍스트 전체가 전송된다. .git 유출을 막는다
  • CMD vs ENTRYPOINT — 무엇인가(ENTRYPOINT) + 기본 인자(CMD)
  • exec form — 대괄호 표기. shell form 이면 PID 1 이 sh 가 되어 시그널이 안 간다
  • ENV vs ARG — 빌드 전용(ARG) vs 런타임에도 남음(ENV). 둘 다 비밀 금지
  • 시크릿 마운트 — RUN --mount=type=secret — 레이어에 남지 않는다
  • 캐시 마운트 — RUN --mount=type=cache — 의존성 캐시를 이미지 밖에
  • 멀티스테이지 — 빌드 도구를 최종 이미지에서 뺀다. 크기와 공격 표면이 함께 준다
  • HEALTHCHECK — interval 30s · timeout 30s · retries 3 · start-period 0s
  • EXPOSE — 문서일 뿐. 실제 공개는 -p 가 한다

출처 — Docker Docs: Dockerfile reference (18개 명령어 · 파서 지시자 · HEALTHCHECK 기본값) · "Build cache" · "Best practices" · "Build secrets" · "Multi-stage builds" · "Build checks"

이미지와 레이어 — 빌드가 빨랐다 느렸다 하는 이유실행과 운영 — PID 1·볼륨·네트워크·로그