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).
backend/container/Containerfile
Section titled “backend/container/Containerfile”| 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:
alembic upgrade head && exec uvicorn app.main:app --host 0.0.0.0 --port "${BACKEND_PORT:-8000}" --proxy-headersMigrations 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.
frontend/container/Containerfile
Section titled “frontend/container/Containerfile”| 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 |
web/container/Containerfile
Section titled “web/container/Containerfile”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. |
Build Trigger
Section titled “Build Trigger”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.
Build Steps
Section titled “Build Steps”- Gate — the workflow’s
testjob calls its matchingci-*.yml(ci-backend.yml,ci-frontend.ymlorci-web.yml); the build only proceeds if it passes. - Registry login —
redhat-actions/podman-login@v2against the registry. - Metadata extraction —
docker/metadata-actionderives semver image tags from the pushed tag:{version},{major}.{minor},{major}. - 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 passesUV_EXTRA_ARGS=--extra postgres --extra redis, so the published backend image includes theasyncpgdriver and theredisclient, which/metricsuses to read the arq worker’s job counts. The same value is theContainerfiledefault, so a plain local build includes both; a build that setsUV_EXTRA_ARGSreplaces it. - Push —
redhat-actions/push-to-registry@v3.
Registry and Image Names
Section titled “Registry and Image Names”| 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.0git.desord.re/eofredj/patrimoine/backend:1.4git.desord.re/eofredj/patrimoine/backend:1The frontend and web images follow the same three-tag pattern under patrimoine/frontend and patrimoine/web respectively.
Secrets
Section titled “Secrets”| 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.