Docker Best Practices cho Production

Tối ưu hóa Docker images và containers cho môi trường production. Security, performance, và reliability best practices

Docker Best Practices cho Production

Docker Best Practices cho Production

Docker đã revolutionize cách chúng ta deploy applications. Nhưng để run Docker trong production an toàn và hiệu quả, bạn cần follow best practices.

1. Multi-Stage Builds

Multi-stage builds giúp giảm image size đáng kể.

Bad Practice

# ❌ Image rất lớn vì chứa build tools
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["npm", "start"]
 
# Image size: 1.2 GB

Best Practice

# ✅ Multi-stage build
# Stage 1: Build
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
 
# Stage 2: Production
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
 
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
 
# Image size: 150 MB (8x smaller!)

2. Use Specific Tags

Bad Practice

# ❌ "latest" tag không predictable
FROM node:latest

Best Practice

# ✅ Specific version tag
FROM node:18.17.1-alpine3.18
 
# Hoặc sử dụng digest cho immutability
FROM node@sha256:abc123...

3. Optimize Layer Caching

Docker caches layers. Optimize để maximize cache hits.

Bad Practice

# ❌ Copy all files trước khi install dependencies
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
# Mỗi lần code change, phải reinstall tất cả dependencies!

Best Practice

# ✅ Copy package files trước, tận dụng cache
FROM node:18-alpine
WORKDIR /app
 
# Copy dependency files first
COPY package*.json ./
RUN npm ci --only=production
 
# Then copy application code
COPY . .
 
# Code changes không invalidate dependency layer!

4. Minimize Image Size

Choose Slim Base Images

# Image size comparison:
FROM node:18          # 996 MB
FROM node:18-slim     # 234 MB (4.2x smaller)
FROM node:18-alpine   # 171 MB (5.8x smaller) ✅

Remove Unnecessary Files

# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.vscode
coverage/
.DS_Store
*.log

Clean Up in Same Layer

