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/.envfrontend/.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,/wslocalhost:3000, file_server cho 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ỏ target build-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.cjs

  • Step 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/Caddyfile

  • Step 1: Ghi đè deploy/Caddyfile cho 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.sh

  • Step 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.sh

  • Step 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/Caddyfile

  • Step 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-19

  • Step 1: Bỏ target build-containersrelease

Sửa dòng .PHONY (dòng 1) — bỏ build-containersrelease:

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.md

  • Step 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 pullbun install → build backend/frontend/docs → chạy
migrations → pm2 reloadcaddy 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/.env là symlink tới ../.env.production và DB/Redis đang chạy.
  • Migration lỗi: cd backend && bun run migration:run chạy tay để xem log.
  • Static 404: đảm bảo frontend/distdocs-website/site đã build
    (chạy lại deploy/scripts/deploy.sh) và APP_ROOT/DOCS_ROOT khớp đường dẫn.

Contact

admin@farmvion.com


- [ ] **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:

  1. Trong bảng make targets: xóa 2 dòng
    | make build-containers| Build Docker images viabuild.sh. |
    | make release| Run full check then deploy viarelease.sh. |.
  2. 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 port 3000 khớp Caddyfile (T2) và main.ts default.
  • No placeholders: mọi bước có nội dung/command cụ thể + expected output.