# runicgateway.com — production. # # Pull-only, in common with the rest of the org: `image:` and no `build:`, so a # production host can never accidentally build. The image is published to the # Gitea registry by .gitea/workflows/build-image.yml on every merge to main. # # Full operator guide, including DNS, TLS and the reverse proxy: DEPLOY.md. # # docker compose pull && docker compose up -d # # One service. §6 records why there is no second one: the dynamic surface is two # routes, and one container is one thing to deploy, one thing to patch and one # log to read. services: site: # IMAGE_TAG defaults to `latest`. Pin a build for a reproducible deploy or a # rollback — e.g. IMAGE_TAG=sha-1806406 in .env; every merge publishes both. image: gitea.whitlocktech.com/runicgateway/runicgateway-site:${IMAGE_TAG:-latest} restart: unless-stopped # Secrets and tuning for the beta signup (§8). The file is optional in the # sense that the site starts without it — but read the note in .env.example # about BETA_IP_SALT and BETA_FORM_KEY before deciding to skip it: their # defaults are random PER PROCESS, so leaving them unset means every restart # forgets who has been rate-limited. env_file: .env volumes: # --------------------------------------------------------------------------- # The branding mount (§7). Read-only: nothing in the container ever writes # here, and the whole point of the directory is that a human puts files in # it from the host. # # May be empty, partial or complete. Every file resolves against this mount # first and the image's brand-default/ second, PER FILE — so a directory # holding only theme.css recolours the site and leaves every logo stock, # and an empty directory produces exactly the stock site. # # Changing a file here takes a RESTART, not a rebuild: `docker compose # restart site` re-runs the boot rewrite, which is what puts a new site # name into forty-nine prerendered pages. The exception is brand.json's # betaOptInUrl, which /beta reads live on every request — so the closed # test can be opened by editing one file, with no restart at all. - ./brand:/app/brand:ro # --------------------------------------------------------------------------- # The beta signup store (§8): beta.sqlite and exports/. Read-WRITE, and the # only thing this site persists. # # A bind mount rather than a named volume because the tester list has to be # reachable from the host — `sqlite3 ./data/beta.sqlite`, a backup by `cp`, # and the CSV the export CLI writes into ./data/exports/ for pasting into # Play. A named volume would put all three behind `docker cp`. # # The container runs as uid 1000. Docker creates a MISSING bind-mount source # as root:root, and the store then fails to open — so create the directory # yourself and, if it is owned by someone else, `chown 1000:1000 ./data`. # DEPLOY.md has the two commands. - ./data:/app/data # Published on ALL interfaces by default, so the site answers on the host's # own address — `http://:4321` — and not only on its loopback. That is # what makes it reachable from the rest of the network: a proxy in another # container or on another machine, a browser on the LAN, a phone on the same # wifi checking the mobile layout. # # It is deliberately a variable rather than a fixed address, because the safe # binding depends on where this host sits. Set SITE_BIND_ADDR in .env to # narrow it without touching this file: # # SITE_BIND_ADDR=127.0.0.1 loopback only — a proxy on THIS host, nothing else # SITE_BIND_ADDR=192.168.1.10 one interface — the LAN, but not a public NIC # SITE_BIND_ADDR=0.0.0.0 every interface (the default) # # On a host with a public address, `0.0.0.0` means port 4321 answers from the # internet directly, beside whatever the proxy serves on 443 — plain HTTP, no # TLS. Firewall the port, or narrow the binding. DEPLOY.md, "Putting a proxy # in front of it". ports: - "${SITE_BIND_ADDR:-0.0.0.0}:${SITE_HOST_PORT:-4321}:4321" # Repeats the image's own HEALTHCHECK so `docker compose ps` reports it even # when the image is pinned to an older tag that predates it. It watches an # actual response rather than the process, because `npm start` runs the brand # rewrite before the server: there is a real window where the container is up # and nothing is listening. # # The command is a QUOTED flow sequence, which is not a style choice: written as a # block sequence, YAML reads the `: ` inside `r.ok ? 0 : 1` as a key/value separator # and `docker compose config` refuses the file with "healthcheck.test.3 must be a # string". Keep the quotes. healthcheck: test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:4321/').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"] interval: 30s timeout: 5s start_period: 40s retries: 3 logging: driver: json-file options: max-size: "10m" max-file: "3"