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

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:

FileVai trò
backend/DockerfileBuild image production cho backend (multi-stage, non-root, tạo sẵn /data/media cho user appuser)
frontend/DockerfileBuild image production cho frontend (Next.js standalone output, non-root)
docker-compose.prod.ymlCompose file production (khác hẳn docker-compose.yml — file đó chỉ dùng cho dev)
nginx/nginx.conf.templateCấu hình Nginx (reverse proxy + TLS + phục vụ /media/)
.env.production.exampleDanh sách đầy đủ biến môi trường cần có trong .env thật trên VPS
scripts/*.shCác script thao tác (xem mục tương ứng bên dưới)
scripts/admin_portal.crontabDanh 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