The deploy workflow is now a single job that builds, pushes, and deploys in one runner. Tag computation moved to docker/metadata-action, the per-deploy .env perl rewrite is gone (host pins LIBRENOTES_IMAGE once; main pushes update :main rolling, releases pin to :vX.Y.Z by manual edit), and both jobs run in our bespoke runner image whose runner user already has socket access via group membership. ci.yml moves to the same image so go/make/node are all available without per-step apt installs. Drops compose.prod.yaml (unused, redundant with compose.netcup.yaml).
4.8 KiB
Operations
This document covers deployment, backups, and rollback for the librenot.es production environment. For developer / contributor docs see the top-level README.
Deployment
The repository ships a CI workflow at .gitea/workflows/deploy.yml
that builds and pushes a Docker image on every push to main and
on every vX.Y.Z tag, then SSHes to the deployment host and runs
docker compose pull && up -d.
The workflow is gated on the repository variable
DEPLOY_ENABLED=true. Deployment is opt-in; flipping the variable
disables the workflow without removing the file.
Required secrets
The workflow expects these secrets and variables on the repo:
| Name | Type | Purpose |
|---|---|---|
REGISTRY |
secret | registry hostname, e.g. git.librete.ch |
REGISTRY_USER |
secret | robot account or PAT username with package:write |
REGISTRY_PASS |
secret | robot/PAT token |
DEPLOY_HOST |
secret | user@host SSH target, e.g. root@netcup |
DEPLOY_KEY |
secret | passphrase-less private key (PEM) |
DEPLOY_PATH |
secret | absolute path on host with the compose stack, e.g. /srv/librenotes |
HEALTH_URL |
secret | e.g. https://ln.cloud.librete.ch/healthz |
DEPLOY_ENABLED |
variable | true to enable the workflow |
The image is pushed to ${REGISTRY}/public/librenotes; main pushes
publish :main and :<sha7>, tag pushes publish :vX.Y.Z and
:latest.
Production compose stack
On the deployment host, place compose.yaml and
compose.netcup.yaml from this repo at $DEPLOY_PATH, together
with an .env file containing LIBRENOTES_IMAGE plus the runtime
configuration (JWT secret, SMTP credentials, public base URL).
See .env.netcup.example for the full key list.
Bring it up with:
docker compose -f compose.yaml -f compose.netcup.yaml pull
docker compose -f compose.yaml -f compose.netcup.yaml up -d
Rollback
Tag pushes publish immutable tags (:vX.Y.Z). To pin or roll back,
edit LIBRENOTES_IMAGE in /srv/librenotes/.env and re-run pull +
up. The deployment workflow itself never rewrites the file — failed
health checks abort the workflow and the operator redeploys
manually.
# Example: roll back to v0.1.0
ssh netcup
cd /srv/librenotes
perl -i -pe 's|^LIBRENOTES_IMAGE=.*|LIBRENOTES_IMAGE=git.librete.ch/public/librenotes:v0.1.0|' .env
docker compose -f compose.yaml -f compose.netcup.yaml pull
docker compose -f compose.yaml -f compose.netcup.yaml up -d
Gitea Actions runner
Workflows run on the netcup act_runner (see runner/ stack in
the netcup umbrella). Both ci.yml and deploy.yml declare
container: image: git.librete.ch/libretech/runner-image:v1 —
a bespoke Ubuntu 24.04 image (built and signed by us, hosted on
the same Gitea instance) that bundles git, make, node,
perl, ssh, and a docker CLI. The image's runner user is
pre-joined to docker group gid 998 so the auto-mounted
/var/run/docker.sock is writable without --user root.
The runner config (runner/config.yaml) whitelists
/var/run/docker.sock under valid_volumes to allow the
auto-mount.
Backups
scripts/backup.sh is a self-contained backup driver suitable for
running from cron, a systemd timer, or the supplied
scripts/librenotes-backup.{service,timer} units.
Required tools on the backup host: sqlite3, tar, gzip, and
optionally rclone for off-site copy. On Debian/Ubuntu:
apt install sqlite3 rclone.
What is backed up
- The SQLite database (
$LIBRENOTES_DB) via the SQLite.backupcommand, which produces a consistent online snapshot without needing to stop the application. - The per-tenant note directory (
$LIBRENOTES_DATA_DIR) as a gzipped tar. - An
info.txtin each archive recording the backup timestamp, hostname, and version.
Off-site copy
Set BACKUP_REMOTE to an rclone destination (e.g.
s3:librenotes-backups). When set, the script invokes
rclone copy to upload the archive after creation. Without it,
backups stay on the local disk only.
Retention
scripts/backup-prune.sh enforces the policy "keep 30 daily
- 12 monthly". Run it from the same timer as the backup. Files
keep the form
librenotes-YYYYMMDD-HHMMSS.tar.gzso the prune script can sort and select by name alone.
Restore test
scripts/backup-restore-test.sh picks the most recent archive,
extracts it into a scratch directory, runs sqlite3 .schema on
the database to confirm it is readable, and verifies that the
note tar lists at least one entry. It is wired into a separate
weekly timer so a silent backup-corruption regression cannot hide
indefinitely.
Systemd
Drop scripts/librenotes-backup.{service,timer} into
/etc/systemd/system/, then:
systemctl daemon-reload
systemctl enable --now librenotes-backup.timer
Confirm with:
systemctl list-timers librenotes-backup.timer