Deployment
July 6, 2026 ยท View on GitHub
Production deployment is tag-driven. Nothing deploys on a push to
main; publishing a git tag does everything.
Flow
-
Merge work into
main(CI builds the Docker image on every push as a smoke test, but does not publish it). -
Create and push a tag, e.g.:
$ git tag 3.3.0 $ git push origin 3.3.0 -
The
release.ymlworkflow then:- builds the image and pushes
ghcr.io/zeecka/aperisolve:<tag>andghcr.io/zeecka/aperisolve:latest; - SSHes to the production server (
PROD_SSH_*secrets), regenerates.envfrom.env.exampleplus the repository's Actions variables/secrets (ports, limits, ads, Sentry, database, ...); - runs
docker compose pull && docker compose down && docker compose up -d(neverdown -v: the volumes hold the database and results).
- builds the image and pushes
The compose stack runs seven services: web (gunicorn), worker (RQ),
cron (RQ cron scheduler driving the retention cleanup), initdb
(one-shot), postgres, redis, plus rqdashboard bound to
localhost:9181 (reach it via SSH tunnelling).
Configuration
All runtime configuration is environment-driven; .env.example documents
every variable. Deploy-time values come from GitHub Actions variables and
secrets โ to change a production setting (e.g. MAX_CONTENT_LENGTH,
ADSENSE_*, SITE_BASE_URL), edit the repository's Actions configuration
and re-deploy with a tag.
Branches
main is the only long-lived branch. The historical prod branch is
obsolete: its hardcoded AdSense snippet and /ads.txt route were replaced
by the GOOGLE_ADS_TXT/CUSTOM_EXTERNAL_SCRIPT/ADSENSE_* environment
variables, and its :prod image tags are no longer built. It can be
deleted; nothing references it.
The old worker/* feature branches (worker/jphide, worker/jsteg,
worket/color_remap, worker/identify_graphicmagick, worker/file) are
fully merged into main (0 commits ahead) and can be deleted.
Local development
$ docker compose -f compose.dev.yml up --build
The dev stack mounts ./aperisolve into the containers and uses
.env.example directly. If you change translatable strings, recompile the
gettext catalogs (pybabel compile -d aperisolve/translations) since the
mounted source shadows the image's compiled catalogs.