DeployEasy
Menu
DockerTrung bình

Docker Compose production: App, PostgreSQL, healthcheck và backup

Hướng dẫn thiết kế Docker Compose production với app, PostgreSQL, network, volume, healthcheck, secret, restart và deploy an toàn.

· 6 phút đọc · 1.230 từ
Mục lục bài viết

Docker Compose giúp định nghĩa nhiều service, network và volume trong một file YAML. Trên VPS nhỏ, Compose là cách thực tế để chạy API, database và service phụ trợ.

Tuy nhiên, file Compose development không nên được đưa nguyên trạng lên production.

Kiến trúc mẫu

Internet

Nginx trên host :443

API container :3000
   ↓ private network
PostgreSQL container :5432

Database không public port ra Internet.

Cấu trúc dự án

deployeasy-api/
├── Dockerfile
├── compose.yaml
├── .env
├── .env.example
├── package.json
├── package-lock.json
└── src/

.gitignore:

.env
.env.*
!.env.example

Dockerfile multi-stage

Ví dụ NestJS:

FROM node:22-alpine AS deps

WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:22-alpine AS builder

WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:22-alpine AS runner

WORKDIR /app
ENV NODE_ENV=production

COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force

COPY --from=builder /app/dist ./dist

USER node

EXPOSE 3000

CMD ["node", "dist/main.js"]

Kiểm tra output thực tế. Monorepo có thể cần copy thêm file.

.dockerignore

node_modules
dist
.git
.env
.env.*
*.log
coverage
Dockerfile*
compose*.yaml
README.md

Không đưa secret và private key vào build context.

Compose production

name: deployeasy

services:
  database:
    image: postgres:18
    restart: unless-stopped

    environment:
      POSTGRES_DB: ${DATABASE_NAME}
      POSTGRES_USER: ${DATABASE_USER}
      POSTGRES_PASSWORD: ${DATABASE_PASSWORD}

    volumes:
      - postgres-data:/var/lib/postgresql/data

    networks:
      - backend

    healthcheck:
      test:
        [
          "CMD-SHELL",
          "pg_isready -U ${DATABASE_USER} -d ${DATABASE_NAME}",
        ]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s

  api:
    build:
      context: .
      dockerfile: Dockerfile

    restart: unless-stopped

    environment:
      NODE_ENV: production
      PORT: 3000
      DATABASE_HOST: database
      DATABASE_PORT: 5432
      DATABASE_NAME: ${DATABASE_NAME}
      DATABASE_USER: ${DATABASE_USER}
      DATABASE_PASSWORD: ${DATABASE_PASSWORD}

    depends_on:
      database:
        condition: service_healthy

    ports:
      - "127.0.0.1:3000:3000"

    networks:
      - backend

    healthcheck:
      test:
        [
          "CMD",
          "node",
          "-e",
          "fetch('http://127.0.0.1:3000/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))",
        ]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s

volumes:
  postgres-data:

networks:
  backend:
    driver: bridge

Compose quản lý services, networks và volumes. healthcheck xác định container có healthy hay không.

File .env

DATABASE_NAME=deployeasy
DATABASE_USER=deployeasy
DATABASE_PASSWORD=CHANGE_ME_WITH_A_LONG_RANDOM_VALUE

Permission:

chmod 600 .env

Kiểm tra:

docker compose config

Lệnh có thể hiển thị secret sau khi thay biến; không đăng output công khai.

Vì sao database không có ports?

API truy cập database qua network nội bộ:

database:5432

Không cần:

ports:
  - "5432:5432"

Điều này giảm bề mặt tấn công.

Service name là DNS nội bộ

Trong API:

DATABASE_HOST=database

Không dùng:

DATABASE_HOST=localhost

localhost trong API container là chính API container.

depends_on và healthcheck

depends_on:
  database:
    condition: service_healthy

Compose chờ dependency đạt healthy theo cấu hình được hỗ trợ. Ứng dụng vẫn nên có retry vì database có thể mất kết nối sau khi app đã chạy.

Restart policy

restart: unless-stopped

Các policy thường gặp:

no
always
on-failure
unless-stopped

Restart không chữa lỗi lặp vô hạn. Kiểm tra restart count:

docker inspect \
  --format='{{.RestartCount}}' \
  CONTAINER_NAME

Resource limit

Tùy phiên bản Compose:

services:
  api:
    mem_limit: 512m
    cpus: 1.0

Kiểm tra:

docker compose config
docker stats

Limit quá thấp gây OOM; quá cao không bảo vệ service khác.

Log rotation

services:
  api:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Ứng dụng nên log ra stdout/stderr.

