Skip to content

Configuration

Required: ADMIN_USERNAME + ADMIN_PASSWORD together in .env, or create the first admin by hand — see Quickstart. Until an admin exists every request returns 503. Setting only one of the pair is a startup error.

Everything else is optional with a working default.

These are baked into the image, so a plain docker run with no environment at all gives you a working self-hosted instance. docker-compose.yml repeats them so the compose file documents its own behaviour; the AWS deployment overrides them in infra/lambda.tf. Don’t change them unless you know what you’re doing.

VariableDefaultWhy
DB_BACKENDsqliteEmbedded SQLite file instead of DynamoDB
STORAGE_BACKENDlocalLocal filesystem storage instead of S3
SECRETS_PROVIDERenvRead JWT_SECRET/ORIGIN_TOKEN/BGG_TOKEN from env instead of AWS SSM
ORIGIN_GUARD_ENABLEDfalseNo CloudFront in front of selfhost — the guard would block everything
PUBLIC_RECOMMENDED_ENABLEDfalseNothing is reachable without logging in

SQLite database file and uploaded images both live under config/app/ on the host — /data/boardsite.db and /data/storage/ inside the container.

The app service’s default limits (docker-compose.yml) are 512MB memory / 1.0 CPU, with 128MB / 0.25 CPU reserved — comfortable for a handful of concurrent users. On a Raspberry Pi or small VPS shared with other containers, 256MB is enough: edit app.deploy.resources.limits.memory in docker-compose.yml.

Create a .env file next to docker-compose.yml (compose auto-loads it) to configure optional features. Every variable, with its default:

.env.example
# Host port the app is published on. The container always listens on 4263;
# this only changes the host side of the mapping, so use it to dodge a clash
# with something else on this machine. Read by docker-compose itself, not by
# the app -- it is deliberately absent from the app's `environment:` block.
APP_PORT=4263
# Board Game Geek API token — required for game search (/api/games/search).
# Leave empty to disable game search (the rest of the app works without it).
BGG_TOKEN=
# UID/GID that owns ./config/ subdirectories and runs the app process inside
# the container. Defaults to 1000:1000. Override if `id -u`/`id -g` differ.
PUID=1000
PGID=1000
# Set both to create an admin user automatically on first boot. If the username
# already exists, these are ignored — safe to leave set permanently. Setting
# only one of the two is a startup error.
#
# Leaving both empty is only valid once a user exists. On a fresh database with
# no users and no credentials here, the app serves 503 on every request --
# including the login page -- until you create the first admin by hand:
# docker compose exec app python3 scripts/create-user.py \
# --username admin --display-name "Admin" --role admin --password <pw>
# That takes effect on the next request, no restart needed. See
# https://mcgamertime-docs.drmaggi.com/self-hosting/quickstart/
ADMIN_USERNAME=
ADMIN_PASSWORD=
# Public URL this instance is reached at (e.g. https://games.example.com).
# Used to build links in outgoing emails (password reset). If unset, the
# app falls back to the incoming request's Host header — fine for local/
# zero-config use, but that header is attacker-controllable if you expose
# the app's port directly without a reverse proxy in front of it. Set this
# in production.
APP_BASE_URL=
# Optional SMTP server for self-service password reset
# (POST /api/auth/forgot-password). Leave SMTP_HOST empty to disable sending —
# reset links are logged to the container's stdout instead.
SMTP_HOST=
SMTP_PORT=587
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_ADDRESS=
# Set to "true" to expose Prometheus-format metrics at GET /metrics
# (request count, latency, status code). No auth beyond ORIGIN_GUARD_ENABLED
# below — disabled by default in self-hosted mode, so scrape from inside your
# Docker network only, never through the public reverse proxy. Leave
# unset/false to skip the prometheus_client import entirely.
METRICS_ENABLED=
# Comma-separated IPs/CIDR ranges trusted to set X-Forwarded-For/X-Forwarded-Proto
# (passed to uvicorn's --forwarded-allow-ips). Needed for rate limiting
# (5 requests/minute on login/password-reset) to see each real visitor's IP instead
# of your reverse proxy's IP -- without this, putting nginx/Traefik/Tailscale in
# front (see the HTTPS guide) collapses rate limiting into one shared bucket for
# every visitor. Defaults to standard private/Docker-internal ranges
# (127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16), which already covers every
# documented reverse-proxy setup. Only override this if your own LAN/Docker network
# isn't fully trusted.
FORWARDED_ALLOW_IPS=
# Comma-separated origins allowed to make credentialed cross-origin API calls
# (browser CORS). Leave empty for the normal self-hosted setup: the web UI and
# the API are served from the same origin, so no CORS grant is needed. Only set
# this if you serve the frontend from a different hostname than the API, e.g.
# CORS_ALLOWED_ORIGINS=https://games.example.com,https://app.example.com
CORS_ALLOWED_ORIGINS=

