Architecture
Audience: Technical administration
Overview
oPBWeb runs as a single Docker Compose project named opbweb. The file docker-compose.yml describes production operation; docker-compose.dev.yml extends it for development. This chapter shows which services exist, how they are connected and where each kind of data is stored.
Services
| Service | Container | Image | Purpose |
|---|---|---|---|
| frontend | opbweb-frontend | opbweb-frontend:latest (nginx:stable-alpine) | serves the web interface and the manual, forwards /api to the API; the only service with published ports |
| api | opbweb-api | opbweb-backend:latest (python:3.13-slim) | REST API and WebSocket (FastAPI/uvicorn on port 8000), sign-in, database migrations at startup, first administrator account |
| worker | opbweb-worker | opbweb-backend:latest | runs all jobs (arq on Redis); the only service that talks to the opsi config servers and decrypts their passwords |
| db | opbweb-db | mariadb:11.4 | all application data (packages, versions, users, jobs, audit log, settings) |
| redis | opbweb-redis | redis:7-alpine | job queue, worker heartbeat, opsi connection status, revocation list of signed-out tokens, depot locks |
| garage | opbweb-garage | dxflrs/garage:v2.2.0 | S3-compatible object storage (single node, replication factor 1) for file contents and built packages |
| garage-init | opbweb-garage-init | opbweb-garage-init:1.0.0 (alpine:3.21) | sets up the layout, the access key opbweb and the bucket packages on every start, then exits |
API and worker use the same image; the worker just starts with a different command (./start-worker.sh). The garage-init image carries the :dev tag in production as well.
All services except garage-init run with restart: unless-stopped and start automatically after a host reboot.
Startup order
- db, redis and garage start and report readiness through their health checks.
- garage-init waits for the Garage admin API, creates missing resources and writes the S3 credentials to the volume
garage-credentials. - api starts once db and redis are healthy and garage-init has exited successfully. It waits up to 60 seconds for the database, runs
alembic upgrade headand creates the first administrator account as long as the user table is empty. - worker starts after the API, writes a first heartbeat and checks the connection to all config servers.
- frontend starts after the API and chooses HTTPS or HTTP depending on the certificates.
Networks
| Network | Property | Members |
|---|---|---|
frontend-net | normal | frontend, api |
backend-net | internal: true (no outside access) | api, worker, db, redis, garage |
opsi-net | normal, outbound | worker |
Only the two frontend ports (FRONTEND_HTTP_PORT, FRONTEND_HTTPS_PORT) are reachable from outside. Database, Redis and Garage have no published ports in production. The API has no route to the outside; only the worker reaches the opsi config servers and their depots via opsi-net (HTTPS, port 4447). The actual Docker networks carry the project prefix, for example opbweb_opsi-net.
Data flow
Browser
│ HTTPS 443 (or HTTP 80)
▼
┌──────────────────────── frontend (nginx) ────────────────────────┐
│ / web interface (Vue SPA) │
│ /manual/ manual in German, /manual/en/ in English │
│ /health static response "healthy" │
│ /api proxy incl. WebSocket ──────────────┐ │
└──────────────────────────────────────────────────┼───────────────┘
frontend-net ▼
┌──── api :8000 ────┐
│ sign-in, REST, │
│ job events (WS) │
└───┬──────┬─────┬──┘
backend-net (internal) │ │ │ enqueue job
┌─────────────────────────┘ │ ▼
▼ ▼ redis ◄──────┐
db (MariaDB) garage (S3, bucket packages) │
▲ ▲ │
└──────────────┬─────────────────┘ │
│ │
┌── worker ──┐ ─────────────────────────┘
│ arq jobs, │ fetches jobs, heartbeat
│ cron │
└─────┬──────┘
│ opsi-net, HTTPS :4447
▼
opsi configserver and depots (opsiconfd)
JSON-RPC and WebDAV /repository, /workbench, /depot
Typical flows:
- Editing in the user interface: browser → nginx → API → database and object storage. The worker is not involved.
- Long-running operations (import, build, install, depot sync, workbench, file manager copies): the API creates a job in the database and enqueues it in Redis. The worker runs it and writes progress and log. The browser follows the job live via WebSocket at
/api/jobs/{id}/events. - File manager on the depots: the API passes single file operations (list, read, save) directly to the worker and waits up to 60 seconds for the answer. If the worker does not respond, “Worker not reachable.” or “The worker does not respond.” appears.
- Uploads: nginx accepts uploads up to 20 GB and passes them to the API unbuffered. The API stores them in the object storage under
uploads/; a job processes them. - Downloads: the API serves built packages from the object storage through short-lived download links (valid for 2 minutes). Files from a depot are first copied to
downloads/by a job.
Data storage
Database
MariaDB (database opbweb, character set utf8mb4) holds all metadata: users, audit log, products and versions with control data, properties, dependencies, the file list of each version with SHA-256 checksum, build results, jobs with logs, snippets, application settings, the config servers with their connection data and stages, and the depots, each of which is assigned to exactly one config server. The password of the technical opsi user is stored there encrypted for each config server (RSA-OAEP): the API encrypts it with secrets/opsi_key_public.pem, only the worker can decrypt it with secrets/opsi_key_private.pem.
Object storage
Garage stores all file contents in the bucket packages. Objects are organized by prefix:
| Prefix | Content | Lifetime |
|---|---|---|
blobs/<2 characters>/<sha256> | contents of package files, addressed by SHA-256 | permanent; identical files of several versions or packages are stored only once (deduplication) |
artifacts/<productId>/<file name> | built packages: .opsi, .md5, .zsync | permanent; a new build replaces the previous result of the version |
uploads/<id>/<file name> | uploaded files until they are processed | temporary, deleted after 24 hours |
downloads/<job ID>/<file name> | files fetched from a depot for download | temporary, deleted after 24 hours |
Because of deduplication, deleting packages or versions leaves blobs behind that no package file uses any more. An administrator removes them deliberately with Clean up … on the System status page.
Physically, the data lives in two Docker volumes: garage-meta (metadata in LMDB, keys, bucket) and garage-data (data blocks). The prefixes above are object keys in the bucket, not directories in the volume. The layout capacity of 100 GB set by garage-init is a weighting for Garage, not a quota; what counts is the free space on the host.
Volumes
| Volume | Service | Content |
|---|---|---|
mariadb-data | db | database files |
redis-data | redis | queue and status (AOF persistence) |
garage-meta | garage | object storage metadata |
garage-data | garage | object storage data blocks |
garage-credentials | garage-init, api, worker | S3 credentials, written by garage-init |
Docker creates the volumes with the prefix opbweb_, for example opbweb_mariadb-data. Which of them to back up is described in Backup and Restore
.
Worker and jobs
The worker processes the jobs from Redis:
- up to 10 jobs at the same time, so that short file manager calls do not wait behind long installations,
- time limit for jobs: 8 hours (
JOB_TIMEOUT_SECONDS), 3600 seconds for the other worker functions, - jobs for the same depot run one after another. A depot lock in Redis ensures this; a job waits up to 6 hours for it and otherwise aborts with “Depot has been busy with another job for too long.”
The worker also runs scheduled tasks:
| Task | Schedule | Effect |
|---|---|---|
heartbeat | every 30 seconds | writes a sign of life to Redis; if it is older than 90 seconds, the system status shows the worker as failed |
check_opsi | every 5 minutes (configurable) | checks every configured config server (sign-in, admin rights, opsi version, depots), syncs the depots of every reachable server, compares the CA certificate and warns 60 days before it expires |
cleanup_storage | daily at 03:17 (configurable) | deletes objects under uploads/ and downloads/ that are older than 24 hours |
The time of day follows TZ (default Europe/Berlin). The automatic cleanup does not delete blobs or build results.
Manual
This manual is part of the frontend image. During the build, a dedicated Hugo stage generates the static website, and nginx serves it:
- German at
/manual/, - English at
/manual/en/.
The book icon in the top bar opens the manual in the language of the user interface in a new tab. The manual is static and reachable without signing in. Changes to the manual appear only with a newly built frontend image, see Build and Delivery .