Docker for Production: 웹 애플리케이션 컨테이너화 완전 가이드

발행: (2026년 1월 4일 오후 10:45 GMT+9)
9 분 소요
원문: Dev.to

Source: Dev.to

Docker는 애플리케이션 배포 방식을 혁신했습니다. 이 포괄적인 가이드는 기본 컨테이너화부터 보안 모범 사례와 오케스트레이션 전략을 포함한 프로덕션 준비 배포까지 모든 것을 다룹니다.

핵심 Docker 구성 요소

ComponentDescription
Docker Engine컨테이너를 빌드하고 실행하는 런타임
Images애플리케이션 코드와 종속성을 포함하는 읽기 전용 템플릿
Containers이미지의 실행 인스턴스
Volumes영구적인 데이터 저장소
Networks컨테이너 간 통신

컨테이너는 호스트 OS 커널을 공유하므로 가상 머신에 비해 가볍지만 여전히 프로세스 격리를 제공합니다.

멀티‑스테이지 빌드

1️⃣ Node.js 애플리케이션

# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Stage 2: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 3: Production
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production

# Create non‑root user
RUN addgroup --system --gid 1001 nodejs \
    && adduser --system --uid 1001 nextjs

# Copy only necessary files
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./

USER nextjs
EXPOSE 3000
CMD ["node", "dist/main.js"]

2️⃣ PHP (Laravel / Symfony) 애플리케이션

# Stage 1: Composer dependencies
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
    --no-dev \
    --no-scripts \
    --no-autoloader \
    --prefer-dist

COPY . .
RUN composer dump-autoload --optimize

# Stage 2: Frontend assets
FROM node:20-alpine AS frontend
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 3: Production image
FROM php:8.3-fpm-alpine AS production

# Install PHP extensions
RUN apk add --no-cache \
    libpng-dev \
    libzip-dev \
    && docker-php-ext-install \
    pdo_mysql \
    gd \
    zip \
    opcache

# Configure OPcache for production
RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/opcache.ini \
    && echo "opcache.memory_consumption=256" >> /usr/local/etc/php/conf.d/opcache.ini \
    && echo "opcache.max_accelerated_files=20000" >> /usr/local/etc/php/conf.d/opcache.ini \
    && echo "opcache.validate_timestamps=0" >> /usr/local/etc/php/conf.d/opcache.ini

WORKDIR /var/www/html

# Copy application
COPY --from=vendor /app/vendor ./vendor
COPY --from=frontend /app/public/build ./public/build
COPY . .

# Set permissions
RUN chown -R www-data:www-data storage bootstrap/cache

USER www-data
EXPOSE 9000
CMD ["php-fpm"]

팁: 프로덕션 환경에서는 항상 컨테이너를 비‑루트 사용자로 실행하세요. 이렇게 하면 컨테이너 탈출 시 발생할 수 있는 잠재적 피해를 최소화할 수 있습니다.

개발 환경

docker-compose.yml

version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - .:/app
      - /app/node_modules
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis

  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"

  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "80:80"
    depends_on:
      - app

volumes:
  postgres_data:
  redis_data:

프로덕션 컴포즈 (docker-compose.prod.yml)

version: '3.8'

services:
  app:
    image: ${REGISTRY}/myapp:${TAG:-latest}
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    networks:
      - app-network
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.prod.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - app
    networks:
      - app-network

networks:
  app-network:
    driver: overlay

보안 스캐닝

# Docker Scout
docker scout cves myapp:latest

# Trivy
trivy image myapp:latest

# Snyk
snyk container test myapp:latest

이미지 모범 사례

# Use a specific version tag, not `latest`
FROM node:20.10.0-alpine3.18

# Do NOT store secrets in images – use build‑time args
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc \
    && npm ci \
    && rm .npmrc

# Prefer COPY over ADD
COPY package*.json ./

# Set proper file permissions
RUN chmod -R 755 /app

# Use a read‑only root filesystem where possible (configure in compose or at runtime)

# Run as non‑root
USER node

# Add a healthcheck
HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:3000/health || exit 1

보안 Compose (docker-compose.secure.yml)

services:
  app:
    image: myapp:latest
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp
      - /var/run
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

