Docker Compose¶
The web UI is how a Zoomies fleet is run day to day, and it does not care how
the controller under it was started. This page is for the people who would
rather start it with docker compose up -d than with the installer, because
that is how everything else on the host runs, because the deployment lives in a
repository, or because the installer's questions have already been answered
once and a file remembers them better than a person does.
There are two ways to get a compose deployment, and they meet at the same place.
From the installer¶
zoomies init offers Docker Compose whenever the host has a compose
command, and makes it the default. It writes a docker-compose.yml and a
fully populated .env (external URL, a freshly generated encryption key,
bind address, TLS mode, trusted proxies, backend, capacity, paths, the image
tag, the published port and the host's real docker group id) then brings the
stack up. Every variable carries a one-line comment saying what it is for; the
file is 0600 because it holds your encryption key; and it is written
atomically, so an interrupted install never leaves a half-written file that
compose would then read.
Re-running it over an existing deployment is an upgrade, not a reinstall. It keeps the existing encryption key, because minting a new one would make every stored secret undecryptable, and backs the old file up beside it.
From the repository's file¶
The docker-compose.yml at the repository root is the same deployment without
the installer, set up for a controller behind Cloudflare or another reverse
proxy that terminates TLS:
git clone https://github.com/eyupio/zoomies && cd zoomies
cp .env.example .env
$EDITOR .env # ZOOMIES_EXTERNAL_URL, ZOOMIES_ENCRYPTION_KEY, DOCKER_GID
mkdir -p data && sudo chown 65532:65532 data # the container runs as 65532
docker compose up -d
docker compose logs zoomies | grep 'setup token'
Three values are required, and compose refuses to start with any of them missing, because each one left to a default produces a container that is healthy and useless:
| Variable | What it is | Why it cannot default |
|---|---|---|
ZOOMIES_EXTERNAL_URL |
The https address you and GitHub reach the controller at. | Webhooks are delivered there, the GitHub App's creation flow sends your browser back there, and the https is what makes the session cookie Secure. |
ZOOMIES_ENCRYPTION_KEY |
openssl rand -base64 32. |
Everything secret in the database is sealed with it. Back it up separately from the database; without it the stored App key cannot be read. |
DOCKER_GID |
The gid that owns /var/run/docker.sock: stat -c '%g' /var/run/docker.sock. |
It is not always the group called docker. With the wrong number the container comes up healthy and can start nothing. The Hosts page then says which group it holds and which line to change, and docker compose up -d recreates it. |
If stat prints 0, the socket belongs to root's group, and the answer is
not to put the container in it: give the socket a group of its own
(sudo groupadd docker, then restart the daemon) or use a rootless daemon.
The database lives in ./data beside the compose file, bind mounted, so a
backup is a copy of a directory and inspecting it needs no docker volume
inspect. It has to be owned by uid 65532: a bind mount keeps the host
directory's ownership, the container is not root, and Docker would create a
missing ./data as root, so the file refuses to create it and fails with
"bind source path does not exist", which is easier to act on than SQLite's
error 14.
Every other ZOOMIES_* setting in Configuration can be
put in .env too, with no matching line in the compose file: it is passed
into the container whole. The explicit environment: entries in the file
still win.
What moves to the browser¶
A container keeps its database in a volume nothing outside it can open, so the three things the native installer does on the console happen in the web UI instead, in this order:
- The first account. Open the external URL and paste the setup token from the container's log. The token is what proves the instance is yours: the origin is reachable the moment the container starts, and an empty database is a thing a stranger can find too. It changes on every restart and stops being printed once an account exists.
- GitHub. Installations → Connect GitHub creates the App through the manifest flow, with exactly the permissions Zoomies needs, and takes the private key and webhook secret directly.
- The first pool. Nothing creates one for you on this path, and nothing runs until one exists. Pools → Create a pool opens the pool editor with a name and a label already filled in; the quick start walks the rest.
The Overview repeats these as a checklist that ticks itself off as you go, ending with a test workflow to run once a pool exists.
Open the https address, not http://<ip>. The session cookie is marked
Secure because the external URL is https, a browser on a plain-http page
throws it away, and Zoomies refuses to sign you in from one and says why
rather than signing you in and out in the same second. To test over plain
http for a moment, set ZOOMIES_COOKIE_SECURE=false for the duration.
The proxy in front¶
The file publishes port 80 and serves plain HTTP on it, with
ZOOMIES_TRUSTED_PROXIES=cloudflare so the audit log and the login rate
limiter see real client addresses rather than the proxy's. Zoomies warns at
startup that it is listening without TLS; in this deployment that is expected,
and the warning says so. Only the proxy should be able to reach the origin:
firewall it to Cloudflare's ranges, or use a Cloudflare Tunnel and publish no
port at all. Behind Cloudflare (or any reverse
proxy) has the
three things that go quietly wrong here.
The controller is also a host¶
The compose file runs the controller with an embedded agent, so this is one
container plus the runners it creates: the container holds the Docker socket,
and every pool on the docker backend can place runners on it. That socket is
Zoomies' own access to Docker, for creating runner containers; it is not
reachable from a job unless a pool's docker_mode says so.
ZOOMIES_AGENT_CAPACITY is the host's slot count, and it is the one value in
.env.example worth a second look: the example sets it to 4, which is right
for a four-core box and wrong for most others. Leave it empty and the
agent works its capacity out from the machine it measures, which also sets the
share a pool sized by its host gives each runner; a constant pins every
deployment of the file to the same slot count however large the host is. The
installer's .env leaves it empty for that reason.
Every further host joins the same way it would on any other deployment: Hosts → Add a host hands you one line to paste on the new machine. Adding a host.
Operating it from a terminal¶
The CLI talks to the controller over the API, so it does not care that the controller is in a container:
export ZOOMIES_URL=https://zoomies.example.com
export ZOOMIES_TOKEN=zoo_... # Settings → API tokens, or zoomies tokens create
zoomies status
zoomies pools list
The deployment itself is driven with compose, and zoomies deployment wraps
the same commands using the file and container name recorded at install:
| Task | With compose | With the installer's record |
|---|---|---|
| Logs | docker compose logs -f zoomies |
zoomies logs |
| Restart | docker compose restart zoomies |
zoomies deployment restart |
| Upgrade | docker compose pull && docker compose up -d |
zoomies deployment update, which also pulls the cached runner images and rolls back on failure |
| Stop, keeping the database | docker compose down |
zoomies deployment down |
docker compose down -v deletes the volume with the database in it, and is
the one command here that cannot be undone. Upgrading a container
deployment says what an
upgrade does to work in flight.
zoomies config set and config unset need the controller stopped, on every
deployment: they write the same settings table the Settings page does, and
writing under a process that has already read it would leave the two
disagreeing. On compose that is docker compose stop zoomies first, or, more
simply, use the Settings page, which is what it is for.
What this is not¶
A compose deployment is a controller on a host you own, with that host's Docker daemon running jobs. For a host you do not install on at all (a platform that builds from source and gives a container no Docker socket) see Deploying on a PaaS; for an instance booted from a provider's marketplace image, see One-click deployment. Both deploy a controller; the runner capacity is still yours.