Build a Personal Blog — Phase 6: Deploy lên Ubuntu VPS
vpsdockernginxdevopsubuntu
Đây là Phase 6 của series Build a Personal Blog. Ở Phase 5, bạn đã containerize blog Next.js và PostgreSQL database với Docker Compose. Mọi thứ chạy ngon lành trong containers trên laptop. Giờ là lúc đưa những containers đó lên server thật và cho cả thế giới truy cập blog của bạn.
✅ Ubuntu VPS trên Hostinger với SSH access an toàn
✅ Docker và Docker Compose đã cài trên server
✅ Blog chạy trong production containers trên VPS
✅ Nginx làm reverse proxy chuyển tiếp traffic đến Next.js
✅ SSL certificates miễn phí từ Let's Encrypt qua Certbot
✅ Quy trình re-deploy không downtime chạy trong 30 giây
✅ Firewall và fail2ban bảo vệ server khỏi tấn công brute-force
Đây là thứ chúng ta sẽ build — traffic đi từ internet qua Nginx vào Docker containers:
Nginx xử lý SSL termination và forward HTTP traffic đến Next.js container trên port 3000. PostgreSQL chạy trong container riêng, chỉ truy cập được từ Next.js container — không bao giờ expose ra internet.
1. Chọn Gói VPS Trên Hostinger
Vào Hostinger VPS và chọn gói. Cho blog cá nhân, gói entry-level là quá đủ:
Gói
RAM
CPU
Storage
Giá
KVM 1
4 GB
1 vCPU
50 GB NVMe
~$5/tháng
KVM 2
8 GB
2 vCPU
100 GB NVMe
~$7/tháng
KVM 1 thừa sức cho blog cá nhân. Next.js standalone dùng ~100-150 MB memory, PostgreSQL tốn ~50-100 MB, và Nginx gần như không đáng kể. Bạn còn dư dả headroom.
Khi setup VPS:
Chọn Ubuntu 24.04 làm OS
Chọn data center gần audience của bạn (US, EU, hoặc Asia)
Đặt root password (sẽ tắt password auth sau)
Ghi lại VPS IP address — bạn sẽ cần nó ở khắp nơi
2. Setup Server Ban Đầu
2.1 Kết Nối Qua SSH
Từ máy local, SSH vào VPS bằng root:
ssh root@YOUR_VPS_IP
Bạn sẽ được hỏi root password đã đặt lúc tạo VPS.
2.2 Tạo User Deploy
Chạy mọi thứ bằng root rất nguy hiểm. Một lệnh rm -rf sai chỗ là server bay. Tạo user riêng:
# Tạo user với home directoryadduser deploy# Thêm vào sudo groupusermod -aG sudo deploy
Đặt password mạnh khi được hỏi.
2.3 Setup SSH Key Authentication
SSH keys an toàn hơn password và tiện hơn nhiều. Trên máy local (không phải VPS):
# Tạo cặp SSH key (nếu chưa có)ssh-keygen -t ed25519 -C "[email protected]"# Copy public key lên VPSssh-copy-id deploy@YOUR_VPS_IP
Test thử:
ssh deploy@YOUR_VPS_IP# Phải login được mà không hỏi password
2.4 Tắt Password Authentication
Giờ SSH keys đã hoạt động, tắt password login để chặn brute-force:
Khi thấy Ready on http://0.0.0.0:3000, blog đã chạy. Nhấn Ctrl+C để thoát logs.
Kiểm tra containers healthy:
docker compose ps
Output mong đợi:
NAME SERVICE STATUS PORTSmy-blog-app-1 app Up (healthy) 0.0.0.0:3000->3000/tcpmy-blog-db-1 db Up (healthy) 5432/tcp
Lúc này blog truy cập được tại http://YOUR_VPS_IP:3000 — nhưng chỉ nếu bạn mở port 3000 trong firewall (đừng làm vậy ở production). Thay vào đó, ta sẽ đặt Nginx phía trước.
5. Setup Nginx Làm Reverse Proxy
Nginx nằm giữa internet và Next.js container. Nó xử lý:
SSL termination — giải mã HTTPS, forward HTTP thuần đến Next.js
Static file caching — serve headers cho browser caching
HTTP/2 — performance tốt hơn cho browser hiện đại
Security headers — HSTS, X-Frame-Options, v.v.
5.1 Cài Nginx
sudo apt install -y nginx
Kiểm tra đang chạy:
sudo systemctl status nginx# Phải hiện "active (running)"
Mở http://YOUR_VPS_IP trên browser — bạn sẽ thấy trang welcome mặc định của Nginx.
5.2 Tạo Cấu Hình Nginx
Xóa site mặc định và tạo config cho blog:
# Xóa defaultsudo rm /etc/nginx/sites-enabled/default# Tạo blog configsudo nano /etc/nginx/sites-available/blog
Thay yourdomain.com bằng domain thật (hoặc VPS IP nếu chưa setup domain).
5.3 Bật Site
# Tạo symlink để enable sitesudo ln -s /etc/nginx/sites-available/blog /etc/nginx/sites-enabled/blog# Test cấu hình xem có lỗi syntax khôngsudo nginx -t# Reload Nginxsudo systemctl reload nginx
Output mong đợi từ nginx -t:
nginx: the configuration file /etc/nginx/nginx.conf syntax is oknginx: configuration file /etc/nginx/nginx.conf test is successful
Giờ mở http://YOUR_VPS_IP — bạn sẽ thấy blog thay vì trang welcome của Nginx!
Tại Sao Cần Reverse Proxy?
Bạn có thể thắc mắc: "Sao không expose port 3000 thẳng ra luôn?" Lý do:
6. SSL Với Let's Encrypt
HTTP không an toàn. Bất kỳ ai nằm giữa user và server đều có thể đọc traffic. Thêm HTTPS với SSL certificates miễn phí từ Let's Encrypt.
6.1 Cài Certbot
sudo apt install -y certbot python3-certbot-nginx
6.2 Lấy SSL Certificates
Điều kiện: Domain phải trỏ đến VPS IP trước khi chạy lệnh này. Nếu chưa setup DNS, bỏ qua section này và quay lại sau Phase 7 (Custom Domain Setup). Tạm thời dùng http://YOUR_VPS_IP.
Sau khi Certbot xong, Nginx config sẽ được cập nhật với SSL settings. Kiểm tra:
cat /etc/nginx/sites-available/blog
Bạn sẽ thấy các directive listen 443 ssl mới và đường dẫn certificate. Certbot lo hết.
6.3 Kiểm Tra Auto-Renewal
Let's Encrypt certificates hết hạn sau 90 ngày. Certbot cài systemd timer để tự động gia hạn:
# Kiểm tra timersudo systemctl status certbot.timer# Test renewal (dry run — không gia hạn thật)sudo certbot renew --dry-run
Nếu dry run thành công, bạn đã xong. Certbot sẽ tự gia hạn certificates trước khi hết hạn.
6.4 Test SSL
Mở https://yourdomain.com và kiểm tra:
Icon ổ khóa xuất hiện trên browser
HTTP tự động redirect sang HTTPS
Certificate do Let's Encrypt cấp
Bạn cũng có thể test bằng SSL Labs scanner: vào SSL Labs và nhập domain. Mục tiêu là rating A hoặc A+.
7. Quy Trình Deploy
Blog đã live! Nhưng bạn sẽ tiếp tục viết bài và thay đổi code. Đây là cách deploy updates.
7.1 Script Deploy Nhanh
Tạo deploy script trên VPS:
nano ~/my-blog/deploy.sh
#!/bin/bashset -eecho "🔄 Pulling latest changes..."git pull origin mainecho "🏗️ Building and restarting containers..."docker compose -f docker-compose.yml -f docker-compose.prod.yml \ --env-file .env.production \ up -d --buildecho "🧹 Cleaning up old images..."docker image prune -fecho "✅ Deploy complete!"echo ""docker compose ps
Cho phép chạy:
chmod +x ~/my-blog/deploy.sh
7.2 Deploy Từ Máy Local
Workflow thường ngày:
# Trên máy localgit add .git commit -m "Add new blog post"git push origin main# SSH vào VPS và deployssh deploy@YOUR_VPS_IP "cd ~/my-blog && ./deploy.sh"
Hoặc gộp lại thành một lệnh duy nhất alias trong shell:
# Thêm vào ~/.zshrc hoặc ~/.bashrcalias deploy-blog='ssh deploy@YOUR_VPS_IP "cd ~/my-blog && ./deploy.sh"'# Giờ chỉ cần chạy:deploy-blog
7.3 Chiến Lược Zero-Downtime
docker compose up -d --build của Docker Compose restart containers lần lượt. Cho blog cá nhân, khoảng restart ngắn (~5-10 giây) là chấp nhận được. Nếu cần zero-downtime thật sự:
Build image trước (không downtime trong lúc build):
Chọn "Yes" khi được hỏi. Ubuntu sẽ tự động cài security patches.
9.2 Không Expose PostgreSQL
Trong docker-compose.prod.yml, đảm bảo PostgreSQL KHÔNG map ra host port:
# ✅ Đúng — chỉ truy cập được từ containers khácdb: ports: []# ❌ Sai — truy cập được từ internetdb: ports: - "5432:5432"
Docker Compose config từ Phase 5 đã xử lý đúng với production override.
9.3 Cập Nhật Docker
# Update Docker cùng system packagessudo apt update && sudo apt upgrade -y
9.4 Tổng Kết Security
Layer
Bảo vệ
Trạng thái
SSH
Key-only auth, không root login
✅
Firewall
Chỉ mở port 22, 80, 443
✅
Brute-force
fail2ban giám sát SSH
✅
Database
Không expose ra internet
✅
SSL/TLS
Let's Encrypt với auto-renewal
✅
OS
Tự động cập nhật security
✅
Containers
Non-root user trong Dockerfile
✅
Troubleshooting
Container không start được
# Kiểm tra logs xem lỗi gìdocker compose logs app# Các vấn đề thường gặp:# 1. DATABASE_URL sai — kiểm tra .env.production# 2. Port 3000 bị chiếm — kiểm tra: lsof -i :3000# 3. Build fail — kiểm tra TypeScript errors trong logs
Nginx hiện 502 Bad Gateway
Nghĩa là Nginx không kết nối được đến Next.js container:
# App container có đang chạy không?docker compose ps# Có đang listen port 3000 không?curl http://127.0.0.1:3000# Kiểm tra Nginx error logsudo tail -20 /var/log/nginx/error.log
Nguyên nhân thường gặp:
Container đang khởi động (đợi vài giây)
Container bị crash — kiểm tra docker compose logs app
Sai port trong Nginx config — xác nhận proxy_pass http://127.0.0.1:3000
SSL certificate không issue được
Certbot cần truy cập server trên port 80 để xác minh domain:
# Kiểm tra DNS trỏ đến VPSdig yourdomain.com +short# Phải trả về VPS IP# Kiểm tra port 80 đã mởsudo ufw status# Phải thấy 80/tcp ALLOW# Kiểm tra Nginx đang chạysudo systemctl status nginx
Hết disk space
Docker images và build cache có thể ăn hết disk:
# Xem cái gì đang chiếm chỗdocker system df# Xóa images, containers, và build cache không dùngdocker system prune -a# Xóa logs cũdocker compose logs --tail 0
Tổng Kết
Trong phase này bạn đã:
✅ Setup Ubuntu VPS trên Hostinger với SSH access an toàn và non-root deploy user
✅ Gia cố server với firewall rules và fail2ban chống brute-force
✅ Cài Docker và Docker Compose trên VPS
✅ Deploy blog với production Docker Compose configuration
✅ Cấu hình Nginx làm reverse proxy với static asset caching
✅ Thêm SSL certificates miễn phí từ Let's Encrypt với tự động gia hạn
✅ Tạo deploy script cho quy trình deploy nhanh, lặp lại được
Blog của bạn giờ đã live trên internet, bảo mật bằng HTTPS, và được bảo vệ bởi nhiều lớp security. Bất kỳ ai cũng có thể truy cập https://yourdomain.com và đọc bài viết của bạn.
Tiếp Theo Là Gì?
Trong Phase 7, bạn sẽ đăng ký custom domain (như chanh.blog) trên Hostinger, cấu hình DNS records trỏ về VPS, setup redirect www, và verify mọi thứ hoạt động end-to-end. Bạn cũng sẽ submit site lên Google Search Console và test social sharing previews.