빌드노트
--build-arg로 넘긴 값이 docker run에서는 없는 이유
Docker 빌드 로그에는 있던 값이 실행한 컨테이너에서는 없을 때 ARG와 ENV를 구분한다. 합성 값으로 이미지의 Config.Env, 컨테이너 출력, 이미지 이력을 비교하고 실행 시 -e로 주입할 값과 이미지에 남길 기본값을 고르는 기준을 설명한다.
Docker 빌드 로그에는 설정값이 찍혔는데, 같은 이미지로 시작한 컨테이너에서 환경변수를 읽으면 비어 있다. Dockerfile에서 그 값을 ARG로만 선언했다면 먼저 전달 시점을 확인한다. --build-arg는 빌드에 주는 입력이다. 실행할 컨테이너의 환경변수를 자동으로 등록하지 않는다.
실행할 때 정할 값이라면 docker run -e 이름=값으로 전달한다. 이미지에 공통 기본값을 남기려는 경우에는 ENV를 쓴다. 빌드할 때만 필요한 값을 실행 환경에 추가할 이유는 없다. 이 구분은 Docker의 빌드 변수 설명 (새 탭에서 열림)에서도 확인할 수 있다.
빌드에서는 보이고 실행에서는 없는 값
운영 앱 대신 합성 문자열 build-blue로 따로 재현했다. Windows PowerShell 5.1, Docker Engine 29.8.0의 Linux/amd64 환경이며, alpine:3.23에서 확인한 Alpine 버전은 3.23.6이었다.
따라 하려면 Docker의 Linux 컨테이너 엔진이 실행 중이어야 하고, 처음에는 Alpine 이미지를 가져올 수 있어야 한다. 새 빈 폴더에서 아래 내용을 UTF-8 파일 Dockerfile.arg로 저장한다. 프로젝트 소스나 .env는 이 폴더에 넣지 않는다.
FROM alpine:3.23
ARG LAB_MODE
RUN printf 'build LAB_MODE=<%s>\n' "$LAB_MODE"
CMD ["sh", "-c", "printf 'runtime LAB_MODE=<%s>\\n' \"${LAB_MODE-UNSET}\""]
RUN은 이미지를 만들 때 실행된다. CMD의 명령은 이 이미지로 컨테이너를 시작할 때 실행된다. UNSET는 변수가 없을 때 이 예제의 셸이 표시하도록 정한 문자열이다. 빈 문자열과 미설정을 구별하기 위해 넣었다.
그 폴더의 PowerShell에서 다음 명령을 한 줄씩 실행한다. 이미지 이름에 붙일 식별자를 새로 만들고, 이후 명령도 같은 세션에서 실행한다. 빌드가 실패하면 다음 단계로 넘어가지 않는다.
$labId = [guid]::NewGuid().ToString('N').Substring(0,12)
$argImage = "arg-env-lab/arg:$labId"
$envImage = "arg-env-lab/env:$labId"
docker build --no-cache --progress=plain --network=none --build-arg LAB_MODE=build-blue -t $argImage -f Dockerfile.arg .
docker run --rm --network none $argImage
실제 빌드 로그의 진행 번호와 시간 접두어를 제외한 출력, 이어서 컨테이너 출력은 다음과 같았다.
build LAB_MODE=<build-blue>
runtime LAB_MODE=<UNSET>
빌드 입력은 전달됐다. 다만 실행 환경에는 등록되지 않았다. 이 경우 --build-arg를 다시 붙여 빌드해도 같은 경계가 유지된다. 여기서 --no-cache는 출력 행을 다시 관찰하려고 사용한 옵션이며, 변수 누락의 해결책은 아니다.
이미지의 환경변수 기본값도 확인할 수 있다. docker image inspect (새 탭에서 열림)로 이번에 만든 이미지만 조회한다.
docker image inspect $argImage --format '{{json .Config.Env}}'
["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]
Config.Env에 LAB_MODE가 없다는 점이 실행 결과와 맞는다. 이것은 이미지의 기본 환경 설정을 읽은 결과다. 컨테이너를 시작할 때 추가할 값까지 보여 주는 목록은 아니다.
ENV로 남긴 기본값과 run -e로 전달한 값
이미지의 공통 기본값으로 build-blue를 남기려면 빌드 입력을 ENV에 명시적으로 연결한다. 같은 폴더에 Dockerfile.env를 다음 내용으로 저장한다. 두 파일의 차이는 ENV LAB_MODE=$LAB_MODE 한 줄이다.
FROM alpine:3.23
ARG LAB_MODE
ENV LAB_MODE=$LAB_MODE
RUN printf 'build LAB_MODE=<%s>\n' "$LAB_MODE"
CMD ["sh", "-c", "printf 'runtime LAB_MODE=<%s>\\n' \"${LAB_MODE-UNSET}\""]
같은 빌드 입력으로 새 이미지를 만들고 조회·실행했다.
docker build --no-cache --progress=plain --network=none --build-arg LAB_MODE=build-blue -t $envImage -f Dockerfile.env .
docker image inspect $envImage --format '{{json .Config.Env}}'
docker run --rm --network none $envImage
["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","LAB_MODE=build-blue"]
runtime LAB_MODE=<build-blue>
이번에는 이미지 설정과 실행 결과 양쪽에 값이 있었다. ENV는 빌드 이후에도 남는 기본값이다. ARG와 연결하는 방식은 Dockerfile의 ARG·ENV 사용 설명 (새 탭에서 열림)에 나와 있다.
실행 환경마다 값이 달라진다면 이미지를 다시 만들 필요 없이 -e로 전달할 수 있다. 아래 세 명령을 순서대로 실행했다.
docker run --rm --network none -e LAB_MODE=runtime-green $argImage
docker run --rm --network none -e LAB_MODE=runtime-green $envImage
docker run --rm --network none -e LAB_MODE= $envImage
runtime LAB_MODE=<runtime-green>
runtime LAB_MODE=<runtime-green>
runtime LAB_MODE=<>
첫 명령은 이미지 기본값이 없는 곳에 값을 추가했고, 둘째는 기존 기본값을 덮어썼다. 셋째는 빈 문자열을 명시해서 build-blue를 덮어썼다. 처음의 UNSET와 마지막의 빈 출력은 서로 다른 상태다. docker run의 환경변수 옵션 (새 탭에서 열림)은 이렇게 컨테이너 생성 시 값을 지정하거나 이미지의 값을 덮어쓴다.
실행 뒤 같은 image inspect 명령을 다시 실행해도 이미지의 Config.Env는 LAB_MODE=build-blue였다. -e는 해당 컨테이너에 적용되며 이미지 자체의 기본값을 고치지 않았다.
실행 환경에 없다고 비밀값이 안전한 것은 아니다
ARG만 쓴 이미지에서는 실행 환경에 LAB_MODE가 없었다. 하지만 다음 명령으로 이미지 이력을 읽었을 때는 값이 남아 있었다. 출력 중 해당 RUN 행만 발췌했다. docker image history (새 탭에서 열림)
docker history --no-trunc --format '{{.CreatedBy}}' $argImage
RUN |1 LAB_MODE=build-blue /bin/sh -c printf 'build LAB_MODE=<%s>\n' "$LAB_MODE" # buildkit
따라서 Config.Env에 없는 것과 이미지에 값의 흔적이 없는 것은 다르다. 이번에는 RUN에서 사용한 합성 문자열이 이력에 남는 것을 직접 확인했다. 모든 빌드 인자가 항상 같은 행에 남는다고 일반화할 수는 없지만, 컨테이너 환경에 없다는 이유만으로 비밀 보호를 판단해서는 안 된다.
토큰이 실행 중에 안 보인다고 ARG를 전부 ENV로 옮기는 것은 해법이 아니다. 그러면 이미지의 기본 환경 설정에 토큰을 남긴다. 빌드 중 인증에만 필요한 값은 Docker의 build secrets (새 탭에서 열림)에 설명된 --secret과 RUN --mount=type=secret으로 전달할 수 있다. 이것은 문서에 근거한 대안이며, 이번 실험에서 비밀 마운트를 실행한 것은 아니다. 실행 중 필요한 비밀값은 배포 환경의 비밀 관리 경로에서 주입해야 한다. 위의 -e 예제는 공개 가능한 합성 설정값으로 동작을 비교한 것이며 비밀 저장 수단을 검증한 결과가 아니다.
자기 앱에서는 해당 값이 필요한 시점부터 정한다. 의존성 버전처럼 빌드에만 필요한 입력이면 ARG, 이미지 사용자에게 제공할 공개 기본값이면 ENV, 같은 이미지를 환경마다 다르게 실행할 공개 설정이면 실행 시 -e가 맞다. 이미 Config.Env와 실행 프로세스 양쪽에 올바른 값이 있는데 앱만 다른 값을 읽는다면 앱의 변수 이름·설정 우선순위를 확인할 차례다.
이 재현이 확인한 범위는 단일 단계 이미지의 빌드 입력, 이미지 기본 환경 설정, 컨테이너 시작 시 덮어쓰기다. Compose의 치환, 다른 빌드 단계로의 변수 상속, 앱 프레임워크의 설정 해석은 비교하지 않았다. Alpine 태그는 갱신될 수 있으므로 재현 시 버전은 달라질 수 있다. 각 컨테이너는 --rm으로 종료 후 제거된다. 실습을 마친 뒤 이미지를 정리하려면 이번 세션에서 만든 두 태그만 대상으로 docker image rm --no-prune $argImage $envImage를 실행한다.