Docker for Production: 웹 애플리케이션 컨테이너화 완전 가이드
Source: Dev.to
Docker는 애플리케이션 배포 방식을 혁신했습니다. 이 포괄적인 가이드는 기본 컨테이너화부터 보안 모범 사례와 오케스트레이션 전략을 포함한 프로덕션 준비 배포까지 모든 것을 다룹니다.
핵심 Docker 구성 요소
| Component | Description |
|---|---|
| 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
(아키텍처에 맞게 네트워크 이름과 연결을 조정하십시오.)
🎯 요점
- 멀티‑스테이지 빌드는 프로덕션 이미지 크기를 작게 유지하고 보안을 강화합니다.
- 비루트 사용자로 실행하고 가능한 경우 읽기 전용 파일 시스템을 적용합니다.
- 베이스 이미지에 명시적인 버전 태그를 사용합니다.
- 레지스트리에 푸시하기 전에 Docker Scout, Trivy, Snyk와 같은 도구로 이미지를 스캔합니다.
- 개발 및 프로덕션 모두에 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 등).