도커 중급 강좌 #5 환경변수와 secrets 관리

7 분 소요

지금까지 환경변수와 비밀 값을 적당히 -e / environment:로 흘려 넣어 왔습니다. 이번 글은 그 주제만 따로 떼어 — 어떻게 주입하고, 어떻게 노출되지 않게 두는가를 본격 정리합니다.

도커 중급 강좌 시리즈에서 이번 글의 위치:

환경변수가 들어오는 경로 #

도커에서 환경변수가 컨테이너에 도달하는 경로는 의외로 여러 갈래입니다. 한곳에 정리하면:

환경변수의 출처들
                          ┌─────────────────────────┐
[Dockerfile]              │                         │
  ENV KEY=value ────────▶ │                         │
                          │                         │
[docker run]              │     컨테이너            │
  -e KEY=val      ──────▶ │     안의 process        │
  --env-file file ──────▶ │     KEY 환경변수        │
                          │                         │
[compose.yaml]            │                         │
  environment:    ──────▶ │                         │
  env_file:       ──────▶ │                         │
                          │                         │
[host shell]              │                         │
  $KEY (보간)     ──────▶ │                         │
                          └─────────────────────────┘

같은 변수가 여러 곳에서 정의되면 나중에 들어온 것이 이기며, compose의 environment: > env_file: > Dockerfile의 ENV 순서로 우선됩니다.

이걸 머리에 두면 “환경변수가 적용 안 됐습니다” 의 90%는 추적이 끝납니다.

.env 파일 — 가장 흔한 입구 #

같은 디렉터리에 .env 파일이 있으면 Compose가 자동으로 읽어 들여, compose.yaml 안의 ${VAR} 보간에 씁니다.

.env
POSTGRES_PASSWORD=secret
APP_VERSION=1.0.0
DB_HOST=pg
compose.yaml
services:
  pg:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
  web:
    image: myapp:${APP_VERSION}
    environment:
      DB_HOST: ${DB_HOST}
      DB_PASSWORD: ${POSTGRES_PASSWORD}

여기서 짚어둘 두 사실:

  1. .env는 compose 파일 자체의 보간에 쓰이며, 컨테이너 안으로 자동 주입되는 게 아닙니다.
  2. 컨테이너에 그 값이 가려면 environment: / env_file:으로 명시해야 합니다.

변수 보간 문법 #

보간 형태들
${VAR}                   # 단순 참조. 없으면 빈 문자열
${VAR:-default}          # VAR가 없거나 빈 문자열이면 default
${VAR-default}           # VAR가 정의 안 됐을 때만 default (빈 문자열은 그대로)
${VAR:?error message}    # VAR가 없거나 빈 문자열이면 에러
${VAR?error message}     # VAR가 정의 안 됐으면 에러

운영에서 자주 쓰는 패턴:

실수 방지
services:
  pg:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}

.env가 빠진 채 compose up을 하면 즉시 에러로 알려주니, 빈 비밀번호로 떠버리는 사고를 막습니다.

environment: vs env_file: #

서비스에 변수를 주입하는 방식은 크게 두 가지입니다.

environment — 인라인
services:
  web:
    environment:
      DEBUG: "1"
      DB_HOST: pg
      DB_PASSWORD: ${POSTGRES_PASSWORD}
env_file — 파일에서
services:
  web:
    env_file:
      - .env.web
      - .env.local
.env.web
DEBUG=1
DB_HOST=pg
LOG_LEVEL=info
environmentenv_file
정의 위치compose.yaml 안별도 파일
변수 개수가 많을 때길어짐깔끔
보간가능 (${VAR})보간 없음 — 리터럴
우선순위env_file보다 높음environment보다 낮음

여러 개의 env_file을 적으면 뒤쪽이 앞쪽을 덮어쓰고, 그 다음에 environment:가 모두를 덮어씁니다. 이 우선순위만 머리에 두면 헷갈리지 않습니다.

흔한 혼동: env_file 안에서 KEY=${OTHER_KEY} 같은 보간은 동작하지 않습니다. env_file은 단순 dotenv 파일이라 모든 값이 리터럴로 읽힙니다. 보간이 필요하면 environment:에서.

변수 우선순위 한 표 #

같은 변수가 여러 경로에서 들어올 때 결과는 가장 위쪽이 이깁니다.

