docker-compose.yml
Rama main · réplica de lectura del repositorio original.
# Zoomies on a single VM, behind Cloudflare.
#
# 1. cp .env.example .env and fill in the three values it asks for:
# ZOOMIES_EXTERNAL_URL the https address GitHub and you reach this at
# ZOOMIES_ENCRYPTION_KEY openssl rand -base64 32
# DOCKER_GID stat -c '%g' /var/run/docker.sock
# 2. mkdir -p data && sudo chown 65532:65532 data
# sudo mkdir -p /var/lib/zoomies/shared && sudo chown 65532:65532 /var/lib/zoomies/shared
# 3. docker compose up -d
# 4. docker compose logs zoomies | grep 'setup token'
# 5. open ZOOMIES_EXTERNAL_URL, paste that token, and create the first admin
#
# Step 4 is the gate on step 5. This origin is reachable the moment the
# container starts, and an empty database is not proof of ownership: the setup
# token, which only somebody who can read this container's log has, is. It
# changes on every restart and stops being printed once an account exists.
#
# Steps 4 and 5 are for a person. Where nobody is watching -- this file driven
# by a Terraform module or a configuration-management run -- set the
# ZOOMIES_BOOTSTRAP_* lines and the secrets block below instead: the first
# account is created from a file at startup, /readyz reports
# bootstrap_required false once it exists, and nothing has to be read out of
# the log. docs/configuration.md has the details.
#
# Compose refuses to start with any of the three missing, on purpose: each one
# left to a default produces a container that is healthy and useless. A wrong
# external URL sends GitHub's webhooks and its App-creation redirect to the
# wrong site; a missing key means nothing secret can be stored; and a wrong
# DOCKER_GID leaves the agent unable to open the socket, so every pool queues
# for ever. The Hosts page diagnoses that last one and names the line to
# change, but it is quicker not to have to.
#
# Step 2 is not optional, and the chown needs root: handing a directory to
# another uid is not something the docker group lets you do. The database
# lives in ./data next to this file, bind
# mounted into the container, and a bind mount keeps the host directory's
# ownership -- unlike a named volume, it does not inherit anything from the
# image. The container runs as uid 65532, so a ./data owned by anyone else (and
# Docker creates a missing one as root) gives you:
#
# store: connecting to /var/lib/zoomies/zoomies.db: unable to open database
# file (14)
#
# Cloudflare terminates TLS and proxies to this origin over plain HTTP on port
# 80, so the container serves HTTP and Zoomies is told its external URL is
# https. That is what makes the session cookie Secure, the webhook URL correct,
# and the UI's links right.
#
# The controller runs an embedded agent, so this is one container plus the
# runners it creates.
name: zoomies
services:
zoomies:
image: ghcr.io/eyupio/zoomies:latest
# Or build locally:
# build:
# context: .
# dockerfile: deploy/Dockerfile
# target: controller
restart: unless-stopped
# Finish admitted runner creation before replacing the service.
stop_grace_period: 20m
# The embedded agent registers this host under the container's hostname.
# Without one that is the container's random twelve-hex-digit ID, which
# then follows the host into every scheduler reason and problem message.
hostname: zoomies
command: ["controller"]
# Compose already reads .env to interpolate the ${...} below; this passes it
# into the container as well, so any of the ZOOMIES_* settings in
# docs/configuration.md can be set by putting it in .env, with no matching
# line needed here. The explicit environment: entries below still win.
#
# required: false keeps a deployment that supplies its own environment --
# systemd, a secrets manager, CI -- from failing on a missing .env. It needs
# Compose v2.24 or newer; on older versions use `env_file: .env`.
env_file:
- path: .env
required: false
environment:
# How GitHub and your browser reach this controller. Webhooks are
# delivered to $ZOOMIES_EXTERNAL_URL/webhooks/github, the GitHub App
# created from the Installations page redirects back to it, and the
# https here is what makes Zoomies mark its session cookie Secure.
ZOOMIES_EXTERNAL_URL: ${ZOOMIES_EXTERNAL_URL:?set this in .env to the https address this controller is reached at}
# 32 bytes, base64. Generate with: openssl rand -base64 32
# Back it up: without it the stored GitHub App key cannot be decrypted.
ZOOMIES_ENCRYPTION_KEY: ${ZOOMIES_ENCRYPTION_KEY:?set this in .env}
# Inside the container we bind to all interfaces; the published port
# below is what actually controls exposure. TLS is off here because
# Cloudflare terminates it -- Zoomies will say so at startup, and that
# warning is expected in this deployment.
ZOOMIES_BIND: "0.0.0.0:8080"
ZOOMIES_TLS_MODE: "off"
# Without this every audit row and every login rate-limit decision sees
# Cloudflare's address instead of the real client's, so the whole
# internet is throttled as one caller and the audit log is useless.
# Refresh from https://www.cloudflare.com/ips/ when Cloudflare publishes
# a change.
ZOOMIES_TRUSTED_PROXIES: ${ZOOMIES_TRUSTED_PROXIES}
ZOOMIES_DB_PATH: /var/lib/zoomies/zoomies.db
ZOOMIES_AGENT_EMBEDDED: "true"
ZOOMIES_AGENT_BACKEND: docker
ZOOMIES_DOCKER_HOST: unix:///var/run/docker.sock
# Empty on purpose: the agent works its own capacity out from the
# machine it measures, which is the only figure here that has seen it. A
# constant would pin every deployment of this file to the same number of
# slots however large the host is -- and the capacity is no longer only
# a slot count: a pool that leaves its runners' size to the host is
# given the machine divided by it. Set this to override.
ZOOMIES_AGENT_CAPACITY: ${ZOOMIES_AGENT_CAPACITY}
# The network runner containers are attached to: the one declared at the
# bottom of this file. Without this line the agent leaves them on the
# daemon's default bridge, whatever the comment down there promises.
ZOOMIES_AGENT_NETWORK: zoomies
ZOOMIES_WORK_DIR: /var/lib/zoomies/work
ZOOMIES_LOG_FORMAT: json
# The gid group_add below is given, echoed into the container so that
# when it is wrong the Hosts page can say "the container holds 999 and
# the socket is owned by 987; change DOCKER_GID in .env" rather than
# leaving you to work out which file the number came from.
DOCKER_GID: ${DOCKER_GID:?set this in .env to the gid that owns /var/run/docker.sock, which stat -c %g /var/run/docker.sock prints}
# The unattended first account. Unset, the setup token above is the way
# in. Set the username and exactly one of the two files; each is read
# once, on a database with no accounts, and ignored with a warning ever
# after. The token file holds a token you generated
# (openssl rand -hex 32) that becomes a platform API token; the password
# file holds a password for a person's account instead.
# ZOOMIES_BOOTSTRAP_ADMIN: platform
# ZOOMIES_BOOTSTRAP_TOKEN_FILE: /run/secrets/zoomies_bootstrap_token
# ZOOMIES_BOOTSTRAP_PASSWORD_FILE: /run/secrets/zoomies_bootstrap_password
# The file behind it, if you set one. The long syntax matters: compose
# mounts a secret 0444 by default, and Zoomies refuses a credential file
# that group or other can read. The uid is the one the image runs as.
# secrets:
# - source: zoomies_bootstrap_token
# uid: "65532"
# mode: 0400
ports:
# Cloudflare connects here over HTTP. Nothing but Cloudflare should be
# able to: put the origin behind a firewall that allows only Cloudflare's
# ranges, or use a Cloudflare Tunnel and publish no port at all.
- "80:8080"
volumes:
# The database, in ./data beside this file -- so backing Zoomies up is
# copying a directory, and inspecting it does not need `docker volume
# inspect`. See step 2 above: this directory must be owned by 65532.
# (Runner containers keep their scratch space in their own filesystem;
# nothing of theirs lands here.)
- type: bind
source: ./data
target: /var/lib/zoomies
bind:
# Compose would otherwise helpfully create a missing ./data -- as
# root, since the daemon is root -- and hand you error 14 at startup.
# Failing with "bind source path does not exist" is far easier to act
# on than a SQLite error code.
create_host_path: false
# The shared folder: runners' caches, one folder per purpose, and where
# later releases add folders of their own -- which is why it has to be
# owned by 65532 (step 2), and why Zoomies creates what it needs inside
# it as it starts. It is mounted at the path it has on the host, because
# the host's Docker daemon binds its subfolders into runner containers,
# and a path that meant something else inside this container would point
# a runner at a folder the daemon cannot see.
- type: bind
source: /var/lib/zoomies/shared
target: /var/lib/zoomies/shared
bind:
create_host_path: false
# The embedded agent needs the Docker socket to create runner containers.
# This is Zoomies' own access to Docker -- it is NOT the same thing as
# pool.docker_mode=host-socket, which would hand the socket to your jobs.
- /var/run/docker.sock:/var/run/docker.sock
# The image runs as uid 65532; that user must be able to use the socket,
# which means joining the group that owns it. That group is read from the
# socket, not assumed to be called docker: it is 987 on one distribution,
# 999 or 998 on another, and something else again for a rootless daemon.
# A container is given its groups when it is created, so after changing
# DOCKER_GID run `docker compose up -d` again and compose recreates it.
group_add:
- ${DOCKER_GID}
networks: [zoomies]
healthcheck:
test: ["CMD", "/usr/local/bin/zoomies", "healthcheck", "--url", "http://127.0.0.1:8080"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
# The secret the commented bootstrap lines above name, kept out of .env and out
# of `docker inspect`.
# secrets:
# zoomies_bootstrap_token:
# file: ./bootstrap-token
networks:
# Runner containers are attached to this network by the embedded agent, which
# keeps them off the host network and off the default bridge.
zoomies:
name: zoomies