Quay lại blog
Tháng Tám 19, 2026Hướng dẫn

Cách triển khai ứng dụng bằng Docker Compose trên VPS

Hướng dẫn production thực tế cho Docker Compose v2 trên Ubuntu: cài engine, viết tệp Compose với mạng và volume, xử lý bí mật, đặt reverse proxy phía trước, cập nhật không hoảng, và sao lưu volume có tên.

Cách triển khai ứng dụng bằng Docker Compose trên VPS

Docker Compose là cách thường gặp để chạy một ngăn xếp nhỏ trên một VPS: ứng dụng web, cơ sở dữ liệu, bộ nhớ đệm Redis, và reverse proxy, mô tả trong một tệp YAML. Nó không phải Kubernetes. Bạn sẽ không có chuyển dự phòng nhiều nút. Bạn sẽ có thiết lập lặp lại được, xem xét được, có thể tạo lại trên VPS Hiddence mới trong vài phút — đúng thứ hầu hết dự án phụ và sản phẩm nhỏ cần.

Bài này giả định bạn muốn thứ gần production hơn docker run tối thứ Sáu. Chúng ta sẽ cài Docker Engine và plugin Compose, tạo người dùng triển khai không phải root, viết tệp Compose với healthcheck và chính sách khởi động lại, giữ bí mật ngoài Git, chỉ công bố cổng reverse proxy ra internet, và định nghĩa quy trình cập nhật cùng sao lưu. Ngăn xếp ví dụ là ứng dụng web điển hình cộng PostgreSQL cộng Caddy, nhưng cùng mẫu hoạt động với Node, PHP, Python, hoặc một đám worker.

Vì sao Compose trên một VPS vẫn hợp lý

Người ta nhảy sang Kubernetes vì thời thượng, rồi tiêu cả cuối tuần cho YAML của một website. Compose vẫn đọc được. Bạn phiên bản hóa tệp, ghi môi trường, và có thể diff thay đổi trước khi áp. Cô lập đủ tốt: container ứng dụng bị xâm nhập không nên giữ socket UNIX của cơ sở dữ liệu nếu bạn dùng mạng và người dùng quyền tối thiểu. Giới hạn tài nguyên ngăn một rò bộ nhớ đóng băng cả VPS.

  • Một tệp mô tả cả ngăn xếp
  • Volume có tên sống sót khi tạo lại container
  • Mạng Docker nội bộ giữ Postgres khỏi internet công cộng
  • restart: unless-stopped phủ hầu hết lần khởi động lại
  • Dễ sao sang VPS thứ hai khi bạn vượt chiếc đầu

Yêu cầu

Dùng VPS đủ RAM cho ứng dụng cộng Postgres. Gói 2 GB là mức tối thiểu thực tế cho ứng dụng + cơ sở dữ liệu + proxy. Giả định Ubuntu 24.04. Đừng cài Docker từ Snap ngẫu nhiên mà không đọc nó làm gì với cgroups; các bước dưới dùng kho apt chính thức của Docker.

  • Ubuntu 22.04/24.04, root hoặc sudo
  • Khuyến nghị 2 GB RAM (1 GB chỉ cho ngăn xếp rất nhỏ không Postgres)
  • Tên miền trỏ tới VPS nếu muốn HTTPS tự động
  • Git hoặc cách khác để chép dự án lên máy chủ

Bước 1: Cài Docker Engine và Compose v2

Compose v2 là plugin gọi bằng docker compose (có khoảng trắng), không phải nhị phân Python docker-compose cũ. Cài cả hai từ kho Docker để nhận bản vá bảo mật hiện tại.

bash
ssh root@YOUR_VPS_IP
apt update && apt -y install ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null

apt update
apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

docker version
docker compose version

Bước 2: Người dùng triển khai và bố cục thư mục

Chạy Compose bằng root được, nhưng người dùng riêng thuộc nhóm docker sạch hơn. Đặt dự án trong /opt hoặc /srv, không phải /root, để sao lưu và quyền rõ ràng. Đừng bao giờ cho cả thế giới ghi thư mục chứa .env.

bash
adduser --disabled-password --gecos '' deploy
usermod -aG docker deploy
mkdir -p /srv/app
chown deploy:deploy /srv/app
chmod 750 /srv/app

# Log in as deploy for the rest of the file editing:
# su - deploy
# cd /srv/app

Bước 3: Viết tệp Compose mang tư duy production

Tệp dưới là mẫu. Thay image ứng dụng bằng của bạn. Postgres không được công bố ra 0.0.0.0:5432 — chỉ Caddy công bố trên 80 và 443. Healthcheck ngăn reverse proxy gửi lưu lượng tới ứng dụng vẫn đang migrate cơ sở dữ liệu. Ghim thẻ image; latest là cách hỏng bất ngờ xảy ra.

bash
# /srv/app/compose.yaml
services:
  caddy:
    image: caddy:2.8-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      app:
        condition: service_healthy

  app:
    image: ghcr.io/example/webapp:1.4.2
    restart: unless-stopped
    env_file: .env
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/health"]
      interval: 10s
      timeout: 3s
      retries: 10
    networks:
      - frontend
      - backend

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    env_file: .env
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - backend

networks:
  frontend: {}
  backend: {}

volumes:
  pgdata:
  caddy_data:
  caddy_config:

Bước 4: Bí mật, .env và Caddyfile

