Running the Matrix hub
deploy/hub is the shared Matrix homeserver that the human queue uses to reach the owner's phone: Tuwunel behind Caddy, with federation off and public registration closed. matrix-bus is the client that talks to it, and queue-mirror mirrors approvals and questions into its #queue room.
It is a compose stack and a few owner-side scripts, not a package. This page follows deploy/hub/README.md, which also records why each setting is what it is.
What runs
| Service | Compose profile | Status |
|---|---|---|
tuwunel on 127.0.0.1:8008 | default | runs |
caddy, TLS for SERVER_NAME | remote | runs on a VPS |
core, the broker service | core | built by CI, not started; it exists for stage 2 |
Stage 1 runs Tuwunel and Caddy only. The core appservice registration is still rendered, so Tuwunel knows its namespaces, but nothing uses them yet.
Prerequisites
- Docker with the compose plugin.
- Node 22 and a repo-root
pnpm install, only for the owner-side scripts. A VPS needs Docker alone. openssl, whichsetup.shuses to generate tokens.envsubst(part of GNU gettext), whichsetup.shuses to render the config templates.- For the remote profile: a DNS name that points at the host, and ports 80 and 443 open.
Local run
cd deploy/hub
./scripts/setup.sh
docker compose up -d --build
curl localhost:8008/_matrix/client/versions
pnpm run bootstrap # owner account, the ac-edge1 mirror user and #queuesetup.sh writes every missing key to .env, generates the tokens, and renders the config templates into data/run. It prints what it rendered:
rendered for tp301.local: tuwunel.toml titan-core.yaml titan-edge1.yamlSERVER_NAME defaults to tp301.local. Rerunning the script keeps the existing .env keys.
Remote run
On a fresh VPS with an empty data/:
cd deploy/hub
printf 'SERVER_NAME=chat.example.org\nPUBLIC_BASE_URL=https://chat.example.org\n' > .env
./scripts/setup.sh && chmod 600 .env
docker compose run --rm tuwunel --execute "users create_user owner" # note the password, then Ctrl-C
docker compose --profile remote up -d --buildSetting PUBLIC_BASE_URL also enables online backups and the client well-known file. The last line starts Tuwunel and Caddy.
Where state lives
Everything is under deploy/hub, and all of it is gitignored.
| Path | Holds |
|---|---|
.env | every token and the owner password; keep it mode 600 and never commit it |
data/db | the Tuwunel database |
data/server_name | the server name the database was created for |
data/run | rendered config and appservice registrations; regenerated by setup.sh |
data/backup | online database backups, the latest 3, when PUBLIC_BASE_URL is set |
data/caddy/data | TLS certificates |
Logs are the containers' own: docker compose logs -f tuwunel or caddy.
Back up data/backup, .env and data/caddy/data. A restore must keep the same SERVER_NAME.
Upgrade
The compose file tracks ghcr.io/matrix-construct/tuwunel:latest. Take a backup, then:
docker compose pull
docker compose --profile remote up -dTuwunel has a 30-minute stop grace period. Let a stop finish; do not kill the container. After a template change, run ./scripts/setup.sh again and restart Tuwunel.
How it fails
| What you see | Why |
|---|---|
data/db was created for <old>, not <new>; wipe data/db or restore SERVER_NAME, exit 1 | the server name changed after the database was created; Tuwunel bakes the name into every id |
The "SERVER_NAME" variable is not set. Defaulting to a blank string. | setup.sh has not run, so .env and data/run do not exist |
curl localhost:8008/… refuses the connection | Tuwunel is still starting, or the container exited; read its logs |
Caddy serves no certificate for SERVER_NAME | the DNS name does not reach the host, or ports 80 and 443 are closed; Caddy binds both |
pnpm run bootstrap fails to connect | Tuwunel is not up on HS_URL (default http://localhost:8008) |
Never copy a laptop's data/ to the VPS: the server name would not match.
What CI checks
The hub-compose job in .github/workflows/ci.yml runs setup.sh with a sample .env, validates the compose file with and without the remote and core profiles, and builds the core image. It starts no container.