Bỏ qua để đến nội dung

Build & đóng gói bộ cài (trên máy dev)

Dùng 2 script riêng cho backend/frontend — mỗi script tự tăng version patch (backend/version.json / frontend/version.json), ghi lại git commit + thời điểm build, rồi mới build image, để version hiển thị ở mục Cài đặt → Phiên bản trên web luôn khớp đúng với image thật đang chạy (xem “Kiểm tra version đang chạy” ở §5):

Terminal window
./scripts/build_backend_image.sh
./scripts/build_frontend_image.sh

Muốn tự chọn số version thay vì để script tự tăng patch (vd bump minor/major cho 1 release thật), truyền số đó làm tham số:

Terminal window
./scripts/build_backend_image.sh 1.2.0
./scripts/build_frontend_image.sh 1.2.0

Mỗi script build ra đúng 1 image, 1 tag duy nhất: admin_portal_backend:v<version> / admin_portal_frontend:v<version>. Nguồn build:

  • backend/Dockerfile (multi-stage: cài dependency bằng uv, copy source, chạy bằng user appuser không phải root).
  • frontend/Dockerfile (Next.js output: "standalone", npm run build, chạy bằng user appuser).

Nếu docker build fail (vd lỗi mạng khi tải dependency), script tự động khôi phục lại version.json về giá trị trước đó — số version không bị “nhảy cóc” cho một bản build chưa từng tồn tại.

2.1. Khai đúng tag vừa build vào docker-compose.prod.yml

Phần tiêu đề “2.1. Khai đúng tag vừa build vào docker-compose.prod.yml”

docker-compose.prod.yml ghim chết tag cụ thể (không dùng tag nổi như latest/prod), nên sau khi build phải tự sửa tay 2 dòng image: cho khớp:

backend:
image: admin_portal_backend:v<version-vừa-build>
frontend:
image: admin_portal_frontend:v<version-vừa-build>

2.2. Đóng gói cấu hình (deploy/) và image (images/) — 2 gói riêng

Phần tiêu đề “2.2. Đóng gói cấu hình (deploy/) và image (images/) — 2 gói riêng”

Cố ý tách làm 2 vì deploy/ (config/script, vài trăm KB) đổi thường xuyên hơn nhiều so với images/ (~200MB) — gộp chung sẽ bắt gửi lại toàn bộ ảnh mỗi lần chỉ sửa 1 dòng .env.production.example.

Terminal window
./scripts/package_deploy.sh

Mặc định script cài dependencies và chạy npm run build:all trong docs-site/, rồi đóng gói output tĩnh vào deploy/guide-site/. Build lỗi thì script dừng trước khi xóa/tạo lại deploy/. Nếu đã chạy build:all thành công cho đúng commit này, có thể dùng ./scripts/package_deploy.sh --skip-guide-build để đóng gói docs-site/dist/ đã có; chỉ dùng cờ này sau khi xác nhận output thuộc đúng phiên bản.

Đóng gói docker-compose.prod.yml, .env.production.example, DEPLOY.md, DEPLOY.html, nginx/, site hướng dẫn tĩnh guide-site/, bộ cron (scripts/admin_portal.crontab

  • scripts/install_cron.sh) và các script vận hành (không copy build_*_image.sh — 2 script đó cần mã nguồn, chỉ chạy trên máy dev) vào deploy/. Không có image, không có mã nguồn backend//frontend/ nào trong deploy/. Thư mục này được tạo lại từ đầu mỗi lần chạy script — đừng sửa tay file bên trong.
Terminal window
./scripts/export_images.sh

