Skip to content

Docker Images

This guide covers how the backend, frontend and web container images are built and published. All three are multi-stage builds, defined in backend/container/Containerfile, frontend/container/Containerfile and web/container/Containerfile, and built by dedicated Forgejo Actions workflows separate from the CI pipeline (see CI/CD Pipeline).


Stage Base image What it does
builder docker.io/library/python:3.13-alpine Copies in uv (from ghcr.io/astral-sh/uv), runs uv sync --locked --no-install-project --no-dev against just pyproject.toml/uv.lock (dependency layer caching), then copies the full source and runs uv sync --locked --no-dev
Runtime docker.io/library/python:3.13-alpine (fresh) Installs sqlite and curl, creates a non-root UID 10001 desordre user, copies the built /app from the builder stage

The python and uv images are referenced by digest (@sha256:...), so a rebuild cannot silently pick up different base layers; the tag each digest was resolved from is kept in a comment above it. Third-party GitHub Actions in the release workflows are pinned to commit SHAs, with the version tag in a trailing # vX comment; the Forgejo-hosted checkout action is pinned by tag.

Runtime entrypoint:

Terminal window
alembic upgrade head && exec uvicorn app.main:app --host 0.0.0.0 --port "${BACKEND_PORT:-8000}" --proxy-headers

Migrations run automatically on every container start, before uvicorn takes over. This is why a routine update needs no manual migration step — see Updating a Deployment.

Stage Base image What it does
builder docker.io/library/node:24-alpine Build context is the repo root (not frontend/); copies only frontend/ into the builder, runs npm ci then npm run build, producing static assets in dist/
Runtime docker.io/library/nginx:1.31-alpine Serves dist/ as the SPA, runs as the non-root nginx user, docker-entrypoint.sh templates nginx.conf.template at container start

A single image serving one nginx vhost — the merged site, with its landing page at /{locale}/ and the manuals under /{locale}/docs/. Build context is the repo root, with ./backend passed as a named openapi build context so the API reference is generated from backend/openapi.json.

Stage Base image What it does
builder docker.io/library/node:24-alpine Builds web/documentation/: landing page, manuals, and the API reference from backend/openapi.json
Runtime docker.io/library/nginx:1.31-alpine Serves dist/ from /usr/share/nginx/html on the single vhost, as the non-root nginx user. docker-entrypoint.sh copies nginx.conf.template into /etc/nginx/conf.d at container start (that directory is a tmpfs when the container runs read-only). The single vhost is nginx’s default server, so it answers any Host header. Links are fixed at build time and nothing is rewritten at runtime, so the served tree stays read-only.

The three images are built by dedicated workflows — .forgejo/workflows/backend.yml, .forgejo/workflows/frontend.yml and .forgejo/workflows/web.yml — not by any ci-*.yml file, though each calls its matching ci-*.yml first as a pre-publish gate (see CI/CD Pipeline). They trigger on:

  • push: tags: v*
  • Manual workflow_dispatch

A plain push to main does not build or publish an image — only a version tag does (see Releases). Android has no container image — see Releases for its separate APK/SBOM release flow.


  1. Gate — the workflow’s test job calls its matching ci-*.yml (ci-backend.yml, ci-frontend.yml or ci-web.yml); the build only proceeds if it passes.
  2. Registry login — redhat-actions/podman-login@v2 against the registry.
  3. Metadata extraction — docker/metadata-action derives semver image tags from the pushed tag: {version}, {major}.{minor}, {major}.
  4. Build — Buildah via redhat-actions/buildah-build@v3, with layer caching (--cache-from/--cache-to, 168h cache TTL) and OCI image format. The backend build passes UV_EXTRA_ARGS=--extra postgres --extra redis, so the published backend image includes the asyncpg driver and the redis client, which /metrics uses to read the arq worker’s job counts. The same value is the Containerfile default, so a plain local build includes both; a build that sets UV_EXTRA_ARGS replaces it.
  5. Push — redhat-actions/push-to-registry@v3.

Property Value
Registry git.desord.re/eofredj
Backend image patrimoine/backend
Frontend image patrimoine/frontend
Web image patrimoine/web

Example tag shape for backend version 1.4.0:

git.desord.re/eofredj/patrimoine/backend:1.4.0
git.desord.re/eofredj/patrimoine/backend:1.4
git.desord.re/eofredj/patrimoine/backend:1

The frontend and web images follow the same three-tag pattern under patrimoine/frontend and patrimoine/web respectively.


Secret Purpose
PRIVATE_REGISTRY_USERNAME Registry login username
PRIVATE_REGISTRY_PASSWORD Registry login password

Both are Forgejo repository secrets referenced via ${{ secrets.* }} — never hardcoded in the workflow files. Android’s release workflow uses a separate set of secrets for APK signing — see Releases.