본문 바로가기
빌드노트 검색

빌드노트

Compose command가 컨테이너 값 대신 호스트 환경변수를 쓰는 이유

서비스 environment는 맞는데 command가 호스트 값을 쓰는 경우를 다룬다. 같은 APP_LABEL로 config와 단발 실행을 비교하고, $$로 Compose 보간을 피한 뒤 컨테이너 셸이 값을 확장하도록 고친다. 셸 없는 인자 대조와 config에 남는 $$ 표기도 실제 확인했다.

Compose 서비스의 environment에는 APP_LABEL=container-green을 넣었는데, command가 출력하는 값은 CLI를 실행한 쪽의 host-purple다. 이때는 환경변수를 다시 주입하기 전에 command의 $APP_LABEL을 확인한다. Compose가 먼저 이 문자열을 보간하면, 컨테이너 셸이 시작할 때는 이미 호스트 값이 박힌 명령을 받는다. 컨테이너 환경값을 나중에 확장하려면 $$APP_LABEL로 Compose 보간을 피하고, 그 문자열을 해석할 셸도 명시해야 한다. Compose의 달러 보간 규칙 (새 탭에서 열림)

이미 문제가 난 프로젝트라면 평소 실행하는 것과 같은 -f, --env-file, -p 옵션으로 docker compose config --format json을 확인한다. 해당 서비스의 environment.APP_LABEL과 command를 나란히 본다. 환경은 맞는데 명령 안에 호스트 값이 들어 있다면, 서비스 환경을 읽기 전인 Compose 단계에서 값이 결정된 것이다. 아래는 이 차이를 합성 값으로 따로 재현한 예제다.

같은 이름을 두 곳에 다르게 지정한다

Linux 컨테이너를 실행할 Docker, Compose와 로컬에 준비된 alpine:3.22가 필요하다. 새 빈 폴더의 compose.yaml에 다음 내용을 저장한다. 이 예제는 포트나 볼륨을 사용하지 않는다. pull_policy: never는 이미지가 없을 때 다운로드 대신 실패하게 한다. 이미지 가져오기 정책 (새 탭에서 열림)

services:
  probe:
    image: alpine:3.22
    pull_policy: never
    network_mode: none
    environment:
      APP_LABEL: container-green
    command: ["/bin/sh", "-c", 'printf "%s\n" "$APP_LABEL"']

새 PowerShell 세션에서 그 폴더로 이동한다. 아래 프로젝트 이름은 사용 중이지 않은 실습용 이름으로 정한다. APP_LABEL은 Compose CLI를 실행하는 쪽에는 host-purple, 서비스에는 container-green으로 달리 두었다. 먼저 Compose가 만든 명령과 서비스 환경만 조회한다.

$project = 'dollar-label-check'
$env:APP_LABEL = 'host-purple'
$cfg = docker compose -p $project -f compose.yaml config --format json | ConvertFrom-Json
if ($LASTEXITCODE -ne 0) { throw 'Compose 설정 해석 실패' }
$cfg.services.probe.command
'service APP_LABEL=' + $cfg.services.probe.environment.APP_LABEL

실제 출력은 다음과 같았다.

/bin/sh
-c
printf "%s\n" "host-purple"
service APP_LABEL=container-green

같은 서비스 안에서도 environment와 command의 값이 다르다. YAML에서 셸 명령을 작은따옴표로 감쌌지만 Compose 보간은 막지 못했다. 이 파일의 $APP_LABEL은 Compose CLI의 환경에서 가져온 host-purple로 바뀌었다. config는 파일 병합과 변수 해석을 거친 모델을 보여 주므로, 컨테이너 실행 전 이 차이를 찾는 데 쓸 수 있다. config의 역할과 출력 형식 (새 탭에서 열림)

이번에는 같은 서비스의 기본 명령을 실행한 뒤, 명령만 printenv로 바꿔 컨테이너 환경을 직접 읽는다. 각 실행은 새 일회성 컨테이너다.

docker compose -p $project -f compose.yaml run --rm --no-deps --pull never -T probe
docker compose -p $project -f compose.yaml run --rm --no-deps --pull never -T probe /bin/busybox printenv APP_LABEL

두 출력은 차례로 host-purple, container-green이었다. 첫 명령의 셸은 printf "%s\n" "host-purple"를 받았으므로 컨테이너의 APP_LABEL을 읽을 자리가 없다. 두 번째 실행은 프로그램이 환경을 직접 읽었고, 서비스에 지정한 값을 확인했다. 환경 주입 실패로 보고 값을 다시 넣을 상황은 아니다. 뒤에 명령을 붙이면 서비스의 command를 바꾸는 동작은 compose run 문서 (새 탭에서 열림)에서도 확인할 수 있다.

