Skip to main content

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.

tip

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

EnvironmentBranchFrontendNakama APINakama console
stagingdevdev.humuus.denakama.dev.humuus.deconsole.dev.humuus.de
productionmainhumuus.denakama.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

  1. New resource in the Coolify project, source techagogics/humuus, build pack Docker Compose, compose file /docker-compose.yaml.
  2. Pick the branch (dev for staging, main for production).
  3. Leave custom start and build commands empty. Anything other than a plain docker compose up bypasses the Traefik labels Coolify generates, which is how the old setup ended up needing hand-written routers.
  4. Give every service exactly one domain — see the warning below.
  5. 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).

Never give a service two domains

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
Do not set NEXT_PUBLIC_NAKAMA_SERVER_HOST

Coolify 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):

EmailPassword
alice@example.comAliceTestPass123!
bob@example.comBobTestPass123!

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 (postgrespostgres-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:

FileHostsStatus
nakamadev1.yamlnk.humuus.de, nkdev.humuus.dedead, safe to delete
nakamadev2.yamlnkdev2.humuus.deserves the Data Decorator Nakama — keep
nakamadev3.yamlnkdev3.humuus.de, nakama-test.humuus.dethe 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.yml still carries the console credentials and runtime.http_key in 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; the x echoes 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.