Kiến trúc tổng quan
Internet │ HTTPS (80/443) ▼┌─────────────────────────── VPS (docker compose) ───────────────────────────┐│ nginx (TLS, reverse proxy) ││ ├─ / → frontend (Next.js, container nội bộ, không có port ││ │ ra host) ││ ├─ /api/* → backend (FastAPI, container nội bộ): ││ │ /api/v1/... Admin API + game-client v1 (Drone cũ) ││ │ /api/v2/games/{g}/client/... game-client v2 (SDK) ││ ├─ /media/* → đọc thẳng volume ảnh (chỉ đọc, cache 1 năm) ││ └─ /health → backend /health (cho uptime-check ngoài) ││ backend → postgres (container nội bộ, không expose ra host) ││ backend → volume ảnh admin_portal_media_prod (ghi) ← nginx (chỉ đọc) ││ certbot (xin/renew chứng chỉ Let's Encrypt, dùng chung volume với nginx) │└──────────────────────────────────────────────────────────────────────────┘Game mobile (Drone Strike và mọi game dùng SDK game-portal-sdk) gọi
thẳng https://<domain>/api/... bằng API key của game và tải icon/avatar từ
https://<domain>/media/... — không có thành phần server riêng cho game.
Cách build/phát hành app: §13.
VPS không bao giờ có mã nguồn backend//frontend/ hay công cụ
build (Node.js, Python, uv…) — image được build sẵn trên máy dev,
nén lại (docker save | gzip), rồi copy nguyên file nén đó lên VPS và
docker load. VPS chỉ cần Docker để chạy image, không cần biết image
được build từ gì.
File cấu hình chính:
| File | Vai trò |
|---|---|
backend/Dockerfile | Build image production cho backend (multi-stage, non-root, tạo sẵn /data/media cho user appuser) |
frontend/Dockerfile | Build image production cho frontend (Next.js standalone output, non-root) |
docker-compose.prod.yml | Compose file production (khác hẳn docker-compose.yml — file đó chỉ dùng cho dev) |
nginx/nginx.conf.template | Cấu hình Nginx (reverse proxy + TLS + phục vụ /media/) |
.env.production.example | Danh sách đầy đủ biến môi trường cần có trong .env thật trên VPS |
scripts/*.sh | Các script thao tác (xem mục tương ứng bên dưới) |
scripts/admin_portal.crontab | Danh sách job định kỳ (§10) |
<!-- clients --> <rect x="60" y="16" width="160" height="42" rx="7" fill="none" stroke="currentColor" opacity="0.6"/> <text x="140" y="41" text-anchor="middle" class="diagram-lbl">Trình duyệt Admin</text> <rect x="660" y="16" width="160" height="42" rx="7" fill="none" stroke="currentColor" opacity="0.6"/> <text x="740" y="41" text-anchor="middle" class="diagram-lbl">Game (SDK / Drone Strike)</text>
<line x1="140" y1="58" x2="380" y2="100" stroke="currentColor" opacity="0.6" marker-end="url(#arrow)"/> <line x1="740" y1="58" x2="500" y2="100" stroke="currentColor" opacity="0.6" marker-end="url(#arrow)"/> <text x="440" y="80" text-anchor="middle" class="diagram-tag">HTTPS :443</text>
<!-- VPS boundary --> <rect x="70" y="100" width="740" height="300" rx="12" fill="none" stroke="currentColor" stroke-dasharray="4 5" opacity="0.45"/> <text x="90" y="122" class="diagram-tag">VPS — docker compose (network nội bộ)</text>
<!-- nginx --> <rect x="370" y="140" width="140" height="46" rx="8" fill="var(--accent-soft)" stroke="var(--accent)"/> <text x="440" y="168" text-anchor="middle" class="diagram-lbl" font-weight="600">nginx (TLS)</text>
<!-- certbot --> <rect x="560" y="140" width="140" height="46" rx="8" fill="none" stroke="currentColor" stroke-dasharray="3 4" opacity="0.7"/> <text x="630" y="168" text-anchor="middle" class="diagram-lbl">certbot</text> <line x1="560" y1="163" x2="510" y2="163" stroke="currentColor" stroke-dasharray="3 4" opacity="0.7" marker-end="url(#arrow)"/> <text x="535" y="150" text-anchor="middle" class="diagram-tag" font-size="10">volume certs</text>
<!-- frontend / backend --> <rect x="170" y="250" width="150" height="50" rx="8" fill="var(--paper-raised)" stroke="currentColor" opacity="0.85"/> <text x="245" y="280" text-anchor="middle" class="diagram-lbl">frontend (Next.js)</text> <text x="245" y="316" text-anchor="middle" class="diagram-tag">không có cổng ra host</text>
<rect x="380" y="250" width="150" height="50" rx="8" fill="var(--paper-raised)" stroke="currentColor" opacity="0.85"/> <text x="455" y="280" text-anchor="middle" class="diagram-lbl">backend (FastAPI)</text> <text x="455" y="316" text-anchor="middle" class="diagram-tag">không có cổng ra host</text>
<line x1="410" y1="186" x2="270" y2="250" stroke="currentColor" marker-end="url(#arrow)"/> <text x="300" y="222" text-anchor="middle" class="diagram-tag">/</text> <line x1="440" y1="186" x2="450" y2="250" stroke="currentColor" marker-end="url(#arrow)"/> <text x="475" y="222" text-anchor="middle" class="diagram-tag">/api/v1, /api/v2, /health</text> <!-- media volume (Phase N) --> <rect x="610" y="250" width="160" height="50" rx="8" fill="var(--accent-2-soft)" stroke="var(--accent-2)"/> <text x="690" y="272" text-anchor="middle" class="diagram-lbl">volume ảnh</text> <text x="690" y="290" text-anchor="middle" class="diagram-tag">admin_portal_media_prod</text> <line x1="530" y1="275" x2="610" y2="275" stroke="currentColor" marker-end="url(#arrow)"/> <text x="570" y="268" text-anchor="middle" class="diagram-tag">ghi</text> <line x1="505" y1="186" x2="660" y2="250" stroke="var(--accent-2)" stroke-dasharray="4 4" marker-end="url(#arrow)"/> <text x="640" y="214" text-anchor="middle" class="diagram-tag">/media/* (chỉ đọc)</text>
<!-- postgres --> <rect x="380" y="345" width="150" height="46" rx="8" fill="var(--paper-raised)" stroke="currentColor" opacity="0.85"/> <text x="455" y="373" text-anchor="middle" class="diagram-lbl">postgres</text> <text x="455" y="410" text-anchor="middle" class="diagram-tag">không có cổng ra host</text> <line x1="455" y1="300" x2="455" y2="345" stroke="currentColor" marker-end="url(#arrow)"/> <text x="500" y="326" text-anchor="middle" class="diagram-tag">asyncpg</text> </svg>Áp dụng cho Portal v1.2.2