[도커 30강] 08강. WORKDIR, ENV, ARG를 안전하게 쓰는 패턴

[도커 30강] 08강. WORKDIR, ENV, ARG를 안전하게 쓰는 패턴

도커 파일을 조금만 다뤄보면 WORKDIR, ENV, ARG는 거의 매번 등장합니다. 문제는 세 명령이 비슷해 보여서, 아무 생각 없이 섞어 쓰기 쉽다는 점입니다. 이렇게 섞이면 빌드 캐시가 불필요하게 깨지고, 런타임 설정이 의도와 다르게 고정되거나, 심하면 민감값이 이미지 히스토리에 남는 사고까지 이어집니다. 오늘은 이 세 가지를 “언제/왜/어떻게” 써야 안전한지 실무 기준으로 정리합니다.

핵심은 역할 분리입니다. WORKDIR는 작업 기준 경로를 고정하는 도구, ENV는 컨테이너 실행 시에도 유지될 환경 변수, ARG는 빌드 시점에만 쓰는 입력값입니다. 이 구분이 명확해지면 Dockerfile 가독성이 올라가고, 팀원 간 오해가 크게 줄어듭니다.


핵심 개념

  • WORKDIR는 이후 RUN, COPY, CMD 등 명령이 동작할 기본 경로를 정의합니다. 디렉터리가 없으면 자동 생성됩니다.
  • ENV는 이미지 메타데이터에 포함되며, 빌드 단계와 컨테이너 실행 단계에서 모두 사용할 수 있습니다.
  • ARG는 빌드 시점 변수입니다. 기본적으로 최종 컨테이너 런타임에는 전달되지 않습니다.
  • 민감한 값(토큰, 비밀번호)은 ENV/ARG에 직접 넣지 않는 것이 원칙입니다. 특히 ARG도 빌드 로그/히스토리 맥락에서 노출될 수 있습니다.
  • 재현성과 캐시 안정성을 위해, 자주 바뀌는 ARG는 Dockerfile 하단에 가깝게 소비하고, 안정적인 ENV만 상단에 배치하는 패턴이 유리합니다.

기본 사용

예제 1) WORKDIR로 경로 실수 제거하기

mkdir -p ~/docker100/lesson08/app && cd ~/docker100/lesson08
cat > app/main.py <<'PY'
print("lesson08: workdir-env-arg")
PY
cat > Dockerfile.workdir <<'EOF_DOCKER'
FROM python:3.12-slim
WORKDIR /srv/app
COPY app/ ./
RUN pwd && ls -al
CMD ["python", "main.py"]
EOF_DOCKER

docker build -f Dockerfile.workdir -t docker100-lesson08:workdir .
docker run --rm docker100-lesson08:workdir

설명:

  • WORKDIR /srv/app를 선언하면 이후 COPY app/ .//srv/app 기준으로 복사됩니다.
  • 절대경로와 상대경로를 섞다가 생기는 “파일 못 찾음” 오류를 크게 줄일 수 있습니다.
  • 팀 규칙으로 앱 루트 경로를 고정해두면 디버깅 속도가 빨라집니다.

예제 2) ENV와 ARG를 역할대로 분리하기

cat > Dockerfile.envarg <<'EOF_DOCKER'
FROM python:3.12-slim
WORKDIR /srv/app

ARG APP_VERSION=dev
ENV APP_HOME=/srv/app \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

COPY app/ ./
RUN echo "build version=$APP_VERSION" > build-info.txt
CMD ["sh", "-c", "echo runtime APP_HOME=$APP_HOME && python main.py && cat build-info.txt"]
EOF_DOCKER

docker build -f Dockerfile.envarg -t docker100-lesson08:envarg --build-arg APP_VERSION=0.8.0 .
docker run --rm docker100-lesson08:envarg

설명:

  • ARG APP_VERSION은 빌드 과정에서만 사용해 build-info.txt 생성에 활용했습니다.
  • ENV APP_HOME은 런타임에서도 확인되므로 컨테이너 실행 시 계속 유지됩니다.
  • 이렇게 분리하면 “배포 버전 주입”과 “실행 환경 설정”이 명확히 구분됩니다.

