Configuration
At a glance
Section titled “At a glance”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.
App service variables
Section titled “App service variables”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.
| Variable | Default | Why |
|---|---|---|
DB_BACKEND | sqlite | Embedded SQLite file instead of DynamoDB |
STORAGE_BACKEND | local | Local filesystem storage instead of S3 |
SECRETS_PROVIDER | env | Read JWT_SECRET/ORIGIN_TOKEN/BGG_TOKEN from env instead of AWS SSM |
ORIGIN_GUARD_ENABLED | false | No CloudFront in front of selfhost — the guard would block everything |
PUBLIC_RECOMMENDED_ENABLED | false | Nothing 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.
Resource requirements
Section titled “Resource requirements”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.
.env variables
Section titled “.env variables”Create a .env file next to docker-compose.yml (compose auto-loads it) to configure optional features. Every variable, with its default:
# 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=1000PGID=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=587SMTP_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.comCORS_ALLOWED_ORIGINS=Host port
Section titled “Host port”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.
BGG Token
Section titled “BGG Token”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:
docker compose up -dPassword reset
Section titled “Password reset”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.
Metrics optional
Section titled “Metrics ”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.
Reverse proxy and rate limiting
Section titled “Reverse proxy and rate limiting”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):
CORS_ALLOWED_ORIGINS=https://games.example.com,https://app.example.comSession cookies are sent with these requests, so only list origins you control.
A wildcard (*) is rejected — it cannot be combined with credentialed requests.
Rotating the JWT secret
Section titled “Rotating the JWT secret”If you suspect the JWT secret has leaked (e.g. a stolen backup, a compromised host), rotate it to invalidate every existing session:
docker compose downrm config/app/.jwt_secretdocker compose up -dA 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.