Đặt mật khẩu trong .env trên máy chủ, chmod 600, và đừng bao giờ commit tệp đó. Sinh mật khẩu bằng openssl rand -base64 32. Caddyfile chỉ cần reverse-proxy tới tên dịch vụ ứng dụng trên mạng Docker.

bash
# /srv/app/.env (permissions 600)
POSTGRES_USER=app
POSTGRES_PASSWORD=change-me-long-random
POSTGRES_DB=app
# plus any APP_SECRET / NEXTAUTH_SECRET your image needs

# /srv/app/Caddyfile
app.example.com {
    encode gzip
    reverse_proxy app:8000
}

chmod 600 /srv/app/.env
cd /srv/app
docker compose up -d
docker compose ps
docker compose logs -f --tail=100

Bước 5: Tường lửa, nhật ký và giới hạn tài nguyên

UFW nên chỉ cho phép 22, 80 và 443. Docker đôi khi vượt UFW với cổng đã công bố; nếu cần tường lửa máy chủ nghiêm, tra tương tác Docker + UFW hiện tại cho phiên bản Ubuntu của bạn, hoặc chỉ công bố cổng trên 127.0.0.1 rồi đặt Caddy máy chủ phía trước. Đặt giới hạn bộ nhớ trong Compose để Postgres không ăn hết VPS.

bash
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

# Optional in compose.yaml under app or db:
#    deploy:
#      resources:
#        limits:
#          memory: 512M

# Logs (do not let json-file grow forever):
# { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
# in /etc/docker/daemon.json then: systemctl restart docker

Bước 6: Cập nhật và rollback

Cập nhật nhàm chán mà hiệu quả: kéo image mới, up -d, theo dõi healthcheck, giữ thẻ trước trong git để ghim lại. Đừng docker compose down trên cơ sở dữ liệu production trừ khi bạn định ngừng nhận lưu lượng. down -v xóa volume — đó là xóa sạch, không phải khởi động lại.

bash
cd /srv/app
git pull   # if the Compose file lives in git

# Change the image tag in compose.yaml, then:
docker compose pull
docker compose up -d
docker compose ps

# Rollback: set the old tag, pull, up -d again
# NEVER: docker compose down -v   # this deletes named volumes

Sao lưu volume có tên

Snapshot VPS thì tốt. Dump logic Postgres tốt hơn nếu bạn cần khôi phục sang máy khác. Lên lịch bằng cron. Thử khôi phục một lần, không thì bạn không có bản sao lưu — bạn có tệp hy vọng là bản sao lưu.

bash
# Postgres dump while the db container is running:
docker compose exec -T db pg_dump -U app app | gzip > /var/backups/app-$(date +%F).sql.gz

# Copy off-box
# scp /var/backups/app-*.sql.gz backup-host:~

# Restore sketch (maintenance window):
# gunzip -c app-2026-08-19.sql.gz | docker compose exec -T db psql -U app app

Khắc phục sự cố

Đọc docker compose ps và docker compose logs service. Kéo image thất bại thường là giới hạn tốc độ ghcr/Docker Hub hoặc image riêng không đăng nhập. 502 từ Caddy nghĩa là ứng dụng chưa khỏe hoặc hostname proxy sai (dùng tên dịch vụ Compose, không phải localhost, từ trong container Caddy). Lỗi quyền trên volume thường nghĩa image chạy uid 1000 trong khi thư mục máy chủ là root.

  • compose: command not found — bạn cài tên nhị phân cũ; dùng docker compose
  • port is already allocated — thứ khác chiếm 80/443 (Apache, Nginx, Caddy khác)
  • database connection refused — ứng dụng khởi động trước khi Postgres sẵn sàng; dùng healthcheck + depends_on condition
  • đĩa đầy — docker system df, rồi prune image không dùng một cách cẩn thận
  • permission denied trên docker.sock — người dùng không trong nhóm docker, hoặc cần phiên đăng nhập mới

Bảo mật

Đừng công bố cổng cơ sở dữ liệu. Đừng chạy privileged: true. Đừng gắn /var/run/docker.sock vào container ứng dụng trừ khi bạn cố ý viết trình quản lý container. Giữ engine cập nhật. Quét image của bạn nếu tự dựng. .env trên đĩa vẫn là tệp bí mật — hạn chế ai SSH được vào máy.

  • Không có 5432 / 3306 / 6379 công cộng
  • Ghim digest image hoặc ít nhất thẻ bất biến
  • chmod 600 .env
  • Unattended-upgrades cho OS máy chủ vẫn quan trọng
  • Một dự án Compose mỗi ứng dụng giữ bán kính nổ nhỏ hơn

Mẹo

  • Đặt compose.yaml trong git; giữ .env.example không có bí mật thật
  • Dùng profiles cho worker tùy chọn (docker compose --profile workers up -d)
  • Xem docker stats khi chọn kích thước VPS
  • Bind-mount mã nguồn không phải mẫu production — nướng image
  • Khi ngăn xếp lớn tới vài VPS, lúc đó mới xem điều phối — không trước đó

Thiết lập Compose gần production trên một VPS là: gói Docker chính thức, .env đã khóa, tệp YAML không công bố cơ sở dữ liệu, healthcheck, reverse proxy trên 80/443, UFW, thẻ image có thể rollback, và dump Postgres bạn đã thực sự khôi phục một lần. Bắt đầu từ mẫu trong bài này, thay image ứng dụng, và coi docker compose down -v như lệnh phá hủy với cùng sự tôn trọng như rm -rf.