Docker: 개발에서 프로덕션까지

애플리케이션 컨테이너화를 위한 완벽한 Docker 가이드. Dockerfile, Docker Compose, 멀티스테이지 빌드 및 프로덕션 배포를 실용적인 예제와 함께 설명합니다.

개발에서 프로덕션까지의 Docker 가이드

Docker는 애플리케이션의 개발, 테스트, 배포 방식을 근본적으로 혁신하고 있습니다. 애플리케이션과 그 의존성을 휴대 가능한 컨테이너에 캡슐화함으로써 Docker는 유명한 "내 컴퓨터에서는 동작합니다" 문제를 제거하고 모든 환경에서의 일관성을 보장합니다. 이 가이드는 첫 번째 Dockerfile부터 프로덕션 배포까지의 전체 여정을 다룹니다.

2026년의 Docker

Docker Desktop 5.x는 네이티브 containerd 지원, 최적화된 리소스 관리, 원활한 Kubernetes 통합을 포함한 주요 성능 개선을 제공합니다. 멀티 아키텍처 이미지(ARM/x86)는 이제 표준 관행이 되었습니다.

컨테이너화 기초

컨테이너는 코드, 런타임, 시스템 라이브러리 및 설정을 패키징하는 경량 소프트웨어 단위입니다. 하드웨어를 가상화하는 가상 머신과 달리 컨테이너는 호스트 시스템의 커널을 공유하므로 시작이 빠르고 리소스 소비가 적습니다.

bash
# terminal
# Docker installation on Ubuntu
sudo apt update
sudo apt install -y docker.io

# Add user to docker group (avoids sudo)
sudo usermod -aG docker $USER

# Verify installation
docker --version
# Docker version 26.1.0, build 1234567

# First container: downloads image and runs
docker run hello-world

이 명령은 Docker Hub에서 hello-world 이미지를 다운로드하고 확인 메시지를 표시하는 컨테이너를 실행합니다.

bash
# terminal
# List running containers
docker ps

# List all containers (including stopped)
docker ps -a

# List downloaded images
docker images

# Remove a container
docker rm <container_id>

# Remove an image
docker rmi <image_name>

이 기본 명령들은 컨테이너와 이미지의 수명 주기를 관리합니다.

첫 번째 Dockerfile 작성

Dockerfile에는 Docker 이미지를 빌드하기 위한 명령이 포함되어 있습니다. 각 명령은 최종 이미지에 레이어를 생성하여 캐싱과 재사용을 가능하게 합니다.

dockerfile
# Dockerfile
# Base image: Node.js 22 on Alpine Linux (lightweight)
FROM node:22-alpine

# Set working directory in the container
WORKDIR /app

# Copy dependency files first (cache optimization)
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Expose port (documentation)
EXPOSE 3000

# Startup command
CMD ["node", "server.js"]

명령의 순서는 캐시 최적화에 매우 중요합니다. 변경 빈도가 낮은 파일(package.json)은 소스 코드보다 먼저 복사해야 합니다.

bash
# terminal
# Build image with a tag
docker build -t my-app:1.0 .

# Run the container
docker run -d -p 3000:3000 --name my-app-container my-app:1.0

# Check logs
docker logs my-app-container

# Access container shell
docker exec -it my-app-container sh

-d 플래그는 컨테이너를 백그라운드에서 실행하고, -p는 컨테이너의 포트 3000을 호스트의 포트 3000에 매핑합니다.

Alpine vs Debian

Alpine 이미지는 상당히 작습니다(Debian의 약 120MB에 비해 약 5MB). 그러나 glibc 대신 musl libc를 사용하므로 일부 네이티브 의존성과 비호환성이 발생할 수 있습니다. 문제가 발생하면 Debian 기반 이미지(node:22-slim)를 사용하는 것이 권장됩니다.

프로덕션을 위한 멀티스테이지 빌드

멀티스테이지 빌드는 빌드 환경을 런타임 환경에서 분리하여 최적화된 프로덕션 이미지를 생성합니다. 최종 이미지에는 필요한 아티팩트만 포함됩니다.

dockerfile
# Dockerfile.production
# ============================================
# Stage 1: Build
# ============================================
FROM node:22-alpine AS builder