순위출처
1 (최강)docker compose run -e KEY=val (CLI 명시)
2compose.yamlenvironment:
3compose.yamlenv_file:
4호스트 셸의 환경변수 (compose가 시작될 때)
5.env 파일 (compose.yaml 보간용 — 컨테이너에 직접 주입 아님)
6Dockerfile의 ENV

docker compose config로 합쳐진 결과를 보면 헷갈림이 빠르게 풀립니다.

비밀 값과 환경변수 — 어디까지 OK 인가 #

DB 비밀번호, API 키, JWT 시크릿 — 이런 값을 환경변수로 주는 건 흔하지만, 보안 등급에 따라 다른 도구가 필요해집니다.

등급적절한 도구
개발 / 로컬.env 파일 (커밋 금지, .gitignore)
CI / 작은 운영CI의 secret store, compose의 environment:
운영 (적당)compose secrets: 또는 K8s Secret
운영 (높음)AWS Secrets Manager, Vault, GCP Secret Manager 같은 외부 매니저

이번 글은 처음 두 단계에 집중하고, 후자는 마지막에 한 단락으로 짚습니다.

.env의 보안 — .gitignore가 첫 줄 #

대부분의 비밀 누출은 단순한 실수에서 옵니다.

.gitignore — 항상 들어가야 할 줄
.env
.env.*
!.env.example

.env.example은 키 이름만 적힌 템플릿. 새 사람이 클론할 때 어떤 값을 채워야 하는지의 안내서입니다.

.env.example (커밋 OK)
POSTGRES_PASSWORD=
DJANGO_SECRET_KEY=
SENTRY_DSN=
.env (커밋 금지)
POSTGRES_PASSWORD=actual-secret
DJANGO_SECRET_KEY=actual-key
SENTRY_DSN=https://...@sentry.io/...

이 패턴이 정착하면 동료에게 비밀을 따로 알려주는 일 없이 — “환경변수 빼고 다른 셋업은 OK 인가?” 같은 디버깅이 빨라집니다.

한 가지 더 — pre-commit으로 누출 방지 #

gitleaks, detect-secrets 같은 도구를 pre-commit 훅에 넣으면, 실수로 비밀이 들어간 커밋을 사전에 차단할 수 있습니다. 한 번 누출된 비밀은 git history에서 깨끗이 지우기가 매우 까다롭습니다 (git filter-branch 또는 BFG Repo-Cleaner). 사전 차단이 가장 쌉니다.

환경변수의 노출 — 어디서 새는가 #

비밀이 환경변수로 들어왔다고 해서 자동으로 안전한 건 아닙니다. 도커 환경에선 다음 지점에서 새기 쉽습니다.

docker inspect #

컨테이너의 환경변수 전체
docker inspect myapp-web-1 --format '{{json .Config.Env}}'
# ["PATH=...", "DB_PASSWORD=secret", ...]

컨테이너에 접근 가능한 사용자라면 이 값을 그대로 봅니다. 호스트 보안의 한 부분입니다.

docker history #

빌드 시점에 환경변수가 박힌 이미지는 docker history로 노출됩니다.

빌드 시점 노출
docker history myapp:1.0
# CREATED BY                         SIZE
# ENV DB_PASSWORD=secret              0B    ← 영원히 박힘

이미지에는 비밀을 절대 박지 마세요. 이미지를 받은 모든 사람이 평문으로 보고, 레지스트리에 푸시하면 더 넓게 퍼집니다.

프로세스 트리 #

컨테이너 안의 환경변수
docker exec myapp-web-1 env | grep PASSWORD
docker exec myapp-web-1 cat /proc/1/environ | tr '\0' '\n'

컨테이너 내부에 셸로 들어갈 권한이 있으면 환경변수는 다 보입니다. 그래서 컨테이너 안에 셸 접근 권한 자체를 통제하는 게 한 층의 방어선입니다.

Compose secrets: — 한 단계 위 도구 #

환경변수보다 안전한 비밀 주입 방식으로, secrets:는 비밀 값을 파일로 컨테이너 안에 마운트해줍니다. 환경변수가 아닙니다.

compose.yaml — secrets
services:
  pg:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: app
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt
./secrets/db_password.txt
super-secret-password
.gitignore
secrets/

이 패턴의 장점:

  • 비밀이 환경변수로 노출되지 않음inspect로 안 보임
  • /run/secrets/db_password 라는 읽기 전용 파일로 마운트
  • Postgres 같은 공식 이미지들이 이미 *_FILE 환경변수 컨벤션을 지원함

외부 secret — Swarm 모드용 #

