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

Biến môi trường

Trang này gom mọi biến môi trường vào một chỗ để tra nhanh. Cách điền .env từng bước khi cài VPS vẫn theo Hướng dẫn triển khai §3. Dùng ô tìm kiếm hoặc Ctrl+F với tên biến để nhảy tới dòng cần xem.

Cách đọc cột “Bắt buộc”:

  • Có: production thiếu hoặc để giá trị mẫu thì container không lên, hoặc có lỗ hổng bảo mật.
  • Khi dùng: chỉ cần khi bật tính năng liên quan.
  • Không: code đã có mặc định an toàn, chỉ đặt khi muốn đổi.

Cột Mặc định là giá trị trong code khi không đặt biến (thường là giá trị cho dev). Cột Production là giá trị nên dùng trên VPS.

FileDùng choGhi chú
.env (thư mục cài đặt trên VPS)docker-compose.prod.yml: Postgres, backend, script backup/giám sátTạo từ .env.production.example; chmod 600
.env (gốc repo, máy dev)docker-compose.yml khi chạy stack devTạo từ .env.example; chỉ giá trị dev
backend/.envBackend chạy trực tiếp bằng uv run trên máy devTạo từ backend/.env.example; chỉ giá trị dev
frontend/.env.localnext dev trên máy devTạo từ frontend/.env.example
Biến export trong shellLệnh chạy một lần (init_letsencrypt.sh, restore.sh)Cố ý không lưu vào .env
BiếnBắt buộcMặc địnhProductionÝ nghĩa
DOMAINCó—portal.tinysoft.io.vnDomain công khai. Nginx dùng để xin chứng chỉ TLS và cấu hình server; script deploy dùng để kiểm /health qua nginx
POSTGRES_USERCóadmin_portal (chỉ compose dev)admin_portalUser Postgres. Compose production không có mặc định: để trống thì Postgres không khởi động
POSTGRES_PASSWORDCóadmin_portal_dev_password (chỉ compose dev)Sinh bằng scripts/generate_production_secrets.shMật khẩu Postgres. Phải khớp phần mật khẩu trong DATABASE_URL
POSTGRES_DBCóadmin_portal_dev (chỉ compose dev)admin_portal_prodTên database

Backend: môi trường, database, khóa bí mật

