컨테이너 이미지는 환경을 가리지 않고 동일하게 동작해야 한다. 개발·스테이징·프로덕션마다 달라지는 DB 접속 정보나 API 키를 이미지 안에 굳히면, 배포할 때마다 이미지를 다시 빌드해야 하는 악순환이 생긴다. Docker Compose는 환경 변수를 세 가지 방식으로 컨테이너에 주입할 수 있어, 이미지를 그대로 두고 설정만 교체하는 구조를 만들 수 있다.
┌──────────────────────────────────────────┐
│ docker-compose.yml │
│ │
│ environment: env_file: │
│ KEY: value ──┐ - .env.prod ──┐ │
│ KEY: ${VAR} ──┤ │ │
│ ▼ ▼ │
│ ┌─────────────────────────┐ │
│ │ 컨테이너 프로세스 │ │
│ └─────────────────────────┘ │
│ ▲ │
│ .env (자동 로드) ──┘ │
└──────────────────────────────────────────┘
environment 블록은 키-값 쌍을 compose 파일 안에 직접 적는 방식이다.
services:
api:
image: my-api:latest
environment:
NODE_ENV: production
PORT: 3000
DATABASE_URL: postgres://user:pass@db:5432/mydb
값을 생략하면 호스트 셸의 동명 변수를 그대로 상속한다.
environment:
DATABASE_URL # 호스트에서 $DATABASE_URL 을 읽어 전달
environment — docker run -e KEY=value 와 동일한 효과. 컨테이너 내부의 /proc/1/environ 에 기록된다.
비밀값을 compose 파일과 분리하려면 env_file 지시어를 사용한다.
services:
api:
image: my-api:latest
env_file:
- .env.common
- .env.prod
각 파일은 KEY=value 형식의 줄로 구성된다. 여러 파일을 지정하면 아래쪽 파일이 우선순위를 가진다.
# .env.prod
DATABASE_URL=postgres://prod-user:s3cr3t@prod-db:5432/mydb
REDIS_URL=redis://prod-redis:6379
env_file 에 지정한 파일은 .gitignore 에 반드시 추가해야 한다.
Compose는 docker-compose.yml 과 같은 디렉터리의 .env 파일을 자동으로 읽어 compose 파일 내부의 ${VAR} 구문을 치환하는 변수 치환 을 수행한다.
# .env
IMAGE_TAG=1.4.2
DB_PORT=5432
services:
api:
image: my-api:${IMAGE_TAG}
ports:
- "${DB_PORT}:5432"
변수 치환(Variable Substitution) — 컨테이너 안에 변수를 넣는 것이 아니라, compose 파일 자체의 텍스트를 치환하는 단계다. environment 블록과 목적이 다르다.
기본값을 설정하려면 ${VAR:-default} 문법을 쓴다.
image: my-api:${IMAGE_TAG:-latest}
| 방식 | 버전 관리 포함 여부 | 용도 |
|---|---|---|
environment (하드코딩) | 포함 가능 | 비밀 아닌 설정값 |
.env 자동 로드 | 제외 (.gitignore) | 로컬 개발 편의 |
env_file (.env.prod) | 제외 | 스테이징·프로덕션 비밀값 |
| Docker Secrets | 해당 없음 | Swarm/프로덕션 민감 데이터 |
실무에서는 .env.example 파일을 버전 관리에 포함시켜 팀원에게 필요한 키 목록을 알려주고, 실제 값이 담긴 .env 는 각자 로컬에서 채우는 패턴이 일반적이다.
# .env.example (버전 관리 O)
DATABASE_URL=
REDIS_URL=
API_SECRET=
세 방식을 조합하면 이미지를 바꾸지 않고도 환경별 설정을 완전히 분리할 수 있다. environment 는 비밀이 아닌 값, env_file 은 비밀값, .env 는 compose 파일 자체의 치환 변수로 역할을 나누는 것이 핵심이다.