Docker Compose sẵn sàng cho production trên VPS (kèm rootless Docker)
Chạy một container bằng docker run để thử nghiệm thì đơn giản, nhưng đưa nó lên production trên VPS lại là câu chuyện khác: container phải tự khởi động lại sau khi reboot, phải có healthcheck để orchestrator biết nó còn sống, dữ liệu phải nằm trong volume để không bay theo lần docker compose down, và log không được phình to đến mức lấp đầy ổ đĩa. Bài này đi qua toàn bộ vòng đời đó bằng Docker Compose v2, kèm cấu hình rootless Docker để giảm bề mặt tấn công. Toàn bộ số liệu và lệnh đã kiểm tra lại theo trạng thái Docker Engine giữa năm 2026.
Docker Engine và Compose v2 năm 2026: cài đúng cách
Điểm cần nhớ đầu tiên: docker-compose (dấu gạch ngang, viết bằng Python) đã ngừng phát triển. Thứ bạn dùng năm 2026 là Compose v2, một plugin CLI viết bằng Go, gọi qua docker compose (dấu cách). Nó đi kèm khi bạn cài Docker Engine từ repository chính thức, dưới dạng gói docker-compose-plugin. Compose v2 yêu cầu Docker Engine 20.10 trở lên, nhưng trên VPS mới bạn sẽ nhận bản Engine 29.x cùng containerd 2.2.
Cách cài chuẩn là dùng script tiện lợi hoặc thêm repo APT/YUM của Docker. Trên Ubuntu/Debian:
curl -fsSL https://get.docker.com | sh
# Kiểm tra
docker version
docker compose version
Script get.docker.com tự thêm repo chính thức và kéo về đủ docker-ce, docker-ce-cli, containerd.io, docker-buildx-plugin và docker-compose-plugin. Nếu muốn kiểm soát tay từng bước, làm theo hướng dẫn tại docs.docker.com/engine/install và docs.docker.com/compose/install/linux. Tránh cài docker.io từ repo distro mặc định vì bản đó thường cũ và không có plugin Compose v2 đi kèm.
Rootless Docker: vì sao nên bật, và đánh đổi gì
Mặc định, Docker daemon chạy bằng root. Ai vào được socket /var/run/docker.sock gần như có quyền root trên host, vì có thể mount toàn bộ filesystem vào một container. Rootless mode chuyển daemon và container sang chạy dưới một user thường, ánh xạ UID trong container sang một dải UID không đặc quyền trên host qua user namespace. Nếu container bị thoát ra (escape), kẻ tấn công chỉ ở trong không gian của user đó, không phải root host.
Đổi lại có hai điểm cần cân nhắc thật kỹ trước khi chọn:
- Port dưới 1024. User thường không bind được các cổng đặc quyền, nên container không nghe trực tiếp trên 80/443 theo mặc định. Ba cách xử lý: cho phép hạ ngưỡng bằng
sysctl net.ipv4.ip_unprivileged_port_start=0; gán capabilityCAP_NET_BIND_SERVICEcho binaryrootlesskitbằngsetcap; hoặc thực dụng nhất là để container nghe cổng cao (8080/8443) rồi đặt một reverse proxy phía trước. - Hiệu năng mạng. Rootless dùng networking ở userspace (slirp4netns, hoặc
pastamới hơn và nhanh hơn ở nhiều workload). So với bridge mode chạy trong kernel, phần này có overhead đo được: với dịch vụ throughput cao có thể giảm khoảng 10-20% băng thông. Với web app thông thường thì gần như không cảm nhận được. Chọnpastanếu bản Engine của bạn hỗ trợ để thu hẹp khoảng cách này.
Ngoài ra rootless không chạy được privileged container, không load được kernel module, không truy cập trực tiếp thiết bị. Nếu workload cần những thứ đó thì phải chạy rootful.
Cài rootless Docker từng bước
Cài các gói phụ trợ (uidmap cung cấp newuidmap/newgidmap cho user namespace), rồi chạy script setup dưới user thường, KHÔNG dùng sudo:
# Với vai trò root: cài phụ thuộc và tắt daemon rootful nếu có
sudo apt-get install -y uidmap dbus-user-session
sudo systemctl disable --now docker.service docker.socket
# Chuyển sang user thường sẽ chạy container
dockerd-rootless-setuptool.sh install
# Cho phép service của user chạy khi chưa đăng nhập, và daemon start lúc boot
sudo loginctl enable-linger $(whoami)
systemctl --user enable docker
Sau đó trỏ client vào socket rootless bằng biến môi trường trong ~/.bashrc:
export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
docker context use rootless
docker info | grep -i rootless # xác nhận: rootless
Nếu cần mở cổng 80/443 trực tiếp trong rootless, gán capability một lần cho rootlesskit:
sudo setcap cap_net_bind_service=ep $(which rootlesskit)
systemctl --user restart docker
Chi tiết đầy đủ, gồm cả cấu hình slirp4netns và pasta, nằm ở docs.docker.com/engine/security/rootless.
compose.yaml mẫu, sẵn sàng production
Đây là file compose.yaml cho một web app kèm PostgreSQL. Nó gom đủ các yếu tố production: restart policy, healthcheck, named volume, resource limit và cấu hình logging có giới hạn. Lưu ý web app nghe cổng cao 8080 để hợp với rootless.
services:
web:
image: ghcr.io/acme/web:1.8.2 # ghim tag cụ thể, KHÔNG dùng :latest
restart: unless-stopped
ports:
- "8080:8080"
environment:
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 1G
volumes:
pgdata:
Đặt DB_PASSWORD trong file .env cạnh compose.yaml (và thêm .env vào .gitignore). Compose tự nạp biến từ file này.
Từng khối cấu hình có tác dụng gì
Restart policy
Dùng restart: unless-stopped cho service dài hạn. Nó khởi động lại container khi crash và sau khi host reboot, nhưng tôn trọng khi bạn chủ động dừng bằng tay. Đừng dùng always nếu không muốn container tự bật lại kể cả sau khi bạn cố tình stop. Lưu ý deploy.restart_policy chỉ áp dụng cho Swarm; với docker compose up đơn node thì dùng khoá restart ở cấp service như trên.
Healthcheck và depends_on
Healthcheck cho Docker biết container đã sẵn sàng nhận request chứ không chỉ đang chạy. Kết hợp với depends_on: condition: service_healthy, web app chỉ khởi động sau khi Postgres báo healthy, tránh lỗi kết nối lúc boot. start_period cho ứng dụng thời gian khởi động ban đầu mà chưa bị tính là fail. Kiểm tra trạng thái bằng:
docker compose ps
docker inspect --format '{{.State.Health.Status}}' <container>
Named volume
Dữ liệu Postgres nằm trong named volume pgdata, do Docker quản lý, tồn tại độc lập với vòng đời container. docker compose down xoá container nhưng giữ volume; chỉ docker compose down -v mới xoá luôn dữ liệu. Ưu tiên named volume hơn bind mount cho dữ liệu stateful vì quyền và tính di động tốt hơn, đặc biệt trong rootless nơi UID được ánh xạ.
Resource limit và logging
Khối deploy.resources.limits đặt trần CPU và RAM để một container lỗi không kéo sập cả VPS. Trong Compose v2 hiện tại các limit này được áp dụng cho docker compose up trên node đơn. Khối logging với max-size và max-file bật xoay vòng log: không có nó, driver json-file mặc định ghi log không giới hạn và sớm muộn cũng lấp đầy ổ đĩa, một trong những nguyên nhân phổ biến nhất khiến VPS hết dung lượng.
Vận hành và cập nhật an toàn
Vòng lặp deploy hằng ngày rất gọn. Vì image được ghim tag cụ thể, cập nhật nghĩa là sửa tag rồi kéo lại đúng service:
# Cập nhật một service, giữ nguyên phần còn lại
docker compose pull web
docker compose up -d web
# Xem log realtime của một service
docker compose logs -f --tail=100 web
# Dọn image cũ không còn dùng
docker image prune -f
Vài nguyên tắc giữ cho production ổn định:
- Ghim tag, không dùng
:latest.:latestkhiến hai lần deploy cho ra hai image khác nhau và không thể rollback rõ ràng. Ghim1.8.2để tái lập được. - Rollback là đổi tag ngược lại. Sửa tag về bản trước,
docker compose up -d, xong. Named volume giữ nguyên nên dữ liệu không mất. - Backup volume trước khi nâng cấp lớn. Với Postgres, chạy
pg_dumpquadocker compose execvà lưu ra ngoài host. - Chạy Compose như systemd user service (kết hợp
enable-lingerở phần rootless) để cả stack tự dựng lại sau reboot mà không cần đăng nhập.
Tóm lược
Đưa Docker Compose lên production trên VPS không phải là thêm một dòng lệnh, mà là vá kín các lỗ hổng vận hành: cài Compose v2 (plugin, không phải bản Python cũ), cân nhắc rootless để hạ rủi ro root với cái giá là port đặc quyền và một chút overhead mạng, rồi viết compose.yaml có đủ restart, healthcheck, named volume, resource limit và logging xoay vòng. Ghim tag image để rollback được, và để systemd lo phần tự khởi động lại. Có sẵn khung này, mỗi lần deploy chỉ còn là pull rồi up -d. Toàn bộ cấu hình trên chạy được trên mọi VPS có full quyền root, không phụ thuộc nhà cung cấp nào.