Phần tiêu đề “Backend: môi trường, database, khóa bí mật”
BiếnBắt buộcMặc địnhProductionÝ nghĩa
APP_ENVCódevproductionMôi trường: dev, test, staging, production. Nhiều cơ chế an toàn dựa vào biến này (chặn IDENTITY_VERIFICATION_MODE=skip, bắt buộc media qua nginx, bắt buộc PROMO_CODE_HMAC_SECRET…)
DATABASE_URLCóPostgres dev localhost:55432/admin_portal_devpostgresql+asyncpg://admin_portal:<mật khẩu>@postgres:5432/admin_portal_prodChuỗi kết nối Postgres của backend (driver asyncpg). Host là tên service postgres trong compose
JWT_SECRET_KEYCóKhóa dev công khai trong repoSinh bằng generate_production_secrets.shKhóa ký access/refresh token của admin. Đổi khóa thì mọi phiên đăng nhập bị đăng xuất
MFA_ENCRYPTION_KEYCóKhóa Fernet dev công khai trong repoSinh bằng generate_production_secrets.shKhóa Fernet mã hóa secret MFA (TOTP) trong DB. Mất hoặc đổi khóa thì MFA của mọi admin không giải mã được — sao lưu theo quy trình secret
IAP_TOKEN_ENCRYPTION_KEYKhông (khuyến nghị khi bật IAP)Trống → dùng MFA_ENCRYPTION_KEYMột khóa Fernet riêngMã hóa purchase token Google Play lưu trong DB. Giữ ổn định như MFA_ENCRYPTION_KEY
PROMO_CODE_HMAC_SECRETCóChuỗi dev công khaiSinh bằng generate_production_secrets.shKhóa HMAC băm mã quà tặng; DB chỉ lưu hash. Đổi khóa thì mọi mã đã phát không còn tra được — giữ ổn định cho từng môi trường
BiếnBắt buộcMặc địnhProductionÝ nghĩa
COOKIE_DOMAINCóTrống (cookie chỉ cho đúng host hiện tại)portal.tinysoft.io.vn (không có https://)Domain gắn cho cookie đăng nhập access_token / refresh
COOKIE_SECURECófalsetruetrue thì trình duyệt chỉ gửi cookie qua HTTPS. Để false trên production là lỗ hổng
CORS_ALLOWED_ORIGINSCó["http://localhost:3000"]["https://portal.tinysoft.io.vn"]Danh sách origin được gọi API từ trình duyệt, dạng mảng JSON
BiếnBắt buộcMặc địnhProductionÝ nghĩa
PUBLIC_ASSET_BASE_URLCóhttp://localhost:8000https://portal.tinysoft.io.vnGốc URL tuyệt đối của ảnh trả cho game client. Production phải là https://, sai thì backend không khởi động
MEDIA_ROOTCó./var/media/data/mediaThư mục lưu ảnh upload bên trong container (khớp volume admin_portal_media_prod)
SERVE_MEDIA_LOCALLYCótruefalsetrue: backend tự phục vụ /media/ (dev). Production do nginx phục vụ; để true thì backend không khởi động
ASSET_MAX_UPLOAD_BYTESKhông5242880 (5 MB)Giữ mặc địnhDung lượng tối đa một file ảnh upload
ASSET_MAX_DIMENSIONKhông4096Giữ mặc địnhChiều rộng/cao tối đa (pixel)
ASSET_MAX_PIXELSKhông16777216Giữ mặc địnhTổng số pixel tối đa (rộng × cao)
ASSET_MAX_FRAMESKhông200Giữ mặc địnhSố khung tối đa của ảnh động

Xác thực người chơi (Play Games / Game Center)

Phần tiêu đề “Xác thực người chơi (Play Games / Game Center)”
BiếnBắt buộcMặc địnhProductionÝ nghĩa
IDENTITY_VERIFICATION_MODEKhôngrealrealreal: xác minh thật với Google/Apple. skip/fake chỉ cho dev/test; đặt ở môi trường khác thì backend từ chối khởi động
IDENTITY_RELINK_POLICYKhônglegacylegacyChính sách liên kết lại tài khoản (Phase L). Chỉ chuyển strict theo điều kiện ở §8.2 bước 6
BiếnBắt buộcMặc địnhProductionÝ nghĩa
ACCESS_TOKEN_TTL_SECONDSKhông900 (15 phút)Giữ mặc địnhThời hạn access token
REFRESH_TOKEN_TTL_SECONDSKhông1209600 (14 ngày)Giữ mặc địnhThời hạn refresh token: sau thời gian này admin phải đăng nhập lại
MFA_TOKEN_TTL_SECONDSKhông300 (5 phút)Giữ mặc địnhThời gian để nhập mã MFA sau bước nhập mật khẩu
MFA_TOTP_DRIFT_STEPSKhông1Giữ mặc địnhSố bước 30 giây lệch giờ được chấp nhận khi kiểm mã TOTP
MAX_FAILED_LOGIN_ATTEMPTSKhông5Giữ mặc địnhSố lần sai mật khẩu liên tiếp trước khi khóa tài khoản
ACCOUNT_LOCKOUT_MINUTESKhông15Giữ mặc địnhThời gian khóa tài khoản sau khi sai quá số lần

Định dạng <số>/<đơn vị>, ví dụ 10/minute. Backend chạy 2 worker, bộ đếm nằm riêng trong từng worker nên ngưỡng thực tế có thể gấp đôi.

BiếnBắt buộcMặc địnhProductionÁp cho
AUTH_RATE_LIMITKhông10/minuteGiữ mặc địnhĐăng nhập / MFA của admin
GAME_LINK_RATE_LIMITKhông60/minuteGiữ mặc địnhGame client liên kết tài khoản người chơi
PLAYER_SESSION_REFRESH_RATE_LIMITKhông30/minuteGiữ mặc địnhLàm mới phiên người chơi (L.1)
PLAYER_PROFILE_RATE_LIMITKhông10/minuteGiữ mặc địnhSửa hồ sơ người chơi (L.2)
PLAYER_NICKNAME_CHECK_RATE_LIMITKhông30/minuteGiữ mặc địnhKiểm tra nickname (L.2)
PLAYER_WALLET_RATE_LIMITKhông120/minuteGiữ mặc địnhĐọc ví người chơi
PLAYER_SYNC_RATE_LIMITKhông60/minuteGiữ mặc địnhĐồng bộ tiến trình / thu nhập từ game
PLAYER_DAILY_CHECKIN_CLAIM_RATE_LIMITKhông10/minuteGiữ mặc địnhNhận thưởng điểm danh
PLAYER_REWARD_CLAIM_RATE_LIMITKhông30/minuteGiữ mặc địnhNhận quà hộp thư (H.4)
PLAYER_PROMO_CODE_REDEEM_RATE_LIMITKhông5/minuteGiữ mặc địnhNhập mã quà (H.7)
CLIENT_BOOTSTRAP_RATE_LIMITKhông60/minuteGiữ mặc địnhGET .../client/bootstrap (K.4)
LEADERBOARD_RATE_LIMITKhông60/minuteGiữ mặc địnhĐọc bảng xếp hạng (L.4)
CLIENT_SHOP_PURCHASE_RATE_LIMITKhông30/minuteGiữ mặc địnhMua trong Shop bằng GOLD/GEM (O.6)
IAP_VERIFY_RATE_LIMITKhông10/minuteGiữ mặc địnhXác minh giao dịch tiền thật (O.7)
ASSET_UPLOAD_RATE_LIMITKhông30/minuteGiữ mặc địnhUpload ảnh trên Portal (N.3)
ALERT_CHANNEL_TEST_RATE_LIMITKhông6/minuteGiữ mặc địnhNút gửi thử kênh cảnh báo trên Portal

Các trần thưởng dưới đây chỉ là mặc định cho game chưa khai báo tiền tệ riêng. Trần thật của từng game sửa ở Portal → Tiền tệ & tiến trình; đổi .env không ảnh hưởng game đã cấu hình.

BiếnBắt buộcMặc địnhProductionÝ nghĩa
MAX_EARNING_PER_SYNCKhông5000Giữ mặc địnhGOLD tối đa nhận trong một lần đồng bộ (chống gian lận)
MAX_EARNING_PER_WINDOWKhông50000Giữ mặc địnhGOLD tối đa trong một cửa sổ thời gian
MAX_EARNING_WINDOW_SECONDSKhông300Giữ mặc địnhĐộ dài cửa sổ thời gian (giây) cho MAX_EARNING_PER_WINDOW
REWARD_MAX_GOLD_PER_CHECKIN_CELLKhông10000Giữ mặc địnhGOLD tối đa của một ô điểm danh
REWARD_MAX_GOLD_PER_GIFTKhông100000Giữ mặc địnhGOLD tối đa của một lần tặng quà
REWARD_MAX_GOLD_PER_CAMPAIGNKhông100000Giữ mặc địnhGOLD tối đa của một chiến dịch khuyến mãi
REWARD_MAX_GOLD_PER_PROMO_CODEKhông100000Giữ mặc địnhGOLD tối đa của một mã quà
GIFT_APPROVAL_GOLD_THRESHOLDKhông5000Giữ mặc địnhTặng quà từ mức GOLD này trở lên phải duyệt hai người (khi game bật approval_workflow)
GIFT_APPROVAL_RECIPIENT_THRESHOLDKhông50Giữ mặc địnhTặng cho từ số người nhận này trở lên phải duyệt hai người
DAILY_CHECKIN_REWARDS_JSONKhôngBảng 7 ngày của Drone StrikeKhông đặtBảng thưởng mặc định khi tạo bản nháp điểm danh đầu tiên. Bảng thật quản lý trên Portal → Điểm danh
DAILY_CHECKIN_RESET_UTC_OFFSET_MINUTESKhông0Không đặtMốc reset điểm danh mặc định (phút lệch UTC) cho bản nháp đầu tiên
RECONCILIATION_PENDING_HOURSKhông24Giữ mặc địnhGiao dịch chờ lâu hơn số giờ này được đếm vào hàng chờ đối soát trên Health

Các biến *_PATH là đường dẫn bên trong container backend. Thư mục credentials/ trong thư mục cài đặt được mount chỉ đọc vào /app/credentials. Cách tạo từng giá trị: Google Play & Cloud.

BiếnBắt buộcMặc địnhProductionÝ nghĩa
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATHKhi dùng IAPTrống/app/credentials/<file>.jsonService account gọi Google Play Developer API: xác minh giao dịch, acknowledge/consume, đối soát hoàn tiền
GOOGLE_PUBSUB_AUDIENCEKhi dùng RTDNTrốngURL push endpoint đã cấu hình ở Pub/SubAudience của JWT mà Pub/Sub ký khi đẩy thông báo tới /webhooks/play-store
GOOGLE_PUBSUB_PUBLIC_KEY_PATHKhi dùng RTDNTrống/app/credentials/<file>Khóa công khai để kiểm chữ ký thông báo Pub/Sub
GOOGLE_PLAY_VERIFY_PUSH_JWTKhôngtruetrueKiểm JWT của push Pub/Sub. Chỉ tắt khi debug ở dev
ADMOB_PUBLISHER_IDKhi dùng AdMobTrốngpub-…Publisher ID để lấy doanh thu quảng cáo
ADMOB_SERVICE_ACCOUNT_JSON_PATHKhi dùng AdMobTrống/app/credentials/<file>.jsonService account đọc báo cáo AdMob
IAP_WEBHOOK_PUBLIC_KEY_PATHKhi dùngTrống/app/credentials/<file>Khóa công khai kiểm webhook IAP kiểu cũ
IAP_WEBHOOK_ISSUERKhi dùngTrốngIssuer của webhookIssuer mong đợi trong webhook IAP kiểu cũ
APPLE_ROOT_CERTIFICATE_PATHKhi dùngTrống/app/credentials/<file>Chứng chỉ gốc Apple để kiểm thông báo App Store (iOS IAP chưa bật)
BiếnBắt buộcMặc địnhProductionÝ nghĩa
SMTP_HOSTKhi gửi emailTrốngHost SMTPMáy chủ gửi mail (cảnh báo qua email, BACKUP_ALERT_EMAIL)
SMTP_PORTKhông587587Cổng SMTP
SMTP_USERNAMEKhi gửi emailTrống—Tài khoản SMTP
SMTP_PASSWORDKhi gửi emailTrống—Mật khẩu SMTP (secret)
SMTP_FROM_EMAILKhi gửi emailTrốngĐịa chỉ gửiĐịa chỉ người gửi
SMTP_STARTTLSKhôngtruetrueBật STARTTLS khi kết nối SMTP

Kênh cảnh báo của ứng dụng (ingestion, đối soát) cấu hình trên Portal → Alert Channels, không cần biến riêng ngoài token Telegram.

BiếnBắt buộcMặc địnhProductionÝ nghĩa
TELEGRAM_BOT_TOKENKhi dùng TelegramTrốngToken bot từ @BotFather (secret)Một bot dùng chung cho mọi kênh Telegram trên Portal và cảnh báo backup. Mỗi kênh chỉ lưu chat id
BACKUP_ALERT_TELEGRAM_CHAT_IDKhôngTrống123456789 hoặc -100…Chat nhận cảnh báo backup; cần TELEGRAM_BOT_TOKEN
BACKUP_ALERT_SLACK_WEBHOOKKhôngTrốngURL webhook Slack (secret)Nhận cảnh báo backup qua Slack
BACKUP_ALERT_EMAILKhôngTrốngops@…Nhận cảnh báo backup qua email; cần SMTP_* và backend đang chạy
BiếnBắt buộcMặc địnhProductionÝ nghĩa
BACKUP_AGE_PUBLIC_KEYCó nếu bật cron backupTrốngage1… (có thể nhiều khóa, cách nhau dấu phẩy)Khóa công khai age mã hóa backup DB, ảnh và cấu hình. Private key không để trên VPS — xem Bảo vệ khóa backup
BACKUP_DIRKhông/opt/backupsGiữ mặc địnhThư mục lưu backup trên VPS
BACKUP_RETENTION_DAYSKhông30Giữ mặc địnhSố ngày giữ backup cục bộ
LOCAL_RETENTION_DAYSKhông—Không đặtTên cũ của BACKUP_RETENTION_DAYS, vẫn được nhận
BACKUP_GROUPKhôngbackup-pullGiữ mặc địnhNhóm Linux được đọc backup (để người vận hành kéo backup về máy)
OFFSITE_REMOTEKhôngTrống (không gửi offsite)remote:admin-portal-backups/ nếu dùngĐích rclone để đẩy backup ra ngoài VPS
MEDIA_VOLUMEKhôngadmin_portal_media_prodGiữ mặc địnhTên Docker volume chứa ảnh, dùng khi backup/restore media
MEDIA_OWNERKhông1000:1000Giữ mặc địnhUID:GID gán lại cho file ảnh sau khi restore (user của backend)
BACKUP_MAX_AGE_HOURSKhông26Giữ mặc địnhBackup mới nhất cũ hơn số giờ này thì cảnh báo
BACKUP_MAX_PULL_AGE_DAYSKhông3Giữ mặc địnhQuá số ngày này chưa ai kéo backup về máy thì cảnh báo
BACKUP_MIN_FREE_PERCENTKhông20Giữ mặc địnhĐĩa trống dưới mức này thì cảnh báo (dưới 10% luôn là lỗi)
BACKUP_DOCKER_PATHKhông/var/lib/dockerGiữ mặc địnhĐường dẫn dữ liệu Docker để kiểm dung lượng đĩa
BiếnBắt buộcMặc địnhProductionÝ nghĩa
BOOTSTRAP_ADMIN_EMAILKhôngTrốngChỉ đặt lúc khởi tạo, xóa ngay sau đóKhi có cả email và mật khẩu, backend tự tạo admin này (Super Admin toàn cục) lúc khởi động nếu email chưa tồn tại
BOOTSTRAP_ADMIN_PASSWORDKhôngTrốngNhư trênMật khẩu của admin bootstrap (secret)
BOOTSTRAP_ADMIN_FULL_NAMEKhôngTrốngNhư trênTên hiển thị của admin bootstrap

Cách khuyến nghị trên production: Tạo tài khoản Super Admin đầu tiên.

Chỉ truyền khi chạy lệnh (không lưu trong .env)

Phần tiêu đề “Chỉ truyền khi chạy lệnh (không lưu trong .env)”
BiếnDùng vớiÝ nghĩa
CERTBOT_EMAILscripts/init_letsencrypt.shEmail đăng ký chứng chỉ Let’s Encrypt; chỉ cần lúc xin chứng chỉ lần đầu
BACKUP_AGE_IDENTITY_FILEscripts/restore.shĐường dẫn private key age để giải mã backup. Chỉ nằm trên máy người vận hành, không bao giờ để trên VPS hay trong .env
RESTORE_ENV_FILEscripts/restore.shFile .env (đã giải mã từ backup cấu hình) dùng khi restore. Mặc định .env
RESTORE_COMPOSE_FILEscripts/restore.shFile compose dùng khi restore. Mặc định docker-compose.prod.yml
RESTORE_PROJECT_NAMEscripts/restore.shTên Compose project đang chạy trên VPS (để chọn đúng container/volume)
RESTORE_REMOTE_LOCK_PATHscripts/restore.sh qua SSHFile lock trên VPS, phải đúng BACKUP_DIR/.lock (mặc định /opt/backups/.lock)
RESTORE_TRUSTED_ENVscripts/restore.sh1 chỉ dùng khi restore cục bộ với file dotenv tin cậy; luôn bị bỏ qua khi restore qua Docker từ xa

Chi tiết: Backup & restore.

BiếnBắt buộcMặc địnhProductionÝ nghĩa
BACKEND_URLKhônghttp://localhost:8000 (dev), http://backend:8000 (image)Giữ mặc định của imageĐích rewrite /api/* của Next.js. Giá trị được ghi cứng lúc build image; đổi ở runtime không có tác dụng
NEXT_PUBLIC_APP_ENVKhôngdev—Tên môi trường hiện trên banner game. Cũng được ghi cứng lúc build; image production hiện chưa truyền biến này nên banner có thể hiện dev
BiếnDùng vớiÝ nghĩa
GUIDE_SITE_URLnpm run buildDomain gốc của site (sitemap, link tuyệt đối). Mặc định https://portal.tinysoft.io.vn; base /guide/ không đổi
PORTAL_E2E_BASE_URLnpm run screenshotsURL Portal để chụp ảnh, chỉ chấp nhận localhost. Mặc định http://localhost:3000
PORTAL_E2E_GAME_IDnpm run screenshotsGame test_t4_demo_<hex> do script seed tạo
PORTAL_E2E_EMAILnpm run screenshotsEmail admin dev để đăng nhập khi chụp (đặt trong shell, không ghi vào file)
PORTAL_E2E_PASSWORDnpm run screenshotsMật khẩu admin dev (đặt trong shell, không ghi vào file)
PORTAL_E2E_STORAGE_STATEnpm run screenshotsFile phiên đăng nhập Playwright, lưu ngoài repo (dùng khi tài khoản có MFA)
PORTAL_T4_STATE_FILEseed-screenshots.pyNơi ghi id game demo. Mặc định file tạm gameops-portal-t4-screenshot-seed.json

Đối chiếu với backend/src/admin_portal/settings.py, các file .env*.example và script trong scripts/ của Portal v1.2.2. npm run build:all báo lỗi nếu có biến mới chưa có trong trang này.

Áp dụng cho Portal v1.2.2