LabHub
배우기 러닝패스 코스

イメージのビルド

Dockerfileは設定ファイルではなくビルドスクリプトだ

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

Dockerfile 의 지시어는 두 부류입니다. 레이어를 만드는 것(RUN, COPY, ADD)과 이미지 설정에 값을 새기는 것(ENV, CMD, ENTRYPOINT, USER, LABEL, EXPOSE). 어느 쪽인지 알면 크기 문제와 보안 문제를 절반쯤 미리 피할 수 있습니다.

階層図: 레이어를 만드는 것・이미지 설정에 값을 새기는 것・이미지 설정에 영구히・ADD 는 COPY 의 상위 호환이 아닙니다.

왜 이게 필요했나

가장 자주 반복되는 사고는 시크릿입니다. ENV NPM_TOKEN=... 로 넣으면 토큰이 이미지 설정에 영구히 남습니다.

docker inspect -f '{{json .Config.Env}}' bad:v1
# "NPM_TOKEN=npm_9fA3xQ2LkD8vR1sT6yU0wZ4bN7mC5eJ"

ARG 로 받아서 쓰고 rm ~/.npmrc 로 지워도 소용이 없습니다. 파일은 아래 레이어에 그대로 남고, 클래식 빌더에서는 히스토리에도 남습니다. 레지스트리에 푸시했다면 그 토큰은 이미 유출된 것으로 간주하고 폐기해야 합니다. 이미지를 지우는 것으로는 되돌릴 수 없습니다.

어떻게 동작하나

몇 가지 지시어는 겉보기와 다르게 동작합니다.

ADDCOPY 의 상위 호환이 아닙니다. ADD 는 로컬 tar 를 자동으로 풀고 URL 도 받습니다. 그 추가 동작이 대부분 문제입니다. 아카이브 안에 ../ 경로가 있으면 의도하지 않은 위치에 파일이 쓰이고, URL 다운로드는 체크섬 검증이 없습니다. 기본값은 COPY 이고, ADD 가 정당한 경우는 로컬 tar 를 의도적으로 풀 때뿐입니다.

COPY --chownRUN chown -R 은 크기가 두 배 차이 납니다. COPY . /app 이 286MB 라면 이어지는 RUN chown -R app:app /app 은 파일 전체를 새 레이어에 다시 쓰므로 286MB 를 한 번 더 씁니다. COPY 에 옵션 하나를 붙이면 레이어가 하나로 끝납니다.

USER 는 반드시 숫자 UID 로 씁니다. 쿠버네티스의 runAsNonRoot 는 이름을 보고는 root 인지 판별하지 못하고 파드를 거부합니다.

Error: container has runAsNonRoot and image has non-numeric user (app),
cannot verify user is non-root

HEALTHCHECK 에는 오해가 둘 있습니다. 실패해도 컨테이너가 재시작되지 않고 unhealthy 로 표시만 되며, 쿠버네티스는 이미지의 HEALTHCHECK 를 아예 무시합니다. 프로브를 따로 정의해야 합니다.

현장에서 만나는 모습

리뷰 체크리스트는 다섯 줄이면 충분합니다.

  1. FROM 이 고정되어 있는가 (latest 는 롤백할 대상을 없앤다)
  2. USER 가 숫자 UID 인가
  3. CMD/ENTRYPOINT 가 JSON 배열(exec 형식)인가
  4. 시크릿이 ARG/ENV 로 들어오지 않는가
  5. 최종 스테이지에 빌드 도구가 남아 있지 않은가

"태그를 고정하면 보안 패치를 못 받는다"는 반론이 흔한데, 갱신은 사람이 아니라 봇이 PR 을 올리고 CI 가 검증한 뒤 병합하면 됩니다. 자동으로 흘러 들어오는 것과 검증을 거쳐 들어오는 것의 차이입니다.

헷갈리는 지시어 짝

차이
COPY vs ADD ADD 는 URL 을 받고 tar 를 자동으로 푼다. 그래서 예측이 어렵다. 기본은 COPY
CMD vs ENTRYPOINT ENTRYPOINT 는 실행할 것, CMD 는 기본 인자. docker run 의 인자가 CMD 를 덮는다
ENV vs ARG ENV 는 실행 시에도 남고, ARG 는 빌드 중에만 있다
RUN vs CMD RUN 은 빌드 때, CMD 는 실행 때

ENTRYPOINT + CMD 조합이 가장 쓸모 있습니다.

ENTRYPOINT ["python", "-m", "app"]
CMD ["--port", "8080"]
docker run myapp                    → python -m app --port 8080
docker run myapp --port 9000        → python -m app --port 9000

ARG 로 비밀을 넘기면 안 됩니다. docker history 에 그대로 남습니다. 빌드 시크릿(RUN --mount=type=secret)을 씁니다.

셸 형식과 exec 형식

CMD python app.py           # 셸 형식 — /bin/sh -c 로 감싸진다
CMD ["python", "app.py"]    # exec 형식 — 그대로 실행

exec 형식을 씁니다. 셸 형식은 sh 가 PID 1 이 되어 SIGTERM 을 애플리케이션에 전달하지 않습니다. 컨테이너를 멈출 때 10초를 기다렸다 강제 종료됩니다.

다만 exec 형식에서는 환경변수 치환이 안 됩니다. CMD ["echo", "$HOME"]$HOME 을 그대로 출력합니다. 변수가 필요하면 진입점 스크립트를 두고 마지막에 exec "$@" 를 씁니다.

빌드를 재현 가능하게

FROM python:3.12-slim@sha256:abc123…    # 태그가 아니라 다이제스트로 고정

태그는 움직입니다. python:3.12-slim 이 어제와 오늘 다를 수 있고, 그러면 "어제는 됐는데" 가 시작됩니다. 운영 이미지는 다이제스트로 고정합니다.

의존성도 고정합니다.

COPY requirements.lock .
RUN pip install --no-cache-dir -r requirements.lock

--no-cache-dir 은 pip 캐시를 레이어에 남기지 않아 이미지가 작아집니다. BuildKit 의 마운트 캐시를 쓰면 빌드는 빠르면서 이미지는 작게 유지할 수 있습니다.

자주 놓치는 것들

다음 실습에서 할 것

지시어를 하나씩 추가하며 태그를 바꿔 빌드하고, 각 지시어가 이미지의 어느 필드에 새겨지는지 inspect 로 확인한 뒤, 마지막에 체크리스트를 통과하는 Dockerfile 을 완성합니다.