빈 값이 만들어지는 두 단계를 먼저 나눈다
Compose에서 환경 변수 문제가 보이면 .env 파일 하나만 열어 보고 끝내기 쉽습니다. 하지만 Compose 파일의 ${VARIABLE}을 문자열로 바꾸는 보간 단계와, 완성된 서비스 정의에서 컨테이너 환경을 만드는 단계는 서로 다릅니다. 첫 단계에서 이미 빈 문자열이 들어갔다면 컨테이너 안에서 secret 파일이나 애플리케이션 기본값을 확인해도 원인을 찾기 어렵습니다.
Docker 문서가 설명하는 보간 입력 순서는 현재 shell이 먼저입니다. --env-file을 지정하지 않았으면 현재 작업 디렉터리의 .env를 다음으로 확인하고, 그다음에는 --env-file로 지정한 파일 또는 프로젝트 디렉터리의 .env를 사용합니다. 이 선택 규칙은 컨테이너 환경의 전체 우선순위 표와 혼동하면 안 됩니다. 진단에서는 먼저 Compose가 어떤 입력을 읽었고 어떤 모델을 Engine에 보낼지 고정한 다음, 마지막에 컨테이너가 받은 값을 확인합니다.
값을 출력하지 않고 상태를 남기는 점검표
| 계층 | 확인 대상 | 판단 기준 |
|---|---|---|
| 실행 shell | 변수 존재 여부와 빈 문자열 여부 | 현재 shell 값이 파일 값을 가리는지 확인합니다. |
| 보간 입력 | 작업 디렉터리, --env-file, 프로젝트 경로의 .env | 명령 옵션과 실행 위치에 따라 의도한 파일이 선택됐는지 봅니다. |
| 렌더링 모델 | docker compose config 결과 | Engine에 전달될 서비스 환경이 비어 있는지 확인합니다. |
| 컨테이너 | 실행 중 프로세스가 받은 환경 | 설정 파일을 고친 뒤 컨테이너를 재생성했는지 구분합니다. |
| 애플리케이션 | secret 로더와 기본값 처리 | Compose 값이 정상일 때만 애플리케이션 단계로 내려갑니다. |
config --environment는 변수의 값까지, config --variables는 기본값까지 출력할 수 있습니다. 둘 다 비밀정보를 가리는 명령이 아닙니다. 전체 모델을 파일에 저장하지 않고 아래처럼 관심 변수의 상태만 추립니다. Bash와 Python 3가 필요하며, app과 DB_PASSWORD는 실제 서비스와 변수 이름으로 바꿉니다.
python3 -c 'import os; v=os.environ.get("DB_PASSWORD"); print("shell:", "unset" if v is None else "set" if v else "empty")'
set -o pipefail
docker compose config --format json |
python3 -c 'import json,sys; e=json.load(sys.stdin)["services"]["app"].get("environment",{}); v=e.get("DB_PASSWORD"); print("model:", "unset" if v is None else "set" if v else "empty")'
첫 줄은 Compose를 실행하는 shell의 export된 환경만 확인합니다. model: empty라면 보간 이후에도 빈 문자열이라는 뜻입니다. JSON은 파이프를 통과하지만 전체 값은 화면이나 파일로 내보내지 않습니다. 오류가 나면 출력 수집을 멈추고 서비스 이름과 Compose 문법부터 확인하세요. 디버그 추적이나 전체 모델 로그를 켜면 이 보호가 사라집니다.
모델이 정상이어도 이미 실행 중인 컨테이너는 그대로입니다. 같은 프로젝트 이름을 여러 경로에서 쓴다면 작업 디렉터리, -f, --env-file, project name을 함께 기록해 다른 배포를 재생성하지 않도록 합니다.
별도 환경에서 확인한 빈 값의 전달 결과
2026-09-06 KST에 Linux x86_64, Compose 5.5.1에서 아래 최소 모델을 검사했습니다. .env에는 실제 비밀번호 대신 DB_PASSWORD=synthetic-file-value를 넣었습니다. Docker Engine이나 애플리케이션을 띄운 실험이 아니라 config 보간만 검증한 결과입니다.
services:
app:
image: busybox:1.37
environment:
DB_PASSWORD: ${DB_PASSWORD}
shell variable absent + .env set -> model: set (exit 0)
shell variable exported empty -> model: empty (exit 0)
required expression + empty shell -> config rejected (exit 1)
--environment -> synthetic value appeared in output
마지막 실패 검사는 모델을 ${DB_PASSWORD:?DB_PASSWORD is required}로 바꿔 실행했습니다. 오류에는 required variable DB_PASSWORD is missing a value가 포함됐습니다. 파일이 있는데도 값이 빈 문제와, 빈 입력을 컨테이너 생성 전에 차단하는 방법을 같은 입력에서 구분할 수 있었습니다. 이 결과는 이미지 내부 secret 로더의 우선순위까지 입증하지 않습니다.
파일은 바뀌었는데 기존 컨테이너는 그대로인 경우
보간 결과가 정상이라는 확인에서 한 단계 더 내려가 보겠습니다. 2026-09-13 KST, 운영 서버와 분리된 Linux 실험 환경에서 Docker Engine 28.3.3, Compose 2.39.4, BusyBox 1.37.0으로 컨테이너를 실제 실행했습니다. 앞의 9월 6일 보간 실험과는 별도 기록입니다. 네트워크를 끄고 볼륨을 연결하지 않았으며, 출력한 값은 모두 DEMO_VALUE에 넣은 더미 문자열입니다.
먼저 비어 있는 전용 디렉터리에 아래 compose.yaml을 저장합니다. 운영 프로젝트에서 실행하지 마세요. sleep은 이 실험의 컨테이너를 잠시 유지하기 위한 장치이며, 실제 서비스가 종료되는 문제의 해결책이 아닙니다.
services:
app:
image: busybox:1.37.0
network_mode: none
command: ["sh", "-c", "exec sleep 600"]
environment:
DEMO_VALUE: ${DEMO_VALUE}
다음은 실제 명령 기록을 Bash에서 재현할 수 있도록 일반 Docker CLI 표기로 정리한 것입니다. 실험에서는 Python 실행기가 전용 Docker socket을 지정해 같은 하위 명령을 호출했습니다. 전용 프로젝트 이름을 고정하고 shell의 같은 이름 변수를 제거합니다. printenv와 전체 모델 출력은 더미 값만 있는 이 디렉터리에서만 사용합니다. 실제 비밀번호를 점검할 때는 앞 절의 상태 필터를 사용하세요.
unset DEMO_VALUE
printf 'DEMO_VALUE=from-file-v1\n' > .env
docker compose -p nole-env-20260913 up -d app
docker compose -p nole-env-20260913 ps -q app
docker compose -p nole-env-20260913 exec -T app printenv DEMO_VALUE
printf 'DEMO_VALUE=from-file-v2\n' > .env
docker compose -p nole-env-20260913 config --format json
docker compose -p nole-env-20260913 restart app
docker compose -p nole-env-20260913 ps -q app
docker compose -p nole-env-20260913 exec -T app printenv DEMO_VALUE
docker compose -p nole-env-20260913 run --rm --no-deps app printenv DEMO_VALUE
docker compose -p nole-env-20260913 exec -T app printenv DEMO_VALUE
docker compose -p nole-env-20260913 up -d --no-deps --force-recreate app
docker compose -p nole-env-20260913 ps -q app
docker compose -p nole-env-20260913 exec -T app printenv DEMO_VALUE
docker compose -p nole-env-20260913 down
| 확인 시점 | 실제 관찰 | 판단 |
|---|---|---|
| 처음 생성 | ID 1dd2fc162a31…, from-file-v1 | 첫 파일 값을 받은 컨테이너 |
| 파일만 v2로 수정 | 렌더링 모델은 from-file-v2 | 다음 생성에 쓸 모델만 바뀜 |
| restart 후 exec | ID 동일, from-file-v1 | 재시작은 환경을 교체하지 않음 |
| run –rm | 일회성 컨테이너는 from-file-v2 | 새로 만든 컨테이너의 결과 |
| 그 직후 기존 app의 exec | from-file-v1 | run 성공이 기존 app 갱신을 뜻하지 않음 |
| force-recreate 후 exec | ID d22c707ca6e7…, from-file-v2 | 대상 컨테이너가 교체되고 새 값을 받음 |
위 실행 명령의 종료 코드는 모두 0이었습니다. 특히 restart가 성공했어도 값은 v1이었습니다. 컨테이너 ID와 exec 결과를 함께 보지 않으면 이 차이를 놓칠 수 있습니다. 같은 실험 모델에서 shell에 빈 값을 전달하자 모델도 빈 문자열이 됐고, 필수 표현식 ${DEMO_VALUE:?DEMO_VALUE is required}으로 바꾸면 config --quiet가 종료 코드 1과 required variable DEMO_VALUE is missing a value로 거부했습니다.
이 결과가 확인한 것은 Compose가 설정을 컨테이너에 전달하는 경계입니다. 데이터베이스 연결, 비밀번호 교체, 애플리케이션의 secret 파일 우선순위는 실험하지 않았습니다. 여러 replica가 있으면 각각의 컨테이너를 확인해야 하며, shell이 없는 이미지에는 이 예제의 sh·printenv를 그대로 적용할 수 없습니다. 원래 서비스를 다시 만들지 않고 관찰만 해야 할 상황인지도 먼저 판단하세요.
빈 값이 확인됐을 때 원인을 가르는 순서
- 현재 shell을 확인합니다. 변수는 존재하지만 길이가 없는 상태도 명시적 값입니다. CI나 예약 작업이 빈 변수를 export했다면 프로젝트의
.env보다 먼저 사용될 수 있습니다. - Compose 프로젝트 경로를 확인합니다. 다른 디렉터리에서 실행하거나 여러
-f파일을 사용하면 예상한.env와 다른 파일이 선택될 수 있습니다. 실행 명령과 작업 디렉터리를 함께 기록합니다. - 렌더링 결과를 확인합니다. 원본 YAML이 아니라
docker compose config결과에서 대상 서비스의environment가 어떻게 완성됐는지 봅니다. - 필수 변수는 조용히 비우지 않게 바꿉니다. 빈 값이 유효하지 않은 항목은
${DB_PASSWORD:?DB_PASSWORD is required}처럼 필수 보간 문법을 사용하면 컨테이너 생성 전에 실패시킬 수 있습니다. - 컨테이너를 재생성합니다. Compose 파일이나 환경 변수를 고친 뒤 단순
restart만 하면 변경된 환경이 반영되지 않습니다. Docker 문서도 restart가 구성 변경을 적용하지 않는다고 명시합니다.
재생성과 검증
먼저 현재 서비스 정의와 볼륨 경로를 백업하고, 영향받은 서비스만 재생성합니다. 데이터베이스 같은 의존 서비스를 불필요하게 내렸다 올리지 않도록 서비스 이름을 명시합니다.
docker compose config --quiet
docker compose up -d --no-deps --force-recreate app
docker compose ps -a app
docker compose exec -T app sh -c 'if [ -n "$DB_PASSWORD" ]; then echo set; else echo empty; exit 1; fi'
exec는 현재 실행 중인 서비스 컨테이너를 검사합니다. run --rm은 새 일회성 컨테이너를 만들어 기존 컨테이너에 변경이 반영됐다는 증거가 되지 않습니다. 마지막 명령은 이미지에 sh가 있어야 하며 빈 값이면 종료 코드 1을 반환합니다. shell이 없는 이미지에서는 애플리케이션이 제공하는 값 비노출 상태 조회를 사용하고, 전체 docker inspect 환경 배열을 출력하지 않습니다.
이 검사는 컨테이너 환경의 존재 여부입니다. PID 1이 시작 후 값을 바꾸거나 애플리케이션이 별도 secret 파일을 우선하면 결과가 다를 수 있으므로, 마지막에는 실제 연결 성공과 애플리케이션 오류 로그도 확인합니다.
되돌리기 기준
재생성 뒤 애플리케이션이 시작되지 않으면 먼저 수정 전 Compose 파일과 env 파일로 되돌리고 같은 서비스만 다시 재생성합니다. secret 저장 방식을 바꿨다면 기존 secret과 새 환경 변수를 동시에 유지하지 않습니다. 두 경로가 함께 남아 있으면 어느 값이 실제로 사용됐는지 다시 불명확해집니다.
이 절차는 빈 값의 전달 경로를 확인하는 방법이지, 모든 애플리케이션의 secret 우선순위를 보장하지 않습니다. Compose 렌더링 결과가 정상인데 애플리케이션이 빈 값으로 해석한다면 그때부터는 이미지 entrypoint와 애플리케이션 설정 로더의 문서를 확인해야 합니다.
조사 기록에는 값 자체 대신 변수 이름, 선택된 입력 파일, 재생성한 서비스, 검증 결과만 남깁니다. 이 방식이면 비밀정보를 노출하지 않고도 다음 배포와 결과를 비교할 수 있습니다.
같이 확인할 글
공식 문서 및 확인 기준
- Docker Compose exec
- Docker Compose variable interpolation – shell, env 파일, 프로젝트 디렉터리와 보간 확인 명령
- Docker Compose environment precedence – 컨테이너 환경 값의 우선순위
- docker compose config reference – 실제 적용 모델, 변수, 보간 환경 출력
- docker compose restart reference – restart가 변경된 환경과 구성을 적용하지 않는다는 제한
이 글에 남아 있던 이전 검증 기록
검증 수준: 공식 문서 대조
Compose 보간과 컨테이너 환경의 경계를 Docker 공식 문서와 대조했습니다. 특정 애플리케이션 이미지는 재현하지 않았으므로 이미지 내부의 secret 로더 동작은 범위에서 제외했습니다.