블로그 목록으로

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

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

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

요약

먼저 읽을 결론

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

dockerdevopssecurityproductioncontainers

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를 사용하세요.

질문

자주 묻는 질문

이 글(Docker 프로덕션 체크리스트: 개발 환경 컨테이너를 운영에 올리기 전에 반드시 확인할 15가지)의 핵심 메시지는 무엇인가요?

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

docker를 우선 검토해야 하는 시점은 언제인가요?

수작업 예외 처리와 운영 병목이 반복되기 시작하면, 구현을 늘리기 전에 아키텍처 경계를 먼저 고정하고 지표로 검증해야 합니다.

devops 관점에서 가장 먼저 확인할 항목은 무엇인가요?

기능 확장 전에 폴백 경로, 로그/모니터링 기준, 책임 경계를 먼저 점검해야 운영 리스크를 줄일 수 있습니다.

다음 질문

이 글의 판단을 내 상황에 맞춰보세요

읽다가 걸린 기술 선택, 운영 리스크, 자동화 경계를 짧게 남기면 다음 판단 기준으로 이어갈 수 있습니다.