빌드노트
ENV가 있어도 JSON CMD의 $APP_MODE는 그대로 전달된다
Docker의 JSON CMD는 셸을 자동 실행하지 않아 $APP_MODE를 인자로 그대로 전달한다. inspect로 환경과 인자를 나눠 보고, -e 변경·환경 직접 조회·명시적 셸 실행의 실제 출력을 비교한다.
Dockerfile에 ENV APP_MODE=image가 있는데 CMD ["/bin/echo", "$APP_MODE"]가 image 대신 $APP_MODE를 출력한다. 환경변수가 누락된 것처럼 보여도, 먼저 실행 인자를 확인해야 한다. JSON 배열로 쓰는 exec-form CMD는 셸을 자동으로 실행하지 않으므로 $APP_MODE를 환경값으로 확장하지 않는다. 앱이 환경을 직접 읽게 하거나, 인자로 변환해야 할 때 명시적으로 셸을 호출한다. Docker의 exec form과 변수 치환 설명 (새 탭에서 열림)
환경값과 인자는 별도로 확인한다
문제를 낸 컨테이너가 남아 있다면 다음 PowerShell 7 명령으로 시작 설정과 APP_MODE만 조회한다. Docker CLI가 해당 엔진에 연결되어 있어야 하며, my-container는 자신의 컨테이너 이름으로 바꾼다. 종료된 컨테이너도 조회할 수 있지만 --rm으로 삭제된 컨테이너는 조회할 수 없다.
$container = 'my-container'
docker container inspect --format 'Path={{json .Path}} Args={{json .Args}}' $container
if ($LASTEXITCODE -ne 0) { throw 'Container lookup failed' }
docker container inspect --format '{{range .Config.Env}}{{println .}}{{end}}' $container |
Where-Object { $_ -like 'APP_MODE=*' }
if ($LASTEXITCODE -ne 0) { throw 'Environment lookup failed' }
다음은 실제 서비스 장애 로그가 아니라, 이 문제를 따로 재현한 컨테이너에 같은 조회를 실행한 결과다.
Path="/bin/echo" Args=["$APP_MODE"]
APP_MODE=image
환경 설정에는 APP_MODE=image가 있고, 실행 파일 /bin/echo에 전달할 인자는 $APP_MODE라는 문자열이다. 환경 목록과 인자 배열은 별도로 전달되므로 두 결과는 모순되지 않는다. 이 조건에서 echo가 받은 인자를 그대로 출력한 것이다. inspect의 출력 템플릿과 JSON 형식 (새 탭에서 열림)
환경 조회가 성공했는데 APP_MODE= 행이 없다면 환경 주입부터 확인한다. 환경값은 맞고 인자만 리터럴이면 CMD의 실행 형식을 확인한다. 단, Path와 Args는 컨테이너의 시작 설정이다. Path가 진입 스크립트라면 그 안에서 인자를 다시 가공할 수 있으므로 이 출력만으로 최종 앱의 인자를 확정할 수는 없다.
-e로 값을 바꿔도 echo의 인자는 그대로다
재현에는 다음 Dockerfile을 사용했다. ENV는 기본 환경을 설정하고, CMD의 두 번째 요소는 실행할 때 넘길 인자다. CMD는 빌드 중에 실행되는 명령이 아니다. Dockerfile의 CMD 설명 (새 탭에서 열림)
FROM alpine:3.22
ENV APP_MODE=image
CMD ["/bin/echo", "$APP_MODE"]
직접 비교하려면 이 내용을 빈 작업 폴더의 Dockerfile로 저장한다. Linux 컨테이너를 실행할 Docker와 로컬에 이미 준비된 alpine:3.22가 필요하다. 아래 태그는 사용하지 않는 실습용 이름으로 정하고, 그 폴더에서 실행한다. 두 번의 기본 CMD 실행 뒤 같은 이미지의 printenv로 환경값을 읽는다.
$image = 'cmd-env-literal-check:local'
docker build --pull=false --network=none -t $image .
if ($LASTEXITCODE -ne 0) { throw 'Build failed' }
docker run --rm --pull=never --network none $image
docker run --rm --pull=never --network none -e APP_MODE=runtime $image
docker run --rm --pull=never --network none -e APP_MODE=runtime $image /bin/busybox printenv APP_MODE
세 실행의 출력은 차례로 $APP_MODE, $APP_MODE, runtime이었다. -e를 준 컨테이너를 따로 남겨 조회했을 때도 APP_MODE=runtime과 Args=["$APP_MODE"]가 함께 있었다. -e는 환경값을 덮어쓰며 CMD 문자열을 해석하는 셸을 추가하지 않는다. 마지막 실행은 CMD를 printenv APP_MODE로 바꾼 것으로, 셸 없이 프로그램이 환경을 조회하면 값이 보인다는 대조다. docker run의 환경변수 옵션 (새 탭에서 열림)
이 비교는 PowerShell 7.6.6, Docker CLI·Linux 엔진 29.8.0, Docker Desktop 4.92.0에서 실행했다. 로컬 alpine:3.22의 내부 버전은 Alpine 3.22.7, BusyBox 1.37.0이었다. 빌드가 실패하면 실행으로 넘어가지 말고 로컬 베이스 이미지와 엔진 연결부터 확인한다. 베이스가 없는 환경에서 --pull=false만으로 모든 빌드 통신이 차단된다고 가정하면 안 된다. 실행 명령의 --pull=never는 실습 이미지가 없을 때 가져오기 대신 실패하게 한다.
앱이 환경을 읽을지, 셸이 인자로 바꿀지 정한다
앱이 환경변수 설정을 지원한다면 APP_MODE를 앱에서 직접 읽도록 하고, CMD에는 실행 파일과 필요한 고정 인자를 둔다. 앞의 printenv처럼 환경을 조회할 주체가 프로그램이면 변수 치환을 위해 셸을 추가할 필요가 없다. 반대로 앱이 명령줄 인자로만 값을 받는다면, 앱을 바꾸거나 시작 단계에서 환경값을 인자로 변환해야 한다.
후자를 echo로 비교하려고 CMD만 다음과 같이 바꾼 별도 이미지를 만들었다. JSON 안의 \"는 셸에 전달할 큰따옴표를 표현한다.
CMD ["/bin/sh", "-c", "exec /bin/echo \"$APP_MODE\""]
이 이미지에서는 기본 실행이 image, -e APP_MODE=runtime을 준 실행이 runtime을 출력했다. JSON 배열이라는 점은 같지만, 이번 실행 파일은 /bin/sh다. 셸이 -c 뒤의 고정 명령 문자열을 해석하면서 환경변수를 확장한다. Docker가 배열의 모든 문자열을 치환하도록 바뀐 것은 아니다. 명시적으로 셸을 호출하는 방식 (새 탭에서 열림)
이 형태를 적용하려면 이미지에 /bin/sh와 대상 실행 파일이 있어야 한다. 명령 문자열에 외부 입력을 이어 붙이지 않는다. 예제의 exec는 셸을 대상 프로그램으로 교체하는 형태지만, 짧게 종료하는 echo 실험으로 장기 실행 앱의 PID 1·종료 신호 처리를 검증한 것은 아니다. Docker의 exec와 PID 1 설명 (새 탭에서 열림)
여기서 확인한 범위는 Linux 컨테이너의 환경 전달과 exec-form 인자의 확장 여부다. 실제 앱의 설정 우선순위, Compose 파일의 변수 치환, Windows 컨테이너 동작은 확인하지 않았다. 환경은 맞는데 $APP_MODE가 그대로 보인다면, 값을 다시 주입하기 전에 그 문자열을 누가 해석하도록 작성했는지 확인한다.