WORKDIR /app

# Copy and install dependencies (including devDependencies)
COPY package*.json ./
RUN npm ci

# Copy source code
COPY . .

# Build the application (TypeScript, bundling, etc.)
RUN npm run build

# ============================================
# Stage 2: Production
# ============================================
FROM node:22-alpine AS production

# Non-root user for security
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

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

# Switch to non-root user
USER nodejs

# Environment variables
ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

# Startup command
CMD ["node", "dist/server.js"]

이 접근 방식은 빌드 도구, devDependencies 및 소스 파일을 제외하여 최종 이미지 크기를 크게 줄입니다.

bash
# terminal
# Build with specific file
docker build -f Dockerfile.production -t my-app:production .

# Compare image sizes
docker images | grep my-app
# my-app    production    abc123    150MB
# my-app    1.0           def456    450MB

크기 감소는 프로젝트에 따라 60-70%에 달할 수 있으며, 배포 시간을 개선하고 공격 표면을 줄입니다.

로컬 오케스트레이션을 위한 Docker Compose

Docker Compose는 멀티 컨테이너 애플리케이션 관리를 간소화합니다. YAML 파일로 모든 서비스, 구성 및 의존성을 선언합니다.

yaml
# docker-compose.yml
version: "3.9"

services:
  # Main application
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    volumes:
      # Mount source code for hot-reload
      - ./src:/app/src
      - ./package.json:/app/package.json
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    networks:
      - app-network

  # PostgreSQL database
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      # Data persistence
      - postgres_data:/var/lib/postgresql/data
      # Initialization script
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  # Redis cache
  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    networks:
      - app-network

# Named volumes for persistence
volumes:
  postgres_data:
  redis_data:

# Dedicated network for isolation
networks:
  app-network:
    driver: bridge

서비스는 Docker 내부 네트워크를 통해 이름(db, cache)으로 통신합니다. 헬스체크는 애플리케이션 시작 전에 의존성이 준비되었는지 확인합니다.

bash
# terminal
# Start all services
docker compose up -d

# View logs from all services
docker compose logs -f

# Logs from a specific service
docker compose logs -f app

# Stop and remove containers
docker compose down

# Removal including volumes (caution: data loss)
docker compose down -v

# Rebuild after Dockerfile changes
docker compose up -d --build

DevOps 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

시크릿 및 환경 변수 관리

시크릿의 안전한 관리는 프로덕션에서 매우 중요합니다. Docker는 상황에 따라 여러 접근 방식을 제공합니다.

yaml
# docker-compose.override.yml (development only)
version: "3.9"

services:
  app:
    env_file:
      - .env.development
    environment:
      - DEBUG=true

프로덕션에서는 Docker secrets가 더 높은 보안을 제공합니다.

yaml
# docker-compose.production.yml
version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.production
    secrets:
      - db_password
      - api_key
    environment:
      - NODE_ENV=production
      - DATABASE_PASSWORD_FILE=/run/secrets/db_password
      - API_KEY_FILE=/run/secrets/api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt

애플리케이션 코드는 마운트된 파일에서 시크릿을 읽습니다.

config/secrets.jsjavascript
const fs = require('fs');
const path = require('path');

// Utility function to read Docker secrets
function readSecret(secretName) {
  const secretPath = `/run/secrets/${secretName}`;

  // Check if secret file exists
  if (fs.existsSync(secretPath)) {
    return fs.readFileSync(secretPath, 'utf8').trim();
  }

  // Fallback to classic environment variables
  const envVar = secretName.toUpperCase();
  return process.env[envVar];
}

module.exports = {
  databasePassword: readSecret('db_password'),
  apiKey: readSecret('api_key'),
};

이 접근 방식은 환경 변수나 Docker 이미지에서 시크릿이 노출되는 것을 방지합니다.

Docker 이미지 최적화

여러 기법으로 이미지 크기를 줄이고 성능을 향상시킬 수 있습니다.

dockerfile
# Dockerfile.optimized
FROM node:22-alpine AS base