Build và chạy

docker compose config
docker compose build
docker compose up -d

Kiểm tra:

docker compose ps
docker compose logs -f api

Health:

docker inspect \
  --format='{{json .State.Health}}' \
  CONTAINER_NAME | jq

Nginx trên host

server {
    listen 80;
    server_name api.deployeasy.vn;

    location / {
        proxy_pass http://127.0.0.1:3000;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

API chỉ publish localhost:

ports:
  - "127.0.0.1:3000:3000"

Migration database

Không để mọi API replica tự chạy migration cùng lúc.

Tạo service:

services:
  migrate:
    build:
      context: .
    command: npm run migration:run
    environment:
      DATABASE_HOST: database
      DATABASE_PORT: 5432
      DATABASE_NAME: ${DATABASE_NAME}
      DATABASE_USER: ${DATABASE_USER}
      DATABASE_PASSWORD: ${DATABASE_PASSWORD}
    depends_on:
      database:
        condition: service_healthy
    networks:
      - backend
    restart: "no"

Chạy:

docker compose run --rm migrate

Migration production cần:

  • Backup.
  • Kiểm tra lock.
  • Tương thích code cũ/mới.
  • Rollback hoặc forward fix.
  • Không dùng synchronize: true.

Backup PostgreSQL

mkdir -p backups

docker compose exec -T database \
  pg_dump \
  -U "$DATABASE_USER" \
  -d "$DATABASE_NAME" \
  -Fc \
  > "backups/deployeasy-$(date +%F-%H%M%S).dump"

Nếu biến chỉ nằm trong .env, shell host chưa chắc đã export chúng. Có thể dùng script đọc biến một cách kiểm soát.

Backup phải được copy ra nơi khác.

Restore thử nghiệm

docker compose exec database \
  createdb -U deployeasy deployeasy_restore_test
docker compose exec -T database \
  pg_restore \
  -U deployeasy \
  -d deployeasy_restore_test \
  --clean \
  --if-exists \
  < backups/file.dump

Không test restore trực tiếp vào production.

Update app

Nếu build trên VPS:

git pull --ff-only
docker compose build api
docker compose run --rm migrate
docker compose up -d --no-deps api

Kiểm tra:

docker compose ps
docker compose logs --tail=200 api
curl -f http://127.0.0.1:3000/health

VPS RAM thấp nên build trong CI rồi pull image.

Dùng image có version

services:
  api:
    image: ghcr.io/username/deployeasy-api:1.2.3

Deploy:

docker compose pull api
docker compose up -d api

Rollback bằng tag cũ.

Không chỉ dùng latest.

Tránh mất volume

docker compose down

mặc định không xóa named volume.

docker compose down -v

có thể xóa volume và dữ liệu.

Kiểm tra:

docker volume ls
docker compose config --volumes

Dọn Docker

docker system df
docker image prune
docker builder prune

Không chạy docker system prune -a --volumes trên production nếu chưa hiểu tài nguyên sẽ bị xóa.

Secret

  • Không commit.
  • Không build vào image.
  • Hạn chế permission file.
  • Tách staging/production.
  • Xoay khi lộ.
  • Dùng secret manager khi hệ thống lớn.

Security cơ bản

  • Chạy app non-root.
  • Không mount Docker socket.
  • Không dùng privileged: true.
  • Chỉ publish port cần thiết.
  • Pin image tag.
  • Update base image.
  • Backup volume.
  • Firewall trên host.

Checklist

[ ] Dockerfile multi-stage
[ ] .dockerignore đúng
[ ] Không secret trong image
[ ] App chạy non-root
[ ] Database dùng volume
[ ] Database không public
[ ] Healthcheck app và DB
[ ] Restart policy
[ ] Log rotation
[ ] Migration service riêng
[ ] Backup trước migration
[ ] Restore đã test
[ ] Image có version
[ ] Rollback bằng tag cũ
[ ] Nginx proxy localhost

Câu hỏi thường gặp

Compose có phù hợp production không?

Có thể phù hợp với hệ thống nhỏ trên một host nếu được vận hành cẩn thận. Nó không tự cung cấp high availability đa node.

Xóa container database có mất dữ liệu không?

Nếu named volume còn, dữ liệu thường vẫn còn. Nhưng volume không phải backup.

restart: always có đủ monitoring không?

Không. Container có thể restart liên tục mà không ai biết.

Kết luận

Docker Compose production cần healthcheck, volume, backup, log rotation, version image, migration và rollback; không chỉ docker compose up -d.

Nguồn tham khảo

Đọc tiếp