예제 3) 캐시 파괴를 줄이는 ARG 소비 위치 설계

cat > Dockerfile.cache <<'EOF_DOCKER'
FROM python:3.12-slim
WORKDIR /srv/app

COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt

COPY app/ ./
ARG BUILD_LABEL=local
RUN echo "$BUILD_LABEL" > /srv/app/label.txt
CMD ["python", "main.py"]
EOF_DOCKER

cat > requirements.txt <<'EOF_REQ'
flask==3.0.3
EOF_REQ

time docker build -f Dockerfile.cache -t docker100-lesson08:cache --build-arg BUILD_LABEL=a1 .
time docker build -f Dockerfile.cache -t docker100-lesson08:cache --build-arg BUILD_LABEL=a2 .

설명:

  • BUILD_LABEL처럼 자주 바뀌는 값은 하단에서 소비하면 상단 의존성 설치 캐시를 보존할 수 있습니다.
  • 반대로 상단에서 ARG를 사용하면 값이 바뀔 때마다 이후 레이어가 연쇄 무효화됩니다.
  • 즉, “변경 빈도가 높은 변수일수록 아래에서 쓰기”가 캐시 친화 패턴입니다.

예제 4) 런타임에서 ENV 오버라이드하기

docker run --rm -e APP_HOME=/opt/custom docker100-lesson08:envarg \
  sh -c 'echo overridden APP_HOME=$APP_HOME && python main.py'

설명:

  • Dockerfile의 ENV는 기본값이며, docker run -e로 덮어쓸 수 있습니다.
  • 운영 환경(스테이징/프로덕션)별 값 차이는 실행 시점 주입으로 다루는 편이 안전합니다.

자주 하는 실수

실수 1) ARG를 런타임에서도 보일 것이라 착각함

  • 원인: ENVARG의 생명주기를 구분하지 않고 사용함.
  • 해결: 런타임 필요값은 ENV 또는 오케스트레이터 주입(Kubernetes Secret/ConfigMap 등)으로 설계합니다.

실수 2) 비밀값을 ARG/ENV에 직접 넣음

  • 원인: 편의성 때문에 --build-arg TOKEN=... 또는 ENV DB_PASSWORD=...를 습관적으로 사용함.
  • 해결: 시크릿 매니저/런타임 주입/BuildKit secret mount를 사용하고, 이미지 히스토리 점검을 자동화합니다.

실수 3) WORKDIR 없이 상대경로 남발

  • 원인: 매 단계에서 현재 경로를 다르게 가정해 Dockerfile이 우연히만 동작함.
  • 해결: 초반에 WORKDIR를 1회 명시하고, 상대경로 기준을 일관되게 유지합니다.

실수 4) 자주 바뀌는 ARG를 상단에서 소비해 빌드를 매번 느리게 만듦

  • 원인: 버전/빌드번호 주입을 Dockerfile 상단에 배치.
  • 해결: 의존성 설치 등 무거운 레이어 뒤쪽에서 소비해 캐시 재사용률을 유지합니다.

실무 패턴

실무에서는 WORKDIR, ENV, ARG를 명령어 문법이 아니라 “팀 계약”으로 다루는 게 중요합니다. 먼저 WORKDIR는 서비스별 표준 경로를 정합니다. 예를 들어 Python 서비스는 /srv/app, Node 서비스는 /workspace처럼 통일해두면 운영자가 컨테이너 내부를 탐색할 때 혼란이 줄어듭니다.

ENV는 두 가지로 분리하면 깔끔합니다. 첫째, 애플리케이션 동작 안정화를 위한 공통값(예: PYTHONUNBUFFERED=1). 둘째, 환경별로 바뀔 수 있지만 기본값이 필요한 값(예: PORT=8000). 후자의 경우 실제 운영값은 배포 파이프라인이나 런타임 변수로 덮어쓰도록 설계합니다.

