Docker Toàn Tập Trên VPS: Từ Chưa Biết Gì Đến Triển Khai Production — Tôi Đã Học Bằng Cách Phá Tan Một Server
1. Lần Đầu Tôi Dùng Docker: Xóa Nhầm Container Đang Chạy Production
Năm 2023, tôi quyết định "học Docker" bằng cách cài lên VPS production luôn. Đừng làm vậy. Tôi gõ docker rm -f $(docker ps -aq) nghĩ là xóa container cũ — hóa ra đang SSH nhầm server. 3 website, 2 API, 1 database — tất cả biến mất trong 0.5 giây.
Đó là bài học đắt giá: Docker mạnh nhưng cũng nguy hiểm nếu không hiểu nó hoạt động thế nào. Sau 3 năm dùng Docker hàng ngày — từ blog cá nhân đến hệ thống 20+ microservices cho khách hàng — tôi viết bài này như một cẩm nang Docker toàn tập cho VPS. Từ cài đặt, Dockerfile, Compose, network, volume, đến bảo mật và Swarm. Không lan man — chỉ những thứ bạn thực sự cần để chạy production.
2. Docker Là Gì? Tại Sao Không Cần Cài Trực Tiếp Lên VPS Nữa?
Trước Docker: cài app Node.js → cài Node.js phiên bản X → cài npm packages → app chạy. 6 tháng sau, cài app Python → cần Python 3.11 → xung đột với Node.js → mất 3 tiếng debug. Trên cùng một server, mỗi app đòi phiên bản runtime khác nhau, thư viện khác nhau → "dependency hell".
Docker giải quyết bằng cách đóng gói app + runtime + thư viện + config vào một container — một môi trường độc lập, nhẹ hơn VM (máy ảo) rất nhiều vì dùng chung kernel Linux với host. Không còn "trên máy tôi chạy mà". Container chạy giống hệt nhau trên mọi VPS, mọi môi trường.
| Tiêu chí | Không Docker (cài trực tiếp) | Docker Container | Virtual Machine (KVM) |
|---|---|---|---|
| Isolation | Không — app dùng chung OS | Có — mỗi container độc lập | Có — mỗi VM có OS riêng |
| Dung lượng | Nhẹ nhất | Vài MB đến vài trăm MB | Vài GB (cả OS) |
| Khởi động | Ngay lập tức | < 1 giây | 30-60 giây |
| Deploy | Thủ công (scp, ssh, cài đặt) | 1 lệnh docker compose up | Clone VM hoặc cài lại |
| Rollback | Khó (phải có backup) | Đổi tag image, up lại | Revert snapshot |
| Tốn RAM | Chỉ RAM của app | RAM app + ~20MB Docker overhead | RAM app + RAM OS (300MB-1GB) |
3. Cài Docker Trên Ubuntu 24.04
# Cài Docker Engine + Compose plugin
curl -fsSL https://get.docker.com | sudo sh
sudo apt install -y docker-compose-plugin
sudo usermod -aG docker $USER
# Đăng xuất & đăng nhập lại để có quyền docker
docker --version # >= 26.x
docker compose version # >= 2.x
4. Dockerfile: Bản Thiết Kế Cho Container
Dockerfile là file text định nghĩa cách build một image. Mỗi dòng là một layer — Docker cache từng layer để build nhanh hơn.
4.1. Dockerfile Cho Node.js App
# Stage 1: Build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: Production
FROM node:22-alpine
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
RUN chown -R app:app /app
USER app
EXPOSE 3000
CMD ["node", "server.js"]
Multi-stage build: Stage 1 cài dependencies, Stage 2 chỉ copy node_modules cần thiết → image nhẹ hơn 60% so với build một stage.
4.2. Dockerfile Cho Python FastAPI
FROM python:3.12-slim
RUN groupadd -r app && useradd -r -g app app
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN chown -R app:app /app
USER app
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
4.3. Các Lệnh Docker Cơ Bản
docker build -t myapp:latest . # Build image
docker images # Xem danh sách image
docker run -d --name myapp -p 3000:3000 myapp:latest # Chạy container
docker ps # Container đang chạy
docker ps -a # Tất cả container (kể cả đã dừng)
docker logs -f myapp # Xem log real-time
docker exec -it myapp sh # Vào shell trong container
docker stop myapp && docker rm myapp # Dừng và xóa container
docker rmi myapp:latest # Xóa image
docker system prune -a # Dọn rác (image, container, network cũ)
5. Docker Compose: Quản Lý Nhiều Container Cùng Lúc
App thực tế không chỉ có 1 container. Một web app điển hình = Node.js + PostgreSQL + Redis + Nginx. Docker Compose quản lý tất cả qua một file docker-compose.yml.
5.1. Stack Node.js + PostgreSQL + Redis + Nginx
version: '3.8'
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: myapp
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 10s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redisdata:/data
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
app:
build: .
restart: unless-stopped
environment:
DATABASE_URL: postgresql://myapp:${DB_PASSWORD}@postgres:5432/myapp
REDIS_URL: redis://redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./static:/usr/share/nginx/html/static
depends_on:
- app
volumes:
pgdata:
redisdata:
condition: service_healthy thay vì depends_on đơn thuần. Mặc định depends_on chỉ đợi container start — không đợi service sẵn sàng. PostgreSQL cần 5-10 giây để ready sau khi container start. Nếu app kết nối quá sớm → lỗi.5.2. Các Lệnh Docker Compose Quan Trọng
docker compose up -d # Khởi động tất cả service
docker compose ps # Xem trạng thái
docker compose logs -f app # Log của service "app"
docker compose restart app # Restart một service
docker compose down # Dừng + xóa container, network
docker compose down -v # + xóa cả volume (MẤT DATA!)
docker compose pull && docker compose up -d --build # Update image
6. Docker Network: Container Giao Tiếp Với Nhau Thế Nào?
Mặc định Docker Compose tạo một bridge network cho project. Các container gọi nhau bằng service name (không cần IP): postgres, redis, app.
# Tạo network riêng cho nhiều project cùng dùng
docker network create shared
# docker-compose.yml
networks:
default:
name: shared
external: true
Pattern này cực kỳ hữu ích khi bạn có: app A cần gọi API của app B, nhưng app A và B ở 2 project Compose khác nhau. Cho chúng dùng chung network → gọi http://app-b:3000 là xong.
ports: "5432:5432"). Nếu bạn expose port database, hacker scan port 5432 → brute force PostgreSQL → toàn bộ dữ liệu bị đánh cắp. App và DB trong cùng Docker network đã giao tiếp được qua service name — không cần expose port ra ngoài.7. Docker Volume: Dữ Liệu Không Biến Mất Khi Container Bị Xóa
Container là ephemeral — xóa container là mất hết dữ liệu bên trong (trừ khi có volume). Volume là nơi lưu dữ liệu bền vững, gắn vào container.
# Named volume (Docker quản lý)
volumes:
pgdata:
driver: local
# Bind mount (thư mục host gắn vào container)
volumes:
- ./uploads:/app/uploads
- ./nginx.conf:/etc/nginx/nginx.conf:ro # ro = read-only
Backup volume PostgreSQL:
# Backup
docker exec -t postgres pg_dumpall -U myapp > backup.sql
# Hoặc dùng volume backup
docker run --rm -v pgdata:/data -v $(pwd):/backup alpine \
tar czf /backup/pgdata_backup.tar.gz -C /data .
8. Bảo Mật Docker Trên VPS
8.1. Không Chạy Container Với --privileged
--privileged cấp cho container toàn bộ quyền của host — gần như root. Chỉ dùng khi thực sự cần (vd: chạy Docker trong Docker). 99% trường hợp không cần.
8.2. Scan Image Vulnerability
# Dùng Docker Scout (tích hợp sẵn)
docker scout quickview myapp:latest
# Hoặc Trivy (mã nguồn mở)
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image myapp:latest
8.3. Read-only Root Filesystem
services:
app:
read_only: true
tmpfs:
- /tmp
- /var/run
Container không thể ghi vào filesystem — hacker có exploit RCE cũng không tạo được file backdoor. Nếu app cần ghi (upload, cache), mount volume riêng vào thư mục đó.
8.4. Resource Limits
services:
app:
deploy:
resources:
limits:
cpus: '2'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
Không giới hạn = một container lỗi memory leak có thể ngốn hết RAM host → toàn bộ service khác chết theo.
8.5. Không Hardcode Secret Trong Dockerfile/Compose
# Dùng .env file (đã có trong .gitignore)
DB_PASSWORD=supersecret
# docker-compose.yml
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
# Hoặc Docker Secrets (Swarm)
echo "supersecret" | docker secret create db_password -
9. Docker Swarm: Khi Một VPS Không Đủ
Docker Swarm là giải pháp orchestration đơn giản nhất, tích hợp sẵn trong Docker. Không cần cài Kubernetes phức tạp. Swarm = nhiều VPS Docker kết nối thành một cluster.
# Khởi tạo Swarm trên manager node
docker swarm init --advertise-addr 1.2.3.4
# Join worker node (chạy trên VPS khác)
docker swarm join --token SWMTKN-xxx 1.2.3.4:2377
# Deploy stack (giống docker compose nhưng trên Swarm)
docker stack deploy -c docker-compose.yml mystack
# Scale service
docker service scale mystack_app=5
# Rolling update
docker service update --image myapp:v2 mystack_app
| Tính năng | Docker Compose (1 VPS) | Docker Swarm (nhiều VPS) |
|---|---|---|
| Số node | 1 | Nhiều (manager + worker) |
| Load balancing | ❌ | ✅ Tích hợp (routing mesh) |
| Rolling update | ❌ | ✅ Zero-downtime deploy |
| Auto restart khi node chết | ❌ | ✅ Reschedule sang node khác |
| Secret management | ❌ (dùng .env) | ✅ docker secret |
| Config management | ❌ (bind mount file) | ✅ docker config |
| Phù hợp | Blog, app nhỏ, dev | Production, multi-service, cần HA |
10. Docker Cheatsheet Cho Người Mới
# Build & Run
docker build -t tên-app . # Build image từ Dockerfile
docker run -d --name tên -p host:container image # Chạy container
# Debug
docker logs -f tên-container # Xem log
docker exec -it tên-container sh # Vào container
docker inspect tên-container # Xem chi tiết config
# Dọn dẹp
docker system df # Xem disk usage
docker system prune -a --volumes # Xóa mọi thứ không dùng
# Compose
docker compose up -d # Start stack
docker compose down # Stop stack
docker compose logs -f # Log tất cả service
docker compose exec app sh # Vào service "app"
11. Kết Luận
Docker không khó — cái khó là hiểu đúng triết lý của nó: container là bò dùng một lần (ephemeral), dữ liệu nằm ở volume, config nằm ở Compose file, và mỗi container chỉ làm một việc. Khi bạn quen với mindset này, việc deploy một stack 5 service chỉ còn là một lệnh docker compose up -d.
Với VPS 4GB RAM (~137K/tháng), bạn có thể chạy: 1 Node.js app + PostgreSQL + Redis + Nginx + Caddy — tất cả trong Docker, tất cả tự restart khi crash, tất cả deploy trong 30 giây. Đây là cách tôi chạy mọi project từ 2 năm nay và chưa có lý do gì để quay lại cài trực tiếp.
🐳 Cần VPS mạnh để chạy Docker 24/7?
Netihot VPS từ 77K/tháng: AMD EPYC, NVMe SSD, hỗ trợ Docker, uptime 99.9%. Cài sẵn Ubuntu + Docker nếu yêu cầu.
Thuê VPS Cho Docker