[도커 30강] 15강. 환경변수와 .env 관리, 시크릿 분리
지난 강의에서 Python + Postgres 조합을 compose로 안정적으로 띄우는 방법을 다뤘다면, 오늘은 그 환경을 팀 단위로 오래 운영할 때 반드시 부딪히는 문제를 해결해보겠습니다. 바로 환경변수 관리입니다. 로컬에서는 되는데 팀원 환경에서는 깨지고, CI에서는 또 다르게 실패하는 이유의 절반 이상은 설정값 관리가 엉켜 있기 때문입니다. 특히 DB_PASSWORD, API_KEY 같은 민감한 값을 코드나 compose 파일에 그대로 넣는 습관은 시간이 지날수록 사고 가능성을 키웁니다.
이번 15강의 목표는 단순히 .env 파일을 쓰는 법을 소개하는 수준이 아닙니다. (1) 공개 가능한 설정과 비밀값을 분리하고, (2) 개발/테스트/운영 환경마다 같은 구조로 주입하며, (3) 누락/오타를 빠르게 진단하는 표준 루틴을 만드는 데 있습니다. 초반에 이 기준을 잡아두면 이후 20강대의 보안/운영 자동화 주제가 훨씬 쉽게 연결됩니다.
핵심 개념
- 환경변수는 “코드 수정 없이 동작을 바꾸는 설정 인터페이스”입니다. 즉, 앱 코드 안에서 하드코딩을 줄이고 실행 시점에 주입하는 것이 핵심입니다.
.env는 편리하지만 비밀 저장소가 아닙니다. Git에 올라가면 비밀이 아니라 사고 기록이 됩니다.- compose의
env_file/environment/쉘 변수 확장($VAR,${VAR})은 동작 시점과 우선순위가 다르므로 팀 기준을 문서화해야 합니다. - 시크릿 분리는 “파일 분리”에서 끝나지 않습니다. 로그 출력, 에러 메시지, 디버그 화면에서 값이 노출되지 않도록 코딩 습관까지 함께 바뀌어야 합니다.
- 실무에서는 보통
기본값(.env.example) + 로컬 비밀(.env.local 또는 .env) + 환경별 주입(CI Secret, 런타임 Secret)구조를 사용합니다.
기본 사용
예제 1) 안전한 환경변수 구조 만들기
아래 예제는 개발자가 바로 시작할 수 있는 템플릿과, 실제 비밀값 파일을 분리해 관리하는 기본 구조입니다. 핵심은 예시 파일은 공유하고, 실제 값 파일은 공유하지 않는 것입니다.
mkdir -p ~/docker100/lesson15/app
cd ~/docker100/lesson15
cat > .env.example <<'EOF'
APP_NAME=docker100-lesson15
APP_ENV=development
APP_PORT=18015
DB_HOST=db
DB_PORT=5432
DB_NAME=appdb
DB_USER=appuser
DB_PASSWORD=change-me
REDIS_URL=redis://redis:6379/0
LOG_LEVEL=info
EOF
cp .env.example .env
# 로컬에서 실제 값으로 교체
sed -i '' 's/DB_PASSWORD=change-me/DB_PASSWORD=local-dev-pass-15/' .env
echo '.env' >> .gitignore
echo '.env.local' >> .gitignore
설명:
.env.example은 새 팀원이 프로젝트를 클론했을 때 “무슨 값이 필요한지” 즉시 알 수 있게 해주는 계약서 역할입니다..env는 개인/머신마다 다른 실제 값이 들어가므로 일반적으로 Git 추적에서 제외합니다.- 개발 생산성을 위해
.env를 쓰더라도 운영에서는 Secret Manager, CI Secret, 배포 플랫폼 Secret 주입으로 전환해야 합니다.
예제 2) compose에서 env_file과 environment를 조합해 주입하기
env_file은 파일에서 여러 값을 가져올 때 편하고, environment는 서비스별 override나 필수 항목 명시에 유리합니다. 두 방식을 혼합할 때는 우선순위와 책임을 정해두어야 혼란이 없습니다.
cd ~/docker100/lesson15
cat > compose.yaml <<'YAML'
services:
app:
image: python:3.12-slim
container_name: lesson15_app
working_dir: /app
command: ["python", "app.py"]
volumes:
- ./app:/app
env_file:
- .env
environment:
APP_PORT: ${APP_PORT:-18015}
LOG_LEVEL: ${LOG_LEVEL:-info}
# 민감값은 로그에 노출하지 않도록 앱에서 마스킹 처리
ports:
- "${APP_PORT:-18015}:18015"
depends_on:
- db
- redis
db:
image: postgres:16-alpine
container_name: lesson15_db
env_file:
- .env
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- lesson15_pg:/var/lib/postgresql/data
redis:
image: redis:7-alpine
container_name: lesson15_redis
volumes:
lesson15_pg:
YAML
설명:
env_file로 공통값을 주입하고, 서비스 문맥에서 필요한 키만environment로 다시 명시하면 가독성이 좋아집니다.${VAR:-default}패턴을 활용하면 누락 시 안전한 기본값을 줄 수 있습니다(단, 비밀값에는 기본값 남발 금지).- 운영에서는
compose.yaml에 비밀번호가 직접 보이지 않도록 주입 경로를 외부 비밀 저장소로 옮기는 것이 바람직합니다.
예제 3) 앱에서 필수 환경변수 검증 + 마스킹 로그 적용
실무에서 자주 발생하는 장애는 “설정 누락”입니다. 앱 시작 시 필수 키를 검증하고, 로그에는 비밀값을 그대로 찍지 않게 막으면 장애 분석 속도와 보안성이 함께 올라갑니다.
cd ~/docker100/lesson15
cat > app/app.py <<'PY'
import os
import sys
REQUIRED_KEYS = [
"APP_NAME", "APP_ENV", "APP_PORT",
"DB_HOST", "DB_PORT", "DB_NAME", "DB_USER", "DB_PASSWORD",
"REDIS_URL"
]
def mask(value: str) -> str:
if not value:
return "<empty>"
if len(value) <= 4:
return "****"
return value[:2] + "***" + value[-2:]
def require_env(keys):
missing = [k for k in keys if not os.getenv(k)]
if missing:
print("[FATAL] Missing required env:", ", ".join(missing), file=sys.stderr)
sys.exit(1)
require_env(REQUIRED_KEYS)
print("[BOOT] APP_NAME=", os.getenv("APP_NAME"))
print("[BOOT] APP_ENV=", os.getenv("APP_ENV"))
print("[BOOT] DB_HOST=", os.getenv("DB_HOST"))
print("[BOOT] DB_PASSWORD(masked)=", mask(os.getenv("DB_PASSWORD")))
print("[BOOT] REDIS_URL(masked)=", mask(os.getenv("REDIS_URL")))
print("[OK] 환경변수 검증 통과. 서비스 시작 준비 완료")
PY
docker compose up -d
docker compose logs --tail=100 app
설명:
- 필수 키 누락을 런타임 초반에 명확히 실패시키면, “나중에 연결 에러로 우회적으로 터지는 문제”를 줄일 수 있습니다.
- 비밀값은 전부 마스킹해서 출력해야 합니다. 디버그 편의 때문에 평문 출력 습관을 들이면 사고 확률이 높아집니다.
- 운영 환경에서는 로깅 파이프라인(예: Elasticsearch, Cloud Logging)까지 고려해 민감정보 필터를 함께 적용하세요.
자주 하는 실수
실수 1) .env를 Git에 커밋
- 원인: 초기에 빠르게 공유하려고
.env를 그대로 push. - 문제: 비밀번호/토큰이 저장소 히스토리에 영구적으로 남음. 삭제해도 기록은 남을 수 있음.
- 해결:
.env는 기본적으로.gitignore처리, 공유는.env.example로만 수행. 유출 시 즉시 키 회전(rotation)까지 포함한 대응 절차를 문서화.
실수 2) 개발/운영에서 같은 비밀값 재사용
- 원인: 편의를 위해 한 번 만든 비밀번호를 전 환경에 복붙.
- 문제: 개발 환경 노출이 운영 침해로 직결될 수 있음.
- 해결: 환경별로 서로 다른 Secret 사용, 권한 범위 최소화, 만료/회전 주기 설정.
실수 3) 로그에 환경변수를 통째로 출력
- 원인: 디버깅을 빨리 하려다
print(os.environ)같은 코드를 남김. - 문제: 토큰/비밀번호가 로그 수집 시스템과 슬랙 알림까지 퍼짐.
- 해결: 필요한 키만 선택 출력 + 마스킹, 민감 키 패턴 필터링(
PASSWORD,TOKEN,SECRET,KEY).
실수 4) 변수 우선순위를 팀이 각자 다르게 이해
- 원인:
env_file,environment, 쉘 export의 우선순위 미정. - 문제: “내 PC에서는 되는데 네 PC에서는 안 됨”이 반복.
- 해결: 문서에 우선순위와 표준 명령을 명확히 고정. 예:
docker compose --env-file .env up -d를 표준으로 지정.
실무 패턴
첫째, 설정 계약(Contract)을 코드로 고정하세요. REQUIRED_KEYS 같은 목록을 앱에 두고 부팅 시 검증하면, 인수인계 과정에서도 누락이 즉시 드러납니다. 운영 이슈는 “설정 누락”이 대부분인데, 이건 코드로 막는 것이 가장 싸고 확실합니다.
둘째, .env.example의 품질이 프로젝트 온보딩 속도를 결정합니다. 키 이름, 용도, 예시값, 필수 여부를 주석으로 정리해두면 신규 팀원이 문서 탭을 10개 열지 않아도 바로 실행할 수 있습니다. 반대로 example 파일이 부실하면 “정답 없는 설정 추측 게임”이 시작됩니다.
셋째, 비밀값 소유권을 분리하세요. 개발자는 로컬 실행을 위해 최소 권한의 개발용 Secret만 가지고, 운영 Secret은 배포 파이프라인/플랫폼이 주입하는 구조가 안전합니다. 사람 손을 덜 탈수록 사고가 줄어듭니다.
넷째, 회전 가능성을 전제로 설계하세요. 비밀번호/토큰은 언젠가 반드시 교체됩니다. 코드와 배포 구조가 이를 전제로 되어 있어야 야간 장애 없이 교체할 수 있습니다. 예를 들어 DB 자격증명 이중화 기간을 두고 단계적으로 전환하는 방식이 실무에서 흔히 쓰입니다.
다섯째, 문제 진단 루틴을 표준화하세요. 권장 순서는 보통 다음과 같습니다.
docker compose config로 변수 치환 결과 확인docker compose ps로 컨테이너 상태 확인docker compose logs app db redis로 오류 분리- 앱 내부에서 실제 환경변수 확인(
docker compose exec app env | sort)
이 루틴을 문서화해두면 장애 시 감정 소모를 줄이고, 누구나 같은 순서로 원인을 좁힐 수 있습니다.
오늘의 결론
한 줄 요약: 환경변수 관리는 편의 기능이 아니라 안정성과 보안을 동시에 좌우하는 운영 기술이며, .env.example 기반 계약 + 비밀 분리 + 부팅 시 검증 루틴을 갖추면 팀의 재현성과 사고 대응력이 크게 올라간다.
연습문제
.env에서DB_PASSWORD를 비운 뒤docker compose up을 실행해 보세요. 앱이 어디에서, 어떤 메시지로 실패하는지 확인하고 “빠른 실패(fail fast)”가 왜 유리한지 설명해보세요..env.example에JWT_SECRET키를 추가하고, 앱에서 필수 검증 목록에 포함하세요. 누락 시 어떤 로그가 나와야 적절한지 직접 설계해보세요.docker compose config출력에서 민감값이 노출되는지 확인하고, 노출 시 팀 문서/파이프라인에서 어떤 방어 장치를 둘지 정리해보세요.
이전 강의 정답
14강 연습문제 해설:
- API를 5회 호출했을 때
access_log가 5 증가하지 않으면, 보통docker compose ps로 상태 확인 →docker compose logs app db로 연결/인증 오류 확인 →docker compose exec db psql ...로 실제 테이블 반영 여부를 점검하는 순서가 가장 빠릅니다. DB_HOST=localhost로 바꾸면 앱 컨테이너 입장에서 localhost는 자기 자신이므로 Postgres에 도달하지 못합니다. compose 네트워크에서는 서비스명(db)을 사용해야 정상 통신됩니다.docker compose down은 컨테이너/네트워크를 정리하지만 named volume은 남겨 데이터가 유지됩니다. 반면docker compose down -v는 볼륨까지 삭제해 데이터가 초기화되므로, 실무에서는 의도적 초기화 상황에서만 제한적으로 사용해야 합니다.
실습 환경/재현 정보
- OS: macOS 15+ (Apple Silicon) 또는 Ubuntu 22.04+
- Docker: Engine/Desktop 25.x 이상
- Compose: v2.x (
docker compose version) - 사용 이미지:
python:3.12-slim,postgres:16-alpine,redis:7-alpine - 권장 파일 구조:
compose.yaml.env.example(공유).env(비공유)app/app.py
- 재현 절차:
.env.example작성 후.env복사docker compose config로 치환 결과 검증docker compose up -d실행docker compose logs app로 부팅/검증 로그 확인- 일부 키를 비워 누락 실패 시나리오 재현
- 체크 포인트:
- 필수 환경변수 누락 시 즉시 실패하는가?
- 비밀값이 로그에 평문으로 남지 않는가?
- 팀원이
.env.example만으로 실행 준비를 이해할 수 있는가? - 개발/운영 Secret가 분리되어 있는가?