Appearance
Configuration reference
Every environment variable read by the code, with its default and purpose, taken from the source. Where DEPLOY.md disagrees, the code wins, and the difference is noted.
Using deploy/docker-compose.prod.yml?
That compose file loads all of deploy/.env into the server container (env_file: .env), so every variable below can be set there. A few are fixed by the compose file and win over .env (DATA_DIR, TRUST_PROXY, DEEP_ENGINE). See Deploy to a VPS.
Server: network and paths
| variable | default | purpose | read in |
|---|---|---|---|
PORT | 8080 | listen port | index.js |
HOST | 0.0.0.0 | bind address | index.js |
PUBLIC_URL | http://localhost:$PORT | base for sign-in, confirmation, unsubscribe and puzzle links in email. Must match the live URL exactly, scheme included. A trailing / is removed | index.js |
TRUST_PROXY | unset | 1 makes rate limiting use the left-most X-Forwarded-For entry. Set it behind any proxy, and never without one. See Rate limiting | index.js |
DATA_DIR | repository root | parent directory for records, database, caches, annotations and the server key | index.js |
RECORDS_DIR | $DATA_DIR/records | signed match records | index.js |
RECORDS_GZIP | on | 0 writes records as plain .json instead of .json.gz | records.js |
DB_DIR | $DATA_DIR | directory of gammonchain.db | index.js |
KEY_DIR | $DATA_DIR | directory of server-key.json | index.js |
SERVER_KEY_PKCS8 | unset | the server's Ed25519 private key as hex PKCS#8. Takes precedence over server-key.json, for platforms with secrets but no disk | identity.js |
ANNOTATIONS_DIR | $DATA_DIR/annotations | stored match analyses | index.js |
CACHE_DIR | $DATA_DIR/analysis-cache | position-level analysis cache | index.js |
NODE_ENV | development | only used as the environment in error reports. The Docker image sets production | errors.js |
Bots and analysis
| variable | default | purpose | read in |
|---|---|---|---|
BOT_ENGINE | auto | engine for the hard bot: auto tries BGBlitz, then gnubg, then built-in. bgblitz or gnubg try only that one before falling back to built-in. Any other value, such as builtin, uses built-in | opponent.js |
DEEP_ENGINE | bgblitz/de.bgblitz.ai.TachiAI | engine id that deep-analysis jobs are queued for. Must equal the id the workers report: gnubg-1.07 for gnubg. The Dockerfile and prod compose file set gnubg-1.07. (DEPLOY.md shortens the default to bgblitz/…) | index.js |
GNUBG_CMD | /usr/games/gnubg | gnubg binary | adapters/gnubg.js |
BGBLITZ_CP | build | Java classpath for the bridge, including your licensed BGBlitz jar | adapters/bgblitz.js |
BGBLITZ_CLASS | unset | the TachiAI engine class to bind by reflection. auto only tries BGBlitz when this is set | adapters/bgblitz.js, opponent.js |
JAVA_CMD | java | Java binary for the BGBlitz bridge | adapters/bgblitz.js |
ENGINE_DEBUG | unset | any value copies engine subprocess stderr to the server's stderr | analysis/process.js |
GAMMONET_KEY | unset | shared key volunteer workers must send as x-gammonet-key. Unset: the worker endpoints return 503 and deep analysis is unavailable | index.js |
STRICT_ANALYSIS | unset | 1 requires two different workers to agree before a result is cached. See Strict mode | index.js |
Email
| variable | default | purpose | read in |
|---|---|---|---|
MAIL_PROVIDER | console | console, mailjet, resend, brevo, mailgun or webhook | mail.js |
MAIL_API_KEY | unset | provider key. For Mailjet, the public key. For webhook, optional, sent as a Bearer token | mail.js |
MAIL_API_SECRET | unset | Mailjet only: the private key. Both are required | mail.js |
MAIL_DOMAIN | unset | Mailgun sending domain | mail.js |
MAIL_WEBHOOK | unset | URL for the webhook provider | mail.js |
MAIL_FROM | gammonchain <no-reply@localhost> | sender. Must use the authenticated domain | mail.js |
MAIL_HOUR | 8 | UTC hour the daily puzzle goes out | index.js |
MAIL_DAILY_CAP | 500 | most addresses mailed in one daily run. Keep it under your provider's free tier (committed configs use 200) | index.js |
CRON_KEY | unset | required to call POST /api/cron/daily?key=…. Unset means the endpoint always answers 403 | index.js |
Rate limits
All values are per client address. Burst is capacity, rate is refill per minute. See Rate limiting.
| variable | default | bucket |
|---|---|---|
LIMIT_CONNECT_BURST / LIMIT_CONNECT_RATE | 120 / 240 | opening WebSockets |
LIMIT_READ_BURST / LIMIT_READ_RATE | 120 / 240 | GET on /api/* and /records/* |
LIMIT_ANALYSIS_BURST / LIMIT_ANALYSIS_RATE | 15 / 30 | /api/analysis/* |
LIMIT_WRITE_BURST / LIMIT_WRITE_RATE | 30 / 60 | non-GET requests |
Error reporting
| variable | default | purpose | read in |
|---|---|---|---|
ERROR_PROVIDER | console | console, sentry or webhook | errors.js |
SENTRY_DSN | unset | https://<key>@<host>/<project>, for sentry | errors.js |
ERROR_WEBHOOK | unset | Discord or Slack webhook URL, for webhook | errors.js |
RELEASE | dev | release tag attached to reports | errors.js |
Backups
| variable | default | purpose | read in |
|---|---|---|---|
BACKUP_S3_ENDPOINT | unset | e.g. https://<account>.r2.cloudflarestorage.com. Backups run only when endpoint, bucket, key and secret are all set | backup.js |
BACKUP_S3_BUCKET | unset | bucket name | backup.js |
BACKUP_S3_KEY / BACKUP_S3_SECRET | unset | S3 credentials | backup.js |
BACKUP_S3_REGION | auto | auto for R2, a real region for B2 or AWS | backup.js |
BACKUP_INTERVAL_H | 6 | hours between snapshots (minimum 1) | backup.js |
BACKUP_PREFIX | gammonchain | object key prefix. Not listed in DEPLOY.md | backup.js |
gammonet worker (bin/gammonet.js)
| variable | default | purpose |
|---|---|---|
GAMMONET_SERVER | http://localhost:8080 | server to pull work from (--server overrides) |
GAMMONET_KEY | unset | worker key (--key overrides). Prefer the env var: argv shows up in ps and docker inspect |
GAMMONET_NAME | random anon-xxxxxxxx per process | worker id sent on both acquire and submit (--name overrides). Distinct ids are what let STRICT_ANALYSIS count two workers as independent |
DNS setup script (bin/setup-dns.js)
| variable | purpose |
|---|---|
MAILJET_API_KEY / MAILJET_API_SECRET | Mailjet key pair (default provider) |
RESEND_API_KEY | for --provider resend. Needs domain access |
CLOUDFLARE_API_TOKEN | scope Zone → DNS → Edit, this zone only. Not needed with --dry-run |
Deploy scripts
| variable | script | default | purpose |
|---|---|---|---|
APP_USER | setup.sh | gammon | unprivileged user to create |
REPO_URL | setup.sh | unset | clone this repo into /opt/gammonchain instead of waiting for push.sh |
APP_DIR | push.sh | /opt/gammonchain | target directory on the server |
SSH_KEY | push.sh | unset | private key to use, with IdentitiesOnly=yes |
CONF | firewall.sh | ports.conf | port list to build the firewall from |
Load test (test/load.js)
| variable | default | purpose |
|---|---|---|
LOAD_MATCH_LENGTH | 1 | match length for the simulated matches |
LOAD_PORT | 8233 | port for the server the test starts |
Hard-coded values worth knowing
These are constants in the code, not configuration:
| value | where |
|---|---|
| reconnect grace before forfeit: 90 s | DEFAULT_GRACE_MS, actor.js |
| idle match expiry: 30 min | IDLE_LIMIT_MS, actor.js |
| handshake timeout: 60 s | HANDSHAKE_LIMIT_MS, actor.js |
| WebSocket ping every 25 s, terminated after 2 missed pings | index.js |
| sign-in token lifetime: 30 min, single use | db.js |
| emails per key/address: 5 per hour | index.js |
| reveal chain length: 4000 | identity.js, actor.js |
| deep analysis wait per position: 20 s | index.js |