예시 네트워크 레이아웃

version: '3.8'

services:
  frontend:
    networks:
      - frontend-network
      - backend-network

  api:
    networks:
      - backend-network
      - database-network

  database:
    networks:
      - database-network

(아키텍처에 맞게 네트워크 이름과 연결을 조정하십시오.)

🎯 요점

  1. 멀티‑스테이지 빌드는 프로덕션 이미지 크기를 작게 유지하고 보안을 강화합니다.
  2. 비루트 사용자로 실행하고 가능한 경우 읽기 전용 파일 시스템을 적용합니다.
  3. 베이스 이미지에 명시적인 버전 태그를 사용합니다.
  4. 레지스트리에 푸시하기 전에 Docker Scout, Trivy, Snyk와 같은 도구로 이미지를 스캔합니다.
  5. 개발 및 프로덕션 모두에 Docker Compose를 활용하고, 프로덕션에는 보안 옵션을 추가합니다.

컨테이너화 즐기세요! 🚀

Docker Compose (docker‑compose.yml)

networks:
  frontend-network:
    driver: bridge
  backend-network:
    driver: bridge
    internal: true  # No external access
  database-network:
    driver: bridge
    internal: true

version: '3.8'

services:
  app:
    logging:
      driver: gelf
      options:
        gelf-address: "udp://logstash:12200"
        tag: "myapp"

  elasticsearch:
    image: elasticsearch:8.11.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data

  logstash:
    image: logstash:8.11.0
    volumes:
      - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
    ports:
      - "12201:12201/udp"

  kibana:
    image: kibana:8.11.0
    ports:
      - "5601:5601"
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200

volumes:
  elasticsearch_data:

Prometheus 구성 (prometheus.yml)

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'docker'
    static_configs:
      - targets: ['cadvisor:8080']

  - job_name: 'app'
    static_configs:
      - targets: ['app:3000']
    metrics_path: '/metrics'

GitHub Actions 워크플로우 (.github/workflows/docker.yml)

name: Build and Deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=
            type=ref,event=branch
            type=semver,pattern={{version}}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Scan for vulnerabilities
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          format: 'sarif'
          output: 'trivy-results.sarif'

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Deploy to production
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /opt/myapp
            docker compose pull
            docker compose up -d --remove-orphans
            docker image prune -f

Dockerfile (멀티‑스테이지, 최적화)

# Order layers from least to most frequently changed
FROM node:20-alpine

WORKDIR /app

# System dependencies (rarely change)
RUN apk add --no-cache dumb-init

# Package files (change occasionally)
COPY package*.json ./
RUN npm ci --only=production

# Application code (changes frequently)
COPY . .

ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/main.js"]

서비스 리소스 제한 (Docker Compose 스니펫)

services:
  app:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1G
        reservations:
          cpus: '0.5'
          memory: 512M

Source:

프로덕션 모범 사례 요약

Docker는 환경 전반에 걸쳐 일관되고 재현 가능한 배포를 가능하게 합니다. 이러한 프로덕션 모범 사례—멀티‑스테이지 빌드, 보안 강화, 적절한 네트워킹, CI/CD 통합—를 따름으로써 확장 가능한 견고한 컨테이너화 애플리케이션을 구축할 수 있습니다.

주요 내용

  • 멀티‑스테이지 빌드를 사용하여 더 작고 안전한 이미지를 만들기.
  • 프로덕션에서는 절대 루트 사용자로 컨테이너를 실행하지 않기.
  • 헬스 체크와 리소스 제한을 구현하여 안정성 확보.
  • 이미지를 정기적으로 취약점 스캔 (예: Trivy 사용).
  • 적절한 로깅 및 모니터링 사용 (GELF, Prometheus, Grafana 등).
Back to Blog

관련 글

더 보기 »

RGB LED 사이드퀘스트 💡

markdown !Jennifer Davis https://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%...

Mendex: 내가 만드는 이유

소개 안녕하세요 여러분. 오늘은 제가 누구인지, 무엇을 만들고 있는지, 그리고 그 이유를 공유하고 싶습니다. 초기 경력과 번아웃 저는 개발자로서 17년 동안 경력을 시작했습니다.