DevToolBox무료
블로그

Docker Compose Secrets & 환경 변수: 올바른 방법

10분 읽기by DevToolBox

환경 변수는 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=4

2. 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.db

3. 셸 환경 변수 패스스루

호스트 셸의 변수를 컨테이너로 전달합니다. 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 포함?시크릿 안전?
environmentNon-sensitive defaultsYesNo
env_fileGrouped config, secretsNo (.gitignore)Partial
Shell passthroughCI/CD, dynamic valuesN/APartial
${} substitutionCompose file templatingYes (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-file

docker-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 exist

Docker 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 접미사 규칙을 지원합니다.

𝕏 Twitterin LinkedIn
도움이 되었나요?

최신 소식 받기

주간 개발 팁과 새 도구 알림을 받으세요.

스팸 없음. 언제든 구독 해지 가능.

Try These Related Tools

🐳Docker Compose GeneratorYMLYAML Validator & Formatter.gi.gitignore Generator

Related Articles

Docker Compose 치트시트: 서비스, 볼륨, 네트워크

Docker Compose 레퍼런스: 서비스 정의, 볼륨, 네트워크, 환경 변수, 스택 예시.

.env 파일 가이드: 환경 변수 모범 사례

.env 파일과 환경 변수를 마스터하세요. 구문, 보안, 프레임워크 설정, Docker 통합을 다룹니다.