Nén 2 image thành images/*.tar.gz (đọc đúng tag vừa khai ở §2.1 — fail sớm nếu image chưa build hoặc tag không khớp).

Gửi cả 2 lên VPS (2 lệnh riêng, images/ không nằm trong deploy/):

Terminal window
# lần đầu (thư mục đích chưa có gì):
ssh <user>@<vps-ip> 'mkdir -p /opt/web_admin'
scp -r deploy/. <user>@<vps-ip>:/opt/web_admin/
scp -r images <user>@<vps-ip>:/opt/web_admin/
# các lần sau (nhanh hơn, chỉ gửi phần thay đổi — LƯU Ý 2 --exclude, xem
# lý do trong header comment của scripts/package_deploy.sh: thiếu chúng
# thì --delete sẽ xóa mất .env thật và images/ trên server):
rsync -av --delete --exclude='.env' --exclude='images/' --exclude='credentials/' deploy/ <user>@<vps-ip>:/opt/web_admin/
rsync -av images/ <user>@<vps-ip>:/opt/web_admin/images/

--exclude='credentials/' chỉ cần khi đã tạo thư mục credential cho IAP (§3.5); thêm sẵn cũng không sao.

Terminal window
ssh <user>@<vps-ip>
cd /opt/web_admin

Từ đây, mọi lệnh trong hướng dẫn này chạy tại /opt/web_admin trên VPS (trừ khi ghi rõ “trên máy dev”).

2.3. Cấu trúc thư mục trên VPS sau khi copy

Phần tiêu đề “2.3. Cấu trúc thư mục trên VPS sau khi copy”

Sau bước 2.2 và khi đã tạo .env (§3), /opt/web_admin phải có đúng các file sau (so bằng find /opt/web_admin -maxdepth 2 | sort):

/opt/web_admin/ ← thư mục cài đặt trên VPS (không phải git repo)
├── .env ← TẠO TAY từ .env.production.example (chmod 600, không bao giờ copy từ máy dev)
├── .env.production.example ← mẫu biến môi trường
├── docker-compose.prod.yml ← compose production (tag image ghim cố định)
├── DEPLOY.md ← tài liệu này
├── DEPLOY.html ← bản HTML
├── guide-site/ ← Astro static build, Nginx phục vụ tại /guide/ sau khi xác thực Portal
│ ├── index.html
│ ├── _astro/ ← asset có hash
│ └── pagefind/ ← chỉ mục tìm kiếm tĩnh
├── credentials/ ← CHỈ khi bật IAP/AdMob (§3.5), tạo tay, chmod 700
├── nginx/
│ └── nginx.conf.template ← reverse proxy + TLS + /media/ + /guide/ (DOMAIN lấy từ .env)
├── scripts/
│ ├── load_images.sh ← nạp images/*.tar.gz vào Docker
│ ├── deploy_local.sh ← up -d + kiểm tra health / migration / API v2
│ ├── rollback_to_version.sh ← quay về image cũ (có chặn migration)
│ ├── generate_production_secrets.sh
│ ├── init_letsencrypt.sh ← xin chứng chỉ lần đầu
│ ├── renew_cert.sh
│ ├── lib_backup.sh ← hàm dùng chung của backup/restore (Phase S)
│ ├── backup_database.sh ← backup DB mã hóa + manifest
│ ├── backup_media.sh ← backup volume ảnh (Phase N)
│ ├── backup_config.sh ← backup .env/credentials/config mã hóa (Phase S)
│ ├── check_backups.sh ← kiểm tra backup quá hạn/hỏng/chưa kéo, dung lượng (Phase S)
│ ├── setup_backup_pull_user.sh ← tạo user chỉ đọc để người vận hành kéo backup (Phase S)
│ ├── restore_drill.sh ← drill nhanh bằng Postgres tạm (Phase S)
│ ├── restore.sh ← restore DB + ảnh từ một thư mục ngày (Phase S)
│ ├── export_erased_players.sh ← lấy danh sách người chơi đã xóa từ bản backup mới hơn
│ ├── restore_database.sh ← cũ, chuyển sang restore.sh --db-only
│ ├── restore_media.sh ← cũ, chuyển sang restore.sh --media-only
│ ├── install_cron.sh ← cài job định kỳ
│ └── admin_portal.crontab ← danh sách job định kỳ
└── images/ ← gửi riêng (rsync images/), không nằm trong deploy/
├── backend.tar.gz ← admin_portal_backend:v<version>
└── frontend.tar.gz ← admin_portal_frontend:v<version>

Không bao giờ có trên VPS: mã nguồn backend/, frontend/, .git/, build_*_image.sh, export_images.sh, package_deploy.sh (chỉ chạy trên máy dev). Chạy lại package_deploy.sh là tạo lại deploy/ từ đầu — đừng sửa tay file trong đó; sửa bản gốc trong repo.

Ngoài thư mục cài đặt (không copy, được tạo khi chạy):

Vị tríNội dungTạo bởi
Docker volume admin_portal_pgdata_prodDữ liệu Postgresdocker compose up
Docker volume admin_portal_media_prodẢnh đã upload (icon game/vật phẩm/tiền tệ/gói, avatar) — backend ghi, Nginx đọcdocker compose up
Docker volume certbot_certs, certbot_wwwChứng chỉ Let’s Encrypt, webroot challengeinit_letsencrypt.sh
/opt/backups/YYYYMMDD/db_*.dump.ageBản backup DB đã mã hóa (đổi bằng BACKUP_DIR)backup_database.sh
/opt/backups/YYYYMMDD/media_*.tar.ageBản backup ảnh đã mã hóabackup_media.sh
/opt/backups/YYYYMMDD/config_*.tar.age.env, credentials, compose, nginx và crontab mã hóabackup_config.sh
/opt/backups/admin_portal_*.sql.gz.age, admin_portal_media_*.tar.ageBackup kiểu cũ (trước Phase S), tự xóa khi quá 30 ngàybản cũ
restore_erasures_*.csv, restore_images_*.override.yml (thư mục cài đặt)Chỉ có khi restore dừng ở bước migration/xóa lại người chơi (§9.2); xóa sau khi xử lý xongrestore.sh
/var/log/admin_portal/*.logLog các job định kỳ (đổi bằng LOG_DIR)install_cron.sh
File private identity age (ví dụ ~/keys/age-portal-ops.txt)Khóa giải mã backup — tuyệt đối không đặt trên VPS; giữ theo runbook khóa Phase S.3người giữ khóa vận hành (cần chỉ định trước go-live)

Áp dụng cho Portal v1.2.2