Installation with Docker Compose¶
Status: draft M0. Guide for an instance on one host. Rules: 16-operations.md. Files: infra/compose/.
Note: The images bothy-server and bothy-postgres are created with the first release. Until
then, this guide is the contract that the release and the portability test fulfill.
Two equivalent paths: the setup wizard in one command (section 1.1) or the manual path (sections 2 to 4). Both create the same instance.
1. Prerequisites¶
| Item | Requirement |
|---|---|
| Runtime | Docker Engine 26 or newer with Compose 2.26 or newer; on the Mac Docker Desktop or Colima |
| Architecture | linux/amd64 or linux/arm64 |
| Memory of the Docker VM (Mac, Windows) | enough for all services; ClamAV alone needs at least 3 GiB (vendor) |
| Disk | according to the model in 16-operations.md 13 |
| Network | egress to push services, OIDC provider, off-site target, ClamAV signatures |
| Edge | a reverse proxy with TLS (profile direct) or a tailnet (profile tailscale) |
| Off-site target | S3 bucket with Object Lock or SFTP with append-only access |
1.1 One command: setup wizard¶
With the prerequisites met (Docker and Compose installed), one command sets up the instance, starts it and creates the first owner (16-operations.md, section 3.1):
curl -fsSL https://github.com/OWNER/bothy/releases/download/v0.0.0/install.sh | sh
The wizard asks for the public address, the edge (direct or tailscale), virus scan, file
storage (fs or s3), observability stack, off-site target (optional) and the first owner,
writes .env and secrets/, enters the gateway of the Compose network in
BOTHY_TRUSTED_PROXIES for the edge direct (section 5.1), starts the stack, waits for
readiness, checks the published port and shows the enrollment code once. Without a terminal (CI,
cloud-init) it takes all answers as options; the portability test passes these options too
(16-operations.md, 3.1 and 11.1):
curl -fsSL https://github.com/OWNER/bothy/releases/download/v0.0.0/install.sh \
| sh -s -- --yes --public-url https://chat.example.com --edge direct --scan on --storage s3 \
--owner-handle alex --owner-name Alex
TS_AUTHKEY for the edge tailscale comes from a hidden prompt or, without a terminal, from the
environment, never from an option. The target folder is --dir (default ./bothy); the stack
then runs in bothy/infra/compose (cd bothy/infra/compose && docker compose ps). The wizard
never calls sudo and changes nothing outside its folder; a second run in the same folder keeps
.env and secrets/. With a bundle that is already downloaded (gh release download
v<version>), --bundle bothy-compose-<version>.tar.gz replaces the download; the checksum
check stays.
Whoever wants to check the script before running it downloads it with SHA256SUMS and the
signature and checks both like any release artifact (release.md, section 5); then
sh install.sh. The wizard does not create supplied secrets (push keys, access to the off-site
target, alert URLs). They go into secrets/ as in the manual path (section 2, item 3); then
docker compose up -d in the target folder.
2. Preparation (manual path)¶
- Get the repository state of the desired version (tag
server/vX.Y.Z) and change intoinfra/compose/. - Copy
.env.exampleto.envand set:BOTHY_REGISTRY,BOTHY_VERSION,BOTHY_PUBLIC_URL,COMPOSE_PROFILES,BOTHY_BACKUP_OFFSITE_TARGET. - Create the directory
secrets/(chmod 700) and place the supplied secrets there (configuration.md 5): at leastoffsite/, for pushapns_key.p8andfcm_service_account.json, for alerts the three webhook URLs, for telemetry the keys of the chosen overlay. - Check:
docker compose config --quiet. - Profile
directonly:docker compose up --no-start(creates networks and containers, starts nothing), read the gateway according to section 5.1 and setBOTHY_TRUSTED_PROXIES=<gateway>/32in.env(IPv6/128). The start in section 3 recreates the containers with the new value. Without this step, the server rejects every request over the published port (section 5.1).
3. Start¶
docker compose up -d
Order that Compose enforces: secrets-init (once), postgres, migrate
(once; allowed on the empty database without a backup), then api, worker, backup,
otel-collector and the profiles.
First owner (only via a server command, Baseline 3):
docker compose exec api bothy setup bootstrap-owner --handle <handle> --display-name "<Name>"
The command prints an enrollment code once; with it, the owner redeems their first device in the
app (08-identity.md, section 4). If all owners later lose their
devices, docker compose exec api bothy setup issue-enrollment-code --user <handle>
issues a code for recovery. Access to the host is the root of trust of the instance
(16-operations.md, section 4.3): shell and container runtime only for
people who could also be owners.
4. Check after the start¶
| Check | Command or place | Expectation |
|---|---|---|
| Services | docker compose ps |
all healthy or running; secrets-init, migrate with exit 0 |
| Readiness | docker compose exec api bothy healthcheck --url http://127.0.0.1:8081/readyz |
exit 0 |
| Secrets | output of secrets-init |
fingerprints; escrow notice followed |
| First backup | docker compose exec backup bothy-backupd run --now full |
run succeeded in the admin console |
| First restore test | docker compose exec backup bothy-backupd run --now restore-test |
all checks green |
| Status page | app, Status section | all components operational |
5. Edge¶
5.1 Profile direct¶
The server is reachable on 127.0.0.1:BOTHY_HTTP_PUBLISH_PORT of the host. A reverse proxy
terminates TLS and forwards, including WebSocket upgrades on /api/v1/gateway. The server does
not see the proxy as the peer, but the gateway of the Compose network. Its address
belongs in BOTHY_TRUSTED_PROXIES (sets server.trusted_proxies):
docker network inspect <project>_default --format '{{(index .IPAM.Config 0).Gateway}}'
Without this entry, the server rejects every plaintext connection (SEC-TLS-1), and rate limits per client IP are wrong. Login: OIDC or local accounts; header identity is disabled in this profile.
5.2 Profile tailscale¶
See tailscale.md.
5.3 Several api nodes (overlay)¶
The overlay infra/compose/compose.multi-node.yaml runs two api nodes, api and api-2, behind
an example edge (nginx in the image nginxinc/nginx-unprivileged, tag BOTHY_NGINX_VERSION):
docker compose -f compose.yaml -f compose.multi-node.yaml up -d
- Profile
directonly. The profiletailscaleruns one api node, becausetailscale servehas no hash rule; with it the overlay starts neitheredgenorapi-2. - Only
edgepublishes a port, on127.0.0.1:BOTHY_HTTP_PUBLISH_PORT; the api nodes publish none. Your TLS reverse proxy stays in front ofedgeand forwards to that port as in 5.1. edgeroutes/api/v1/terminal/sessions/{session_id}/streamand/api/v1/hosts/sessions/{session_id}/streamby consistent hashing over the session ID to the same api node, where the relay pairs them. A side that lands on another node closes afterterminal.open_timeoutwith4102and counts inbothy.terminal.relay.unpaired; check that metric after changing the edge.- The server sees the address of
edge, which appends toX-Forwarded-Forthe address it sees for your proxy, the gateway of the network.BOTHY_TRUSTED_PROXIEStherefore names the subnet of the network<project>_default, not only its gateway:
docker network inspect <project>_default --format '{{(index .IPAM.Config 0).Subnet}}'
edgerestarts when an api node is recreated (for example by an upgrade), because nginx resolves the node names only at start.- Both api nodes share the key store volume
signing-keys(ADR 0066) and run withcluster.mode = multi, as the worker does. What crosses the nodes and how failures show is in multi-node.md. - The idle timeouts of
edgefollow the defaults ofterminal.idle_timeoutplusterminal.idle_graceon the stream paths and ofterminal.host_heartbeat_intervaltimesterminal.host_heartbeat_miss_limitelsewhere. If you change these keys, changeinfra/compose/edge/nginx.confand the timeouts of your own proxy with them.
6. Mac notes¶
- Docker runs in a VM; volumes live on its disk. The VM needs enough memory and disk; both can only be changed while the VM is stopped.
- With Colima, the VM does not start by itself after a restart of the Mac if no service
starts it. The instance operator sets up the autostart (for example
brew services start colima[assumed, not checked]). - Sleep interrupts everything. A server Mac needs "Prevent automatic sleeping".
7. Update and remove¶
Update: upgrade.md. Remove without data: docker compose down. With all
data: docker compose down --volumes; check a final backup and the secrets bundle first,
because afterwards the database, files and local backups are gone.
8. Moving from Discord¶
An installation that takes over an existing Discord server with agent bots follows discord-migration.md: dry run, pilot, parallel operation and switch-over channel by channel.