# Install necessary tools in a single layer
RUN apk add --no-cache \
    dumb-init \
    && rm -rf /var/cache/apk/*

# ============================================
# Stage: Dependencies
# ============================================
FROM base AS deps

WORKDIR /app

# Copy only lock files for caching
COPY package.json package-lock.json ./

# Install with mounted npm cache (BuildKit)
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

# ============================================
# Stage: Builder
# ============================================
FROM base AS builder

WORKDIR /app

COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

COPY . .
RUN npm run build

# ============================================
# Stage: Production
# ============================================
FROM base AS production

# Image metadata
LABEL maintainer="team@example.com"
LABEL version="1.0"
LABEL description="Production-ready Node.js application"

# Non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# Copy production dependencies
COPY --from=deps --chown=nodejs:nodejs /app/node_modules ./node_modules

# Copy build output
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

USER nodejs

ENV NODE_ENV=production

# dumb-init as PID 1 for signal handling
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]

dumb-init을 사용하면 Unix 시그널의 올바른 처리가 보장되어 컨테이너의 그레이스풀 셧다운이 가능해집니다.

bash
# terminal
# Enable BuildKit for advanced features
export DOCKER_BUILDKIT=1

# Build with cache and detailed output
docker build --progress=plain -t my-app:optimized .

# Analyze image layers
docker history my-app:optimized

# Detailed image inspection
docker inspect my-app:optimized
이미지 보안

이미지는 Trivy나 Snyk과 같은 도구를 사용하여 정기적으로 취약점 스캔을 수행해야 합니다. 베이스 이미지는 보안 패치를 포함하기 위해 정기적으로 업데이트해야 합니다.

고급 Docker 네트워킹

Docker는 다양한 사용 사례를 위한 여러 네트워크 드라이버를 제공합니다.

yaml
# docker-compose.networking.yml
version: "3.9"

services:
  # Publicly accessible frontend
  frontend:
    build: ./frontend
    ports:
      - "80:80"
    networks:
      - frontend-network
      - backend-network

  # API accessible only from frontend
  api:
    build: ./api
    networks:
      - backend-network
      - database-network
    # No external ports exposed

  # Isolated database
  database:
    image: postgres:16-alpine
    networks:
      - database-network
    # Accessible only by API

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

이 구성은 최소 권한 원칙에 따라 서비스를 격리합니다. 데이터베이스는 API에서만 접근할 수 있습니다.

bash
# terminal
# Inspect Docker networks
docker network ls

# Details of a specific network
docker network inspect app-network

# Create a custom network
docker network create --driver bridge --subnet 172.28.0.0/16 custom-network

# Connect a container to an existing network
docker network connect custom-network my-container

볼륨과 데이터 영속성

Docker 볼륨은 컨테이너 수명 주기를 넘어 데이터를 보존합니다.

yaml
# docker-compose.volumes.yml
version: "3.9"

services:
  app:
    image: my-app:latest
    volumes:
      # Named volume for persistent data
      - app_data:/app/data
      # Bind mount for development
      - ./uploads:/app/uploads:rw
      # Read-only mount for configuration
      - ./config:/app/config:ro

  backup:
    image: alpine
    volumes:
      # Access same volume for backups
      - app_data:/data:ro
      - ./backups:/backups
    command: |
      sh -c "tar czf /backups/backup-$$(date +%Y%m%d).tar.gz /data"

volumes:
  app_data:
    driver: local
    driver_opts:
      type: none
      device: /path/to/host/data
      o: bind

명명된 볼륨과 바인드 마운트의 구분은 중요합니다. 볼륨은 Docker가 관리하고 바인드 마운트는 호스트 파일 시스템을 직접 사용합니다.

bash
# terminal
# List volumes
docker volume ls

# Inspect a volume
docker volume inspect app_data

# Remove orphaned volumes
docker volume prune

# Backup a volume
docker run --rm -v app_data:/data -v $(pwd):/backup alpine \
  tar czf /backup/volume-backup.tar.gz /data

프로덕션 배포

견고한 배포 워크플로우에는 빌드, 테스트 및 레지스트리로의 푸시가 포함됩니다.

bash
# deploy.sh
#!/bin/bash
set -e

# Variables
REGISTRY="registry.example.com"
IMAGE_NAME="my-app"
VERSION=$(git describe --tags --always)

echo "Building version: $VERSION"

# Build production image
docker build \
  -f Dockerfile.production \
  -t $REGISTRY/$IMAGE_NAME:$VERSION \
  -t $REGISTRY/$IMAGE_NAME:latest \
  --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
  --build-arg VERSION=$VERSION \
  .

# Security scan
echo "Running security scan..."
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image $REGISTRY/$IMAGE_NAME:$VERSION

# Push to registry
echo "Pushing to registry..."
docker push $REGISTRY/$IMAGE_NAME:$VERSION
docker push $REGISTRY/$IMAGE_NAME:latest

echo "Deployment complete: $REGISTRY/$IMAGE_NAME:$VERSION"

서버 배포를 위해 별도의 프로덕션 compose 파일이 구성을 조정합니다.

yaml
# docker-compose.prod.yml
version: "3.9"

services:
  app:
    image: registry.example.com/my-app:latest
    restart: always
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: "0.5"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 256M
      update_config:
        parallelism: 1
        delay: 10s
        failure_action: rollback
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

이 구성은 안정적인 배포를 위한 리소스 할당, 업데이트 전략 및 헬스체크를 정의합니다.

bash
# terminal
# Production deployment with Docker Compose
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Zero-downtime update (rolling update)
docker compose -f docker-compose.yml -f docker-compose.prod.yml pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps app

# Rollback if issues occur
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps \
  --scale app=0 && \
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps app

컨테이너 모니터링 및 디버깅

컨테이너 모니터링은 프로덕션에서 필수적입니다.

bash
# terminal
# Real-time statistics for all containers
docker stats

# Statistics for a specific container with custom format
docker stats my-app --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"

# Inspect processes in a container
docker top my-app

# Real-time Docker events
docker events --filter container=my-app

# Copy files from/to a container
docker cp my-app:/app/logs/error.log ./error.log

심층 디버깅을 위해 여러 기법을 사용할 수 있습니다.

bash
# terminal
# Interactive shell in a running container
docker exec -it my-app sh

# Execute a single command
docker exec my-app cat /app/config/settings.json

# Start a container in debug mode
docker run -it --rm --entrypoint sh my-app:latest

# Inspect environment variables
docker exec my-app printenv

# Analyze logs with filters
docker logs my-app --since 1h --tail 100 | grep ERROR
yaml
# docker-compose.monitoring.yml
version: "3.9"

services:
  app:
    # ... existing configuration
    labels:
      - "prometheus.scrape=true"
      - "prometheus.port=3000"
      - "prometheus.path=/metrics"

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=15d'

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3001:3000"
    volumes:
      - grafana_data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=secret

volumes:
  prometheus_data:
  grafana_data:

이 모니터링 스택은 컨테이너 메트릭의 수집과 시각화를 가능하게 합니다.

결론

Docker는 모든 환경에서의 일관성을 보장하여 개발 주기를 혁신합니다. 컨테이너화는 이식성, 격리 및 재현성을 제공합니다. 이는 현대 애플리케이션에 필수적인 특성입니다.

프로덕션을 위한 Docker 체크리스트

  • ✅ 최적화된 이미지를 위한 멀티스테이지 빌드
  • ✅ 컨테이너 내 non-root 사용자
  • ✅ 모든 서비스에 헬스체크 구성
  • ✅ Docker secrets 또는 안전한 환경 변수로 시크릿 관리
  • ✅ 리소스 제한(CPU, 메모리) 정의
  • ✅ 중요 데이터 영속성을 위한 볼륨
  • ✅ 파일 로테이션 기능의 중앙 집중식 로깅
  • ✅ 배포 전 이미지 보안 스캔
  • ✅ 다운타임 없는 업데이트 전략
  • ✅ 서비스 간 격리된 네트워크

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

Docker 숙달은 모든 현대 개발자에게 기본적인 기술입니다. 로컬 환경에서 프로덕션 배포까지 Docker는 워크플로우를 표준화하고 운영을 간소화합니다. 여기서 제시된 개념들은 Kubernetes와 대규모 컨테이너 오케스트레이션을 탐구하기 위한 견고한 기반을 형성합니다.

태그

#docker
#containerization
#devops
#docker compose
#deployment

공유

관련 기사