[개발 깨알 상식_Tips] / Docker Compose에서 env_file 값이 빈 문자열로 덮이는 원인.md

Docker Compose에서 env_file 값이 빈 문자열로 덮이는 원인

조회

2026년 7월 18일 | 개발 깨알 상식_Tips


컨테이너 안의 API_URL이 비어 있었는데 service.env에는 분명 주소가 들어 있었다. 파일을 다시 읽고 컨테이너도 재생성했지만 결과는 같았다. 원인은 파일 로딩 실패가 아니라 Compose 서비스의 environmentenv_file보다 우선한다는 병합 규칙이었다.

더 헷갈린 부분은 프로젝트 루트의 .env와 서비스의 env_file이었다. 둘 다 같은 모양의 키-값 파일이라 한 덩어리처럼 생각하기 쉽지만, 하나는 Compose 파일의 자리 표시자를 해석하는 입력이고 다른 하나는 컨테이너 환경을 채우는 입력이다. 앞 단계에서 만들어진 빈 문자열이 뒤 단계의 정상값을 덮을 수 있었다.

Docker Compose environment와 env_file 우선순위 병합 도식

Figure 1. 보간 결과로 생긴 빈 environment 값도 서비스 env_file의 정상값보다 높은 우선순위로 컨테이너에 들어간다.

.env와 env_file은 들어가는 단계가 달랐다

프로젝트 .env나 셸 환경은 Compose가 compose.yaml 안의 자리 표시자를 해석할 때 쓰인다. 반면 서비스 아래의 env_file은 컨테이너가 받을 환경 변수를 공급한다. 서비스의 env_fileAPI_URL이 있다고 해서 Compose 파일 보간에 그 값을 자동으로 가져다 쓰는 것은 아니다.

내 설정은 개념적으로 아래와 같았다. service.env에는 정상 주소가 있었지만, 서비스의 environment에도 같은 키가 빈 값으로 남아 있었다.

# service.env
API_URL=https://api.internal.example

# compose.yaml
services:
  worker:
    env_file:
      - service.env
    environment:
      API_URL: ""

이 구성에서 최종값은 주소가 아니라 빈 문자열이다. Docker Compose 문서도 서비스에 env_fileenvironment가 함께 있으면 environment가 우선하며, 그 값이 비어 있거나 정의되지 않은 경우에도 이 규칙이 유지된다고 설명한다. 빈 값은 “설정 없음”이 아니라 높은 층에서 명시된 값으로 취급될 수 있다.

자리 표시자 누락이 빈 override로 바뀌었다

실제 파일에는 처음부터 빈 문자열을 적어 둔 것이 아니었다. environment에서 API_URL 자리 표시자를 사용했고, Compose를 실행한 셸과 프로젝트 .env에는 그 키가 없었다. 보간 단계에서 값이 비어 최종 서비스 모델에 빈 API_URL이 남았고, 그 모델이 service.env의 주소를 덮었다.

여기서 서비스의 env_file만 열어 보면 원인이 보이지 않는다. 파일 내용도 맞고 경로도 맞으니 네트워크나 애플리케이션 설정 로더를 의심하게 된다. 나도 처음에는 HTTP 클라이언트의 기본 URL 처리부터 봤다. 하지만 애플리케이션에 도달하기 전에 컨테이너 환경 자체가 이미 빈 값이었다.

같은 이름의 .env 파일이 여러 위치에 있는 구성도 주의해야 한다. 실행 디렉터리, 프로젝트 디렉터리, 명시한 CLI --env-file에 따라 보간 입력이 달라질 수 있다. 반면 서비스의 env_file 경로는 Compose 파일을 기준으로 컨테이너 입력을 만든다. 보간용 입력과 런타임 입력을 따로 기록해야 재현 위치가 고정된다.

docker compose config에서 먼저 빈 값을 찾았다

나는 애플리케이션 로그를 더 늘리기 전에 docker compose config로 해석이 끝난 서비스 모델을 확인한다. 원본 YAML과 달리 이 출력에는 보간과 병합 결과가 드러난다. 여기서 environmentAPI_URL이 빈 값이면 문제는 컨테이너 시작 이후가 아니라 Compose 해석 단계에 있다.

docker compose config

docker compose config --environment

docker compose run --rm --no-deps worker printenv API_URL

config --environment는 보간에 사용된 환경을 확인하는 데 도움이 되고, 일반 config는 최종 서비스 모델을 보여 준다. 마지막 명령은 실제 컨테이너가 받은 값을 확인한다. 이미지에 printenv가 없다면 애플리케이션의 안전한 설정 진단 명령으로 바꾸되, 토큰이나 비밀번호 원문을 로그에 찍지는 않는다.

docker compose config 출력도 공유 전에 한 번 더 본다. 해석된 환경 변수와 이미지 설정이 함께 노출될 수 있어, 그대로 이슈나 CI artifact에 붙이면 디버깅 자료가 새 유출 경로가 된다. 나는 필요한 서비스와 키의 상태만 추려 missing·empty·present로 기록하고, 실제 값은 로컬 확인 뒤 버린다.

세 출력을 나누면 “어디서 값이 사라졌나”를 빠르게 좁힐 수 있다. 보간 환경에 없음, 최종 모델에서 빈 값, 컨테이너에서도 빈 값이면 우선순위 문제다. 최종 모델에는 주소가 있는데 컨테이너만 다르면 CLI -e, 별도 override 파일, 실행 래퍼처럼 더 높은 층을 확인한다.

한 키를 두 층에서 관리하지 않게 고쳤다

수정 방법은 빈 값에 기본 주소를 넣는 것이 아니었다. 같은 키를 environmentenv_file 양쪽에서 관리하던 구조를 먼저 없앴다. 서비스별 고정 런타임 값이면 env_file 한곳에 두고 environment에서 같은 키를 제거했다. 배포마다 셸에서 반드시 주입할 값이면 반대로 environment만 남기고 누락 시 Compose가 조용히 빈 값으로 진행하지 않도록 시작 전 검증을 붙였다.

빈 문자열이 기능 비활성화를 뜻하는 설정도 있다. 그런 키는 빈 값이 실수인지 정책인지 구분할 수 있도록 변수 이름과 문서에 의미를 적고, 테스트도 missing·empty·present 세 상태로 나눈다. fallback보다 소유 층을 하나로 고정하는 편이 재발을 줄였다.

  • 프로젝트 .env와 서비스 env_file의 역할을 같은 것으로 취급하지 않는다.
  • 동일 키가 environmentenv_file에 동시에 있는지 확인한다.
  • docker compose config에서 빈 문자열이 생긴 최초 단계를 찾는다.
  • 실제 컨테이너 값은 별도로 확인하되 secret 원문은 출력하지 않는다.
  • 필수 URL·리전·버킷은 빈 값에서 fail-fast하고 기본값으로 숨기지 않는다.

이 문제를 겪고 나서 환경 변수 파일을 “나중에 읽힌 파일이 이긴다”는 식으로 설명하지 않게 됐다. Compose는 보간 입력, 서비스 병합, 컨테이너 실행 override를 서로 다른 층에서 처리한다. 파일을 더 들여다보기보다 해석된 모델과 실제 컨테이너 값을 차례로 비교하면, 정상값이 빈 문자열로 바뀐 위치를 훨씬 빨리 찾을 수 있다.

댓글

홈으로 돌아가기

검색 결과

"" 검색 결과입니다.