- Cloudflare Tunnel expose VPS ra public HTTPS không cần mở port firewall.
- Mỗi PR GitHub tự deploy preview riêng: pr-123.preview.app.com, giống Vercel.
- Auto SSL Cloudflare miễn phí, không cần Let Encrypt setup.
- VPS 2GB chạy được 5-10 preview song song nhờ PM2 cluster theo PR number.
- Tổng chi phí: 199k VPS + miễn phí Cloudflare = 199k/tháng cho mọi PR preview.
Vercel free tier 100GB bandwidth/tháng nghe ngon, nhưng project bạn vừa nhận traffic là hết quota, charge bất ngờ. Hoặc bạn không muốn deploy code sensitive lên cloud Mỹ vì lý do compliance. Self-host Next.js trên Cloud VPS 2GB + Cloudflare Tunnel là giải pháp pro: public HTTPS, PR preview, không charge bất ngờ.
Cloudflare Tunnel (cloudflared) là tool free tạo tunnel ngược từ VPS lên Cloudflare edge. VPS không cần mở port 80/443, không cần public IP, vẫn được expose ra Internet với SSL Cloudflare. Hoàn hảo cho VPS sau NAT, lab, hoặc dev preview.
Bài này hướng dẫn full pipeline: cài cloudflared trên VPS, setup tunnel cho main app, tạo PR preview tự động qua GitHub Actions với subdomain riêng pr-123.preview.app.com. Test trên Cloud VPS TND 2GB Ubuntu 24.04.
1. Cài Cloudflare Tunnel trên VPS
# Ubuntu/Debian
curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb
# Verify
cloudflared --version
# Login Cloudflare
cloudflared tunnel login
# Mở browser, chọn domain quản lý, authorizeSau khi login, cert file lưu ở ~/.cloudflared/cert.pem. Tạo tunnel:
cloudflared tunnel create my-app-tunnel
# Output: Created tunnel my-app-tunnel with id xxxxxxxx-xxxx
# Credentials written to /root/.cloudflared/xxxxxxxx-xxxx.json
cloudflared tunnel list2. Config tunnel route DNS
# Tạo file ~/.cloudflared/config.yml
tunnel: xxxxxxxx-xxxx
credentials-file: /root/.cloudflared/xxxxxxxx-xxxx.json
ingress:
- hostname: app.com
service: http://localhost:3000
- hostname: preview.app.com
service: http://localhost:4000
- hostname: "*.preview.app.com"
service: http://localhost:4000
- service: http_status:404
# Route DNS qua Cloudflare
cloudflared tunnel route dns my-app-tunnel app.com
cloudflared tunnel route dns my-app-tunnel preview.app.com
cloudflared tunnel route dns my-app-tunnel "*.preview.app.com"Wildcard *.preview.app.com cho phép mọi subdomain dạng pr-123.preview.app.com tự route về VPS. Đây là trick chính cho PR preview.
3. Chạy tunnel như systemd service
sudo cloudflared service install
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared
# Logs
sudo journalctl -u cloudflared -fTunnel chạy nền 24/7, auto reconnect khi mạng đứt. RAM ngốn khoảng 30-50MB, không đáng kể.
4. Build Next.js production và chạy main app
# next.config.js
module.exports = {
output: 'standalone',
}
cd /var/app/main
npm ci
npm run build
node .next/standalone/server.js
# Listen on port 3000
# Hoặc với PM2
pm2 start .next/standalone/server.js --name main-app -- -p 3000
pm2 saveTruy cập https://app.com - Cloudflare Tunnel forward request về localhost:3000 trên VPS. SSL handled by Cloudflare edge, không cần Let Encrypt.
5. Router cho PR preview - Caddy reverse proxy
Mỗi PR có 1 port riêng (4001, 4002, ...), Caddy ở port 4000 route request theo subdomain:
# /etc/caddy/Caddyfile
:4000 {
@pr {
host_regexp pr_id ^pr-(d+).preview.app.com$
}
handle @pr {
# Extract PR number, route tới port 4000 + pr_number
reverse_proxy 127.0.0.1:{re.pr_id.1}
}
handle {
respond "Not found" 404
}
}
sudo systemctl reload caddyCaddy parse subdomain, extract PR number, forward tới port tương ứng. PR 123 -> port 4123, PR 456 -> port 4456.
6. GitHub Actions deploy PR preview tự động
# .github/workflows/preview.yml
name: PR Preview
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches: [main]
jobs:
deploy-preview:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Deploy to VPS
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: deploy
key: ${{ secrets.SSH_KEY }}
script: |
PR_NUM=${{ github.event.pull_request.number }}
PORT=$((4000 + PR_NUM))
DIR=/var/preview/pr-$PR_NUM
mkdir -p $DIR
cd $DIR
git clone --depth 1 --branch ${{ github.head_ref }}
https://github.com/${{ github.repository }} . || git pull
git checkout ${{ github.event.pull_request.head.sha }}
npm ci --no-audit
npm run build
pm2 delete preview-$PR_NUM || true
PORT=$PORT pm2 start .next/standalone/server.js
--name preview-$PR_NUM --max-memory-restart 300M
- name: Comment PR
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Preview: https://pr-${context.issue.number}.preview.app.com`
})7. Cleanup preview khi PR đóng
# .github/workflows/cleanup-preview.yml
name: Cleanup Preview
on:
pull_request:
types: [closed]
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Stop preview
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: deploy
key: ${{ secrets.SSH_KEY }}
script: |
PR_NUM=${{ github.event.pull_request.number }}
pm2 delete preview-$PR_NUM || true
rm -rf /var/preview/pr-$PR_NUM8. Tối ưu RAM cho nhiều preview song song
VPS 2GB chạy được khoảng 5-8 PR preview song song nếu mỗi preview giới hạn 200-300MB. Set max_memory_restart trong PM2:
pm2 start server.js
--name preview-$PR_NUM
--max-memory-restart 300M
--node-args="--max-old-space-size=256"Nếu cần chạy 15+ preview, upgrade VPS lên 4GB (399k/tháng) hoặc tắt preview cũ không dùng tay.
9. So sánh với Vercel
| Feature | Vercel | VPS + Cloudflare Tunnel |
|---|---|---|
| Preview URL tự động | Có (mặc định) | Có (setup 1 lần) |
| SSL HTTPS | Có | Có (Cloudflare edge) |
| Cold start | ~100ms (serverless) | 0ms (PM2 always-on) |
| Bandwidth quota | 100GB/tháng free | Không giới hạn |
| Charge bất ngờ | Có khi vượt quota | Không |
| Custom runtime | Hạn chế | Toàn quyền VPS |
| Edge caching | Tự động | Cấu hình Cloudflare rules |
| Chi phí/tháng | 0-20 USD | 199k VND VPS |
10. Bảo vệ preview - không cho public truy cập
Preview chỉ team xem được, đừng để public crawler. Dùng Cloudflare Access policy hoặc basic auth:
# Caddy basic auth
*.preview.app.com {
basicauth {
team JDJhJDE0J... # bcrypt hash của password
}
reverse_proxy ...
}
# Hoặc Cloudflare Access (free 50 user):
# Cloudflare dashboard -> Access -> Applications
# Add preview.app.com với policy email = @company.com11. Monitoring và logs
# Xem trạng thái tất cả preview
pm2 list
pm2 logs preview-123 --lines 50
pm2 monit
# Cloudflare tunnel logs
sudo journalctl -u cloudflared -f --since "10 min ago"
# Cleanup stale preview (chạy daily cron)
cat > /usr/local/bin/cleanup-stale.sh <<'SH'
#!/bin/bash
pm2 jlist | jq -r '.[] | select(.pm2_env.created_at < (now - 86400*7)*1000) | .name' | xargs -I{} pm2 delete {}
SH
chmod +x /usr/local/bin/cleanup-stale.sh
echo '0 3 * * * /usr/local/bin/cleanup-stale.sh' >> /etc/crontab12. Troubleshooting common issues
- 502 Bad Gateway: tunnel up nhưng app local crash. Check pm2 logs preview-XXX.
- SSL cert pending: wildcard DNS chưa propagate, đợi 5-10 phút sau khi tạo route.
- OOM kill: quá nhiều preview, giảm max-memory-restart hoặc upgrade VPS.
- Port conflict: 2 PR cùng số -> đảm bảo cleanup workflow chạy đúng.
- Slow build: dùng npm ci thay npm install, cache .next/cache giữa lần build.
Self-host Next.js + PR preview chỉ với VPS 199k/tháng
Cloud VPS TND sẵn AlmaLinux 9, Ubuntu 22/24, Debian 12/13. SSD CEPH, snapshot 1-click, backup hằng ngày, network 200Mbps trong nước. Cài cloudflared trong 5 phút, deploy PR preview không lo Vercel charge bất ngờ.
Xem 8 cấu hình Cloud VPS →FAQ
Cloudflare Tunnel có thật sự free không?
Có. Cloudflare Tunnel (cloudflared) miễn phí hoàn toàn cho mọi domain bạn quản lý trên Cloudflare. Không giới hạn bandwidth, không giới hạn tunnel.
Tunnel có chậm hơn deploy thông thường không?
Có thêm ~20-50ms latency vì traffic qua Cloudflare edge. Bù lại được CDN cache, DDoS protection, SSL free. Cho preview site thì không vấn đề gì.
VPS 2GB chạy được tối đa bao nhiêu PR preview?
Khoảng 5-8 preview nếu giới hạn mỗi preview 200-300MB RAM. Trừ Postgres + Redis nếu có. Cần nhiều hơn thì upgrade lên 4GB chạy 12-15 preview.
Có nên dùng ngrok thay Cloudflare Tunnel?
Ngrok free chỉ cho 1 tunnel, URL random thay đổi mỗi lần restart. Cloudflare Tunnel free không giới hạn, custom domain, ổn định 24/7. Cloudflare thắng cho production use case.
Build Next.js trên CI hay trên VPS tốt hơn?
Build trên VPS đỡ phải transfer 100MB .next folder. Nhưng nếu VPS yếu (1 vCPU) thì build chậm, blocking. Compromise: build trên CI, transfer dist .next qua scp.
Bảo mật preview thế nào để Google không index?
Thêm X-Robots-Tag: noindex header trong response, hoặc dùng Cloudflare Access policy yêu cầu login email. Cách dứt khoát: basic auth qua Caddy, password chia sẻ qua 1password team.
- Google Workspace Business Starter vs Standard vs Plus
- Bun runtime trên VPS: chạy Next.js nhanh hơn Node.js 2x?
- Hermes AI Agent: cài đặt trên VPS + gắn proxy chạy automation ổn định
- Cấu hình MCP cho Antigravity: kết nối Gemini 3 vào WordPress + Postgres tự host
- Caddy server vs Nginx cho dev solo: cái nào dễ setup hơn?
- OpenClaw là gì? Nền tảng AI Agent mã nguồn mở đáng chú ý cho cá nhân và doanh nghiệp



