Self-hosting
A real deployment is one container, one Postgres, and a reverse proxy. Nothing else is required.
Build and run
Section titled “Build and run”docker build -f website/Dockerfile -t peekseek .docker run --env-file website/.env -p 8000:8000 peekseekThe image builds docs/ with Node in a throwaway stage and copies only the result in,
so the running container has no Node in it. The app serves that build at /docs, and
mounts nothing if it is absent, which is why a local uvicorn with no docs build still
starts.
The build runs minify.py over public/assets after copying it in, so the image serves
compressed CSS and JS while the repository keeps readable source. The compressors are
uninstalled in the same layer and never reach the running container.
Behind a reverse proxy
Section titled “Behind a reverse proxy”The container listens on 8000. Point your proxy at it and terminate TLS there.
Database
Section titled “Database”Point DB at Postgres. The schema builds itself on startup: create_all adds missing
tables, then add_missing_columns adds columns the models have gained, printing each
ALTER as it runs.
There is no migration tool and no migration step. Dropping, renaming or retyping a column is not covered and has to be done by hand. See Architecture for what that trade buys.
One consequence worth knowing before you wire anything up to read this database: a new
table, or a column add_missing_columns fills in, only exists once the app has started
with the model that declares it. So a schema change reaches anything querying the database
directly - a dashboard, a reporting tool - one deploy later, never sooner. That is the
only way a new table ever gets there.
Back the database up yourself. Nothing in the application does it for you.
First run
Section titled “First run”Two bootstraps exist because a brand-new database cannot be used without them, and neither should stay set.
# Grants admin to one account, creating it if it has never logged in.BOOTSTRAP_ADMIN_STEAM64=76561198000000000
# Allow-lists game servers: address=region,address=regionBOOTSTRAP_SERVERS=203.0.113.10=paris,203.0.113.11=utahEvery /server/v1/* route returns 401 to an address not in AuthorizedServers, and
nothing in the site adds one, so without BOOTSTRAP_SERVERS no game server can ever
talk to your instance. Regions come from regions in config.yaml. Addresses
already present are left alone, so changing a region here does not overwrite the stored
one.
Unset both once they have done their job.
Connecting a game server
Section titled “Connecting a game server”- Put the compiled plugin in the server’s
Rocket/Plugins/<name>/. - Point its configuration at your site’s base URL.
- Add the server’s public IP to
BOOTSTRAP_SERVERS, with a region that exists in[regions.names]. - Restart the site once so the allow-list entry is created.
The server then polls GET /server/v1/session and the
match lifecycle takes over.
Multiple environments
Section titled “Multiple environments”One image serves all of them. Three environment variables override the app block in
config.yaml:
| Variable | Overrides |
|---|---|
DOMAIN |
app.domain, the public hostname with no scheme |
PROTOCOL |
[app].protocol |
BUILD |
[app].build, the label the site shows |
A staging environment should also leave BOT_TOKEN, LOG_WEBHOOK and
QUEUE_LOG_CHANNEL unset. Every one of those callers no-ops on an empty value, so
staging can never DM real players or post into production Discord channels. That is the
mechanism, not a convention, and it is worth preserving.
What PeekSeek itself runs on
Section titled “What PeekSeek itself runs on”Dokploy, with Traefik in front, on a single host, with production and staging as two separate applications off the same branch. Nothing in the application depends on any of that, and any Docker host will do.
Two lessons from running it that are cheaper to read than to rediscover:
- Never let two applications claim the same hostname. Traefik will hold two routers
with the same rule, pick one deterministically, and a redeploy can flip which one wins.
A healthy container and a
502at the same time usually means this. - A valid certificate proves nothing about your backend. Traefik answers the ACME
challenge itself and never touches the container, so a fresh certificate alongside a
502is exactly the signature of a live router pointed at a dead service.
- Configuration, every setting in one table
- Forking for another game
