Native Deploy (SSH + pm2 + Caddy) - Design
- Issue: #221
- Date: 2026-07-13
- Status: Approved (design)
1. Bối cảnh & mục tiêu
Hiện production deploy bằng Docker: build image (docker/*.Dockerfile) → push ghcr.io → SSH pull + docker compose -f docker-compose.prod.yml up. Caddy, backend, frontend, docs, Postgres, Redis, Portainer đều là container.
Mục tiêu: chuyển app (backend + frontend + docs) sang chạy native trên server (SSH + git pull + build tại chỗ + pm2 + Caddy native), giữ Docker cho infra (Postgres/TimescaleDB, Redis, ThingsBoard). Đơn giản hoá vận hành, bỏ tầng image registry cho app.
Ngoài phạm vi (out of scope): mobile, gateway firmware, CI pipeline, thay đổi logic ứng dụng.
2. Quyết định đã chốt
| Chủ đề | Quyết định |
|---|---|
| Phạm vi native | Chỉ app (backend/frontend/docs). Infra giữ Docker (docker-compose.infra.yml). |
| Build/delivery | git pull + build trên server. |
| Reverse proxy/TLS | Caddy cài native (systemd), auto-HTTPS Let’s Encrypt. |
| Serve static | Caddy file_server phục vụ thẳng frontend/dist và docs-website/site. |
| Process manager | pm2, backend chạy bằng bun, fork mode 1 instance. |
| File Docker cũ | Xóa các file Docker production. Giữ infra + dev Dockerfiles. |
3. Kiến trúc runtime
Internet (80/443)
│
┌─────▼─────┐ Caddy native (systemd)
│ Caddy │ auto-HTTPS
└──┬────┬───┘
/api,/ws → :3000 │ │ / → file_server
┌────────▼─┐ ├──────────────► frontend/dist (SPA static)
pm2 (bun)→ │ backend │ └──────────────► docs-website/site (static)
└────┬─────┘
│ localhost
Docker infra (docker-compose.infra.yml):
Postgres/TimescaleDB (5432) · Redis (6379) · ThingsBoard (8090 REST/WS, 1883 MQTT)
Chỉ backend là process do pm2 quản lý. Frontend/docs là file tĩnh do Caddy phục vụ (không cần process riêng).
4. Thành phần chi tiết
4.1 Backend dưới pm2
Facts:
nest build→dist/src/main.js; app listenPORT(default 3000).ConfigModule.forRoot({ envFilePath: '.env' })→ đọc.envtheo cwd của backend.migration:runkhi có bản dist dùngbunx typeorm migration:run -d dist/src/database/migrations.js.
deploy/ecosystem.config.cjs:
- 1 app
hmpiot-backend cwd: đường dẫn thư mụcbackendscript:dist/src/main.jsinterpreter:bun(bun trong PATH)exec_mode:fork,instances: 1 (an toàn cho WebSocket/ws+ subscription MQTT in-memory; cluster sẽ gây lệch state)env:NODE_ENV=productionautorestart: true, giới hạn restart hợp lý
Boot persistence: pm2 startup + pm2 save.
4.2 Env - single source of truth
- 1 file
.env.productionở root server (không commit; đã có.env.production.example). - Backend: symlink
backend/.env -> ../.env.production(vì ConfigModule đọc.env). Tạo/refresh symlink trong deploy script. - Frontend: symlink
frontend/.env.production -> ../.env.production.vite buildmặc định modeproductionsẽ tự load file này qua dotenv parser (xử lý đúng giá trị có dấu cách nhưVITE_APP_NAME=HMP IoT Platform) và chỉ expose biếnVITE_*ra client. Tránhsource .env.productionvì sẽ vỡ với value không quote có dấu cách.
4.3 Caddy native (/etc/caddy/Caddyfile)
app.iot.farmvion.com:handle /api/*,handle /ws/*,handle /api→reverse_proxy localhost:3000(giữ header up như hiện tại;/wsgiữConnection/Upgrade).handle {}→root * <path>/frontend/dist+file_server+try_files {path} /index.html(SPA fallback).- Security headers + gzip/zstd giữ như bản Docker.
docs.iot.farmvion.com→root * <path>/docs-website/site+file_server.- Bỏ block
portainervàemqx(chỉ dành cho Docker). - (Tùy chọn)
tb.iot.farmvion.com→reverse_proxy localhost:8090cho dashboard ThingsBoard.
4.4 Deploy flow - deploy/scripts/deploy.sh (chạy trên server)
1. kiểm tra .env.production tồn tại
2. git pull (branch hiện tại / master)
3. bun install (frozen lockfile)
4. refresh symlink backend/.env -> ../.env.production
5. backend: bun run build
6. frontend: (source env) bun run build
7. docs: bun run build
8. backend: bun run migration:run
9. pm2 reload deploy/ecosystem.config.cjs (hoặc start nếu chưa chạy)
10. caddy validate + reload nếu Caddyfile đổi
Idempotent: chạy lại nhiều lần an toàn. Lỗi ở bước build/migration thì dừng (set -e), không reload pm2.
4.5 Provisioning lần đầu - deploy/scripts/server-setup.sh
Cài + cấu hình:
- Docker + compose plugin (chỉ để chạy infra).
- bun (curl installer), Node.js (đủ cho pm2 CLI), pm2 (global).
- Caddy (apt repo chính thức), enable systemd.
- Khởi động
docker compose -f docker-compose.infra.yml up -d. - Hướng dẫn tạo
.env.productiontừ example;pm2 startup.
Firewall: 22, 80, 443, 1883 (MQTT). Không cần mở 5432/6379/8090 ra ngoài.
4.6 Dọn dẹp file Docker production
Xóa:
docker-compose.prod.ymldeploy.sh(root, luồng ghcr.io),release.sh,build.sh.vps.config,.vps.config.exampledocker/backend.Dockerfile,docker/frontend.Dockerfile,docker/docs.Dockerfile(production)docker/nginx.conf,docker/nginx-frontend.conf,docker/nginx-docs.conf(chỉ dùng cho prod image, Caddy nay serve static)caddy/Caddyfile(bản trùng, không được tham chiếu ở đâu)- Makefile targets
build-containers,release
Giữ:
docker-compose.infra.yml,docker-compose.yml(dev) vàdocker/*.dev.Dockerfile,docker/backend.dev.start.sh,.dockerignoredeploy/Caddyfile,deploy/scripts/*(viết lại),deploy/scripts/backup.sh(pg_dump quadocker execvẫn hợp lệ vì infra còn Docker)
5. Data flow (deploy 1 lần)
- Dev SSH lên server, vào
REMOTE_APP_DIR. deploy/scripts/deploy.sh→ pull + build + migrate + pm2 reload.- Caddy (đang chạy) phục vụ static mới + proxy tới backend đã reload.
- Không downtime backend nhờ
pm2 reload(graceful).
6. Xử lý lỗi
deploy.shset -e: build/migration fail → dừng trước khi reload, giữ bản đang chạy.pm2 reloadgraceful; nếu backend crash-loop,pm2giữ log,autorestartcó giới hạn.- Caddy
caddy validatetrướcreload; config lỗi → không reload, giữ cấu hình cũ. - Infra Docker
restart: unless-stoppedtự lên lại sau reboot; pm2startup+save; Caddy systemd enabled.
7. Kiểm thử / nghiệm thu
Vì đây là thay đổi hạ tầng (script/config), không phải code app, “test” ở đây là kiểm chứng vận hành:
bunx @biomejs/biomekhông áp cho shell/Caddyfile; đảm bảo không phá lint TS hiện có.deploy/scripts/deploy.shchạy được vớibash -n(syntax) và dry-run ở môi trường staging.caddy validate --config deploy/Caddyfile.- Nghiệm thu E2E trên staging: HTTPS lên,
/api/v1trả 200, đăng nhập được, WebSocket/wskết nối, docs mở được, backend tự lên lại saureboot.
8. Rủi ro & lưu ý
- Build trên server tốn RAM/CPU lúc deploy (frontend Vite build) — server 4-8GB đủ nhưng nên deploy lúc traffic thấp.
- Cần cả bun (build+runtime) và node (pm2 CLI) trên server.
- Symlink
backend/.envphải được refresh mỗi deploy (git pull không đụng file untracked nhưng symlink cần đảm bảo tồn tại). - ThingsBoard first boot mất vài phút (đã ghi chú trong infra compose).