2 Giờ Sáng và Ứng Dụng Của Bạn Đang Down
Bạn vừa push phiên bản mới. Đồng nghiệp triển khai thẳng lên VPS — không container, chỉ là lệnh node server.js chạy trần trong tmux session. Bây giờ process đã chết, port 80 bị chiếm bởi thứ gì đó, và SSL cert đã hết hạn ba ngày trước mà không ai hay. Nghe quen không?
Tôi đã từng như vậy. Nhiều lần. Sau khi chuyển sang Docker + Nginx + Certbot cho bốn dịch vụ production, những sự cố lúc 2 giờ sáng đó đã chấm dứt. Đây là cách tôi thiết lập.
Nguyên Nhân Gốc Rễ: Tại Sao Triển Khai Bare-Metal Hay Gặp Sự Cố
Nên hiểu rõ vấn đề với cách tiếp cận “cứ chạy thôi” trước khi đi vào giải pháp:
- Quản lý process thủ công — nếu process crash, không có gì khởi động lại nó.
- Xung đột port — hai service tranh nhau port 80 hoặc 3000 mà không có cách cô lập.
- Địa ngục dependency — Node 16 trên server, Node 20 trong code. Mọi thứ âm thầm vỡ vụn.
- SSL là chuyện tính sau — hầu hết team chỉ thêm HTTPS sau khi bị cảnh báo trình duyệt ngay trước mặt khách hàng.
Docker xử lý cô lập. Nginx xử lý định tuyến và SSL termination. Certbot tự động hóa việc gia hạn certificate. Mỗi công cụ có một nhiệm vụ. Cùng nhau, chúng giải quyết những sự cố hay xảy ra vào lúc tệ nhất có thể.
So Sánh Các Lựa Chọn
Ba chiến lược triển khai phổ biến, xếp hạng thẳng thắn:
Lựa chọn 1: Bare-metal (không container)
- Nhanh để bắt đầu, đau đầu để duy trì
- Không có cơ chế rollback, xung đột dependency tích lũy theo thời gian
- Ổn cho dự án hobby, nguy hiểm cho bất cứ thứ gì nghiêm túc
Lựa chọn 2: Chỉ Docker (không Nginx)
- Giải quyết vấn đề cô lập, nhưng vẫn cần expose port 80/443 trực tiếp
- Không có quản lý SSL tập trung, khó host nhiều app trên một VPS
Lựa chọn 3: Docker + Nginx + Certbot (hướng dẫn này)
- Nginx hoạt động như reverse proxy — app của bạn chỉ lắng nghe trên port nội bộ
- Certbot xử lý SSL từ Let’s Encrypt với tự động gia hạn
- Tách biệt rõ ràng: thêm app mới bằng cách thêm block config Nginx mới, không thay đổi infrastructure
- Chạy setup này trên năm dịch vụ hơn 14 tháng, tôi không phải lo về gia hạn cert lần nào và không có xung đột port giữa các app
Lựa chọn 3 là thứ chúng ta sẽ xây dựng.
Điều Kiện Tiên Quyết
- VPS chạy Ubuntu 22.04 (bất kỳ distro nào dựa trên Debian đều được)
- Tên miền trỏ đến IP VPS của bạn (đã cấu hình bản ghi
A) - Quyền truy cập SSH với quyền sudo
- App của bạn có
Dockerfile(chúng ta sẽ thêm nếu chưa có)
Bước 1: Cài Đặt Docker trên VPS
# Cập nhật và cài đặt các gói phụ thuộc
sudo apt update && sudo apt install -y ca-certificates curl gnupg
# Thêm GPG key chính thức của Docker
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Thêm repo Docker
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Cài đặt Docker
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Cho phép user hiện tại chạy Docker không cần sudo
sudo usermod -aG docker $USER
newgrp docker
Bước 2: Đóng Gói App vào Container
Chưa có Dockerfile? Đây là file tối giản cho Node.js. Hãy điều chỉnh cho Python, Go, hoặc bất kỳ ngôn ngữ nào bạn đang dùng.
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Build và test local trước khi động vào VPS:
docker build -t myapp .
docker run -p 3000:3000 myapp
Truy cập http://localhost:3000 và xác nhận hoạt động đúng. Đừng bỏ qua bước này — debug trên VPS mà không có context local thực sự rất khổ sở.
Bước 3: Thiết Lập Docker Compose
Trên VPS, tạo thư mục dự án và thêm file docker-compose.yml. Cách này giúp cấu hình container có thể tái tạo và quản lý phiên bản.
mkdir -p /opt/myapp && cd /opt/myapp
# docker-compose.yml
services:
app:
image: myapp:latest
restart: always
ports:
- "127.0.0.1:3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://user:pass@db:5432/mydb
networks:
default:
name: web
Cấu hình ports rất quan trọng: 127.0.0.1:3000:3000 chỉ expose container ra localhost. Nginx (chạy như system service) kết nối đến đó. Port 3000 không bao giờ tiếp xúc trực tiếp với internet công cộng.
restart: always chính là thứ giải quyết vấn đề crash lúc 2 giờ sáng mà không ai hay biết. Docker tự khởi động lại container nếu nó chết — không cần tmux session nữa.
Bước 4: Cài Đặt và Cấu Hình Nginx
sudo apt install -y nginx
sudo systemctl enable nginx
Tạo file cấu hình site cho tên miền của bạn:
sudo nano /etc/nginx/sites-available/myapp
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
proxy_cache_bypass $http_upgrade;
}
}
# Kích hoạt site
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
App của bạn bây giờ có thể truy cập qua HTTP. Xác nhận bằng curl http://yourdomain.com trước khi chuyển sang SSL. Đừng bỏ qua bước kiểm tra này — xử lý SSL trên nền HTTP đã bị hỏng sẽ rất mất thời gian.
Bước 5: Thêm SSL Miễn Phí với Certbot
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Certbot sẽ:
- Xác minh quyền sở hữu tên miền qua HTTP challenge
- Lấy certificate từ Let’s Encrypt
- Tự động chỉnh sửa cấu hình Nginx để thêm HTTPS và chuyển hướng HTTP → HTTPS
Test timer tự động gia hạn — bước này ngăn sự cố cert hết hạn:
sudo certbot renew --dry-run
Certbot cài đặt systemd timer chạy kiểm tra gia hạn hai lần mỗi ngày. Cert được luân chuyển tự động khi còn 30 ngày trước khi hết hạn. Bạn không cần động đến nó nữa.
Bước 6: Triển Khai Container App
Chuyển image lên VPS. Cách nhanh nhất khi không có registry:
# Trên máy local của bạn
docker save myapp:latest | gzip > myapp.tar.gz
scp myapp.tar.gz user@your-vps-ip:/opt/myapp/
# Trên VPS
cd /opt/myapp
docker load < myapp.tar.gz
docker compose up -d
Với CI/CD pipeline, push lên Docker Hub hoặc private registry rồi pull trên VPS — gọn hơn và nhanh hơn cho các lần deploy lặp lại:
docker pull youruser/myapp:latest
docker compose up -d --pull always
Bước 7: Xác Nhận Mọi Thứ Đang Chạy
# Kiểm tra trạng thái container
docker compose ps
# Xem log trực tiếp
docker compose logs -f app
# Kiểm tra trạng thái Nginx
sudo systemctl status nginx
# Xác nhận SSL đang hoạt động
curl -I https://yourdomain.com
Bạn sẽ thấy HTTP/2 200 và SSL header hợp lệ. Thấy vòng lặp redirect? Kiểm tra Nginx có truyền đúng X-Forwarded-Proto không và app của bạn có tự redirect hai lần không.
Thêm App Thứ Hai vào Cùng VPS
Đây là lúc infrastructure này phát huy giá trị. Cần thêm service trên cùng máy chủ? Tạo config Nginx mới trỏ đến port khác, thêm Docker Compose stack khác, chạy Certbot cho tên miền mới. Không xung đột port, không dependency dùng chung, không rắc rối.
sudo nano /etc/nginx/sites-available/anotherapp
# Trỏ proxy_pass đến http://localhost:4000
sudo certbot --nginx -d anotherapp.com
docker compose -f /opt/anotherapp/docker-compose.yml up -d
Stack Thực Sự Bền Vững
Docker để cô lập, Nginx để định tuyến, Certbot để SSL. Bộ ba đó giải quyết 90% những thứ khiến người ta mất ngủ. Mỗi phần có một nhiệm vụ và làm tốt nhiệm vụ đó.
Khi có sự cố, log nằm ở hai nơi rõ ràng: docker compose logs và /var/log/nginx/. Rollback nghĩa là docker compose down && docker compose up -d với tag image trước đó. Hãy bắt đầu với stack này, giữ nó đơn giản, và chỉ thêm độ phức tạp khi có lý do cụ thể — không phải vì nó có vẻ hay ho lúc 11 giờ đêm trước ngày launch.

