Native Deploy (SSH + pm2 + Caddy) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Chuyển deployment production của app (backend/frontend/docs) từ Docker sang native trên server: git pull + build tại chỗ, backend chạy dưới pm2 (interpreter bun), Caddy native phục vụ static + proxy; infra (Postgres/Redis/ThingsBoard) giữ Docker.
Architecture: Backend là process duy nhất do pm2 quản lý (bun dist/src/main.js, fork 1 instance). Frontend/docs build ra file tĩnh và Caddy native file_server phục vụ thẳng. Env dùng 1 file .env.production ở root server, symlink vào backend/.env và frontend/.env.production. Một script deploy.sh chạy trên server làm toàn bộ: pull → build → migrate → pm2 reload → caddy reload. Toàn bộ file Docker production bị xóa.
Tech Stack: bash, pm2, Caddy v2, bun, Docker (chỉ cho infra), NestJS (dist CommonJS), Vite, docmd.
Spec: docs/superpowers/specs/2026-07-13-native-deploy-pm2-design.md (issue #221)
Testing note: Đây là thay đổi hạ tầng (script + config), không phải code app nên không có unit test theo TDD. “Verify” ở mỗi task dùng: bash -n (syntax shell), node --check (syntax JS config), caddy validate khi có caddy, và grep để xác nhận không còn tham chiếu file đã xóa. Không được claim thành công nếu lệnh verify không chạy đúng như “Expected”.
Branch: issue-221 (đã tạo). Mỗi task commit riêng.
File Structure
Files được tạo/sửa/xóa:
- Create
deploy/ecosystem.config.cjs— cấu hình pm2 cho backend (1 app, interpreter bun, fork). - Rewrite
deploy/Caddyfile— Caddy native: proxy/api,/ws→localhost:3000,file_servercho frontend/docs, bỏ portainer/emqx. - Rewrite
deploy/scripts/deploy.sh— pull + build + migrate + pm2 reload + caddy reload (chạy trên server). - Create
deploy/scripts/server-setup.sh— provisioning lần đầu (docker infra, bun, node+pm2, caddy). - Rewrite
deploy/README.md— hướng dẫn deploy native. - Modify
README.md— thay section deployment (docker/ghcr → native). - Modify
Makefile— bỏ targetbuild-containers,release. - Delete (Docker production):
docker-compose.prod.yml,deploy.sh,release.sh,build.sh,.vps.config,.vps.config.example,docker/backend.Dockerfile,docker/frontend.Dockerfile,docker/docs.Dockerfile,docker/nginx.conf,docker/nginx-frontend.conf,docker/nginx-docs.conf,caddy/Caddyfile.
Giữ nguyên: docker-compose.infra.yml, docker-compose.yml (dev), docker/*.dev.Dockerfile, docker/backend.dev.start.sh, .dockerignore, deploy/scripts/backup.sh, .env.production.example.
Task 1: pm2 ecosystem config cho backend
Files:
Create:
deploy/ecosystem.config.cjsStep 1: Viết file cấu hình pm2
Tạo deploy/ecosystem.config.cjs:
const path = require('node:path');
// Backend chạy native dưới pm2, interpreter bun.
// cwd trỏ tới thư mục backend để ConfigModule đọc đúng backend/.env,
// và migration/typeorm resolve đúng dist paths.
module.exports = {
apps: [
{
name: 'hmpiot-backend',
cwd: path.resolve(__dirname, '../backend'),
script: 'dist/src/main.js',
interpreter: 'bun',
exec_mode: 'fork',
instances: 1,
autorestart: true,
max_restarts: 10,
restart_delay: 5000,
max_memory_restart: '512M',
env: {
NODE_ENV: 'production',
},
},
],
};
- Step 2: Verify syntax
Run: node --check deploy/ecosystem.config.cjs && node -e "const c=require('./deploy/ecosystem.config.cjs'); console.log(c.apps[0].name, c.apps[0].interpreter, c.apps[0].exec_mode, c.apps[0].instances)"
Expected: in ra hmpiot-backend bun fork 1 và không có lỗi.
- Step 3: Commit
git add deploy/ecosystem.config.cjs
git commit -m "feat(deploy): add pm2 ecosystem config for native backend (#221)"
Task 2: Caddyfile native
Files:
Rewrite:
deploy/CaddyfileStep 1: Ghi đè
deploy/Caddyfilecho native
Thay toàn bộ nội dung deploy/Caddyfile bằng:
# =============================================================================
# Caddyfile for HMP IoT Platform - NATIVE deploy
# Auto HTTPS via Let's Encrypt. Serves static SPA/docs from disk and proxies
# API/WS to the backend running under pm2 on localhost:3000.
# Domains and doc roots are overridable via environment variables.
# =============================================================================
{
email {$ACME_EMAIL:admin@farmvion.com}
# Uncomment to test without hitting Let's Encrypt rate limits:
# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
# =============================================================================
# Main Application
# =============================================================================
{$APP_DOMAIN:app.iot.farmvion.com} {
encode gzip zstd
# API routes -> backend (pm2)
handle /api/* {
reverse_proxy localhost:3000 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
# WebSocket routes -> backend (pm2)
handle /ws/* {
reverse_proxy localhost:3000 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
header_up Connection {http.request.header.Connection}
header_up Upgrade {http.request.header.Upgrade}
}
}
# Swagger docs endpoint
handle /api {
reverse_proxy localhost:3000
}
# Everything else -> frontend SPA static files
handle {
root * {$APP_ROOT:/opt/iot_platform/frontend/dist}
try_files {path} /index.html
file_server
}
header {
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
log {
output file /var/log/caddy/access.log {
roll_size 10mb
roll_keep 5
}
}
}
# =============================================================================
# Docs Website (static)
# =============================================================================
{$DOCS_DOMAIN:docs.iot.farmvion.com} {
encode gzip zstd
handle {
root * {$DOCS_ROOT:/opt/iot_platform/docs-website/site}
file_server
}
header {
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
log {
output file /var/log/caddy/docs-access.log {
roll_size 10mb
roll_keep 5
}
}
}
- Step 2: Verify không còn tham chiếu Docker service names / block đã bỏ
Run: grep -nE "backend:3000|frontend:80|docs:80|portainer|emqx" deploy/Caddyfile || echo "CLEAN"
Expected: in ra CLEAN (không còn backend:3000, portainer, emqx…).
- Step 3: Verify có localhost proxy + file_server
Run: grep -c "reverse_proxy localhost:3000" deploy/Caddyfile; grep -c "file_server" deploy/Caddyfile
Expected: dòng đầu 3, dòng sau 2.
- Step 4: (Nếu có caddy trên máy) validate cú pháp
Run: command -v caddy >/dev/null && caddy validate --adapter caddyfile --config deploy/Caddyfile || echo "caddy not installed - skip (validate on server)"
Expected: Valid configuration HOẶC dòng caddy not installed - skip....
- Step 5: Commit
git add deploy/Caddyfile
git commit -m "feat(deploy): rewrite Caddyfile for native static serve + localhost proxy (#221)"
Task 3: Deploy script (chạy trên server)
Files:
Rewrite:
deploy/scripts/deploy.shStep 1: Ghi đè
deploy/scripts/deploy.sh
Thay toàn bộ nội dung bằng:
#!/usr/bin/env bash
# =============================================================================
# HMP IoT Platform - Native deployment (run ON the server)
# git pull -> bun install -> build (backend/frontend/docs)
# -> migrations -> pm2 reload -> caddy reload
# Infra (Postgres/Redis/ThingsBoard) runs separately via docker-compose.infra.yml.
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
ENV_FILE="$PROJECT_DIR/.env.production"
log() { printf '\n==> %s\n' "$1"; }
cd "$PROJECT_DIR"
if [ ! -f "$ENV_FILE" ]; then
echo "ERROR: $ENV_FILE not found. Copy .env.production.example and fill it in." >&2
exit 1
fi
log "Pulling latest changes"
git pull --ff-only
log "Installing dependencies (frozen lockfile)"
bun install --frozen-lockfile
log "Linking env files to root .env.production"
# Backend reads ./.env (ConfigModule envFilePath: '.env').
ln -sf "$ENV_FILE" "$PROJECT_DIR/backend/.env"
# Vite loads frontend/.env.production in production build mode.
ln -sf "$ENV_FILE" "$PROJECT_DIR/frontend/.env.production"
log "Building backend"
( cd "$PROJECT_DIR/backend" && bun run build )
log "Building frontend"
( cd "$PROJECT_DIR/frontend" && bun run build )
log "Building docs"
( cd "$PROJECT_DIR/docs-website" && bun run build )
log "Running database migrations"
( cd "$PROJECT_DIR/backend" && bun run migration:run )
log "Reloading backend via pm2"
if pm2 describe hmpiot-backend >/dev/null 2>&1; then
pm2 reload "$PROJECT_DIR/deploy/ecosystem.config.cjs"
else
pm2 start "$PROJECT_DIR/deploy/ecosystem.config.cjs"
fi
pm2 save
log "Reloading Caddy"
if command -v caddy >/dev/null 2>&1; then
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
else
echo "WARNING: caddy not found on PATH; skipping reload."
fi
log "Deployment complete"
pm2 status
- Step 2: Verify cú pháp shell
Run: bash -n deploy/scripts/deploy.sh && echo "SYNTAX OK"
Expected: SYNTAX OK.
- Step 3: Verify không còn lệnh docker compose prod
Run: grep -nE "docker-compose.prod|docker compose .* up|ghcr.io" deploy/scripts/deploy.sh || echo "CLEAN"
Expected: CLEAN.
- Step 4: Verify các bước bắt buộc có mặt
Run: grep -c "bun run build" deploy/scripts/deploy.sh; grep -c "pm2" deploy/scripts/deploy.sh; grep -c "migration:run" deploy/scripts/deploy.sh
Expected: 3, một số >=3, 1.
- Step 5: Đặt executable + commit
chmod +x deploy/scripts/deploy.sh
git add deploy/scripts/deploy.sh
git commit -m "feat(deploy): rewrite deploy.sh for native build + pm2 + caddy (#221)"
Task 4: Server provisioning script
Files:
Create:
deploy/scripts/server-setup.shStep 1: Tạo
deploy/scripts/server-setup.sh
#!/usr/bin/env bash
# =============================================================================
# HMP IoT Platform - One-time server provisioning (Ubuntu 22.04+)
# Installs: Docker (infra only), bun, Node.js + pm2, Caddy.
# Starts infra via docker-compose.infra.yml.
# Idempotent-ish: safe to re-run; skips installs that already exist.
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
log() { printf '\n==> %s\n' "$1"; }
# ----- Docker (for infra: Postgres/Redis/ThingsBoard) -----
if ! command -v docker >/dev/null 2>&1; then
log "Installing Docker"
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker "$USER"
echo "NOTE: log out/in for docker group to take effect."
else
log "Docker already installed"
fi
# ----- Node.js + pm2 (pm2 CLI needs node) -----
if ! command -v node >/dev/null 2>&1; then
log "Installing Node.js LTS"
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs
else
log "Node.js already installed"
fi
if ! command -v pm2 >/dev/null 2>&1; then
log "Installing pm2"
sudo npm install -g pm2
else
log "pm2 already installed"
fi
# ----- bun (build + backend runtime) -----
if ! command -v bun >/dev/null 2>&1; then
log "Installing bun"
curl -fsSL https://bun.sh/install | bash
echo "NOTE: add \$HOME/.bun/bin to PATH (installer updates your shell rc)."
else
log "bun already installed"
fi
# ----- Caddy (native reverse proxy + static server) -----
if ! command -v caddy >/dev/null 2>&1; then
log "Installing Caddy"
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt-get update
sudo apt-get install -y caddy
else
log "Caddy already installed"
fi
log "Preparing Caddy config + log dir"
sudo mkdir -p /var/log/caddy
sudo cp "$PROJECT_DIR/deploy/Caddyfile" /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl enable caddy
sudo systemctl restart caddy
log "Starting infra containers"
docker compose -f "$PROJECT_DIR/docker-compose.infra.yml" up -d
log "Enabling pm2 startup on boot"
pm2 startup systemd -u "$USER" --hp "$HOME" || true
cat <<EOF
==> Server setup done.
Next steps:
1. cp .env.production.example .env.production (fill in secrets)
2. Run: deploy/scripts/deploy.sh
3. Ensure DNS A records point here; firewall opens 22, 80, 443, 1883.
EOF
- Step 2: Verify cú pháp shell
Run: bash -n deploy/scripts/server-setup.sh && echo "SYNTAX OK"
Expected: SYNTAX OK.
- Step 3: Verify cài đủ 4 thành phần + start infra
Run: grep -Eic "docker|node|pm2|bun|caddy" deploy/scripts/server-setup.sh; grep -c "docker-compose.infra.yml" deploy/scripts/server-setup.sh
Expected: số đầu >=5; số sau 1.
- Step 4: Đặt executable + commit
chmod +x deploy/scripts/server-setup.sh
git add deploy/scripts/server-setup.sh
git commit -m "feat(deploy): add native server provisioning script (#221)"
Task 5: Xóa file Docker production
Files:
Delete:
docker-compose.prod.yml,deploy.sh,release.sh,build.sh,.vps.config,.vps.config.example,docker/backend.Dockerfile,docker/frontend.Dockerfile,docker/docs.Dockerfile,docker/nginx.conf,docker/nginx-frontend.conf,docker/nginx-docs.conf,caddy/CaddyfileStep 1: Xóa các file (dùng
git rm)
git rm docker-compose.prod.yml deploy.sh release.sh build.sh \
.vps.config .vps.config.example \
docker/backend.Dockerfile docker/frontend.Dockerfile docker/docs.Dockerfile \
docker/nginx.conf docker/nginx-frontend.conf docker/nginx-docs.conf \
caddy/Caddyfile
# caddy/ giờ trống -> git tự bỏ thư mục theo dõi; xóa thư mục rỗng còn sót nếu có:
rmdir caddy 2>/dev/null || true
- Step 2: Verify các file đã biến mất
Run: for f in docker-compose.prod.yml deploy.sh release.sh build.sh .vps.config .vps.config.example docker/backend.Dockerfile docker/frontend.Dockerfile docker/docs.Dockerfile docker/nginx.conf docker/nginx-frontend.conf docker/nginx-docs.conf caddy/Caddyfile; do [ -e "$f" ] && echo "STILL EXISTS: $f"; done; echo "CHECK DONE"
Expected: chỉ in CHECK DONE (không có dòng STILL EXISTS).
- Step 3: Verify dev vẫn còn nguyên (không xóa nhầm)
Run: for f in docker-compose.infra.yml docker-compose.yml docker/backend.dev.Dockerfile docker/frontend.dev.Dockerfile docker/backend.dev.start.sh .dockerignore deploy/scripts/backup.sh; do [ -e "$f" ] || echo "MISSING: $f"; done; echo "CHECK DONE"
Expected: chỉ in CHECK DONE.
- Step 4: Commit
git commit -m "chore(deploy): remove obsolete Docker production deploy files (#221)"
Task 6: Cập nhật Makefile
Files:
Modify:
Makefile:1,Makefile:12-13,Makefile:18-19Step 1: Bỏ target
build-containersvàrelease
Sửa dòng .PHONY (dòng 1) — bỏ build-containers và release:
Từ:
.PHONY: lint build build-containers check release setup dev dev-infra dev-stop backend-dev frontend-dev test backend-test frontend-test db-reset db-migrate db-seed
Thành:
.PHONY: lint build check setup dev dev-infra dev-stop backend-dev frontend-dev test backend-test frontend-test db-reset db-migrate db-seed
Xóa khối build-containers (dòng 12-13):
build-containers:
bash ./build.sh
Xóa khối release (dòng 18-19) và đổi check không còn được release phụ thuộc (giữ nguyên check như cũ):
release: check
bash ./release.sh
Sau khi sửa, phần đầu Makefile phải như sau (khoảng dòng 4-19):
lint:
bun run backend:lint
bun run frontend:lint
build:
bun run backend:build
bun run frontend:build
check: lint build
bun run check
dev-infra:
docker compose -f docker-compose.infra.yml up -d
docker compose -f docker-compose.infra.yml up -d --wait postgres redis thingsboard
- Step 2: Verify không còn tham chiếu script đã xóa
Run: grep -nE "build.sh|release.sh|build-containers|^release:" Makefile || echo "CLEAN"
Expected: CLEAN.
- Step 3: Verify Makefile vẫn parse được
Run: make -n dev-infra >/dev/null && echo "MAKEFILE OK"
Expected: MAKEFILE OK.
- Step 4: Commit
git add Makefile
git commit -m "chore(build): drop docker image build/release make targets (#221)"
Task 7: Viết lại deploy/README.md (hướng dẫn native)
Files:
Rewrite:
deploy/README.mdStep 1: Ghi đè
deploy/README.md
# HMP IoT Platform - Native Production Deployment
App (backend/frontend/docs) chạy **native** trên server. Infra (Postgres/TimescaleDB,
Redis, ThingsBoard) chạy qua Docker (`docker-compose.infra.yml`).
- Backend: pm2 (`bun dist/src/main.js`, fork 1 instance) trên `localhost:3000`.
- Frontend/docs: file tĩnh do **Caddy native** phục vụ.
- Caddy native: auto-HTTPS + reverse proxy `/api`,`/ws` → backend.
## Prerequisites
- VPS Ubuntu 22.04+ (4-8GB RAM).
- DNS A records trỏ về server IP:
- `app.iot.farmvion.com`
- `docs.iot.farmvion.com`
- Firewall mở: 22 (SSH), 80/443 (HTTP/HTTPS), 1883 (MQTT).
## 1. Lần đầu: provisioning
```bash
git clone https://github.com/hieunvce/iot_platform.git /opt/iot_platform
cd /opt/iot_platform
deploy/scripts/server-setup.sh # cài docker(infra)/bun/node+pm2/caddy, start infra
Nếu vừa được thêm vào group docker, log out/in rồi chạy lại bước infra nếu cần.
2. Cấu hình secrets
cp .env.production.example .env.production
nano .env.production # điền JWT_SECRET, POSTGRES_*, REDIS_PASSWORD, SMTP_*, VITE_* ...
Sinh secret:
openssl rand -base64 64 # JWT_SECRET
openssl rand -base64 32 # DB / Redis password
Đổi domain (nếu khác mặc định) qua biến môi trường cho Caddy, ví dụ trong
/etc/caddy/Caddyfile service unit hoặc export trước khi restart caddy:
APP_DOMAIN, DOCS_DOMAIN, APP_ROOT, DOCS_ROOT, ACME_EMAIL.
3. Deploy / update
cd /opt/iot_platform
deploy/scripts/deploy.sh
Script sẽ: git pull → bun install → build backend/frontend/docs → chạy
migrations → pm2 reload → caddy reload.
Common Operations
# Logs backend (pm2)
pm2 logs hmpiot-backend
# Trạng thái / restart backend
pm2 status
pm2 restart hmpiot-backend
# Logs Caddy
sudo journalctl -u caddy -f
# Infra
docker compose -f docker-compose.infra.yml ps
docker compose -f docker-compose.infra.yml logs -f postgres
# Backup DB (pg_dump qua docker exec)
deploy/scripts/backup.sh
Troubleshooting
- HTTPS chưa lên: kiểm tra DNS đã trỏ đúng (
dig app.iot.farmvion.com),
cổng 80/443 mở,sudo journalctl -u caddy -f. - Backend crash-loop:
pm2 logs hmpiot-backend --lines 200; kiểm tra
backend/.envlà symlink tới../.env.productionvà DB/Redis đang chạy. - Migration lỗi:
cd backend && bun run migration:runchạy tay để xem log. - Static 404: đảm bảo
frontend/distvàdocs-website/siteđã build
(chạy lạideploy/scripts/deploy.sh) vàAPP_ROOT/DOCS_ROOTkhớp đường dẫn.
Contact
- [ ] **Step 2: Verify không còn hướng dẫn Docker prod**
Run: `grep -niE "docker-compose.prod|portainer|ghcr|release.sh|build.sh" deploy/README.md || echo "CLEAN"`
Expected: `CLEAN`.
- [ ] **Step 3: Verify có hướng dẫn native**
Run: `grep -c "pm2" deploy/README.md; grep -c "server-setup.sh" deploy/README.md; grep -c "deploy.sh" deploy/README.md`
Expected: mỗi số `>=1`.
- [ ] **Step 4: Commit**
```bash
git add deploy/README.md
git commit -m "docs(deploy): rewrite deploy README for native workflow (#221)"
Task 8: Cập nhật root README.md
Files:
Modify:
README.md(section deployment, khoảng dòng 60-123)Step 1: Đọc section deployment hiện tại
Run: sed -n '55,123p' README.md
Expected: hiển thị bảng make targets + mô tả deploy qua release.sh/GHCR/docker-compose.prod.yml.
- Step 2: Thay thế nội dung deploy docker/ghcr bằng mô tả native
Thực hiện các sửa đổi:
- Trong bảng make targets: xóa 2 dòng
|make build-containers| Build Docker images viabuild.sh. |
và|make release| Run full check then deploy viarelease.sh. |. - Thay toàn bộ đoạn văn/section mô tả deploy (các dòng nhắc
release.sh,
build.sh,.vps.config, GHCR,docker-compose.prod.yml, “The script pushes
images to GHCR”, “uploads docker-compose.prod.yml”) bằng đoạn sau:
## Deployment
Production chạy **native** trên VPS (không Docker cho app). Infra
(Postgres/Redis/ThingsBoard) chạy qua Docker (`docker-compose.infra.yml`).
- Backend: pm2 (`bun dist/src/main.js`).
- Frontend/docs: static, phục vụ bởi Caddy native.
Quy trình:
1. Provisioning lần đầu trên server: `deploy/scripts/server-setup.sh`.
2. Tạo `/opt/iot_platform/.env.production` từ `.env.production.example`.
3. Mỗi lần deploy/update: SSH lên server rồi chạy `deploy/scripts/deploy.sh`
(`git pull` → build → migrate → `pm2 reload` → `caddy reload`).
Chi tiết: [`deploy/README.md`](deploy/README.md).
- Step 3: Verify không còn tham chiếu file/flow đã xóa
Run: grep -niE "release.sh|build.sh|.vps.config|docker-compose.prod|build-containers|ghcr" README.md || echo "CLEAN"
Expected: CLEAN.
- Step 4: Verify có mô tả native
Run: grep -c "server-setup.sh" README.md; grep -c "deploy/scripts/deploy.sh" README.md
Expected: mỗi số >=1.
- Step 5: Commit
git add README.md
git commit -m "docs: update root README deployment section for native flow (#221)"
Task 9: Quét toàn repo, đảm bảo không còn tham chiếu treo
Files:
(kiểm tra; sửa nếu phát hiện)
Step 1: Grep toàn repo (trừ node_modules, docs specs/plans, lockfiles) các file đã xóa
Run:
grep -rniE "docker-compose\.prod|release\.sh|build\.sh|\.vps\.config|build-containers|docker/backend\.Dockerfile|docker/frontend\.Dockerfile|docker/docs\.Dockerfile" \
--include="*.md" --include="*.yml" --include="*.yaml" --include="*.sh" --include="Makefile" --include="*.json" \
. 2>/dev/null | grep -vE "node_modules|docs/superpowers/(specs|plans)/|bun.lock|release-manifest.json" || echo "NO DANGLING REFS"
Expected: NO DANGLING REFS. Nếu có kết quả (ví dụ trong AGENTS.md, CODEBASE_MAP.md, docs/), mở file đó và sửa/xóa dòng tham chiếu, rồi commit kèm.
- Step 2: Verify tổng thể lint TS không bị ảnh hưởng (không đụng code app)
Run: bun run check
Expected: pass (Biome không báo lỗi mới; các thay đổi chỉ là script/config/markdown/Makefile nên không phát sinh lỗi TS/JS mới). Nếu Biome format ecosystem.config.cjs, chấp nhận thay đổi format và git add lại.
- Step 3: Commit (nếu có sửa)
git add -A
git commit -m "chore: scrub dangling references to removed docker deploy files (#221)"
Task 10: PR
- Step 1: Push branch
git push -u origin issue-221
- Step 2: Tạo PR chống
master
bunx -y gh-axi pr create --base master --head issue-221 \
--title "Native deploy: SSH + pm2 + Caddy native (infra giữ Docker) (#221)" \
--body "Closes #221. Chuyển deploy production sang native. Xem docs/superpowers/specs/2026-07-13-native-deploy-pm2-design.md."
Expected: in ra URL của PR.
Self-Review notes (đã kiểm)
- Spec coverage: ecosystem (§4.1)→T1; env symlink (§4.2)→T3; Caddyfile (§4.3)→T2; deploy flow (§4.4)→T3; server-setup (§4.5)→T4; cleanup (§4.6)→T5,T6,T7,T8,T9; testing (§7)→verify steps mỗi task.
- Type/tên nhất quán: pm2 app name
hmpiot-backend(T1) dùng thống nhất ở T3 (pm2 describe hmpiot-backend), README (T7). Backend port3000khớp Caddyfile (T2) và main.ts default. - No placeholders: mọi bước có nội dung/command cụ thể + expected output.