환경 변수는 Docker Compose에서 컨테이너를 구성하는 주요 메커니즘입니다. 하지만 4가지 다른 전달 방법이 있으며, 각각 다른 동작, 보안 영향, 주의사항이 있습니다. 이 가이드는 간단한 .env 파일부터 프로덕션급 Docker secrets까지, 멀티 환경 설정의 실제 패턴을 다룹니다.
Docker Compose에서 환경 변수를 전달하는 4가지 방법
Docker Compose는 컨테이너에 환경 변수를 주입하는 4가지 방법을 지원합니다. 각각의 사용 시기를 이해하는 것이 유지보수 가능하고 안전한 설정의 핵심입니다.
1. environment 지시어로 인라인 정의
docker-compose.yml에 키-값 쌍을 직접 정의합니다. 민감하지 않고, 서비스별이며, 거의 변경되지 않는 설정에 가장 적합합니다.
services:
api:
image: node:20-alpine
environment:
NODE_ENV: production
DB_HOST: postgres
DB_PORT: "5432"
LOG_LEVEL: info
# List syntax (also valid)
worker:
image: node:20-alpine
environment:
- NODE_ENV=production
- QUEUE_NAME=tasks
- CONCURRENCY=42. env_file로 외부 파일 로드
하나 이상의 외부 파일에서 변수를 로드합니다. 관련 변수 그룹 관리와 버전 관리에서 시크릿을 제외하는 데 적합합니다.
services:
api:
image: node:20-alpine
env_file:
- .env # Shared variables
- .env.api # Service-specific variables
- .env.local # Local overrides (gitignored)
db:
image: postgres:16
env_file:
- .env
- .env.db3. 셸 환경 변수 패스스루
호스트 셸의 변수를 컨테이너로 전달합니다. CI/CD 파이프라인과 동적 값에 가장 적합합니다.
# In your shell:
export API_KEY=sk-abc123
export DEBUG=true
# docker-compose.yml
services:
api:
image: node:20-alpine
environment:
- API_KEY # Passes host $API_KEY into container
- DEBUG # Passes host $DEBUG into container
- CI # Passes host $CI (empty string if unset)4. docker-compose.yml에서 변수 치환
Compose 파일 값에 호스트 환경 변수를 직접 보간합니다. Compose 파일을 환경 간에 구성 가능하게 만드는 데 적합합니다.
# .env file
POSTGRES_VERSION=16
APP_PORT=3000
REPLICAS=3
# docker-compose.yml
services:
db:
image: postgres:${POSTGRES_VERSION} # Resolved from .env or shell
app:
ports:
- "${APP_PORT}:3000" # Resolved from .env or shell
deploy:
replicas: ${REPLICAS} # Resolved from .env or shell비교 표
| 방법 | 최적 용도 | VCS 포함? | 시크릿 안전? |
|---|---|---|---|
| environment | Non-sensitive defaults | Yes | No |
| env_file | Grouped config, secrets | No (.gitignore) | Partial |
| Shell passthrough | CI/CD, dynamic values | N/A | Partial |
| ${} substitution | Compose file templating | Yes (file), No (.env) | No |
.env 파일: 구문, 배치, 보간
Docker Compose는 docker-compose.yml과 같은 디렉토리에 있는 .env 파일을 자동으로 읽습니다. 설정 외부화의 가장 일반적인 방법입니다.
구문 규칙
# Comments start with #
# Blank lines are ignored
# Simple key=value (no spaces around =)
DB_HOST=localhost
DB_PORT=5432
# Values with spaces need quotes
APP_NAME="My Docker App"
GREETING='Hello World'
# Multi-line values (double quotes only)
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"
# Variable expansion (Compose v2.20+)
BASE_URL=https://api.example.com
HEALTH_URL=${BASE_URL}/health
# Export prefix is ignored (compatible with shell source)
export SECRET_KEY=mysecretkey
# Empty values
EMPTY_VAR=
ALSO_EMPTY=''파일 배치 및 우선순위
Compose는 프로젝트 디렉토리(docker-compose.yml 위치)에서 .env를 찾습니다. --env-file 플래그로 재정의할 수 있습니다:
# Default: reads .env from project directory
docker compose up
# Override with --env-file
docker compose --env-file .env.staging up
# Multiple --env-file flags (Compose v2.24+)
docker compose --env-file .env --env-file .env.local up
# Project directory structure:
my-project/
docker-compose.yml # Compose file
.env # Auto-loaded by Compose
.env.staging # Loaded with --env-file
.env.production # Loaded with --env-filedocker-compose.yml에서의 보간
.env의 변수는 docker-compose.yml에서 치환에 사용할 수 있지만, 자동으로 컨테이너에 주입되지 않습니다:
# .env
TAG=3.2.1
EXTERNAL_PORT=8080
# docker-compose.yml
services:
web:
image: myapp:${TAG} # Uses TAG from .env -> myapp:3.2.1
ports:
- "${EXTERNAL_PORT}:80" # Uses EXTERNAL_PORT from .env -> 8080:80
environment:
- TAG # WRONG! This passes host $TAG, not .env TAG
- TAG=${TAG} # RIGHT! Explicitly interpolate .env value
# Key insight:
# .env -> docker-compose.yml interpolation (automatic)
# .env -> container environment (NOT automatic, use env_file)env_file vs environment: 차이점과 주의사항
이 두 지시어는 비슷해 보이지만 동작이 다릅니다. 차이를 이해하면 미묘한 버그를 방지할 수 있습니다.
| 지시어 | 동작 |
|---|---|
| environment: | Inlined in docker-compose.yml. Visible in version control. Supports variable substitution. Overrides env_file. |
| env_file: | Loaded from external file(s). Can be gitignored. No variable substitution inside the file. Lower priority than environment. |
| .env (auto) | Only for Compose file interpolation (${} syntax). NOT injected into containers. Lowest priority. |
핵심 주의사항
- 1. 우선순위: environment 값은 같은 키의 env_file 값을 덮어씁니다. 이는 오버라이드를 위해 의도적으로 설계되었습니다.
- 2. .env 파일(자동 로드)은 Compose 파일 보간 전용입니다. env_file 또는 environment를 명시적으로 사용하지 않으면 컨테이너에 주입되지 않습니다.
- 3. env_file 경로는 docker-compose.yml 파일 기준 상대 경로이며, 현재 작업 디렉토리 기준이 아닙니다.
- 4. env_file이 없으면 Compose가 오류로 실패합니다. required: false(Compose v2.24+)로 선택적으로 만들 수 있습니다.
# Precedence demonstration:
# .env.shared
DB_HOST=shared-db
DB_PORT=5432
# docker-compose.yml
services:
api:
env_file:
- .env.shared # DB_HOST=shared-db, DB_PORT=5432
environment:
DB_HOST: override-db # Overrides env_file! DB_HOST=override-db
# DB_PORT not listed here # Keeps env_file value: DB_PORT=5432
# Result inside container:
# DB_HOST=override-db (from environment, overrides env_file)
# DB_PORT=5432 (from env_file)
# Optional env_file (Compose v2.24+):
services:
api:
env_file:
- path: .env.local
required: false # Won't fail if file doesn't existDocker Secrets: 프로덕션을 위한 파일 기반 시크릿
Docker secrets는 민감한 데이터에 대해 환경 변수보다 더 안전한 대안을 제공합니다. 시크릿은 환경 변수가 아닌 컨테이너 내부에 파일로 마운트됩니다.
단계 1: 시크릿 파일 생성
# Create secret files (don't commit these!)
echo "SuperSecretPassword123" > db_password.txt
echo "sk-prod-abc123xyz789" > api_key.txt
# Add to .gitignore
echo "*.txt" >> .gitignore
echo "secrets/" >> .gitignore
# Alternative: use a secrets directory
mkdir secrets
echo "SuperSecretPassword123" > secrets/db_password
echo "sk-prod-abc123xyz789" > secrets/api_key단계 2: docker-compose.yml에서 시크릿 정의
# docker-compose.yml
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password # Note: _FILE suffix
secrets:
- db_password # Grant access to this secret
api:
image: myapp:latest
secrets:
- db_password
- api_key
# Secrets available at:
# /run/secrets/db_password
# /run/secrets/api_key
# Top-level secrets definition
secrets:
db_password:
file: ./secrets/db_password # From local file
api_key:
file: ./secrets/api_key # From local file
# Alternative: use environment variable as source
# jwt_secret:
# environment: JWT_SECRET_VALUE # From host env var (Compose v2.23+)단계 3: 애플리케이션에서 시크릿 읽기
# Inside the container, secrets are plain text files:
$ docker compose exec api cat /run/secrets/db_password
SuperSecretPassword123
$ docker compose exec api cat /run/secrets/api_key
sk-prod-abc123xyz789
$ docker compose exec api ls -la /run/secrets/
total 8
-r--r--r-- 1 root root 24 Jan 1 00:00 db_password
-r--r--r-- 1 root root 22 Jan 1 00:00 api_key단계 4: 코드에서 시크릿 처리
// Node.js - Read secret from file
const fs = require('fs');
function getSecret(name) {
const secretPath = `/run/secrets/${name}`;
try {
return fs.readFileSync(secretPath, 'utf8').trim();
} catch (err) {
// Fallback to environment variable (for development)
return process.env[name.toUpperCase()];
}
}
const dbPassword = getSecret('db_password');
const apiKey = getSecret('api_key');# Python - Read secret from file
import os
def get_secret(name: str) -> str:
secret_path = f"/run/secrets/{name}"
try:
with open(secret_path) as f:
return f.read().strip()
except FileNotFoundError:
# Fallback to environment variable
return os.environ.get(name.upper(), "")
db_password = get_secret("db_password")
api_key = get_secret("api_key")환경 변수 대신 Secrets를 선택하는 이유
- 환경 변수는 docker inspect, /proc/environ, 오류 로그, 크래시 덤프를 통해 유출될 수 있습니다
- 시크릿은 파일 기반이며 컨테이너 내 /run/secrets/에서만 접근 가능합니다
- 시크릿은 서비스별 세분화된 접근 제어를 지원합니다
- 많은 데이터베이스와 서비스가 _FILE 접미사의 시크릿 파일을 네이티브 지원합니다 (예: POSTGRES_PASSWORD_FILE)
변수 치환: ${VAR:-default} 구문
Docker Compose는 기본값과 오류 처리가 포함된 셸 스타일 변수 치환을 지원합니다.
| 구문 | 결과 |
|---|---|
| ${VAR} | Value of VAR. Error if unset. |
| ${VAR:-default} | Value of VAR if set, otherwise "default". |
| ${VAR-default} | Value of VAR if set (even if empty), otherwise "default". |
| ${VAR:?error msg} | Value of VAR if set, otherwise exit with "error msg". |
| ${VAR?error msg} | Value of VAR if set (even if empty), otherwise exit with "error msg". |
| ${VAR:+replacement} | "replacement" if VAR is set and non-empty, otherwise empty. |
| ${VAR+replacement} | "replacement" if VAR is set (even if empty), otherwise empty. |
실전 예제
# docker-compose.yml with variable substitution
services:
web:
image: nginx:${NGINX_VERSION:-1.25-alpine} # Default: 1.25-alpine
ports:
- "${WEB_PORT:-80}:80" # Default: 80
- "${SSL_PORT:-443}:443" # Default: 443
volumes:
- ${CONFIG_PATH:-./nginx.conf}:/etc/nginx/nginx.conf:ro
api:
image: ${REGISTRY:-docker.io}/myapp:${TAG:?TAG is required}
# ^ Fails if TAG is not set
environment:
NODE_ENV: ${NODE_ENV:-development}
LOG_LEVEL: ${LOG_LEVEL:-info}
DATABASE_URL: postgres://${DB_USER:-postgres}:${DB_PASS:?DB_PASS required}@db:5432/${DB_NAME:-myapp}
db:
image: postgres:${PG_VERSION:-16}
volumes:
- ${DATA_DIR:-./data}/postgres:/var/lib/postgresql/data멀티 환경 설정: 개발/스테이징/프로덕션 패턴
실제 프로젝트에는 개발, 스테이징, 프로덕션의 다른 설정이 필요합니다. 검증된 패턴을 소개합니다.
패턴 1: 오버라이드 파일 (권장)
기본 docker-compose.yml과 환경별 오버라이드 파일을 사용합니다. Compose가 자동으로 병합합니다.
# File structure:
project/
docker-compose.yml # Base configuration
docker-compose.override.yml # Dev overrides (auto-loaded!)
docker-compose.staging.yml # Staging overrides
docker-compose.prod.yml # Production overrides# docker-compose.yml (base)
services:
api:
image: myapp:${TAG:-latest}
restart: unless-stopped
depends_on:
- db
- redis
db:
image: postgres:16
volumes:
- pg-data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
pg-data:# docker-compose.override.yml (development - auto-loaded)
services:
api:
build: .
volumes:
- .:/app # Hot reload
- /app/node_modules
ports:
- "3000:3000"
- "9229:9229" # Debugger
environment:
NODE_ENV: development
LOG_LEVEL: debug
DB_HOST: db
db:
ports:
- "5432:5432" # Expose DB for local tools
environment:
POSTGRES_PASSWORD: devpassword # OK for dev only# docker-compose.prod.yml (production)
services:
api:
# No build, no volumes, no debug port
ports:
- "3000:3000"
environment:
NODE_ENV: production
LOG_LEVEL: warn
env_file:
- .env.production
secrets:
- db_password
- api_key
deploy:
replicas: 3
resources:
limits:
cpus: '1.0'
memory: 512M
db:
# No exposed ports
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
deploy:
resources:
limits:
cpus: '2.0'
memory: 1G
secrets:
db_password:
file: ./secrets/db_password
api_key:
file: ./secrets/api_key# Usage:
# Development (auto-loads docker-compose.override.yml)
docker compose up
# Staging
docker compose -f docker-compose.yml -f docker-compose.staging.yml up -d
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Validate merged config before deploying
docker compose -f docker-compose.yml -f docker-compose.prod.yml config패턴 2: 환경별 .env 파일
--env-file 플래그로 환경별 다른 변수 세트를 로드합니다.
# .env.development
TAG=latest
APP_PORT=3000
DB_PASSWORD=devpassword
LOG_LEVEL=debug
REPLICAS=1
# .env.staging
TAG=rc-1.2.3
APP_PORT=3000
DB_PASSWORD=staging-secret-pw
LOG_LEVEL=info
REPLICAS=2
# .env.production
TAG=1.2.3
APP_PORT=3000
DB_PASSWORD=prod-ultra-secret-pw
LOG_LEVEL=warn
REPLICAS=3
# Usage:
docker compose --env-file .env.development up # Dev
docker compose --env-file .env.staging up -d # Staging
docker compose --env-file .env.production up -d # Production패턴 3: 선택적 서비스를 위한 Profiles
Compose profiles로 환경별 서비스를 포함하거나 제외합니다.
# docker-compose.yml
services:
api:
image: myapp:${TAG:-latest}
ports:
- "3000:3000"
db:
image: postgres:16
volumes:
- pg-data:/var/lib/postgresql/data
# Dev-only services
adminer:
image: adminer
ports:
- "8080:8080"
profiles:
- dev # Only starts with --profile dev
mailhog:
image: mailhog/mailhog
ports:
- "1025:1025"
- "8025:8025"
profiles:
- dev # Only starts with --profile dev
# Monitoring (staging + production)
prometheus:
image: prom/prometheus
profiles:
- monitoring # Only starts with --profile monitoring
volumes:
pg-data:
# Usage:
docker compose --profile dev up # Includes adminer + mailhog
docker compose --profile monitoring up -d # Includes prometheus
docker compose --profile dev --profile monitoring up # Both profiles보안 체크리스트: 환경 변수에 절대 넣지 말아야 할 것
환경 변수는 편리하지만 민감한 데이터에는 본질적으로 안전하지 않습니다. 다음 규칙을 따르세요:
| 절대 하지 마세요 | 대신 이렇게 하세요 |
|---|---|
| docker-compose.yml에 비밀번호 하드코딩 | .env 파일(gitignore 처리) 또는 Docker secrets 사용 |
| .env 파일을 버전 관리에 커밋 | .env를 .gitignore에 추가하고 대신 .env.example 커밋 |
| 환경 변수로 개인키나 인증서 전달 | 볼륨 또는 Docker secrets로 파일로 마운트 |
| 애플리케이션 코드에서 환경 변수 로깅 | 로그에서 민감한 값 마스킹 |
| Slack, 이메일, 채팅으로 .env 파일 공유 | 시크릿 매니저 사용 (Vault, AWS Secrets Manager, 1Password) |
| 개발/스테이징/프로덕션에서 같은 시크릿 사용 | 환경별 고유 시크릿 생성 |
# .gitignore — essential entries for Docker projects
.env
.env.*
!.env.example
secrets/
*.pem
*.key
*.crt# .env.example — commit this as a template
# Database
DB_HOST=localhost
DB_PORT=5432
DB_USER=myapp
DB_PASSWORD=CHANGE_ME
# API Keys
API_KEY=CHANGE_ME
JWT_SECRET=CHANGE_ME
# Application
NODE_ENV=development
LOG_LEVEL=debug
APP_PORT=3000디버깅: 환경 변수가 로드되지 않는 이유
환경 변수가 컨테이너에 도달하지 않을 때 이 체계적인 플로우차트를 따르세요.
단계 1: 변수가 정의되어 있나요?
.env 파일, env_file 또는 environment 지시어에 변수가 있는지 확인합니다.
단계 2: 컨테이너에 도달하고 있나요?
docker compose exec <service> env를 실행하여 컨테이너 내 모든 환경 변수를 확인합니다.
단계 3: .env 파일이 올바른 위치에 있나요?
.env 파일은 docker-compose.yml과 같은 디렉토리에 있거나 --env-file로 지정해야 합니다.
단계 4: 이름 충돌이 있나요?
오타, 대소문자 구분, 우선순위(environment > env_file > .env)를 확인합니다.
단계 5: Compose 파일이 유효한가요?
docker compose config를 실행하여 모든 변수가 치환된 전체 설정을 확인합니다.
단계 6: 변수가 덮어쓰여지고 있나요?
셸 환경 변수가 .env 파일 값을 덮어씁니다. 호스트에서 echo $VAR_NAME으로 확인하세요.
유용한 디버그 명령어
# 1. See the fully resolved Compose config (all variables substituted)
docker compose config
# 2. Check environment variables inside a running container
docker compose exec api env
# 3. Check a specific variable
docker compose exec api sh -c 'echo $DATABASE_URL'
# 4. Inspect container config (shows env vars, mounts, etc.)
docker inspect <container_id> --format '{{json .Config.Env}}' | jq .
# 5. Check if .env file is being read
docker compose config --format json | jq '.services.api.environment'
# 6. Verify env_file exists and is readable
docker compose config 2>&1 | grep -i "env_file"
# 7. Run a one-off container to test environment
docker compose run --rm api env | sort
# 8. Check for shell variable conflicts
env | grep -i "DB_"
# 9. Validate Compose file syntax
docker compose config --quiet && echo "Valid" || echo "Invalid"
# 10. Show variable substitution warnings
docker compose --verbose up 2>&1 | grep -i "variable"자주 묻는 질문
Docker Compose에서 .env와 env_file의 차이는?
.env 파일은 Docker Compose가 docker-compose.yml 자체의 변수 치환을 위해 자동으로 읽습니다. env_file 지시어는 변수를 컨테이너 환경에 직접 로드합니다. 용도가 다릅니다: .env는 Compose 파일 보간용, env_file은 컨테이너 환경 주입용입니다.
호스트에서 Docker 컨테이너로 환경 변수를 전달하려면?
environment 지시어에서 값 없이 변수 이름만 나열(예: "- MY_VAR")하거나, docker-compose.yml에서 ${MY_VAR} 변수 치환 구문을 사용합니다. docker compose up 실행 전에 호스트 셸에서 변수를 설정해야 합니다.
Docker 환경 변수는 안전한가요?
아닙니다. 환경 변수는 docker inspect로 볼 수 있고, 컨테이너 내 /proc/environ에 나타나며, 애플리케이션에서 로깅될 수 있습니다. 비밀번호와 API 키 같은 민감한 데이터에는 Docker secrets 또는 시크릿 매니저를 사용하세요.
Docker Compose에서 환경 변수의 우선순위는?
높은 순서대로: 1) docker compose run -e, 2) 호스트 셸 환경 변수, 3) docker-compose.yml의 environment 지시어, 4) env_file 지시어, 5) Dockerfile ENV 명령. 높은 우선순위가 낮은 우선순위를 덮어씁니다.
개발과 프로덕션에서 다른 환경 변수를 사용하려면?
오버라이드 파일 사용: 공유 설정의 기본 docker-compose.yml을 만들고, 개발용 docker-compose.override.yml과 프로덕션용 docker-compose.prod.yml을 준비합니다. 프로덕션은 docker compose -f docker-compose.yml -f docker-compose.prod.yml up으로 실행합니다.
Docker secrets와 환경 변수의 차이는?
Docker secrets는 환경 변수가 아닌 파일(/run/secrets/<secret_name>)로 마운트됩니다. docker inspect, 프로세스 목록, 크래시 덤프에 나타나지 않아 더 안전합니다. 많은 공식 Docker 이미지가 _FILE 접미사 규칙을 지원합니다.