이 비교는 명령이 정상 종료했을 때의 출력이다. 이미지가 없거나 엔진 연결에 실패했다면 오류와 종료 코드부터 확인한다. printenv에도 값이 없으면 서비스의 변수 이름과 환경 설정을 먼저 확인하고, command의 달러만 바꿔 해결됐다고 판단하지 않는다.

컨테이너 셸까지 넘길 변수에만 달러를 하나 더 쓴다

같은 파일에서 command 한 줄만 다음처럼 바꾼다. environment는 그대로 둔다.

    command: ["/bin/sh", "-c", 'printf "%s\n" "$$APP_LABEL"']

앞의 config 조회와 기본 명령 실행을 다시 했다. Docker Compose v5.5.1에서는 command의 세 번째 요소가 printf "%s\n" "$$APP_LABEL"로 표시됐고, 실제 실행 출력은 container-green이었다. 이 버전의 config 출력에는 이스케이프용 $$가 남는다. 이 표현을 컨테이너가 받는 최종 인자와 그대로 같다고 읽으면 안 된다.

별도로 실행 후 남긴 컨테이너의 Config.Cmd도 조회했다. 셸에 전달된 명령은 printf "%s\n" "$APP_LABEL"이었다. Compose가 호스트 값으로 바꾸지 않고 달러를 넘겼고, 컨테이너의 /bin/sh -c가 자신의 환경에서 container-green을 읽은 것이다. $$를 셸까지 두 개 전달해 셸 문법으로 처리한 결과가 아니다.

호스트 쪽 APP_LABEL을 뺀 추가 비교에서도 차이가 났다. 단일 달러에서는 Compose의 미설정 경고가 나왔고 출력은 빈 줄이었다. 이중 달러에서는 그 경고 없이 container-green이 나왔다. 호스트 변수가 없다는 이유로 서비스의 같은 이름을 보간 입력으로 대신 찾지는 않았다. 이 경우에도 고칠 곳은 서비스의 값보다 command가 어느 단계에서 값을 읽도록 쓰였는지다.

반대로 배포 도구가 Compose를 실행할 때 정한 값을 명령에 넣으려는 의도라면 단일 달러가 맞다. 모든 달러를 일괄 치환하기보다, 이 변수를 CLI 쪽에서 결정할지 컨테이너가 시작할 때 결정할지 고른다.

이중 달러만으로 셸이 생기지는 않는다

같은 파일의 command를 이번에는 셸 없는 인자 배열로 바꿔 비교했다.

    command: ["/bin/echo", "$$APP_LABEL"]

기본 명령을 실행하니 container-green 대신 $APP_LABEL이 그대로 출력됐다. 실행 후 조회한 인자도 ["/bin/echo", "$APP_LABEL"]이었다. 이중 달러는 Compose 보간을 막았지만, echo는 받은 문자열을 환경값으로 확장하지 않았다. 이 줄을 단일 달러로 바꾸면 출력은 다시 host-purple가 됐다. 셸이 없어도 앞선 Compose 보간은 일어난다.

컨테이너 환경값을 명령줄 인자로 변환해야 한다면 위의 명시적 /bin/sh -c와 $$APP_LABEL을 함께 쓴다. 이때 이미지에 /bin/sh가 있어야 한다. 앱이 환경변수를 직접 읽는다면, 앞의 printenv 대조처럼 달러 문자열을 인자로 만들 필요가 없다. Compose의 command는 셸을 자동 실행하지 않는다는 조건을 command 공식 설명 (새 탭에서 열림)에서 확인할 수 있다.

확인 환경은 Windows의 PowerShell 7.6.6, Docker CLI·Linux 엔진 29.8.0, Compose v5.5.1이었다. 사용한 로컬 Alpine은 3.22.7, BusyBox는 1.37.0이며 이미지의 기본 진입점이 없는 조건이다. 본문에 제시한 단발 실행 명령은 --rm으로 컨테이너를 정리한다. 실습용 PowerShell 세션을 닫으면 그 세션에 넣은 호스트 변수도 남지 않는다.

이 실험은 Compose 파일 보간과 Linux 컨테이너 셸 확장까지만 확인했다. 기존 실행 컨테이너의 재생성, 진입 스크립트가 인자를 다시 바꾸는 이미지, Windows 컨테이너는 확인하지 않았다. config의 명령이 의도한 형태인데 앱만 다른 값을 쓴다면, 다음에는 이미지의 진입 스크립트와 앱의 환경 조회를 확인한다.

← 전체 글 보기