Nạp image & tạo secrets/`.env` (trên VPS)
-
Nạp 2 image vừa copy lên (
images/*.tar.gz) vào Docker cục bộ trên VPS:Terminal window ./scripts/load_images.shKiểm tra lại:
docker images | grep admin_portalphải thấy đúng 2 tag đã khai ở §2.1. -
Sinh các secret ngẫu nhiên (chạy được cả trên VPS lẫn máy dev — script chỉ in ra stdout, không ghi file). Chi tiết đầy đủ:
tasks/phase_f/task_f.3_secrets_and_config_management.md.Terminal window ./scripts/generate_production_secrets.shIn ra
JWT_SECRET_KEY,MFA_ENCRYPTION_KEY,PROMO_CODE_HMAC_SECRET,POSTGRES_PASSWORD. -
Copy file mẫu và điền giá trị thật:
Terminal window cp .env.production.example .envchmod 600 .env -
Mở
.env, điền các biến theo bảng dưới đây. Bắt buộc = để trống/giữ giá trị mẫu là sai (container không lên được, hoặc tạo lỗ hổng bảo mật thật). Tùy chọn = code đã có default an toàn sẵn, chỉ cần đổi nếu thực sự dùng tính năng đó hoặc muốn chỉnh tham số.
3.1. Bắt buộc
Phần tiêu đề “3.1. Bắt buộc”| Biến | Giá trị | Vì sao bắt buộc |
|---|---|---|
DOMAIN | portal.tinysoft.io.vn (đã trỏ DNS về 20.51.243.159) | Nginx/TLS (Task F.2) và CORS_ALLOWED_ORIGINS dựa vào đây |
POSTGRES_USER | giữ admin_portal hoặc đổi tùy ý | docker-compose.prod.yml không có giá trị mặc định — để trống container Postgres không khởi động được |
POSTGRES_PASSWORD | giá trị vừa sinh ở bước 2 | như trên |
POSTGRES_DB | giữ admin_portal_prod hoặc đổi tùy ý | như trên |
APP_ENV | production | Nếu để mặc định (dev), các cơ chế chặn dev/test (bootstrap script, IDENTITY_VERIFICATION_MODE=skip…) sẽ hiểu sai môi trường |
DATABASE_URL | thay __POSTGRES_PASSWORD__ bằng giá trị thật | Mặc định trong code trỏ vào Postgres dev cục bộ, không dùng được cho production |
JWT_SECRET_KEY | giá trị vừa sinh ở bước 2 | Giá trị mặc định trong code là khóa dev công khai trong repo — dùng nguyên là lộ khóa ký token |
MFA_ENCRYPTION_KEY | giá trị vừa sinh ở bước 2 | Tương tự — khóa mã hóa MFA secret mặc định cũng công khai trong repo |
PROMO_CODE_HMAC_SECRET | giá trị vừa sinh ở bước 2 | Bắt buộc ở staging/production; cần giữ ổn định để tra cứu mã đã phát và nên sao lưu theo quy trình quản lý secret |
COOKIE_DOMAIN | domain thật (không có https://) | Để trống cookie chỉ áp dụng đúng 1 host, dễ gây lỗi khó hiểu khi có subdomain |
COOKIE_SECURE | true | Mặc định false (cho dev) — để false trên production nghĩa là cookie có thể bị gửi qua HTTP không mã hóa |
CORS_ALLOWED_ORIGINS | ["https://portal.tinysoft.io.vn"] | Mặc định chỉ cho phép localhost:3000 — domain thật sẽ bị chặn CORS nếu không đổi |
PUBLIC_ASSET_BASE_URL | https://portal.tinysoft.io.vn | Phase N: game client nhận icon dạng URL tuyệt đối ghép từ biến này. Production mà thiếu hoặc không phải https:// thì backend không khởi động |
MEDIA_ROOT | /data/media | Thư mục volume ảnh bên trong container (khớp docker-compose.prod.yml) |
SERVE_MEDIA_LOCALLY | false | Production do Nginx phục vụ /media/; để true thì backend không khởi động |
3.2. Tùy chọn — chỉ điền nếu dùng
Phần tiêu đề “3.2. Tùy chọn — chỉ điền nếu dùng”| Nhóm biến | Ví dụ | Dùng khi |
|---|---|---|
ADMOB_PUBLISHER_ID, ADMOB_SERVICE_ACCOUNT_JSON_PATH | Có bật dashboard doanh thu AdMob | |
IAP_TOKEN_ENCRYPTION_KEY | một Fernet key riêng (khuyến nghị) | Mã hóa purchase token Google Play tại DB (Phase O.7); nếu bỏ trống sẽ dùng MFA_ENCRYPTION_KEY |
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH | /app/credentials/play.json | Bắt buộc khi bật IAP (Phase O.7): xác minh giao dịch, acknowledge/consume, đối soát hoàn tiền. Xem §3.5 |
GOOGLE_PUBSUB_PUBLIC_KEY_PATH, GOOGLE_PUBSUB_AUDIENCE, GOOGLE_PLAY_VERIFY_PUSH_JWT | Nhận Google Play Real-time Developer Notifications (hoàn tiền/hủy ngay lập tức; không có thì cron đối soát hằng ngày vẫn bắt được) | |
IAP_WEBHOOK_PUBLIC_KEY_PATH, IAP_WEBHOOK_ISSUER | Có nhận webhook xác thực IAP kiểu cũ | |
APPLE_ROOT_CERTIFICATE_PATH | Có xác thực thông báo từ App Store (iOS IAP chưa bật ở Phase O) | |
SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_FROM_EMAIL, SMTP_STARTTLS | Có tính năng gửi email | |
BOOTSTRAP_ADMIN_EMAIL, BOOTSTRAP_ADMIN_PASSWORD, BOOTSTRAP_ADMIN_FULL_NAME | Chỉ lúc bootstrap Super Admin đầu tiên — xóa khỏi .env ngay sau đó | |
ACCESS_TOKEN_TTL_SECONDS, REFRESH_TOKEN_TTL_SECONDS, MFA_TOKEN_TTL_SECONDS, MFA_TOTP_DRIFT_STEPS, MAX_FAILED_LOGIN_ATTEMPTS, ACCOUNT_LOCKOUT_MINUTES | Muốn đổi thời hạn token/khóa tài khoản khỏi default | |
Rate limit: AUTH_RATE_LIMIT, GAME_LINK_RATE_LIMIT, PLAYER_WALLET_RATE_LIMIT, PLAYER_SYNC_RATE_LIMIT, PLAYER_REWARD_CLAIM_RATE_LIMIT (30/minute), PLAYER_PROMO_CODE_REDEEM_RATE_LIMIT (5/minute), CLIENT_BOOTSTRAP_RATE_LIMIT (60/minute), PLAYER_SESSION_REFRESH_RATE_LIMIT, PLAYER_PROFILE_RATE_LIMIT, PLAYER_NICKNAME_CHECK_RATE_LIMIT, LEADERBOARD_RATE_LIMIT (Phase L), ASSET_UPLOAD_RATE_LIMIT (30/minute, Phase N), CLIENT_SHOP_PURCHASE_RATE_LIMIT (30/minute), IAP_VERIFY_RATE_LIMIT (10/minute, Phase O) | 10/minute | Muốn đổi ngưỡng rate limit khỏi default. Lưu ý: backend chạy --workers 2, bộ đếm rate limit nằm trong từng worker nên ngưỡng thực tế có thể gấp đôi |
ASSET_MAX_UPLOAD_BYTES, ASSET_MAX_DIMENSION, ASSET_MAX_PIXELS, ASSET_MAX_FRAMES | 5242880 / 4096 / 16777216 / 200 | Giới hạn ảnh upload (Phase N, D11) |
MAX_EARNING_PER_SYNC, MAX_EARNING_PER_WINDOW, MAX_EARNING_WINDOW_SECONDS, REWARD_MAX_GOLD_PER_CHECKIN_CELL, REWARD_MAX_GOLD_PER_GIFT, REWARD_MAX_GOLD_PER_CAMPAIGN, REWARD_MAX_GOLD_PER_PROMO_CODE | 5000 / 100000 | Chỉ là giá trị mặc định cho GOLD khi game chưa có cấu hình tiền tệ riêng. Từ Phase O.1 mọi game đều có dòng GOLD/GEM; trần thật của từng game sửa ở Portal → Tiền tệ, đổi .env không còn tác dụng với game đã cấu hình |
GIFT_APPROVAL_GOLD_THRESHOLD, GIFT_APPROVAL_RECIPIENT_THRESHOLD | 5000 / 50 | Ngưỡng bắt buộc duyệt 2 người khi tặng quà (chỉ khi game bật approval_workflow) |
PLAYER_DAILY_CHECKIN_CLAIM_RATE_LIMIT | 10/minute | Rate limit endpoint nhận Điểm danh. Bảng thưởng và mốc reset cấu hình theo game trong Portal; DAILY_CHECKIN_REWARDS_JSON và DAILY_CHECKIN_RESET_UTC_OFFSET_MINUTES chỉ còn là mặc định khi tạo draft đầu tiên |
RECONCILIATION_PENDING_HOURS | 24 | Muốn đổi ngưỡng hiển thị hàng chờ đối soát (Task B.9) |
TELEGRAM_BOT_TOKEN | 123456789:AA... | Muốn dùng kênh cảnh báo Telegram (trong Portal và cho backup). Token của bot tạo bằng @BotFather; xem §10 bước 2 |
BACKUP_ALERT_TELEGRAM_CHAT_ID | 123456789 hoặc -1001234567890 | Muốn nhận cảnh báo backup qua Telegram; cần TELEGRAM_BOT_TOKEN |
BACKUP_ALERT_SLACK_WEBHOOK | Muốn nhận cảnh báo backup qua Slack (F.9, S.5) | |
BACKUP_ALERT_EMAIL | ops@example.com | Muốn nhận cảnh báo backup qua email; cần SMTP_* và backend đang chạy (S.5) |
BACKUP_DIR, BACKUP_RETENTION_DAYS, BACKUP_GROUP, OFFSITE_REMOTE | /opt/backups / 30 / backup-pull / trống | Đổi nơi lưu, số ngày giữ (mặc định 30, S-D3; LOCAL_RETENTION_DAYS cũ vẫn được nhận), nhóm Linux được đọc backup (dùng cho việc người vận hành kéo backup về, S.4). OFFSITE_REMOTE để trống thì không gọi rclone |
BACKUP_MAX_AGE_HOURS, BACKUP_MAX_PULL_AGE_DAYS, BACKUP_MIN_FREE_PERCENT | 26, 3, 20 | Muốn đổi ngưỡng giám sát backup (S.5; đĩa dưới 10% luôn là lỗi) |
IDENTITY_VERIFICATION_MODE | real | Đã đúng theo default — chỉ cần set tường minh nếu muốn dễ audit; không được đổi thành skip ngoài dev/test (code tự chặn khởi động nếu vi phạm) |
IDENTITY_RELINK_POLICY | legacy (mặc định) / strict | Phase L: chỉ chuyển strict theo điều kiện ở §8.2 bước 6 |
3.3. Bắt buộc nếu dùng backup (Phase S)
Phần tiêu đề “3.3. Bắt buộc nếu dùng backup (Phase S)”| Biến | Giá trị |
|---|---|
BACKUP_AGE_PUBLIC_KEY | Một public key age là đủ (S-D13); muốn thêm khóa dự phòng thì phân tách bằng dấu phẩy; dùng để mã hóa backup DB, ảnh và cấu hình (.env/credentials). Cấu hình khóa vận hành và khóa break-glass theo runbook Phase S. Chỉ public key nằm trong .env; private identity được giữ ngoài VPS. |
backup_config.sh chạy lúc 04:00 sau backup DB và media. Archive cấu hình
được stream thẳng vào age; hash trong manifest được tính trên đúng bytes
được tar gửi vào age, manifest chỉ ghi tên file, SHA256 và image tags.
Nếu .env tham chiếu credential /app/credentials/<file> mà file tương ứng
trên host bị thiếu, job thất bại và phát cảnh báo.
3.4. Không lưu trong .env (cố ý, vì lý do bảo mật)
Phần tiêu đề “3.4. Không lưu trong .env (cố ý, vì lý do bảo mật)”| Biến | Cách truyền | Vì sao không để trong .env |
|---|---|---|
CERTBOT_EMAIL | export CERTBOT_EMAIL=... && ./scripts/init_letsencrypt.sh | Chỉ cần đúng 1 lần lúc xin cert, không phải cấu hình runtime |
BACKUP_AGE_IDENTITY_FILE | export BACKUP_AGE_IDENTITY_FILE=... && ./scripts/restore.sh /opt/backups/<YYYYMMDD> (§9) | Đây là private key giải mã backup — để chung .env với public key sẽ mất hết ý nghĩa mã hóa |
3.5. File credential (Google Play / AdMob / Apple)
Phần tiêu đề “3.5. File credential (Google Play / AdMob / Apple)”Các biến GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH, ADMOB_SERVICE_ACCOUNT_JSON_PATH,
GOOGLE_PUBSUB_PUBLIC_KEY_PATH, IAP_WEBHOOK_PUBLIC_KEY_PATH,
APPLE_ROOT_CERTIFICATE_PATH là đường dẫn bên trong container backend.
docker-compose.prod.yml mount ./credentials (trong thư mục cài đặt) vào
backend tại /app/credentials, chỉ đọc; deploy_local.sh tự tạo thư mục với
quyền 700. Khi bật IAP (Phase O.7) hoặc AdMob:
- Đặt file vào
credentials/(chmod 600). Backend chạy bằng UID 1000: nếu user deploy không có UID 1000 thìsudo chown 1000file đó. - Đặt biến dạng
/app/credentials/<file>.jsontrong.env, rồidocker compose -f docker-compose.prod.yml up -d backend.
Không bật các tính năng đó thì bỏ qua mục này.
Không cần lo .env lọt vào git trên VPS — deploy/ không phải git
repo (package_deploy.sh ở máy dev cũng chỉ copy
.env.production.example, không bao giờ copy .env thật).
Áp dụng cho Portal v1.2.2