Installation with Docker Compose
This page describes the same deployment that Installation with byroctl (recommended) sets
up, but managed by hand with docker compose. Use it if you run your own
Docker environment and prefer to control the files yourself. The Compose
files, the image and the configuration format are identical, so you can adopt
byroctl later.
Prerequisites
Docker Engine 24 or newer with the Compose plugin 2.20 or newer.
A reverse proxy that terminates TLS, or ports 80 and 443 free for the Caddy add-on.
An SMTP server to send mail.
Files
The deployment consists of a base file with the byro services and optional,
purely additive add-on files. They live in the deploy/ directory of the
byro repository and are versioned with every release; always take the files of
the release you install.
The base file starts the web service, the periodic task runner and defines the
manage service for one-off commands:
# byro - base Compose file: the byro services only (web, periodic, manage).
#
# Add-ons are separate, purely additive files selected through COMPOSE_FILE in
# byro.conf (symlinked as .env):
# compose/postgres.yml built-in PostgreSQL (default)
# compose/caddy.yml Caddy reverse proxy with automatic HTTPS
#
# Managed by byroctl - do not edit. Put local changes into
# docker-compose.override.yml, which byroctl never touches.
x-byro: &byro
image: ${BYRO_DEPLOY_IMAGE_REPO:-ghcr.io/byro/byro}:${BYRO_DEPLOY_VERSION:?set BYRO_DEPLOY_VERSION in byro.conf}${BYRO_DEPLOY_IMAGE_DIGEST:+@${BYRO_DEPLOY_IMAGE_DIGEST}}
env_file: byro.conf
volumes:
- ./data:/var/byro/data
extra_hosts:
- "host.docker.internal:host-gateway"
security_opt:
- no-new-privileges:true
services:
web:
<<: *byro
command: web
ports:
- "${BYRO_DEPLOY_BIND:-127.0.0.1}:${BYRO_DEPLOY_PORT:-8345}:8345"
restart: unless-stopped
periodic:
<<: *byro
command: periodic
environment:
# web migrates; periodic only waits for a healthy web service.
BYRO_AUTO_MIGRATE: "false"
# The image health check probes gunicorn, which does not run here.
healthcheck:
disable: true
depends_on:
web:
condition: service_healthy
restart: unless-stopped
# One-off management commands: docker compose run --rm manage <command>
manage:
<<: *byro
profiles: [tools]
entrypoint: ["/usr/local/bin/byro-entrypoint", "manage"]
command: help
healthcheck:
disable: true
restart: "no"
The built-in PostgreSQL is an add-on. Leave it out to use an external database:
# Add-on: built-in PostgreSQL (default). Include it via COMPOSE_FILE in
# byro.conf. To use an external database instead, leave this file out and
# point BYRO_DB_HOST / BYRO_DB_PORT / BYRO_DB_NAME / BYRO_DB_USER / BYRO_DB_PASS
# at your server.
#
# The major version is pinned through BYRO_DEPLOY_POSTGRES_MAJOR and is never
# changed implicitly by an update; a major upgrade is a separate procedure.
# Note: postgres:18+ moved its data directory to /var/lib/postgresql, so the
# volume target below is tied to the pinned major.
services:
db:
image: postgres:${BYRO_DEPLOY_POSTGRES_MAJOR:-17}-alpine
environment:
POSTGRES_DB: ${BYRO_DB_NAME:-byro}
POSTGRES_USER: ${BYRO_DB_USER:-byro}
POSTGRES_PASSWORD: ${BYRO_DB_PASS:?set a database password (BYRO_DB_PASS) in byro.conf}
volumes:
- ./db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${BYRO_DB_USER:-byro} -d ${BYRO_DB_NAME:-byro}"]
interval: 5s
timeout: 5s
retries: 10
security_opt:
- no-new-privileges:true
restart: unless-stopped
# The byro services wait for the built-in database.
web:
depends_on:
db:
condition: service_healthy
periodic:
depends_on:
db:
condition: service_healthy
manage:
depends_on:
db:
condition: service_healthy
Caddy as a reverse proxy with automatic HTTPS is the second add-on:
# Add-on: Caddy reverse proxy with automatic HTTPS (Let's Encrypt).
# Requires ports 80 and 443 to be free on the host and BYRO_SITE_URL to be the
# public URL without a path (e.g. https://byro.example.org). With an http://
# URL Caddy serves plain HTTP (for tests and LANs).
services:
caddy:
image: caddy:2.10
ports:
- "80:80"
- "443:443"
- "443:443/udp"
environment:
BYRO_SITE_URL: ${BYRO_SITE_URL:?set BYRO_SITE_URL in byro.conf}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy/data:/data
- ./caddy/config:/config
depends_on:
- web
security_opt:
- no-new-privileges:true
restart: unless-stopped
web:
environment:
# Caddy terminates TLS and sets X-Forwarded-Proto, so byro may trust it.
# This overrides BYRO_TRUST_PROXY from byro.conf on purpose.
BYRO_TRUST_PROXY: "true"
The third add-on switches the byro services to an image built locally with plugins (see Plugins):
# Add-on: byro with plugins. byroctl puts this file into COMPOSE_FILE while
# plugins/plugins.txt names at least one plugin and removes it when the list
# becomes empty ("byroctl plugin add|remove|update|rebuild").
#
# web, periodic and manage then run an image built locally from
# plugins/Dockerfile, which installs plugins/plugins.txt on top of the pinned
# release image. The image name carries the Compose project and the byro
# version: two installations on one host never share an image, and every byro
# update rebuilds it (byroctl update does that). Managed by byroctl - do not edit.
x-plugins: &plugins
image: ${COMPOSE_PROJECT_NAME:-byro}-plugins:${BYRO_DEPLOY_VERSION:?set BYRO_DEPLOY_VERSION in byro.conf}
build:
context: ./plugins
args:
BYRO_BASE_IMAGE: ${BYRO_DEPLOY_IMAGE_REPO:-ghcr.io/byro/byro}:${BYRO_DEPLOY_VERSION:?set BYRO_DEPLOY_VERSION in byro.conf}${BYRO_DEPLOY_IMAGE_DIGEST:+@${BYRO_DEPLOY_IMAGE_DIGEST}}
services:
web:
<<: *plugins
periodic:
<<: *plugins
manage:
<<: *plugins
That image is built from plugins/Dockerfile and the plugin list
plugins/plugins.txt in the installation directory:
# syntax=docker/dockerfile:1
# Derived byro image with the plugins from plugins.txt.
# Managed by byroctl - do not edit; byroctl replaces it on update.
#
# Build context: the plugins/ directory of the installation (this file,
# plugins.txt, optional local plugin checkouts referenced by relative path).
# docker compose build web (what byroctl plugin add|remove|update|rebuild run)
# docker build --build-arg BYRO_BASE_IMAGE=ghcr.io/byro/byro:vYYYY.M.P@sha256:... plugins/
#
# Stage 1 builds one wheel per line of plugins.txt (git is needed for git+https
# specs); stage 2 installs the wheels into an untouched copy of the base image,
# so git and build tooling never reach the final image. No default for
# BYRO_BASE_IMAGE on purpose: an unset build argument fails the build instead
# of silently deriving from an unpinned image.
ARG BYRO_BASE_IMAGE
FROM ${BYRO_BASE_IMAGE} AS builder
RUN apt-get update \
&& apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY . /build
# Only the plugins themselves; their dependencies are resolved in stage 2
# against the base image. Isolated builds: setuptools is fetched per build (the
# base image ships none) and Django is not importable while building, so plugin
# .po files ship uncompiled; the rebuild in stage 2 compiles them.
RUN pip wheel --no-deps --requirement plugins.txt --wheel-dir /wheels
FROM ${BYRO_BASE_IMAGE}
ARG BYRO_BASE_IMAGE
LABEL org.byro.base-image="${BYRO_BASE_IMAGE}"
COPY --from=builder /wheels /wheels
# The list this image was built from; byroctl reads it as the applied state.
COPY plugins.txt /byro/plugins.txt
# The package set of the base image is the constraint: a plugin may add
# packages, but it can never upgrade, downgrade or replace Django, byro or
# their dependencies; pip fails instead. pip list (not pip freeze) includes the
# editable byro as byro==X, so a plugin's "byro>=..." requirement is checked
# against the installed release. An empty plugins.txt leaves /wheels empty and
# the build still succeeds.
RUN pip list --format=freeze --disable-pip-version-check >/tmp/base-constraints.txt \
&& set -- /wheels/*.whl \
&& if [ -e "$1" ]; then pip install --constraint /tmp/base-constraints.txt "$@"; fi \
&& rm -rf /wheels /tmp/base-constraints.txt
# Plugin translations (byro's plugin-aware rebuild), static files and the
# offline compressor cache. A throw-away BYRO_DATA_DIR keeps the generated
# SECRET_KEY out of the image, exactly as in the base image.
RUN BYRO_DATA_DIR=/tmp/byro-build python -m byro rebuild \
&& rm -rf /tmp/byro-build
All values come from one file, byro.conf, which Docker Compose reads as
.env for interpolation and passes to the containers as env_file:
# byro deployment configuration (KEY=VALUE, one per line, no inline comments).
#
# This single file configures Docker Compose (COMPOSE_*), the deployment
# (BYRO_DEPLOY_*) and byro itself (BYRO_*). It is read by Docker Compose as
# .env (symlink) for variable interpolation and passed to the byro containers
# as env_file. Keep it at mode 0600: it contains passwords.
#
# Quoting: put values that contain $, #, spaces, quotes or backslashes in
# SINGLE quotes, e.g. BYRO_DB_PASS='p$ss #1'. Compose interpolates $VAR inside
# double quotes and unquoted values, and an unquoted " #" starts a comment.
# Inside single quotes write an apostrophe as \' ; backslashes stay as they are.
# byroctl config set applies these rules automatically.
# ---------------------------------------------------------------------------
# Docker Compose
# ---------------------------------------------------------------------------
COMPOSE_PROJECT_NAME=byro
# Which Compose files make up the stack. Add-ons are additive:
# compose/postgres.yml built-in PostgreSQL (remove it to use an external DB)
# compose/caddy.yml Caddy reverse proxy with automatic HTTPS
# Example with Caddy:
# COMPOSE_FILE=docker-compose.yml:compose/postgres.yml:compose/caddy.yml
COMPOSE_FILE=docker-compose.yml:compose/postgres.yml
# ---------------------------------------------------------------------------
# Deployment (ignored by byro itself)
# ---------------------------------------------------------------------------
# The byro release tag these files belong to, e.g. v2026.3.0. byroctl sets it;
# for a manual setup enter the tag you downloaded the files from.
BYRO_DEPLOY_VERSION=
# Image digest pin written by byroctl (sha256:...). Leave empty for a manual
# setup, and clear it whenever you change BYRO_DEPLOY_VERSION by hand.
BYRO_DEPLOY_IMAGE_DIGEST=
# Alternative image repository, e.g. a registry mirror.
#BYRO_DEPLOY_IMAGE_REPO=ghcr.io/byro/byro
# PostgreSQL major version of the built-in database. Never change this on an
# existing installation without migrating the data (see documentation).
BYRO_DEPLOY_POSTGRES_MAJOR=17
# Where the web service listens on the host. Keep 127.0.0.1 behind a reverse proxy.
BYRO_DEPLOY_BIND=127.0.0.1
BYRO_DEPLOY_PORT=8345
BYRO_DEPLOY_WEB_WORKERS=4
# Interval in seconds between two runs of byro's periodic tasks.
BYRO_DEPLOY_PERIODIC_INTERVAL=600
BYRO_DEPLOY_CHANNEL=stable
# ---------------------------------------------------------------------------
# byro: site
# ---------------------------------------------------------------------------
# Public URL without a path. Determines allowed hosts and absolute links.
BYRO_SITE_URL=https://byro.example.org
BYRO_HTTPS=true
# Trust X-Forwarded-Proto from a reverse proxy in front of byro. Set to true
# when you run your own reverse proxy. compose/caddy.yml enables it automatically.
BYRO_TRUST_PROXY=false
BYRO_LANGUAGE_CODE=de
BYRO_TIME_ZONE=Europe/Berlin
# ---------------------------------------------------------------------------
# byro: database
# ---------------------------------------------------------------------------
BYRO_DB_ENGINE=postgresql
# "db" is the built-in PostgreSQL from compose/postgres.yml.
BYRO_DB_HOST=db
BYRO_DB_PORT=5432
BYRO_DB_NAME=byro
BYRO_DB_USER=byro
# Required. The stack refuses to start while this is empty.
BYRO_DB_PASS=
# ---------------------------------------------------------------------------
# byro: outgoing mail
# ---------------------------------------------------------------------------
# For a mail server running on the Docker host use host.docker.internal.
BYRO_MAIL_HOST=
BYRO_MAIL_PORT=587
BYRO_MAIL_USER=
BYRO_MAIL_PASSWORD=
BYRO_MAIL_FROM=byro@example.org
BYRO_MAIL_TLS=true
BYRO_MAIL_SSL=false
# ---------------------------------------------------------------------------
# byro: PGP (mail signing and encryption)
# ---------------------------------------------------------------------------
BYRO_PGP_HOME=/var/byro/data/gnupg
# ---------------------------------------------------------------------------
# byro: OpenID Connect login (optional)
# ---------------------------------------------------------------------------
#BYRO_OIDC_ISSUER_URL=
#BYRO_OIDC_CLIENT_ID=
#BYRO_OIDC_CLIENT_SECRET=
#BYRO_OIDC_ADMIN_GROUP=
#BYRO_OIDC_AUTO_CREATE_ACCOUNT=false
#BYRO_OIDC_USERNAME_FIELD=preferred_username
# ---------------------------------------------------------------------------
# Container behaviour (rarely needed)
# ---------------------------------------------------------------------------
# Run migrations automatically when the web service starts (default true).
#BYRO_AUTO_MIGRATE=true
# Run byro as a different uid/gid, e.g. to read files in data/ with your own
# host user. Must be numeric, not 0 and not already in use inside the image.
#BYRO_UID=1000
#BYRO_GID=1000
Set up
Create a directory, download the files of the release you want (replace the
tag), and link the configuration as .env:
$ sudo mkdir -p /opt/byro && sudo chown "$(id -u):$(id -g)" /opt/byro && cd /opt/byro
$ T=v2026.3.0
$ R="https://raw.githubusercontent.com/byro/byro/$T/deploy"
$ curl -fsSLO "$R/docker-compose.yml"
$ mkdir -p compose && curl -fsSL -o compose/postgres.yml "$R/compose/postgres.yml"
$ curl -fsSL -o compose/caddy.yml "$R/compose/caddy.yml" && curl -fsSLO "$R/Caddyfile"
$ curl -fsSL -o byro.conf "$R/byro.conf.example" && chmod 600 byro.conf && ln -s byro.conf .env
Edit byro.conf:
BYRO_DEPLOY_VERSION: the tag you downloaded, e.g.v2026.3.0. The imageghcr.io/byro/byro:<tag>is pulled from GitHub’s container registry.COMPOSE_FILE:docker-compose.yml:compose/postgres.ymlfor the built-in database, append:compose/caddy.ymlfor Caddy. For an external database leavecompose/postgres.ymlout and fill inBYRO_DB_HOSTand the otherBYRO_DB_*values.BYRO_DB_PASS: a long random password for the built-in database. The stack refuses to start while it is empty.BYRO_SITE_URL,BYRO_HTTPS, language, time zone, mail settings.BYRO_TRUST_PROXY=trueif your own reverse proxy terminates TLS. The Caddy add-on sets it for you.
Put values that contain $, #, spaces, quotes or backslashes in single
quotes; Compose interpolates $ in double quotes and unquoted values.
Then start the stack and create the first administrator:
$ docker compose pull
$ docker compose up -d
$ docker compose run --rm manage createsuperuser
The web service runs the database migrations when it starts; the periodic
service waits until the web service is healthy and then runs byro’s periodic
tasks every ten minutes. byro listens on 127.0.0.1:8345 unless you change
BYRO_DEPLOY_BIND and BYRO_DEPLOY_PORT.
Plugins
To run byro with plugins, download plugins/Dockerfile and
compose/plugins.yml of the same release, list the plugins as pinned pip
requirements and build the derived image:
$ mkdir -p plugins && curl -fsSL -o plugins/Dockerfile "$R/plugins/Dockerfile"
$ curl -fsSL -o compose/plugins.yml "$R/compose/plugins.yml"
$ printf '%s\n' 'byro-finance-import-bank-files @ git+https://github.com/byro/byro-finance-import-bank-files.git@v1.2.0' > plugins/plugins.txt
$ docker compose build web
$ docker compose run --rm manage migrate
$ docker compose up -d
and append :compose/plugins.yml to COMPOSE_FILE before the build. The
image is named <COMPOSE_PROJECT_NAME>-plugins:<BYRO_DEPLOY_VERSION>; rebuild
it after every change to plugins.txt and after every byro update. Details
and the plugin catalog: Plugins.
Everyday operations
$ docker compose ps
$ docker compose logs -f web
$ docker compose run --rm manage <byro command>
$ docker compose up -d apply configuration changes
$ docker compose stop
Updates
Read the release notes of the new version first. Then, in the installation directory:
Dump the database and copy the secret key:
$ docker compose exec -T db pg_dump -Fc -U byro byro > pre-update.dump $ cp data/.secret byro.conf /somewhere/safe/
Download the Compose files of the new release as above (they may have changed) and set
BYRO_DEPLOY_VERSIONto the new tag. ClearBYRO_DEPLOY_IMAGE_DIGESTif you had pinned a digest.Compare
byro.conf.exampleof the new release with yourbyro.confand add new options you need.Pull and restart; the web service applies the migrations:
$ docker compose pull $ docker compose up -d
With plugins, pull the base image explicitly and rebuild the derived image before
up:$ docker pull ghcr.io/byro/byro:<new tag> $ docker compose pull --ignore-buildable $ docker compose build web $ docker compose up -d
Downgrades are not supported: restore the dump and the previous files instead.
Backups
Back up data/ (documents, uploads, keys and .secret), the database
(db/ while stopped, or a pg_dump) and byro.conf. Losing
data/.secret invalidates all sessions and MFA devices.
Custom Compose settings
Keep local changes in a docker-compose.override.yml in the same directory
and add it to COMPOSE_FILE. Do not edit the downloaded files, so that you
can replace them on the next update.