The container listens on 4263 and Compose publishes it as 4263:4263. Set APP_PORT in .env to publish it on a different host port — the container’s own port never changes, so a reverse proxy reaching app over the Docker network keeps targeting 4263.

Game search (/api/games/search) requires a Board Game Geek API token. Without it, the game catalog search is disabled — you can still log results for games already in your library.

Set BGG_TOKEN=your-token in .env and restart the stack:

Terminal window
docker compose up -d

Users can reset their own password from /forgot-password if their account has an email address on file (set via --email on scripts/create-user.py or scripts/update-user.py — there’s no UI for it yet).

Without an SMTP server configured, reset links are logged to the container’s stdout (docker compose logs app) instead of emailed — useful for testing, or for an admin to retrieve a link on a user’s behalf. To actually send email, set the SMTP_* variables in .env and restart the stack.

Set APP_BASE_URL (e.g. https://games.example.com) to the URL you actually use — the reset link is built from it. If unset, the link falls back to the incoming request’s Host header, which is fine for local zero-config use but spoofable by anyone who can reach the app’s port directly without going through your reverse proxy. Set it explicitly once you’re past local testing.

You can also set this from Settings → Instance URL in the app itself (admin only) — no restart needed. The Settings-page value takes precedence over APP_BASE_URL when both are set, so you can override or correct it without touching .env.

Set METRICS_ENABLED=true in .env and restart to expose GET /metrics in Prometheus text format — request counts, latency histograms, and status codes, labeled by HTTP method and route path. /metrics is a normal route — it has no authentication beyond the container’s ORIGIN_GUARD_ENABLED setting (see App service variables), which is disabled by default in self-hosted mode, so the endpoint is reachable unauthenticated. Only expose it inside your Docker network (e.g. scrape from a Prometheus container on the same docker compose network), never through the public reverse proxy. If you enable origin guard, scraping will also need the x-origin-token header.

Login and password-reset rate limiting (5 requests/minute) keys off each request’s real IP. Behind a reverse proxy (see HTTPS / Reverse Proxy) that only works if the app trusts the proxy’s X-Forwarded-* headers — FORWARDED_ALLOW_IPS lists who is trusted. The default covers nginx on the host, Traefik on the compose network, and Tailscale serve (standard private and Docker-internal ranges), so most self-hosters never set it. Override it only if your own LAN shouldn’t be trusted to set those headers.

TRUSTED_PROXY_HOPS also exists in the codebase but is for the AWS deployment only; the self-hosted stack ignores it.

Cross-origin frontend optional

Section titled “Cross-origin frontend ”

The default stack serves the web UI and the API from the same origin, so the browser never issues a cross-origin request and no CORS configuration is needed.

If you put the frontend on a different hostname than the API, list the frontend origins in CORS_ALLOWED_ORIGINS (comma-separated, scheme included):

Terminal window
CORS_ALLOWED_ORIGINS=https://games.example.com,https://app.example.com

Session cookies are sent with these requests, so only list origins you control. A wildcard (*) is rejected — it cannot be combined with credentialed requests.

If you suspect the JWT secret has leaked (e.g. a stolen backup, a compromised host), rotate it to invalidate every existing session:

Terminal window
docker compose down
rm config/app/.jwt_secret
docker compose up -d

A new secret is generated automatically on the next boot (docker/entrypoint.sh). Every user — including you — is signed out and must log in again. This does not affect any other data (games, results, posts); only active sessions are invalidated.