ARG는 빌드 메타데이터(버전, 커밋 SHA, 빌드 시간) 주입에 적합합니다. 다만 이 값이 자주 바뀌면 캐시를 깨므로, 메타데이터 기록 단계를 Dockerfile 하단에 배치합니다. 또한 ARG가 필요한 단계에서만 선언/소비해 영향 범위를 최소화하는 습관이 좋습니다.

보안 관점에서는 “이미지에 남아도 되는 값인가?”를 기준 질문으로 둡니다. 이 질문에 확신이 없다면 ENV/ARG에 넣지 않습니다. 특히 사고는 대개 편의성에서 시작됩니다. 로컬에서 잠깐 테스트하려고 넣은 토큰이 CI 로그, 이미지 레이어, 스캐너 리포트에서 발견되는 경우가 실제로 매우 흔합니다.

협업 관점에서는 Dockerfile 리뷰 체크리스트에 다음 항목을 넣어두면 효과가 큽니다: (1) WORKDIR 선언이 명확한가, (2) ENVARG 역할이 분리되어 있는가, (3) 민감정보가 하드코딩되지 않았는가, (4) 캐시를 해치는 ARG 위치는 아닌가. 이 네 가지만 지켜도 빌드 안정성과 보안 수준이 동시에 올라갑니다.

오늘의 결론

한 줄 요약: WORKDIR는 경로 기준, ENV는 런타임 기본값, ARG는 빌드 입력값으로 분리하면 Dockerfile은 안전하고 빨라진다.

세 명령 모두 자주 쓰지만, 섞어 쓰면 문제도 자주 생깁니다. 오늘 기준대로 역할을 나누고, 특히 민감값/캐시 영향까지 함께 고려하면 “돌아가는 Dockerfile”에서 “팀이 신뢰하는 Dockerfile”로 수준이 올라갑니다.

연습문제

  1. Dockerfile.envarg에서 APP_VERSION을 바꿔 2회 빌드하고, 어떤 레이어가 다시 실행되는지 로그로 확인해보세요.
  2. ENV PORT=8000을 선언한 뒤 docker run -e PORT=9000 ...으로 오버라이드해, 컨테이너 내부에서 최종 값이 무엇인지 검증해보세요.
  3. 현재 프로젝트 Dockerfile을 열어 민감정보가 ARG/ENV에 하드코딩된 부분이 있는지 점검하고, 대체 방법(시크릿 주입 방식)을 제안해보세요.

이전 강의 정답

7강 연습문제 해설:

  • Dockerfile.slowDockerfile.fast의 두 번째 빌드 차이는 의존성 설치 레이어 캐시 재사용 여부에서 발생합니다. requirements.txt를 먼저 복사한 구조가 재빌드에서 훨씬 유리합니다.
  • .dockerignore 적용 전에는 .git, 로그, 로컬 캐시가 컨텍스트를 흔들어 캐시 적중률을 떨어뜨릴 수 있습니다. 적용 후에는 불필요 파일이 제외되어 빌드 재현성과 속도가 개선됩니다.
  • BuildKit 캐시 마운트는 반복 빌드 시 패키지 다운로드 비용을 줄여주지만, CI에서는 캐시 저장/복원 정책과 보안 범위를 함께 설계해야 효과가 안정적으로 유지됩니다.

실습 환경/재현 정보

  • OS: macOS 15+ 또는 Ubuntu 22.04+
  • Docker 버전: Docker Engine/Desktop 25.x 이상
  • 실행 순서:
    1. 샘플 앱과 Dockerfile 3종(workdir, envarg, cache) 준비
    2. WORKDIR 기준 경로 동작 확인
    3. ARG 빌드 주입과 ENV 런타임 유지 차이 확인
    4. ARG 변경 시 캐시 영향 비교
    5. docker run -e로 런타임 오버라이드 검증
  • 재현 체크:
    • 컨테이너 실행 시 ENV가 유지되는가?
    • ARG 값은 런타임 환경변수로 직접 노출되지 않는가?
    • ARG 위치 조정으로 상단 무거운 레이어 캐시가 보존되는가?
    • 민감값이 Dockerfile/빌드 로그/이미지 히스토리에 남지 않는가?