# ❌ Bad: Cleanup in different layer
RUN apt-get update
RUN apt-get install -y wget
RUN wget http://example.com/file
RUN rm -rf /var/lib/apt/lists/*  # Không giảm size!
 
# ✅ Good: Cleanup trong cùng layer
RUN apt-get update && \
    apt-get install -y --no-install-recommends wget && \
    wget http://example.com/file && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

5. Security Best Practices

Don't Run as Root

# ❌ Bad: Running as root
FROM node:18-alpine
WORKDIR /app
COPY . .
CMD ["node", "app.js"]
 
# ✅ Good: Create and use non-root user
FROM node:18-alpine
WORKDIR /app
 
# Create user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001
 
COPY --chown=nodejs:nodejs . .
 
USER nodejs
CMD ["node", "app.js"]

Scan for Vulnerabilities

# Scan image for vulnerabilities
docker scan myimage:latest
 
# Use Trivy
trivy image myimage:latest
 
# Use Snyk
snyk container test myimage:latest

Use Read-Only Filesystem

# Make filesystem read-only
FROM node:18-alpine
WORKDIR /app
COPY . .
 
# Use tmpfs for writable directories
VOLUME ["/tmp"]
 
USER node
CMD ["node", "app.js"]
# Run container with read-only filesystem
docker run --read-only --tmpfs /tmp myimage:latest

6. Health Checks

Add Dockerfile HEALTHCHECK

FROM node:18-alpine
WORKDIR /app
COPY . .
 
EXPOSE 3000
 
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD node healthcheck.js
    
CMD ["node", "app.js"]
// healthcheck.js
const http = require('http');
 
const options = {
    host: 'localhost',
    port: 3000,
    path: '/health',
    timeout: 2000
};
 
const request = http.request(options, (res) => {
    if (res.statusCode === 200) {
        process.exit(0);
    } else {
        process.exit(1);
    }
});
 
request.on('error', () => process.exit(1));
request.end();

7. Environment Variables

Bad Practice

# ❌ Hardcode secrets
FROM node:18-alpine
ENV DATABASE_PASSWORD=mysecretpassword

Best Practice

# ✅ Use ARG và ENV properly
FROM node:18-alpine
 
# Build-time variable
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
 
# Runtime will override from docker-compose or -e flag
ENV DATABASE_URL=
ENV API_KEY=
 
CMD ["node", "app.js"]
# Pass secrets at runtime
docker run -e DATABASE_PASSWORD=secret myimage
 
# Or use Docker secrets
docker secret create db_password password.txt
docker service create --secret db_password myimage

8. Logging Best Practices

Log to STDOUT/STDERR

FROM node:18-alpine
WORKDIR /app
COPY . .
 
# Don't write logs to files inside container
# Let Docker handle log collection
CMD ["node", "app.js"]
// app.js
// ✅ Log to stdout
console.log('Application started');
console.error('Error occurred');
 
// ❌ Don't write to files
// fs.appendFileSync('/var/log/app.log', 'message');

Configure Log Drivers

# docker-compose.yml
services:
  app:
    image: myapp:latest
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

9. Resource Limits

Set Memory và CPU Limits

# docker-compose.yml
services:
  app:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
# Docker run
docker run -m 512m --cpus="0.5" myimage

10. Complete Production Example

Dockerfile

# Multi-stage build for Node.js app
FROM node:18.17.1-alpine3.18 AS builder
 
WORKDIR /app
 
# Install dependencies
COPY package*.json ./
RUN npm ci --only=production && \
    npm cache clean --force
 
# Build application
COPY . .
RUN npm run build
 
# Production stage
FROM node:18.17.1-alpine3.18
 
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
 
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001
 
WORKDIR /app
 
# Copy built application
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs package*.json ./
 
# Set environment
ENV NODE_ENV=production
 
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
    CMD node healthcheck.js || exit 1
 
# Switch to non-root user
USER nodejs
 
EXPOSE 3000
 
# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]

docker-compose.yml

version: '3.8'
 
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    image: myapp:1.0.0
    container_name: myapp
    restart: unless-stopped
    
    # Environment variables
    environment:
      - NODE_ENV=production
      - PORT=3000
    
    # Secrets (use Docker secrets in production)
    env_file:
      - .env.production
    
    # Resource limits
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1G
        reservations:
          cpus: '0.5'
          memory: 512M
    
    # Logging
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    
    # Network
    networks:
      - app-network
    
    # Ports
    ports:
      - "3000:3000"
    
    # Security
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp
 
networks:
  app-network:
    driver: bridge

.dockerignore

# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Testing
coverage
.nyc_output
test

# Development
.vscode
.idea
.git
.gitignore

# Environment
.env
.env.local
.env.*.local

# Build
dist
build

# Documentation
README.md
CHANGELOG.md
docs

# Misc
.DS_Store
*.log

11. Monitoring và Observability

Export Metrics

// Prometheus metrics
const prometheus = require('prom-client');
 
const register = new prometheus.Registry();
prometheus.collectDefaultMetrics({ register });
 
app.get('/metrics', (req, res) => {
    res.set('Content-Type', register.contentType);
    res.end(register.metrics());
});

Container Stats

# Monitor container resources
docker stats
 
# Get detailed info
docker inspect mycontainer

12. CI/CD Integration

# .github/workflows/docker.yml
name: Docker Build and Push
 
on:
  push:
    branches: [main]
    tags: ['v*']
 
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: myregistry/myapp
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
      
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
      
      - name: Scan image
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ steps.meta.outputs.tags }}
          severity: 'CRITICAL,HIGH'

Checklist

Trước khi deploy lên production:

  • Multi-stage build để minimize size
  • Specific version tags (không dùng latest)
  • Non-root user
  • Health checks configured
  • Resource limits set
  • Logging to stdout/stderr
  • Secrets không hardcode
  • Image scanned for vulnerabilities
  • .dockerignore configured
  • Read-only filesystem where possible
  • Proper signal handling (dumb-init)
  • Monitoring và metrics exposed

Kết luận

Docker trong production requires discipline và attention to detail:

  • Security first: Non-root, scan vulnerabilities, secrets management
  • Optimize: Multi-stage builds, layer caching, minimal images
  • Reliability: Health checks, resource limits, proper logging
  • Observability: Metrics, logs, monitoring

Follow những best practices này giúp Docker containers chạy stable, secure, và efficient trong production!

7 min read
DockerDevOpsContainersProductionSecurity

Bài viết liên quan