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

Nạp image & tạo secrets/`.env` (trên VPS)

  1. 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.sh

    Kiểm tra lại: docker images | grep admin_portal phải thấy đúng 2 tag đã khai ở §2.1.

  2. 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.sh

    In ra JWT_SECRET_KEY, MFA_ENCRYPTION_KEY, PROMO_CODE_HMAC_SECRET, POSTGRES_PASSWORD.

  3. Copy file mẫu và điền giá trị thật:

    Terminal window
    cp .env.production.example .env
    chmod 600 .env
  4. 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ố.

BiếnGiá trịVì sao bắt buộc
DOMAINportal.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_USERgiữ 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_PASSWORDgiá trị vừa sinh ở bước 2như trên
POSTGRES_DBgiữ admin_portal_prod hoặc đổi tùy ýnhư trên
APP_ENVproductionNế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_URLthay __POSTGRES_PASSWORD__ bằng giá trị thậtMặc định trong code trỏ vào Postgres dev cục bộ, không dùng được cho production
JWT_SECRET_KEYgiá trị vừa sinh ở bước 2Giá 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_KEYgiá trị vừa sinh ở bước 2Tương tự — khóa mã hóa MFA secret mặc định cũng công khai trong repo
PROMO_CODE_HMAC_SECRETgiá trị vừa sinh ở bước 2Bắ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_DOMAINdomain 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_SECUREtrueMặ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_URLhttps://portal.tinysoft.io.vnPhase 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/mediaThư mục volume ảnh bên trong container (khớp docker-compose.prod.yml)
SERVE_MEDIA_LOCALLYfalseProduction do Nginx phục vụ /media/; để true thì backend không khởi động
Nhóm biếnVí dụDùng khi
ADMOB_PUBLISHER_ID, ADMOB_SERVICE_ACCOUNT_JSON_PATHCó bật dashboard doanh thu AdMob
IAP_TOKEN_ENCRYPTION_KEYmộ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.jsonBắ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_JWTNhậ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_ISSUERCó nhận webhook xác thực IAP kiểu cũ
APPLE_ROOT_CERTIFICATE_PATHCó 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_STARTTLSCó tính năng gửi email
BOOTSTRAP_ADMIN_EMAIL, BOOTSTRAP_ADMIN_PASSWORD, BOOTSTRAP_ADMIN_FULL_NAMEChỉ 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_MINUTESMuố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/minuteMuố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_FRAMES5242880 / 4096 / 16777216 / 200Giớ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_CODE5000 / 100000Chỉ 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_THRESHOLD5000 / 50Ngưỡ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_LIMIT10/minuteRate 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_HOURS24Muốn đổi ngưỡng hiển thị hàng chờ đối soát (Task B.9)
TELEGRAM_BOT_TOKEN123456789: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_ID123456789 hoặc -1001234567890Muốn nhận cảnh báo backup qua Telegram; cần TELEGRAM_BOT_TOKEN
BACKUP_ALERT_SLACK_WEBHOOKMuốn nhận cảnh báo backup qua Slack (F.9, S.5)
BACKUP_ALERT_EMAILops@example.comMuố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_PERCENT26, 3, 20Muốn đổi ngưỡng giám sát backup (S.5; đĩa dưới 10% luôn là lỗi)
IDENTITY_VERIFICATION_MODErealĐã đú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_POLICYlegacy (mặc định) / strictPhase L: chỉ chuyển strict theo điều kiện ở §8.2 bước 6
BiếnGiá trị
BACKUP_AGE_PUBLIC_KEYMộ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ếnCách truyềnVì sao không để trong .env
CERTBOT_EMAILexport CERTBOT_EMAIL=... && ./scripts/init_letsencrypt.shChỉ cần đúng 1 lần lúc xin cert, không phải cấu hình runtime
BACKUP_AGE_IDENTITY_FILEexport 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:

  1. Đặ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 1000 file đó.
  2. Đặt biến dạng /app/credentials/<file>.json trong .env, rồi docker 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