Deployment
humuus runs on a single Hetzner server managed by Coolify,
with Traefik as the reverse proxy. Every environment is one Coolify application
built from the same docker-compose.yaml.
For day-to-day work you rarely need this page. It matters when you add an environment, change domains, or a preview deployment misbehaves.
Topology
| Environment | Branch | Frontend | Nakama API | Nakama console |
|---|---|---|---|---|
| staging | dev | dev.humuus.de | nakama.dev.humuus.de | console.dev.humuus.de |
| production | main | humuus.de | nakama.humuus.de | — |
Pull requests against dev get an automatic preview at <pr>.dev.humuus.de,
with its own database, its own Nakama and its own console. Coolify destroys the
whole stack when the PR closes.
The stack is defined once, in docker-compose.yaml:
web Next.js frontend :3000
nakama Game server, API :7350
nakama-console socat forwarder to Nakama's console :8080 → nakama:7351
postgres Nakama's database :5432
docs This documentation site :3001
n8n LLM workflow automation :5678
All ports use expose:, never ports: — Traefik reaches the containers over the
Coolify network, nothing is published on the host.
Adding an environment
- New resource in the Coolify project, source
techagogics/humuus, build pack Docker Compose, compose file/docker-compose.yaml. - Pick the branch (
devfor staging,mainfor production). - Leave custom start and build commands empty. Anything other than a plain
docker compose upbypasses the Traefik labels Coolify generates, which is how the old setup ended up needing hand-written routers. - Give every service exactly one domain — see the warning below.
- Set the environment variables, then deploy.
Domains
One hostname per compose service, and the port belongs in Coolify's field, not in the URL:
web https://dev.humuus.de
nakama https://nakama.dev.humuus.de:7350
nakama-console https://console.dev.humuus.de:8080
docs https://docs.dev.humuus.de
n8n https://n8n.dev.humuus.de
The :7350 tells Traefik which container port to forward to. From the outside
only 443 is reachable (plus a custom nakama entrypoint on 7350 that predates
this setup and is only used by the old production stack).
Coolify prefixes the whole comma-separated domain string when it builds preview hostnames, instead of each entry on its own. The result is a syntactically valid but unmatchable router:
Host(`442.dev.humuus.de,https`) && PathPrefix(`//442.www.dev.humuus.de`)
Every hostname of that preview then answers 404 with Traefik's default
certificate. Traefik logs nothing — it counts the router, it just never matches.
Tracked as coollabsio/coolify#7915, fix pending in #11181.
This is why the Nakama console is a separate nakama-console service rather than
a second domain on nakama, and why Coolify's auto-added www. variants have to
be deleted after typing a domain.
Environment variables
NEXT_PUBLIC_RESOURCE_LOCATION Base URL for workshop media (MinIO)
POSTGRES_PASSWORD Only applied when the volume is first created
NAKAMA_HTTP_KEY Server-to-server auth
NEXT_LLM_SHARED_SECRET Shared secret for the LLM proxy routes
NEXT_LLM_URL_GENERATE n8n endpoints
NEXT_LLM_URL_CLUSTER
OPENAI_API_KEY
N8N_DOMAIN
NEXT_PUBLIC_NAKAMA_SERVER_HOSTCoolify appends a bare --build-arg <NAME> for every variable it knows, and a
CLI build-arg overrides the value from docker-compose.yaml. Setting it would
point every preview at the same backend.
The compose file derives the value from SERVICE_FQDN_NAKAMA, which Coolify
maintains per deployment — including a separate value for each preview.
NAKAMA_DOMAIN remains only as a fallback for running docker compose up
outside Coolify.
The frontend bakes this value into its JavaScript bundle at build time, so changing it requires a redeploy, not a restart.
Testing a preview
A preview starts with an empty database. Nakama seeds two host-capable accounts
on boot (apps/nakama/src/createDefaultUsers.ts):
| Password | |
|---|---|
alice@example.com | AliceTestPass123! |
bob@example.com | BobTestPass123! |
Sign in as either, then open /StoragePush to load the workshop definitions from
apps/web/workshops/ into Nakama storage. Only then can you start a workshop.
Debugging
Which Nakama does the frontend talk to? The host is compiled into the bundle, so read it back from the deployed site:
curl -s https://dev.humuus.de/ -o page.html
for c in $(grep -oE '/_next/static/[^"]+\.js' page.html | sort -u); do
curl -s "https://dev.humuus.de$c"
done | grep -oE '\("defaultkey","[^"]*","[^"]*"'
Does Traefik know the route? A 404 on plain HTTP means no router matched; a
TRAEFIK DEFAULT CERT on HTTPS means the same thing seen from the TLS side.
docker exec coolify-proxy wget -qO- http://127.0.0.1:8080/api/http/routers \
| python3 -c "
import json,sys
for r in json.load(sys.stdin):
print(r.get('status'), r.get('rule'), r.get('name'))
"
Is a container healthy? Traefik silently skips unhealthy containers — no log entry, the routers simply never appear.
docker ps --format '{{.Names}}\t{{.Status}}' | grep <service>
Can services reach each other? Coolify renames compose services in previews
(postgres → postgres-pr-442), which breaks hostnames hardcoded in entrypoints.
postgres and nakama therefore pin an explicit network alias in
docker-compose.yaml; verify with:
docker inspect <container> \
-f '{{range $n,$v := .NetworkSettings.Networks}}{{$n}} -> {{$v.Aliases}}{{"\n"}}{{end}}'
Traefik dynamic configuration
/data/coolify/proxy/dynamic/ on the server still holds hand-written router
files from the previous setup. New environments do not need them — Coolify
generates the labels — but three are still load-bearing:
| File | Hosts | Status |
|---|---|---|
nakamadev1.yaml | nk.humuus.de, nkdev.humuus.de | dead, safe to delete |
nakamadev2.yaml | nkdev2.humuus.de | serves the Data Decorator Nakama — keep |
nakamadev3.yaml | nkdev3.humuus.de, nakama-test.humuus.de | the current production bundle points here — do not delete until production has moved |
All three address their backends by fixed container IP, which is why they had to be edited after every restart. Use the compose service name instead if you ever need a file-based router again.
Known gaps
apps/nakama/nkconfig.ymlstill carries the console credentials andruntime.http_keyin plain text. Nakama accepts all of them as CLI flags (--console.username,--console.password,--runtime.http_key,--socket.server_key,--session.encryption_key,--session.refresh_encryption_key, and repeatable--runtime.env "KEY=value").- The entrypoint uses
sh -ecx; thexechoes every command, so the database password appears in the deploy log. - Previews run their own n8n with an empty workflow set, because the workflows live only in n8n's database and are not versioned.
- Staging, production and all previews share one server, so a preview build competes with production for CPU.