TimewareTimeware
블로그 목록으로
블로그

Docker 프로덕션 체크리스트: 개발 환경 컨테이너를 운영에 올리기 전에 반드시 확인할 15가지

개발에서 잘 돌던 Docker 컨테이너가 프로덕션에서 터지는 이유 15가지. 루트 실행, 헬스체크 누락, 시크릿 이미지 레이어 포함 등 실제 사고 사례와 체크리스트.

2026년 3월 5일Timeware Engineeringdockerdevopssecurityproductioncontainers
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)되면 호스트를 루트 권한으로 장악합니다.

dockerfile
1# 나쁜 예
2FROM node:20
3WORKDIR /app
4COPY . .
5RUN npm ci
6CMD ["node", "server.js"]
7# root로 실행됨
8
9# 좋은 예
10FROM node:20-alpine
11WORKDIR /app
12
13# 의존성 먼저 설치 (캐시 최적화)
14COPY package*.json ./
15RUN npm ci --omit=dev
16
17COPY --chown=node:node . .
18
19# node 사용자로 전환 (uid 1000)
20USER node
21
22CMD ["node", "server.js"]

2. latest 태그 사용

위험도: 🔴 Critical

dockerfile
1# 나쁜 예 - 오늘 잘 되어도 내일 다를 수 있음
2FROM node:latest
3FROM postgres:latest
4
5# 좋은 예 - 정확한 버전 고정
6FROM node:20.11.1-alpine3.19
7FROM postgres:17.2-alpine3.20

latest 태그는 예고 없이 바뀝니다. 월요일 아침 재배포에서 갑자기 메이저 버전이 올라가 패키지 호환성이 깨지는 사고를 방지합니다.

3. 시크릿이 이미지 레이어에 포함

위험도: 🔴 Critical

dockerfile
1# 절대 하지 말 것
2RUN apt-get install -y curl && \
3 curl -H "Authorization: ${API_KEY}" https://api.example.com/config > config.json
4# 다음 레이어에서 삭제해도 이전 레이어에 API_KEY가 남아있음!
5
6# docker image history로 모든 레이어 내용 확인 가능

올바른 방법:

dockerfile
1# BuildKit secrets 사용 (Docker 18.09+)
2# syntax=docker/dockerfile:1
3FROM node:20-alpine
4
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.json
bash
1# 빌드 명령
2docker build --secret id=api_key,src=./api_key.txt .

4. HEALTHCHECK 미설정

위험도: 🟡 High