external secret (Swarm)
secrets:
  db_password:
    external: true

external: true는 Compose가 secret을 만들지 않고 이미 존재하는 secret을 참조한다는 뜻인데, 이는 Swarm 모드에서나 진가를 발휘합니다. 일반 compose up에선 file 형태가 일상적입니다.

빌드 시점 비밀 — --mount=type=secret #

#2에서 한 번 짚었던 BuildKit 시크릿으로, 빌드 도중에만 필요한 비밀(예: 사설 패키지 레지스트리 토큰)을 다룰 때 씁니다.

Dockerfile — 빌드 시크릿
# syntax=docker/dockerfile:1.7
FROM python:3.14-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=secret,id=pypi_token \
    pip install --extra-index-url \
      https://__token__:$(cat /run/secrets/pypi_token)@pypi.example.com/simple \
      -r requirements.txt
compose.yaml — build secret
services:
  web:
    build:
      context: .
      secrets:
        - pypi_token

secrets:
  pypi_token:
    file: ./secrets/pypi_token.txt

빌드 동안에만 /run/secrets/pypi_token으로 읽히고 — 이미지의 어떤 레이어에도 남지 않습니다. docker history로도 안 보입니다. 사설 패키지 인덱스 / GitHub Personal Access Token / 사내 미러 인증 같은 경우에 정석입니다.

외부 secret manager — 한 단락 #

운영 규모가 커지면 비밀을 파일이나 환경변수로 들고 있는 것 자체가 부담입니다. 클라우드 환경에선 외부 매니저로 옮기게 됩니다.

환경관용 도구
AWSSecrets Manager, Parameter Store
GCPSecret Manager
AzureKey Vault
Self-hostedHashiCorp Vault, Bitnami Sealed Secrets
K8sExternal Secrets Operator + 위 매니저

흐름은 보통 컨테이너가 시작 시점에 매니저로부터 시크릿을 받아 환경변수나 파일로 자기 안에 주입하는 방식입니다. 이미지에는 비밀이 없고, 컨테이너 정의에도 비밀이 없고, 매니저의 권한만 잘 통제하면 — 누출 표면이 매우 작아집니다.

도커 / Compose 단독 환경에서는 여기까지 갈 일이 잘 없지만, 운영이 ECS / EKS / GKE로 가면 자연스럽게 만나는 영역입니다.

자주 만나는 실수 #

  • 이미지에 ENV API_KEY=... 넣기docker history에 평문으로 영원히. 빌드 시크릿 / 런타임 주입으로.
  • .env를 git 커밋 — 한 번 push 되면 history에서 빼는 게 매우 까다롭다. pre-commit gitleaks가 가장 싼 방어.
  • docker compose logs가 비밀을 출력 — 앱이 시작 시점에 환경변수 전체를 로그로 찍는 패턴(어떤 라이브러리는 기본이 그래요). 마스킹 또는 출력 끄기.
  • 호스트 셸의 변수가 의도치 않게 컨테이너에 들어감environment: - MY_VAR (값 없음) 형식은 호스트 환경에서 끌어옵니다. 의도가 아니면 명시 값으로.
  • secret 파일의 권한secrets/ 디렉터리는 chmod 600으로 묶어두는 편이 안전.

정리 #

이번 글에서 잡은 그림:

  • 컨테이너 안 환경변수의 출처는 6가지. compose CLI > environment: > env_file: > 호스트 셸 > .env 보간 > Dockerfile ENV 순으로 이김
  • **.env**는 compose.yaml 보간용. 컨테이너에 직접 가는 게 아님. .gitignore + .env.example 셋업이 기본
  • 이미지에 비밀을 박지 말 것docker history 노출. ENV로 적지 마라
  • Compose **secrets:**는 환경변수가 아닌 읽기 전용 파일로 비밀 주입 — *_FILE 컨벤션과 잘 어울림
  • 빌드 시점 비밀은 BuildKit --mount=type=secret — 어떤 레이어에도 남지 않음
  • 운영 규모가 커지면 외부 secret manager (AWS Secrets Manager, Vault 등)로

다음 글(#6 로깅과 디버깅)에서는 중급 시리즈를 마무리합니다. 여러 컨테이너의 로그를 한곳에서 보고, log driver를 바꾸고, docker compose logs의 자주 쓰는 옵션, 그리고 컨테이너 안 디버깅 도구(exec, inspect, stats, dive)까지 다룹니다.

X