Microservices Trên 1 VPS Với Docker Compose: 10 Service Không Cần Kubernetes
Năm 2023 tôi nhận maintain một SaaS nho nhỏ từ anh bạn. 3 service (auth, API, worker), chạy bằng PM2 trên 1 VPS. "Có gì khó đâu," tôi nghĩ. 3 tháng sau, user tăng gấp 5, tôi tự tách API thành 6 microservices. Lúc đó PM2 bắt đầu lòi ra vấn đề: port conflict, env variable rối nùi, dependency giữa các service không kiểm soát được, một service leak RAM kéo theo OOM kill giết luôn service bên cạnh.
Giải pháp: Docker Compose. KHÔNG phải Kubernetes. Bài này là toàn bộ hành trình tôi chuyển 1 project từ PM2 monolithic lên Docker Compose 10 microservices — những gì hoạt động, những gì suýt giết production, và tại sao bạn có thể chưa cần Kubernetes.
Tại Sao Docker Compose, Không Phải Kubernetes?
Đây là câu hỏi tôi được hỏi nhiều nhất. Câu trả lời ngắn: Kubernetes cho 1 VPS giống như dùng xe tải chở 1 thùng mì.
| Tiêu chí | Docker Compose | Kubernetes (k3s/microk8s) |
|---|---|---|
| RAM tối thiểu | 512MB (chỉ Docker) + app | 2GB chỉ riêng k8s control plane |
| Số file config | 1 file docker-compose.yml | Deployment + Service + Ingress + ConfigMap + Secret + PVC = ít nhất 6 file/service |
| Learning curve | 2-3 ngày | 2-3 tháng để production-ready |
| Auto-scaling | Không (scale thủ công hoặc dùng Watchtower) | Có HPA (Horizontal Pod Autoscaler) |
| Rolling update | Không built-in (dùng docker stack với Swarm) | Có, zero-downtime |
| Self-healing | restart: unless-stopped đơn giản | Tự restart pod chết, reschedule sang node khác |
| Phù hợp | 1-3 server, <20 services, team <5 người | 3+ server, 20+ services, team 5+ người |
Nếu bạn có 1 VPS và dưới 20 service: Docker Compose. Nếu bạn có 3 VPS trở lên và cần auto-scale, zero-downtime deploy: Kubernetes. Đơn giản vậy thôi.
Kiến Trúc Thực Tế: 10 Service Trên 1 VPS 4GB RAM
Đây là stack tôi đang chạy cho một web app bán hàng nho nhỏ:
Docker Network: app_network (bridge, internal DNS)
┌─────────────────────────────────────────────────┐
│ NGINX (port 80/443) │
│ Reverse Proxy + SSL Terminate │
└────┬────┬────┬────┬────┬────┬────┬────┬────┬────┘
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐
│Auth││API ││Wrk││Cron││WS ││Adm││PG ││Rds││Rab││Mel│
│Svc ││Svc ││Svc││Svc ││Svc││Svc││DB ││Cch││MQ ││Svc│
└────┘└────┘└────┘└────┘└────┘└────┘└────┘└────┘└────┘└────┘
Node Node Node Node Go Remix Pstg Rds Rbbt Minio
3001 3002 3003 3004 3005 3006 5432 6379 5672 9000
10 service, chạy trên VPS 4GB RAM, CPU load trung bình 25%. Tổng chi phí: 160K VPS + 0K orchestration = 160K/tháng. Nếu dùng managed Kubernetes (GKE/EKS) thì riêng control plane đã $70/tháng (~1.8 triệu), chưa tính node.
docker-compose.yml Production: Từng Dòng Một
Đây không phải file mẫu "Hello World". Đây là config tôi đang chạy thật. Tôi giải thích từng phần.
version: "3.8"
# ===== NETWORK =====
networks:
app_network:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
# Database network riêng — không expose ra app_network
db_network:
driver: bridge
internal: true # Không có internet access
# ===== VOLUMES =====
volumes:
postgres_data:
driver: local
redis_data:
driver: local
rabbitmq_data:
driver: local
minio_data:
driver: local
nginx_logs:
driver: local
# ===== SERVICES =====
services:
# 1. NGINX Reverse Proxy
nginx:
image: nginx:1.25-alpine
container_name: prod_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- nginx_logs:/var/log/nginx
networks:
- app_network
depends_on:
- auth_service
- api_service
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
deploy:
resources:
limits:
cpus: "0.5"
memory: 256M
reservations:
cpus: "0.25"
memory: 128M
# 2. Auth Service (Node.js + JWT)
auth_service:
build:
context: ./services/auth
dockerfile: Dockerfile
container_name: prod_auth
restart: unless-stopped
expose:
- "3001"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASS}@postgres:5432/${DB_NAME}
- REDIS_URL=redis://redis:6379
- JWT_SECRET=${JWT_SECRET}
networks:
- app_network
- db_network
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3001/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.25"
memory: 256M
# 3. API Service (Node.js + Express)
api_service:
build:
context: ./services/api
dockerfile: Dockerfile
container_name: prod_api
restart: unless-stopped
expose:
- "3002"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASS}@postgres:5432/${DB_NAME}
- REDIS_URL=redis://redis:6379
- RABBITMQ_URL=amqp://${RABBIT_USER}:${RABBIT_PASS}@rabbitmq:5672
- MINIO_ENDPOINT=minio
- MINIO_PORT=9000
networks:
- app_network
- db_network
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
rabbitmq:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3002/health"]
interval: 30s
timeout: 5s
retries: 3
deploy:
resources:
limits:
cpus: "1.0"
memory: 1G
reservations:
cpus: "0.5"
memory: 512M
# 4. PostgreSQL 15
postgres:
image: postgres:15-alpine
container_name: prod_postgres
restart: unless-stopped
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASS}
- POSTGRES_DB=${DB_NAME}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- db_network # Chỉ nối vào db_network, không expose ra app_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.5"
memory: 256M
command:
- "postgres"
- "-c"
- "shared_buffers=128MB"
- "-c"
- "effective_cache_size=384MB"
# 5. Redis 7
redis:
image: redis:7-alpine
container_name: prod_redis
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
networks:
- db_network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: "0.5"
memory: 384M
reservations:
cpus: "0.25"
memory: 128M
# 6. RabbitMQ
rabbitmq:
image: rabbitmq:3.12-management-alpine
container_name: prod_rabbitmq
restart: unless-stopped
environment:
- RABBITMQ_DEFAULT_USER=${RABBIT_USER}
- RABBITMQ_DEFAULT_PASS=${RABBIT_PASS}
volumes:
- rabbitmq_data:/var/lib/rabbitmq
networks:
- db_network
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
interval: 30s
timeout: 10s
retries: 5
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.25"
memory: 256M
# 7. MinIO (S3-compatible storage)
minio:
image: minio/minio:latest
container_name: prod_minio
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
- MINIO_ROOT_USER=${MINIO_USER}
- MINIO_ROOT_PASSWORD=${MINIO_PASS}
volumes:
- minio_data:/data
networks:
- db_network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 5s
retries: 3
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
# 8. Worker Service (xử lý job queue)
worker_service:
build:
context: ./services/worker
dockerfile: Dockerfile
container_name: prod_worker
restart: unless-stopped
environment:
- NODE_ENV=production
- RABBITMQ_URL=amqp://${RABBIT_USER}:${RABBIT_PASS}@rabbitmq:5672
- MINIO_ENDPOINT=minio
networks:
- db_network
depends_on:
rabbitmq:
condition: service_healthy
minio:
condition: service_healthy
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
# 9. Cron Service (scheduled tasks)
cron_service:
build:
context: ./services/cron
dockerfile: Dockerfile
container_name: prod_cron
restart: unless-stopped
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASS}@postgres:5432/${DB_NAME}
networks:
- db_network
depends_on:
postgres:
condition: service_healthy
deploy:
resources:
limits:
cpus: "0.25"
memory: 256M
# 10. Melisearch (full-text search)
meilisearch:
image: getmeili/meilisearch:v1.5
container_name: prod_meilisearch
restart: unless-stopped
environment:
- MEILI_MASTER_KEY=${MEILI_KEY}
- MEILI_ENV=production
- MEILI_NO_ANALYTICS=true
volumes:
- meili_data:/meili_data
networks:
- db_network
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
5 Gotcha Production Khi Dùng Docker Compose
depends_on đợi PostgreSQL container STARTED, nhưng PostgreSQL có thể mất 5-10 giây để accept connection. Kết quả: auth_service start trước khi DB sẵn sàng → crash loop. Fix: dùng condition: service_healthy (Compose v3+) hoặc script wait-for-it.sh trong entrypoint.network_mode: host (chỉ Linux, mất isolation).deploy.resources.limits, worker bị memory leak → dùng hết 4GB RAM → OOM killer của kernel giết ... postgres (process dùng nhiều RAM nhất). Luôn set memory limit cho mọi service. Dùng docker stats để theo dõi../data:/app/data — path tương đối tính từ thư mục chạy docker compose up. Nếu chạy từ cron job trong thư mục khác, volume mount sai chỗ. Dùng absolute path: /opt/app/data:/app/data.logging.options.max-size và max-file cho TỪNG service.So Sánh Các Phương Án Orchestration Trên VPS
| Phương án | RAM cần | Độ phức tạp | Auto-restart | Network isolation | Phù hợp |
|---|---|---|---|---|---|
| PM2 thuần | 128MB | Thấp nhất | Có | Không | 1-3 service Node.js |
| systemd + service file | 0MB extra | Thấp | Có (Restart=always) | Không | Đa ngôn ngữ, cần native performance |
| Docker Compose | 200-400MB (Docker daemon) | Trung bình | Có (restart: unless-stopped) | Có (bridge network) | 5-20 service, multi-language |
| Docker Swarm | 300-500MB | Trung bình-cao | Có + rolling update | Có (overlay network) | 2-5 node, cần HA đơn giản |
| k3s (Kubernetes nhẹ) | 2GB+ | Cao | Có + self-healing | Có (CNI plugin) | 3+ node, team DevOps |
| Nomad | 200-400MB | Trung bình | Có + reschedule | Có | Multi-datacenter, non-container |
Deploy Flow: Từ Git Push Đến Production
Đây là flow deploy tôi dùng — đơn giản, không cần CI/CD phức tạp:
# Trên máy dev: push code lên Git
git push origin main
# SSH vào VPS, pull + deploy
ssh user@vps-ip
cd /opt/app
# Pull code mới
git pull origin main
# Build image mới (chỉ build service thay đổi)
docker compose build api_service worker_service
# Zero-downtime restart: scale lên 2 instance, đợi healthy, scale cũ xuống
docker compose up -d --scale api_service=2 --no-recreate nginx postgres redis
# Đợi instance mới healthy (10s)
sleep 10
docker compose up -d --scale api_service=1 --no-recreate
# Cleanup image cũ (giữ 2 version gần nhất)
docker image prune -a --filter "until=48h" -f
--scale api_service=2 tạo container mới (với image mới) trong khi container cũ vẫn chạy. NGINX upstream với 2 server tự động load balance. Sau khi container mới healthy, scale về 1 — container cũ bị xóa. Downtime: 0 giây nếu NGINX config đúng.Monitoring + Logging Cho Docker Compose Stack
Không có Kubernetes dashboard, bạn cần tool riêng. Đây là stack monitoring tôi dùng trên cùng VPS:
# docker-compose.monitoring.yml
services:
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "127.0.0.1:9090:9090"
grafana:
image: grafana/grafana
ports:
- "127.0.0.1:3030:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASS}
# Loki + Promtail cho log aggregation
loki:
image: grafana/loki:2.9
promtail:
image: grafana/promtail:2.9
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./promtail-config.yml:/etc/promtail/config.yml
Đọc thêm: Hướng dẫn Monitoring VPS với Prometheus + Grafana và Quản lý Log Docker với Loki + Grafana.
Khi Nào Nên Lên Kubernetes?
Tôi không anti-Kubernetes. Tôi anti-Kubernetes-cho-mọi-thứ. Đây là checklist để bạn biết đã đến lúc migrate chưa:
- Bạn có 3+ server và cần phân phối service giữa các node
- Bạn cần auto-scale theo CPU/memory/requests (HPA)
- Bạn cần deploy zero-downtime với canary/blue-green mặc định
- Team bạn có 1+ người chuyên DevOps (k8s không phải "cài xong để đó")
- Bạn cần secret management tập trung (Vault, Sealed Secrets)
- Bill VPS hàng tháng đã vượt 3-5 triệu và optimize riêng lẻ không đủ
Nếu chưa đạt ít nhất 3/6 tiêu chí trên: Docker Compose vẫn là bạn thân.
Chi Phí Thực Tế: Docker Compose vs Kubernetes
| Hạng mục | Docker Compose (1 VPS) | k3s (3 VPS) | Managed K8s (GKE) |
|---|---|---|---|
| VPS/Node | 1x 4GB = 160K | 3x 4GB = 480K | 3x e2-medium + control plane |
| Control plane | 0K | 0K (k3s embedded) | $73/tháng (~1.85M) |
| Load balancer | NGINX trong VPS | MetalLB/Traefik | $18/tháng (~450K) |
| Storage (PVC) | Local volume | Longhorn/local-path | $0.1/GB/tháng |
| Monitoring | Prometheus + Grafana (free) | kube-prometheus-stack | Google Cloud Monitoring (free tier) |
| Tổng/tháng | ~160K VND | ~480K VND | ~$100 (~2.5M VND) |
Khác biệt 15x giữa Docker Compose và managed Kubernetes. Với 160K/tháng, bạn có thể chạy 10 services + database + cache + queue + storage + monitoring. Cùng stack đó trên GKE: 2.5 triệu/tháng. Với startup nhỏ, đó là cả một khoản tiết kiệm khổng lồ.
🚀 Bắt đầu với Docker Compose ngay hôm nay
VPS 4GB RAM chỉ 160K/tháng tại TrumVPS — đủ chạy 10+ microservices
Thuê VPS NgayXem thêm: Cài Docker + Docker Compose trên VPS hoặc Netihot Hosting giá rẻ