Docker 프로덕션 체크리스트: 개발 환경 컨테이너를 운영에 올리기 전에 반드시 확인할 15가지
개발에서 잘 돌던 Docker 컨테이너가 프로덕션에서 터지는 이유 15가지. 루트 실행, 헬스체크 누락, 시크릿 이미지 레이어 포함 등 실제 사고 사례와 체크리스트.

요약
개발에서 잘 돌던 Docker 컨테이너가 프로덕션에서 터지는 이유 15가지. 루트 실행, 헬스체크 누락, 시크릿 이미지 레이어 포함 등 실제 사고 사례와 체크리스트.
Docker 프로덕션 체크리스트: 개발 환경 컨테이너를 운영에 올리기 전에 반드시 확인할 15가지
Executive Summary - Topic: Docker 컨테이너 프로덕션 배포 전 보안·안정성 체크리스트 - Target: 백엔드 개발자, DevOps 엔지니어, 플랫폼 팀 - TL;DR 1: 개발 Dockerfile과 프로덕션 Dockerfile은 달라야 한다 - TL;DR 2: 루트 실행, 시크릿 레이어 포함, 무한 로그는 프로덕션 3대 사고 원인 - TL;DR 3: HEALTHCHECK와 리소스 제한 없는 컨테이너는 시한폭탄
"개발에서는 잘 됐는데 프로덕션에서 왜 이러죠?" — 이 말을 들을 때마다 Dockerfile을 먼저 봅니다.
대부분의 컨테이너 문제는 개발 편의를 위해 만든 설정이 프로덕션에 그대로 올라가서 발생합니다. 실제로 마주쳤던 사고들을 기반으로 15개 체크리스트를 정리했습니다.
1. 루트(root)로 실행하는 컨테이너
위험도: 🔴 Critical
기본 컨테이너는 루트로 실행됩니다. 컨테이너가 탈출(container escape)되면 호스트를 루트 권한으로 장악합니다.
1# 나쁜 예2FROM node:203WORKDIR /app4COPY . .5RUN npm ci6CMD ["node", "server.js"]7# root로 실행됨8 9# 좋은 예10FROM node:20-alpine11WORKDIR /app12 13# 의존성 먼저 설치 (캐시 최적화)14COPY package*.json ./15RUN npm ci --omit=dev16 17COPY --chown=node:node . .18 19# node 사용자로 전환 (uid 1000)20USER node21 22CMD ["node", "server.js"]2. latest 태그 사용
위험도: 🔴 Critical
1# 나쁜 예 - 오늘 잘 되어도 내일 다를 수 있음2FROM node:latest3FROM postgres:latest4 5# 좋은 예 - 정확한 버전 고정6FROM node:20.11.1-alpine3.197FROM postgres:17.2-alpine3.20latest 태그는 예고 없이 바뀝니다. 월요일 아침 재배포에서 갑자기 메이저 버전이 올라가 패키지 호환성이 깨지는 사고를 방지합니다.
3. 시크릿이 이미지 레이어에 포함
위험도: 🔴 Critical
1# 절대 하지 말 것2RUN apt-get install -y curl && \3 curl -H "Authorization: ${API_KEY}" https://api.example.com/config > config.json4# 다음 레이어에서 삭제해도 이전 레이어에 API_KEY가 남아있음!5 6# docker image history로 모든 레이어 내용 확인 가능올바른 방법:
1# BuildKit secrets 사용 (Docker 18.09+)2# syntax=docker/dockerfile:13FROM node:20-alpine4 5# 빌드 시에만 사용되는 시크릿 (레이어에 저장 안 됨)6RUN --mount=type=secret,id=api_key \7 API_KEY=$(cat /run/secrets/api_key) \8 curl -H "Authorization: $API_KEY" https://api.example.com/config > config.json1# 빌드 명령2docker build --secret id=api_key,src=./api_key.txt .4. HEALTHCHECK 미설정
위험도: 🟡 High
1# 나쁜 예 - 프로세스가 살아있어도 앱이 죽어있으면 모름2FROM node:20-alpine3CMD ["node", "server.js"]4 5# 좋은 예6FROM node:20-alpine7CMD ["node", "server.js"]8 9HEALTHCHECK --interval=30s \10 --timeout=10s \11 --start-period=40s \12 --retries=3 \13 CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1Kubernetes에서는 livenessProbe와 readinessProbe로 대체하는 것이 일반적이지만, Docker Compose 환경에서는 HEALTHCHECK가 필수입니다.
5. 빌드 의존성이 프로덕션 이미지에 포함
위험도: 🟡 High
1# 나쁜 예 - 빌드 도구가 모두 포함됨 (이미지 크기 3GB 이상)2FROM node:203WORKDIR /app4COPY . .5RUN npm install # devDependencies까지 설치6RUN npm run build7CMD ["node", ".next/standalone/server.js"]8 9# 좋은 예 - 멀티스테이지 빌드10FROM node:20-alpine AS builder11WORKDIR /app12COPY package*.json ./13RUN npm ci14COPY . .15RUN npm run build16 17FROM node:20-alpine AS runner18WORKDIR /app19ENV NODE_ENV=production20 21# 프로덕션 파일만 복사22COPY --from=builder /app/.next/standalone ./23COPY --from=builder /app/.next/static ./.next/static24COPY --from=builder /app/public ./public25 26USER node27EXPOSE 300028CMD ["node", "server.js"]29# 결과: 3GB → 200MB6. 무제한 리소스 사용
위험도: 🟡 High
한 컨테이너가 메모리 누수로 호스트 전체를 먹어버리는 사고를 방지합니다.
1# docker-compose.yml2services:3 api:4 image: myapp:1.2.35 deploy:6 resources:7 limits:8 cpus: '1.0' # 1 CPU 코어 최대9 memory: 512M # 512MB 메모리 최대10 reservations:11 cpus: '0.25' # 최소 보장12 memory: 128M1# Kubernetes Deployment2resources:3 requests:4 memory: "128Mi"5 cpu: "250m"6 limits:7 memory: "512Mi"8 cpu: "1000m"7. 로그 관리 미설정
위험도: 🟡 High
기본 Docker 로그 드라이버는 JSON 파일을 디스크에 무한정 씁니다. 몇 주 만에 디스크가 가득 차는 사고를 막으려면:
1// /etc/docker/daemon.json2{3 "log-driver": "json-file",4 "log-opts": {5 "max-size": "10m",6 "max-file": "3"7 }8}또는 컨테이너별:
1services:2 api:3 logging:4 driver: json-file5 options:6 max-size: "10m"7 max-file: "3"8. 불필요한 포트 노출
1# 나쁜 예 - 내부 포트까지 노출2EXPOSE 3000 5432 6379 80803 4# 좋은 예 - 외부에서 접근이 필요한 포트만5EXPOSE 3000Docker EXPOSE는 문서화 목적이지만, --publish-all 옵션 사용 시 모두 바인딩됩니다.
9. .dockerignore 미설정
1# .dockerignore 없으면 node_modules, .git, .env 등이 모두 이미지에 포함2 3# .dockerignore4node_modules5.git6.env7.env.local8.env.*.local9*.log10.next11coverage12.nyc_output13dist14.DS_Store15Dockerfile*16docker-compose*17*.md18.github19test20tests21__tests__.env 파일이 이미지에 포함되면 레지스트리에 올라갔을 때 모든 시크릿이 노출됩니다.
10. 컨테이너 내부에서 데이터 저장
컨테이너는 stateless해야 합니다. 파일을 컨테이너 내부에 저장하면 재시작 시 모두 사라집니다.
1# 볼륨 마운트 필수2services:3 app:4 volumes:5 - uploads:/app/uploads # 업로드 파일6 - logs:/app/logs # 로그 파일7 8 db:9 volumes:10 - postgres_data:/var/lib/postgresql/data # DB 데이터11 12volumes:13 uploads:14 logs:15 postgres_data:11. 패키지 캐시가 이미지에 포함
1# 나쁜 예 - apt 캐시 포함 (이미지 크기 증가)2RUN apt-get update3RUN apt-get install -y curl4 5# 좋은 예 - 같은 레이어에서 캐시 삭제6RUN apt-get update && \7 apt-get install -y --no-install-recommends curl && \8 rm -rf /var/lib/apt/lists/*9 10# Node.js11RUN npm ci --omit=dev && \12 npm cache clean --force13 14# Python15RUN pip install --no-cache-dir -r requirements.txt12. 환경변수로 직접 시크릿 전달
1# 나쁜 예 - docker inspect로 노출됨2services:3 api:4 environment:5 DB_PASSWORD: my_super_secret_password6 7# 좋은 예 - Docker secrets 또는 외부 시크릿 관리자8services:9 api:10 environment:11 DB_PASSWORD_FILE: /run/secrets/db_password12 secrets:13 - db_password14 15secrets:16 db_password:17 external: true # Docker Swarm의 external secret또는 AWS Secrets Manager, HashiCorp Vault를 앱 코드에서 직접 참조합니다.
13. PID 1 문제 (좀비 프로세스)
1# 나쁜 예 - node가 PID 1이 됨2# 좀비 프로세스 리핑과 시그널 처리를 제대로 못 함3CMD ["node", "server.js"]4 5# 좋은 예 - tini 사용 (경량 init 프로세스)6FROM node:20-alpine7RUN apk add --no-cache tini8ENTRYPOINT ["/sbin/tini", "--"]9CMD ["node", "server.js"]10 11# 또는 docker run --init 플래그12# docker-compose에서: init: true14. 이미지 취약점 스캔 미실시
1# Trivy로 취약점 스캔 (CI/CD 파이프라인에 포함)2APT_OUTPUT=$(trivy image \3 --severity HIGH,CRITICAL \4 --exit-code 1 \5 myapp:1.2.3)6 7echo "$APT_OUTPUT"1# GitHub Actions 예시2- name: Scan Docker image3 uses: aquasecurity/trivy-action@master4 with:5 image-ref: 'myapp:${{ github.sha }}'6 format: 'sarif'7 exit-code: '1'8 severity: 'HIGH,CRITICAL'15. 재현 불가능한 빌드
1# 나쁜 예 - RUN 중에 외부에서 파일 다운로드2RUN curl -o /usr/local/bin/tool https://example.com/tool && chmod +x /usr/local/bin/tool3# 오늘은 v1.2, 내일은 v1.3이 받아질 수 있음4 5# 좋은 예 - 버전과 해시 고정6RUN curl -fsSL -o /tmp/tool.tar.gz \7 https://example.com/tool-1.2.3.tar.gz && \8 echo "abc123def456 /tmp/tool.tar.gz" | sha256sum -c - && \9 tar -xzf /tmp/tool.tar.gz -C /usr/local/bin/ && \10 rm /tmp/tool.tar.gz최종 Dockerfile 예시 (Next.js)
1# syntax=docker/dockerfile:12FROM node:20.11.1-alpine3.19 AS base3 4# 의존성 설치 스테이지5FROM base AS deps6WORKDIR /app7COPY package.json package-lock.json ./8RUN npm ci9 10# 빌드 스테이지11FROM base AS builder12WORKDIR /app13COPY --from=deps /app/node_modules ./node_modules14COPY . .15RUN npm run build16 17# 프로덕션 스테이지18FROM base AS runner19WORKDIR /app20ENV NODE_ENV=production21 22# tini 설치 (PID 1 문제 해결)23RUN apk add --no-cache tini && \24 addgroup --system --gid 1001 nodejs && \25 adduser --system --uid 1001 nextjs26 27COPY --from=builder /app/public ./public28COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./29COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static30 31USER nextjs32EXPOSE 300033 34HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \35 CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 136 37ENTRYPOINT ["/sbin/tini", "--"]38CMD ["node", "server.js"]FAQ
Q. Docker Desktop의 Scout 기능으로 취약점 스캔이 되나요? A. 네, 로컬에서 빠르게 확인할 수 있습니다. CI/CD 파이프라인에는 Trivy나 Snyk을 사용하는 것이 더 자동화하기 좋습니다. 엔터프라이즈 환경에서는 Amazon ECR의 Enhanced Scanning (Snyk 통합) 또는 Google Artifact Registry의 On-demand scanning을 권장합니다.
Q. 멀티스테이지 빌드가 로컬 개발에도 필요한가요? A. 로컬은 일반적으로 빌드 속도와 편의성을 위해 단순 Dockerfile을 써도 됩니다. 그러나 docker-compose.yml에서 프로덕션 Dockerfile과 개발용 Dockerfile을 분리해 두는 것이 좋습니다: Dockerfile (프로덕션), Dockerfile.dev (개발).
Q. Kubernetes 환경에서도 이 체크리스트가 동일하게 적용되나요? A. 대부분 적용됩니다. HEALTHCHECK는 livenessProbe/readinessProbe로 대체하고, 리소스 제한은 Pod spec에서 설정합니다. 시크릿은 Kubernetes Secrets 또는 External Secrets Operator를 사용하세요.