Patrimoine runs as a three-container Podman pod (or Docker Compose stack): backend (FastAPI), frontend (nginx serving the SPA and proxying /api/v1/ and /auth/) and docs (nginx serving this documentation site). Data is persisted in a SQLite database file on a bind-mounted volume.
flowchart TD
accTitle: Deployment architecture
accDescr: The browser reaches Traefik over HTTPS, which forwards to the frontend container running nginx. The frontend serves the single-page app and proxies API and auth requests to the FastAPI backend, which stores its SQLite database on a mounted data volume.
Browser["Browser"] -- HTTPS --> Traefik["Traefik (TLS termination)"]
subgraph Pod["Podman Pod / Compose"]
Traefik --> Frontend
subgraph Frontend["frontend (docker.io/library/nginx:1.31-alpine)<br/>port 8080 (public)"]
SPA["/usr/share/nginx/html/ (SPA)"]
ProxyAPI["/api/v1/ -- proxy"]
ProxyAuth["/auth/ -- proxy"]
end
ProxyAPI --> Backend
ProxyAuth --> Backend
subgraph Backend["backend (docker.io/library/python:3.13-alpine)<br/>port 8000 (internal)<br/>FastAPI + uvicorn"]
DBPath["/data/patrimoine.db"]
end
DBPath --> Volume
subgraph Volume["Volume: ./data"]
SQLite["patrimoine.db (SQLite WAL)"]
end
end
arq background worker for the scheduled jobs (daily backup, recurring snapshots, planned-transaction sweep, FX and price refresh, audit-log and webhook-delivery cleanup, delta cache, scheduled reports, alert check); session and token purges and the webhook retry loop stay in the API
redis
REDIS_URL points at the pod loopback with ./podman.sh, or an external Redis
Queue for the arq worker; also read by /metrics for the worker’s job counts
postgres
docker-compose.postgres.yml, or ./podman.sh when DATABASE_URL is a loopback postgresql+asyncpg:// URL
Browser → nginx (port 8080) — The browser sends all requests to the frontend container on port 8080. TLS is terminated by an external reverse proxy (Traefik/Caddy) in production.
Static files — nginx serves SPA assets from /usr/share/nginx/html/ via try_files $uri $uri/ /index.html (SPA fallback).
API proxy — Requests matching /api/v1/ or /auth/ are proxied to BACKEND_URL (http://backend:8000 under Compose, http://127.0.0.1:<BACKEND_PORT> in the podman.sh pod) with X-Real-IP, X-Forwarded-For and X-Forwarded-Proto headers. Rate limiting is per zone: api defaults to 10 r/s (burst 20) and auth to 5 r/s (burst 10), both tunable with FRONTEND_API_RATE_LIMIT / FRONTEND_AUTH_RATE_LIMIT. /health is proxied unlogged and unlimited.
Backend → SQLite — The backend reads and writes to /data/patrimoine.db using async SQLAlchemy (aiosqlite driver). WAL mode and foreign key enforcement are set per connection via an event listener.
Volume persistence — The ./data host directory is bind-mounted to /data in the backend container, persisting the SQLite database across container restarts.
The backend /health endpoint is a dependency-free liveness check (status only, nothing else is exposed publicly) — DB connectivity is verified separately by GET /health/db. The frontend healthcheck confirms nginx is serving.
GET /metrics serves Prometheus metrics. It answers 404 until METRICS_TOKEN (at least 32 characters) is set, requires Authorization: Bearer <token>, and is not proxied by nginx: scrape it on the container network.
All three containers apply the following security measures via docker-compose.yml:
Measure
Configuration
Non-root user
Backend: desordre (UID 10001, GID 10001); Frontend: nginx user
Read-only filesystem
read_only: true on every service
Capability drop
cap_drop: [ALL] — all Linux capabilities removed
No new privileges
security_opt: [no-new-privileges:true]
tmpfs mounts
/tmp on every container; frontend and docs also mount /var/cache/nginx, /var/run and /etc/nginx/conf.d (the entrypoint writes the rendered nginx config there)