dockerfile
1# 나쁜 예 - 프로세스가 살아있어도 앱이 죽어있으면 모름
2FROM node:20-alpine
3CMD ["node", "server.js"]
4
5# 좋은 예
6FROM node:20-alpine
7CMD ["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 1

Kubernetes에서는 livenessProbereadinessProbe로 대체하는 것이 일반적이지만, Docker Compose 환경에서는 HEALTHCHECK가 필수입니다.

5. 빌드 의존성이 프로덕션 이미지에 포함

위험도: 🟡 High

dockerfile
1# 나쁜 예 - 빌드 도구가 모두 포함됨 (이미지 크기 3GB 이상)
2FROM node:20
3WORKDIR /app
4COPY . .
5RUN npm install # devDependencies까지 설치
6RUN npm run build
7CMD ["node", ".next/standalone/server.js"]
8
9# 좋은 예 - 멀티스테이지 빌드
10FROM node:20-alpine AS builder
11WORKDIR /app
12COPY package*.json ./
13RUN npm ci
14COPY . .
15RUN npm run build
16
17FROM node:20-alpine AS runner
18WORKDIR /app
19ENV NODE_ENV=production
20
21# 프로덕션 파일만 복사
22COPY --from=builder /app/.next/standalone ./
23COPY --from=builder /app/.next/static ./.next/static
24COPY --from=builder /app/public ./public
25
26USER node
27EXPOSE 3000
28CMD ["node", "server.js"]
29# 결과: 3GB → 200MB

6. 무제한 리소스 사용

위험도: 🟡 High

한 컨테이너가 메모리 누수로 호스트 전체를 먹어버리는 사고를 방지합니다.

yaml
1# docker-compose.yml
2services:
3 api:
4 image: myapp:1.2.3
5 deploy:
6 resources:
7 limits:
8 cpus: '1.0' # 1 CPU 코어 최대
9 memory: 512M # 512MB 메모리 최대
10 reservations:
11 cpus: '0.25' # 최소 보장
12 memory: 128M
yaml
1# Kubernetes Deployment
2resources:
3 requests:
4 memory: "128Mi"
5 cpu: "250m"
6 limits:
7 memory: "512Mi"
8 cpu: "1000m"

7. 로그 관리 미설정

위험도: 🟡 High

기본 Docker 로그 드라이버는 JSON 파일을 디스크에 무한정 씁니다. 몇 주 만에 디스크가 가득 차는 사고를 막으려면:

json
1// /etc/docker/daemon.json
2{
3 "log-driver": "json-file",
4 "log-opts": {
5 "max-size": "10m",
6 "max-file": "3"
7 }
8}

또는 컨테이너별:

yaml
1services:
2 api:
3 logging:
4 driver: json-file
5 options:
6 max-size: "10m"
7 max-file: "3"

8. 불필요한 포트 노출

dockerfile
1# 나쁜 예 - 내부 포트까지 노출
2EXPOSE 3000 5432 6379 8080
3
4# 좋은 예 - 외부에서 접근이 필요한 포트만
5EXPOSE 3000

Docker EXPOSE는 문서화 목적이지만, --publish-all 옵션 사용 시 모두 바인딩됩니다.

9. .dockerignore 미설정

bash
1# .dockerignore 없으면 node_modules, .git, .env 등이 모두 이미지에 포함
2
3# .dockerignore
4node_modules
5.git
6.env
7.env.local
8.env.*.local
9*.log
10.next
11coverage
12.nyc_output
13dist
14.DS_Store
15Dockerfile*
16docker-compose*
17*.md
18.github
19test
20tests
21__tests__

.env 파일이 이미지에 포함되면 레지스트리에 올라갔을 때 모든 시크릿이 노출됩니다.

10. 컨테이너 내부에서 데이터 저장

컨테이너는 stateless해야 합니다. 파일을 컨테이너 내부에 저장하면 재시작 시 모두 사라집니다.

yaml
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. 패키지 캐시가 이미지에 포함

dockerfile
1# 나쁜 예 - apt 캐시 포함 (이미지 크기 증가)
2RUN apt-get update
3RUN apt-get install -y curl
4
5# 좋은 예 - 같은 레이어에서 캐시 삭제
6RUN apt-get update && \
7 apt-get install -y --no-install-recommends curl && \
8 rm -rf /var/lib/apt/lists/*
9
10# Node.js
11RUN npm ci --omit=dev && \
12 npm cache clean --force
13
14# Python
15RUN pip install --no-cache-dir -r requirements.txt

12. 환경변수로 직접 시크릿 전달

yaml
1# 나쁜 예 - docker inspect로 노출됨
2services:
3 api:
4 environment:
5 DB_PASSWORD: my_super_secret_password
6
7# 좋은 예 - Docker secrets 또는 외부 시크릿 관리자
8services:
9 api:
10 environment:
11 DB_PASSWORD_FILE: /run/secrets/db_password
12 secrets:
13 - db_password
14
15secrets:
16 db_password:
17 external: true # Docker Swarm의 external secret

또는 AWS Secrets Manager, HashiCorp Vault를 앱 코드에서 직접 참조합니다.

13. PID 1 문제 (좀비 프로세스)

dockerfile
1# 나쁜 예 - node가 PID 1이 됨
2# 좀비 프로세스 리핑과 시그널 처리를 제대로 못 함
3CMD ["node", "server.js"]
4
5# 좋은 예 - tini 사용 (경량 init 프로세스)
6FROM node:20-alpine
7RUN apk add --no-cache tini
8ENTRYPOINT ["/sbin/tini", "--"]
9CMD ["node", "server.js"]
10
11# 또는 docker run --init 플래그
12# docker-compose에서: init: true

14. 이미지 취약점 스캔 미실시

bash
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"
yaml
1# GitHub Actions 예시
2- name: Scan Docker image
3 uses: aquasecurity/trivy-action@master
4 with:
5 image-ref: 'myapp:${{ github.sha }}'
6 format: 'sarif'
7 exit-code: '1'
8 severity: 'HIGH,CRITICAL'

15. 재현 불가능한 빌드

dockerfile
1# 나쁜 예 - RUN 중에 외부에서 파일 다운로드
2RUN curl -o /usr/local/bin/tool https://example.com/tool && chmod +x /usr/local/bin/tool
3# 오늘은 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)

dockerfile
1# syntax=docker/dockerfile:1
2FROM node:20.11.1-alpine3.19 AS base
3
4# 의존성 설치 스테이지
5FROM base AS deps
6WORKDIR /app
7COPY package.json package-lock.json ./
8RUN npm ci
9
10# 빌드 스테이지
11FROM base AS builder
12WORKDIR /app
13COPY --from=deps /app/node_modules ./node_modules
14COPY . .
15RUN npm run build
16
17# 프로덕션 스테이지
18FROM base AS runner
19WORKDIR /app
20ENV NODE_ENV=production
21
22# tini 설치 (PID 1 문제 해결)
23RUN apk add --no-cache tini && \
24 addgroup --system --gid 1001 nodejs && \
25 adduser --system --uid 1001 nextjs
26
27COPY --from=builder /app/public ./public
28COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
29COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
30
31USER nextjs
32EXPOSE 3000
33
34HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
35 CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
36
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를 사용하세요.