operations/deploy-checklist.md

Deploy Checklist — Glamvoo Production

Checklist 1-shot: làm tuần tự từ trên xuống. Mỗi mục check xong thì tick [x]. Reference chi tiết: docker-deploy.md.

Phase 1 — One-time setup (chỉ làm 1 lần đầu)

1.1 GitHub — Container Registry & Secrets

Repos cần có (đã xong):

  • novagoo/booking-api — pushed
  • novagoo/booking-web — pushed
  • novagoo/booking-docs — pushed

booking-web Variables (https://github.com/novagoo/booking-web/settings/variables/actions → Variables tab):

Name Value
- [ ] NEXT_PUBLIC_API_URL https://glamvoo.com/api
- [ ] NEXT_PUBLIC_APP_NAME Glamvoo
- [ ] NEXT_PUBLIC_APP_SHORT_NAME GV
- [ ] NEXT_PUBLIC_STORAGE_BUCKET booking-assetsphải khớp STORAGE_BUCKET ở §1.2

booking-web Secrets (Secrets tab):

Name Value
- [ ] NEXT_PUBLIC_GOOGLE_CLIENT_ID Google OAuth Client ID (giống dev)

Verify: re-run workflow để image có env đúng:

gh workflow run "Build & Push booking-web" --repo novagoo/booking-web --ref main
gh run watch --repo novagoo/booking-web
  • Build success (xanh)

1.2 VNPT S3

  • Login VNPT cloud console
  • Tạo bucket tên booking-assets (PHẢI giống NEXT_PUBLIC_STORAGE_BUCKET)
  • Set bucket private (default)
  • Tạo Access Key riêng cho prod (best practice) HOẶC dùng key dev:
    • Copy STORAGE_ACCESS_KEY
    • Copy STORAGE_SECRET_KEY

1.3 DNS — point domain về VPS

Trên DNS provider (Cloudflare/Namecheap/etc.):

Record Type Value
- [ ] glamvoo.com A <IP VPS>
- [ ] www.glamvoo.com A <IP VPS> (hoặc CNAME → glamvoo.com)
- [ ] images.glamvoo.com A <IP VPS>
- [ ] docs.glamvoo.com A <IP VPS>

Verify DNS đã propagate:

dig +short glamvoo.com
dig +short www.glamvoo.com
dig +short images.glamvoo.com
dig +short docs.glamvoo.com
# Cả 4 phải trả về IP VPS

1.4 Server bootstrap

Postgres (đã xong nếu user/db/listen_addresses đã set)

  • User glamvoo_user + DB glamvoo tạo
  • listen_addresses = '*' + restart
  • pg_hba.conf thêm host glamvoo glamvoo_user 172.16.0.0/12 scram-sha-256 + reload

Verify:

sudo ss -tlnp | grep 5432   # phải thấy 0.0.0.0:5432

UFW

  • sudo ufw allow 22/tcp
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw allow from 172.16.0.0/12 to any port 5432 proto tcp
    sudo ufw enable
    
> KHÔNG `ufw deny 5432/tcp` — sẽ block luôn allow specific. Default policy đã deny.

Verify: `sudo ufw status` → 22/80/443 ALLOW.

#### Docker

- [ ] `curl -fsSL https://get.docker.com | sh`
- [ ] `sudo usermod -aG docker $USER`
- [ ] `sudo systemctl enable --now docker`
- [ ] `newgrp docker`
- [ ] `docker --version` → in version

#### TLS certs

- [ ] `sudo certbot certonly --standalone -d glamvoo.com -d www.glamvoo.com`
- [ ] `sudo certbot certonly --standalone -d images.glamvoo.com`
- [ ] `sudo certbot certonly --standalone -d docs.glamvoo.com`

Verify:
```bash
ls /etc/letsencrypt/live/glamvoo.com/         # fullchain.pem + privkey.pem
ls /etc/letsencrypt/live/images.glamvoo.com/  # fullchain.pem + privkey.pem
ls /etc/letsencrypt/live/docs.glamvoo.com/    # fullchain.pem + privkey.pem

Basic auth cho docs.glamvoo.com

Docs site public-accessible nhưng cần password để xem (internal only).

# Cài htpasswd tool
sudo apt install -y apache2-utils

# Tạo file (lệnh `-c` = create new, ghi đè nếu có)
htpasswd -c /opt/booking-system/nginx/.htpasswd <username>
# Nhập password 2 lần

# Thêm user khác (KHÔNG -c kẻo ghi đè)
htpasswd /opt/booking-system/nginx/.htpasswd <username2>
  • Tạo nginx/.htpasswd với ít nhất 1 user TRƯỚC khi docker compose up nginx

⚠️ Quan trọng: tạo .htpasswd (file) trước khi start nginx container. Nếu start nginx trước → Docker bind-mount path không tồn tại → tự tạo DIRECTORY rỗng → nginx restart loop. Fix: docker compose stop nginx && rmdir nginx/.htpasswd && htpasswd -c nginx/.htpasswd admin && docker compose up -d nginx.

File nginx/.htpasswd đã được gitignore, không commit. Chỉ tồn tại trên server.

GHCR login

  • Tạo PAT classic ở https://github.com/settings/tokens với scope read:packages
  • Nếu org novagoo enable SSO → click "Configure SSO" trên token, authorize cho novagoo
  • echo ghp_xxxxx | docker login ghcr.io -u <USERNAME> --password-stdinLogin Succeeded

Verify:

docker pull ghcr.io/novagoo/booking-api:latest
# Phải pull được

1.5 Clone deploy stack + config env

sudo mkdir -p /opt/booking-system
sudo chown $USER:$USER /opt/booking-system
cd /opt/booking-system
git clone git@github.com:novagoo/booking-docs.git .
cp .env.example .env
  • Clone OK

Generate keys (paste output vào .env step kế):

echo "JWT_SECRET=$(openssl rand -hex 32)"
echo "PAYMENT_ENCRYPTION_KEY=$(openssl rand -hex 32)"
echo "IMGPROXY_KEY=$(openssl rand -hex 32)"
echo "IMGPROXY_SALT=$(openssl rand -hex 32)"

Edit .env (nano .env):

Field Value
- [ ] DATABASE_URL Thay STRONG_PASSWORD bằng pass glamvoo_user
- [ ] JWT_SECRET Paste từ openssl
- [ ] BULL_BOARD_PASS Password mạnh tự chọn
- [ ] SMTP_HOST console (dev test) hoặc SMTP provider thật
- [ ] GOOGLE_CLIENT_ID Google OAuth Client ID (giống NEXT_PUBLIC_GOOGLE_CLIENT_ID)
- [ ] PAYMENT_ENCRYPTION_KEY Paste từ openssl
- [ ] STORAGE_ACCESS_KEY VNPT key
- [ ] STORAGE_SECRET_KEY VNPT secret
- [ ] STORAGE_BUCKET booking-assets (giống GHA Variable)
- [ ] IMGPROXY_KEY Paste từ openssl
- [ ] IMGPROXY_SALT Paste từ openssl

Lưu file (Ctrl+O → Enter → Ctrl+X).

Phase 2 — First deploy

cd /opt/booking-system
./deploy.sh
  • Script chạy tới end, không lỗi
  • docker compose -f docker-compose.prod.yml ps → tất cả service healthy

Verify:

curl -i https://glamvoo.com/api/health
# Phải trả 200 + JSON {"status":"ok",...}
  • Health 200 OK

Mở browser:

  • https://glamvoo.com → web load OK
  • https://www.glamvoo.com → 301 redirect về root
  • https://images.glamvoo.com → 404 (đúng — chưa có signed URL nào để test, nhưng response phải đến từ imgproxy)

Phase 3 — Tạo super-admin lần đầu

docker compose -f docker-compose.prod.yml exec api yarn reset:superadmin
# Nhập email + password (min 6 ký tự)
  • Super-admin tạo OK
  • Login https://glamvoo.com/admin → vào được dashboard

Phase 4 — Subsequent deploys (lần thứ 2+)

Khi push code mới lên booking-api hoặc booking-web:

  1. Trên devgit push → GHA tự build image (~2-3 phút)
  2. Trên server:
    cd /opt/booking-system
    ./deploy.sh
    

Đặc biệt:

  • Scale: API_REPLICAS=4 ./deploy.sh
  • Rollback: IMAGE_TAG=sha-<old-sha> ./deploy.sh
  • Logs: docker compose -f docker-compose.prod.yml logs -f api

Common pitfalls

Triệu chứng Fix
docker login ghcr.io denied PAT chưa SSO authorize cho org novagoo (xem §1.4)
connection refused đến Postgres pg_hba.conf chưa allow 172.16.0.0/12, hoặc listen_addresses vẫn là localhost
Image avatar/portfolio không hiện NEXT_PUBLIC_STORAGE_BUCKET GHA ≠ STORAGE_BUCKET .env. Hoặc bucket VNPT khác tên
Throttler chặn nhầm IP TRUST_PROXY chưa set 1 (đã set sẵn ở compose, không cần làm gì)
502 sau deploy Đợi healthcheck pass — deploy.sh đã handle, nhưng khi update DNS/cert thì có blip
images.glamvoo.com 502 Cert subdomain chưa issue, hoặc DNS chưa propagate