Technical Administration

Operation, configuration, backup, updates and troubleshooting

Audience: Technical administration

Overview

This part is for everyone who runs the oPBWeb Docker stack: host, containers, configuration files, secrets, backup and updates. You do not need a role in oPBWeb for this work, but you do need access to the Docker host and the project directory.

Run all commands in the project directory on the Docker host. The settings an administrator makes in the user interface (config servers, depot stages, users, object storage cleanup) are described in Functional Administration . The initial setup is covered in Installation .

Chapters

  1. Architecture – services, networks, data flow, object storage layout, worker and jobs, how the manual is served.
  2. Configuration – variables in .env, secrets, TLS and COOKIE_SECURE, ports, time limits, image overrides and settings without an .env variable.
  3. LDAP Sign-in – sign-in via LDAP or Active Directory, mapping groups to roles, CA certificate, troubleshooting.
  4. Operation and Updates – start.sh and stop.sh, containers, logs, health endpoints, updates, versioning, development stack, scheduled tasks.
  5. Backup and Restore – what to back up, commands, order and consistency when restoring.
  6. Troubleshooting – typical symptoms with cause and fix, table of important error messages.
  7. Build and Delivery – images, GitLab CI pipeline, building the manual locally and as PDF.

Architecture

Services, networks, data flow, object storage and background jobs of the oPBWeb stack

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

ServiceContainerImagePurpose
frontendopbweb-frontendopbweb-frontend:latest (nginx:stable-alpine)serves the web interface and the manual, forwards /api to the API; the only service with published ports
apiopbweb-apiopbweb-backend:latest (python:3.13-slim)REST API and WebSocket (FastAPI/uvicorn on port 8000), sign-in, database migrations at startup, first administrator account
workeropbweb-workeropbweb-backend:latestruns all jobs (arq on Redis); the only service that talks to the opsi config servers and decrypts their passwords
dbopbweb-dbmariadb:11.4all application data (packages, versions, users, jobs, audit log, settings)
redisopbweb-redisredis:7-alpinejob queue, worker heartbeat, opsi connection status, revocation list of signed-out tokens, depot locks
garageopbweb-garagedxflrs/garage:v2.2.0S3-compatible object storage (single node, replication factor 1) for file contents and built packages
garage-initopbweb-garage-initopbweb-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

  1. db, redis and garage start and report readiness through their health checks.
  2. garage-init waits for the Garage admin API, creates missing resources and writes the S3 credentials to the volume garage-credentials.
  3. 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 head and creates the first administrator account as long as the user table is empty.
  4. worker starts after the API, writes a first heartbeat and checks the connection to all config servers.
  5. frontend starts after the API and chooses HTTPS or HTTP depending on the certificates.

Networks

NetworkPropertyMembers
frontend-netnormalfrontend, api
backend-netinternal: true (no outside access)api, worker, db, redis, garage
opsi-netnormal, outboundworker

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:

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:

PrefixContentLifetime
blobs/<2 characters>/<sha256>contents of package files, addressed by SHA-256permanent; identical files of several versions or packages are stored only once (deduplication)
artifacts/<productId>/<file name>built packages: .opsi, .md5, .zsyncpermanent; a new build replaces the previous result of the version
uploads/<id>/<file name>uploaded files until they are processedtemporary, deleted after 24 hours
downloads/<job ID>/<file name>files fetched from a depot for downloadtemporary, 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

VolumeServiceContent
mariadb-datadbdatabase files
redis-dataredisqueue and status (AOF persistence)
garage-metagarageobject storage metadata
garage-datagarageobject storage data blocks
garage-credentialsgarage-init, api, workerS3 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:

The worker also runs scheduled tasks:

TaskScheduleEffect
heartbeatevery 30 secondswrites a sign of life to Redis; if it is older than 90 seconds, the system status shows the worker as failed
check_opsievery 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_storagedaily 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:

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 .

Configuration

Variables in .env, secrets, TLS, ports, time limits and image overrides

Audience: Technical administration

Overview

oPBWeb splits its configuration into three levels:

./setup-opbweb.sh creates .env and the missing secrets, see Installation . Afterwards you can edit .env directly or run the script again; the existing values are then the defaults. Changes to .env take effect with the next ./start.sh; Docker Compose then recreates the affected containers.

Variables in .env

The template .env.example contains all variables of the following tables; commented-out lines show the default. If a variable is missing from .env, its default applies. setup-opbweb.sh writes the variables for database, Redis, object storage and CORS_ORIGINS only if they were already in the previous .env; variables the script does not know are kept at the end of the file.

General and sign-in

VariableDefaultServiceMeaning
TZEurope/Berlinalltime zone of the containers, also for the time of the daily cleanup (see Settings → Worker )
LOG_LEVELINFOapi, workerlog verbosity: DEBUG, INFO, WARNING or ERROR. From INFO on, the API logs the uvicorn start, every HTTP request and problems with LDAP sign-in, see Logs
DEBUGfalseapi, workertrue starts uvicorn with --reload (hot reload of the API) and the worker with file watching, and also enables the debug output of httpx and python-opsi; for development only. Outside --dev, setup-opbweb.sh warns if the value is true
OPBWEB_LANGsystem localescriptsLanguage of setup-opbweb.sh, start.sh and scripts/init-secrets.sh: de or en. Set by setup-opbweb.sh (option --lang); if missing, the locale decides (de… → German, otherwise English). Does not affect the oPBWeb user interface
ADMIN_USERNAMEadminapiname of the first administrator account; only applies while the user table is empty
COOKIE_SECUREfalseapi, workersetup-opbweb.sh sets true, because oPBWeb always runs via HTTPS (see TLS and COOKIE_SECURE ); false only in the development stack
CORS_ORIGINS["http://localhost:3100"]apiallowed origins as a JSON list; only needed for the Vite dev server, in production everything comes from the same origin
ACCESS_TOKEN_MINUTES15apivalidity of the access token in minutes (renewed automatically in the browser)
REFRESH_TOKEN_DAYS7apivalidity of the refresh cookie in days, i.e. how long a sign-in lasts without entering the password again
ACCOUNT_MAX_FAILED_ATTEMPTS5apifailed attempts after which a local account is locked for the address they came from. One address may have four times as many failed attempts in total (all accounts, LDAP included); after that the API answers with 429. An account is locked for all addresses only after ten times as many failed attempts.
ACCOUNT_LOCKOUT_MINUTES15apiduration of the locks and period in which failed attempts count
LDAP_*–apisign-in via LDAP/AD, see LDAP Sign-in

opsi and packages

VariableDefaultServiceMeaning
OPSI_TIMEOUT_SECONDS60workertime limit in seconds for all other opsi calls (JSON-RPC, WebDAV listing)
OPSI_INSTALL_TIMEOUT_SECONDS3600workertime limit in seconds for the upload to the repository and for depot_installPackage or depot_uninstallPackage
JOB_TIMEOUT_SECONDS28800workerupper limit in seconds for a whole job, e.g. an installation on several depots including waiting for busy depots
BUILD_COMPRESSIONzstdworkercompression of built .opsi files: zstd (opsi 4.3 default) or gzip

BUILD_COMPRESSION only affects new builds. Packages that are already built keep their compression until they are built again.

Frontend and images

VariableDefaultMeaning
FRONTEND_HTTP_PORT80host port for HTTP
FRONTEND_HTTPS_PORT443host port for HTTPS
BACKEND_IMAGEopbweb-backend:latest (dev: :dev)image for api and worker
FRONTEND_IMAGEopbweb-frontend:latestimage for frontend
GARAGE_INIT_IMAGEopbweb-garage-init:1.0.0image for garage-init
OPBWEB_REGISTRYregistry of the GitLab CI (registry.gitlab.open-mind.space/pages/opsipackagebuilder-web)only for setup-opbweb.sh: prefix from which the script builds the image names (<prefix>/backend:<version> etc.); can also be set as an environment variable when calling the script

If the three image variables are empty, the locally built images apply, see Image overrides and registry mirror .

Database, Redis, object storage

You normally do not need to set these variables. They exist for special cases, such as external services instead of the bundled containers; .env.example lists them commented out.

VariableDefaultMeaning
DB_NAMEopbwebname of the database that MariaDB creates on first start
DB_USERopbwebdatabase user
DATABASE_URLmysql+asyncmy://opbweb@db:3306/opbwebapplication connection (without password; the application inserts the password from the secret)
DATABASE_URL_SYNCmysql+pymysql://opbweb@db:3306/opbwebconnection for the migrations
REDIS_URLredis://redis:6379/0Redis (without password)
S3_ENDPOINTgarage:3900Garage S3 endpoint
S3_BUCKETpackagesbucket; garage-init creates it under this name
S3_SECUREfalseTLS to the S3 endpoint

Secrets

scripts/init-secrets.sh creates missing secrets in the directory secrets/. setup-opbweb.sh and start.sh call the script, start.sh on every start. It never overwrites existing files. Do not share the contents of secrets in tickets or logs.

The secrets/ directory has permissions 750, the files 444; the script sets them again on every run. Docker passes the secret files into the containers with the host permissions, and the processes there run as other users (MariaDB, Redis, the application as appuser with UID 1000). With 600 they could not read the files on a Linux host. The directory with 750 keeps other users of the host out.

FileGenerated asUsed byPurpose
db_password.txtrandom value (hex)db, api, workerpassword of the MariaDB user opbweb
db_root_password.txtrandom value (hex)dbMariaDB root password, also used for dumps
redis_password.txtrandom value (hex)redis, api, workerRedis password
redis.aclfrom redis_password.txtredisRedis ACL file
jwt_secret_key.txtrandom value (hex, 64 characters)apisignature of the sign-in tokens
opsi_key_public.pemRSA key (3072 bit), public partapi, workerencrypts the stored opsi password
opsi_key_private.pemRSA key (3072 bit), private partworkerdecrypts the opsi password; only the worker talks to opsi
settings_encryption_key.txtrandom value (hex, 64 characters)workerAES-256 key of versions before 0.18.6; the worker reads old entries with it and re-encrypts them at start
garage_rpc_secret.txtrandom value (hex, 64 characters)garageGarage RPC secret
garage_admin_token.txtrandom value (hex, 64 characters)garage, garage-inittoken for the Garage admin API
admin_password.txt20 charactersapipassword of the first administrator; shown once when the account is created
ldap_bind_password.txtempty; setup-opbweb.sh writes the password you enter into itapipassword of the LDAP service account

The S3 credentials are not in secrets/. garage-init generates the key in Garage and writes it to the volume garage-credentials.

Changing secrets later

Not every secret can simply be replaced after the first start:

FileConsequence of a change
db_password.txt, db_root_password.txtMariaDB adopts the passwords only on first initialization. After that, a changed file no longer matches the database; you would also have to change the password in MariaDB itself.
redis_password.txtstart.sh creates redis.acl from the new password on the next start and restarts Redis.
jwt_secret_key.txtAll sessions become invalid; everyone has to sign in again.
opsi_key_private.pem, opsi_key_public.pemReplace only as a pair (delete both, init-secrets.sh creates a new pair). The stored opsi passwords can no longer be decrypted afterwards; enter them again under Depots → config server for each config server.
settings_encryption_key.txtOnly needed for entries from versions before 0.18.6; do not change it while such entries still need converting.
garage_rpc_secret.txt, garage_admin_token.txtDo not change; they belong to the existing object storage.
admin_password.txtOnly applies while no user exists. Later, the administrator changes their password in the user interface.
ldap_bind_password.txtNot critical; restart the stack after the change.

The files are read-only (444). To change one, briefly remove the protection and set it again afterwards, for example:

chmod u+w secrets/ldap_bind_password.txt
# edit the file
chmod 444 secrets/ldap_bind_password.txt

You can also enter the LDAP password again with ./setup-opbweb.sh. The services read the secrets at startup. After a change, restart the stack (./stop.sh, then ./start.sh).

TLS and COOKIE_SECURE

On every start, the frontend container decides based on the files in the directory certs/:

Presentnginx serves
certs/fullchain.pem and certs/key.pemHTTPS on container port 443 (host: FRONTEND_HTTPS_PORT), HTTP/2, TLS 1.2 and 1.3; container port 80 (host: FRONTEND_HTTP_PORT) redirects with 301 to https://<host>:<FRONTEND_HTTPS_PORT>
one or both files missingHTTP only on container port 80 (only in the development stack or with a manually created .env)

Outside the development stack, setup-opbweb.sh always provides a certificate: supplied (fullchain.pem and key.pem next to the script), existing or self-signed, see Installation . The container log shows the choice: „TLS-Zertifikat gefunden – HTTPS auf Port 443“ (TLS certificate found – HTTPS on port 443) or „Kein TLS-Zertifikat – HTTP auf Port 80“ (no TLS certificate – HTTP on port 80). fullchain.pem contains the server certificate with all intermediate certificates. The container gets the target port of the redirect as HTTPS_PORT (= FRONTEND_HTTPS_PORT).

With HTTPS, nginx sends the header Strict-Transport-Security (one year). Browsers that have opened oPBWeb via HTTPS once will refuse HTTP for this host name afterwards.

After replacing a certificate (put the new files next to the script and run ./setup-opbweb.sh --updateonly, or replace them directly in certs/), restarting the frontend is enough:

docker restart opbweb-frontend

In addition, nginx lets at most 10 sign-in attempts per minute and address through (briefly 10 more). Both limits require oPBWeb to see the real client address; if another proxy sits in front or Docker hides the addresses, all users share one address.

COOKIE_SECURE controls the Secure flag of the refresh cookie opbweb_refresh (valid for 7 days, httpOnly, SameSite=Strict). If the sign-in arrives over HTTPS, the API always sets the flag, even with false. For the setting:

OperationCOOKIE_SECURE
HTTPStrue
HTTP onlyfalse; with true, the browser does not store the cookie and the session cannot be renewed

Ports

Container portHost portService
80FRONTEND_HTTP_PORT (80)nginx, HTTP
443FRONTEND_HTTPS_PORT (443)nginx, HTTPS

Both ports are always published, but only one of them responds. All other services are not reachable from outside in production. The additional ports of the development stack are listed under Operation and Updates .

Outbound, the worker needs HTTPS on port 4447 to the opsi configserver and the depots.

Time limits and sizes

LimitValueConfigurable
Upload and install/uninstall on the depot3600 sOPSI_INSTALL_TIMEOUT_SECONDS
Other opsi calls60 sOPSI_TIMEOUT_SECONDS
Job runtime8 h (28800 s)JOB_TIMEOUT_SECONDS
Parallel jobs in the worker10no
File manager calls API → worker60 sno
Waiting for an occupied depotup to 6 hno
Upload size through nginx20 GBno (client_max_body_size in frontend/nginx/common.inc)
nginx timeout to the API660 s without data transferno
Download links2 minno
Text files in the editor5 MB (package files), 2 MB (file manager)no

Image overrides and registry mirror

Without overrides, ./start.sh --build builds your own images locally and names them opbweb-backend:latest, opbweb-frontend:latest and opbweb-garage-init:1.0.0. With BACKEND_IMAGE, FRONTEND_IMAGE and GARAGE_INIT_IMAGE you use prebuilt images instead, for example from the GitLab registry (see Build and Delivery ). setup-opbweb.sh sets the three variables when you choose r at the images question; the prefix comes from OPBWEB_REGISTRY. Done manually, it looks like this (example, adapt it to your registry):

BACKEND_IMAGE=registry.example.com/gruppe/opsipackagebuilder-web/backend:<version>
FRONTEND_IMAGE=registry.example.com/gruppe/opsipackagebuilder-web/frontend:<version>
GARAGE_INIT_IMAGE=registry.example.com/gruppe/opsipackagebuilder-web/garage-init:latest

Then pull the images and start without --build:

docker login registry.example.com
docker compose pull
./start.sh

LDAP Sign-in

Set up sign-in via LDAP or Active Directory and map groups to roles

Audience: Technical administration

Overview

In addition to local accounts, oPBWeb can sign users in against an LDAP directory or Active Directory. The role in oPBWeb is then derived from the group memberships in the directory. You configure the connection with LDAP_* variables in .env and the password of the service account in secrets/ldap_bind_password.txt. There are no settings for this in the user interface.

How sign-in works

  1. If oPBWeb has a local account with the entered user name, oPBWeb signs in locally only. The directory is not queried, even if the password is wrong.
  2. Otherwise, with LDAP_ENABLED=true, oPBWeb connects with the service account (LDAP_BIND_DN), or anonymously if no bind DN is set.
  3. oPBWeb searches with LDAP_USER_FILTER below LDAP_USER_SEARCH_BASE (empty: LDAP_BASE_DN). The search must return exactly one entry.
  4. oPBWeb binds as the user with the DN it found and the entered password, which verifies the password.
  5. oPBWeb determines the role from the memberOf attribute (see below). Without a matching group, there is no access.
  6. On the first successful sign-in, oPBWeb creates the account with the source Directory (LDAP). Display name (displayName), email (mail) and role are taken from the directory again on every sign-in.

Local accounts therefore always stay local, including the first administrator account. This way you can still get into oPBWeb if the directory is down.

Variables

VariableDefaultMeaning
LDAP_ENABLEDfalseenable LDAP sign-in
LDAP_HOST–host name of the domain controller or LDAP server
LDAP_PORT636port
LDAP_USE_SSLtrueLDAPS (TLS from connection setup); StartTLS is not supported
LDAP_TLS_VERIFYtrueverify the server certificate
LDAP_CA_CERTS_FILE–CA file inside the container, e.g. /run/certs/ldap/ca.pem
LDAP_BASE_DN–base DN of the directory
LDAP_USER_SEARCH_BASE–search base for users; empty = LDAP_BASE_DN
LDAP_USER_FILTER(&(objectClass=user)(sAMAccountName={username}))search filter; {username} is replaced by the entered name (escaped)
LDAP_BIND_DN–DN of the service account used for the search; empty = anonymous search
LDAP_ADMIN_GROUPS–group DNs for the Administrator role
LDAP_RELEASER_GROUPS–group DNs for the Releaser role
LDAP_PACKAGER_GROUPS–group DNs for the Packager role
LDAP_VIEWER_GROUPS–group DNs for the Viewer role

The service account password is not in .env but in secrets/ldap_bind_password.txt (created empty by init-secrets.sh). The connection has a time limit of 10 seconds for setup and 15 seconds for responses.

Active Directory example

An example for the domain example.local. Replace all names with your values:

LDAP_ENABLED=true
LDAP_HOST=dc01.example.local
LDAP_PORT=636
LDAP_USE_SSL=true
LDAP_TLS_VERIFY=true
LDAP_CA_CERTS_FILE=/run/certs/ldap/ca.pem
LDAP_BASE_DN=dc=example,dc=local
LDAP_USER_SEARCH_BASE=ou=Benutzer,dc=example,dc=local
LDAP_USER_FILTER=(&(objectClass=user)(sAMAccountName={username}))
LDAP_BIND_DN=cn=svc-opbweb,ou=Dienstkonten,dc=example,dc=local
LDAP_ADMIN_GROUPS=cn=opbweb-admins,ou=Gruppen,dc=example,dc=local
LDAP_RELEASER_GROUPS=cn=opbweb-releaser,ou=Gruppen,dc=example,dc=local
LDAP_PACKAGER_GROUPS=cn=opbweb-packager,ou=Gruppen,dc=example,dc=local;cn=client-team,ou=Gruppen,dc=example,dc=local
LDAP_VIEWER_GROUPS=cn=opbweb-leser,ou=Gruppen,dc=example,dc=local

More filters, adapt them to your directory:

PurposeFilter
sign-in with UPN instead of account name(&(objectClass=user)(userPrincipalName={username}))
exclude disabled AD accounts(&(objectClass=user)(sAMAccountName={username})(!(userAccountControl:1.2.840.113556.1.4.803:=2)))
OpenLDAP with uid(&(objectClass=inetOrgPerson)(uid={username})) – requires the memberOf overlay

Setup

  1. Enter the variables in .env as shown above.

  2. Write the service account password to secrets/ldap_bind_password.txt (without a trailing line break is safest; oPBWeb strips leading and trailing whitespace).

  3. If you use your own company CA, place the CA certificate in PEM format as ldap-ca.pem next to setup-opbweb.sh before the setup (the setup moves it to certs/ldap/ldap-ca.pem and proposes it as CA file) or directly in certs/ldap/. The directory certs/ldap/ is mounted read-only in the container as /run/certs/ldap/; LDAP_CA_CERTS_FILE specifies the path inside the container. Without a CA file, oPBWeb verifies against the container’s default CAs.

  4. Restart the stack so that the API reads the new values and the secret:

    ./stop.sh
    ./start.sh
    
  5. Sign in with a directory account. Under Users and roles, the account appears with the source Directory (LDAP) and the assigned role.

Groups and roles

What each role is allowed to do is described in Users and roles .

Behavior of directory accounts

TopicBehavior
Rolecomes from the groups and is set again on every sign-in; cannot be changed in the user interface
Role change in the directorytakes effect only at the next sign-in with password; a running session renews itself for up to 7 days without asking the directory
Passwordis changed in the directory; Change password and Set password are not available for these accounts
Lock after failed attemptsthe account lock applies only to local accounts; for directory accounts, the directory’s lockout policy applies. The limit for failed attempts per address also applies to LDAP sign-ins.
Deactivating in oPBWebpossible; the account can then no longer sign in despite a valid directory password
Revoke access immediatelydeactivate the account under Users and roles; this also ends running sessions

Accounts with the same name: if a local account has the same name as a directory account, the local account always applies.

Troubleshooting

Message at sign-inCauseFix
“Directory service not reachable.”connection, TLS or search with the service account failedcheck the API log (see below): host, port, CA file, bind DN and password
“Wrong user name or password.”wrong password, user not found, or the filter returns more than one entrycheck filter and search base; test sign-in with the same account directly against the directory
“No permission for oPBWeb (no matching group).”sign-in succeeded, but no group matchescompare the group DNs exactly with the user’s memberOf; check direct membership
“Account is disabled.”account deactivated in oPBWebreactivate it under Users and roles

The API writes connection and search errors to its log:

docker logs opbweb-api 2>&1 | grep -i "LDAP search failed"

Typical causes are a host name that does not match the certificate, a missing or wrong CA file (path inside the container, not on the host), an empty secrets/ldap_bind_password.txt, or a domain controller that cannot be reached from the Docker host.

To cross-check the search, use ldapsearch on a machine with the OpenLDAP tools. Example, adapt it to your values:

ldapsearch -H ldaps://dc01.example.local:636 \
  -D "cn=svc-opbweb,ou=Dienstkonten,dc=example,dc=local" -W \
  -b "ou=Benutzer,dc=example,dc=local" \
  "(&(objectClass=user)(sAMAccountName=mmuster))" memberOf displayName mail

You can copy the memberOf values from the output directly into the LDAP_*_GROUPS variables.

Operation and Updates

Starting and stopping the stack, logs, health endpoints, updates, versioning and development stack

Audience: Technical administration

Overview

For day-to-day operation you need two scripts in the project directory: start.sh and stop.sh. This chapter also describes where to find logs and service status, how to update oPBWeb and what to keep in mind on the opsi side. Run all commands in the project directory.

Starting and stopping the stack

start.sh

./start.sh [--build] [--logs] [--dev | --test] [--lang de|en]
OptionEffect
(none)starts the production stack (docker-compose.yml) with the existing images
--buildrebuilds your own images before starting
--logsfollows the logs of all containers after starting (logs -f --tail=50); Ctrl+C only ends the output
--devadds docker-compose.dev.yml (development stack, see below); only in a clone with the sources
--testadds docker-compose.test.yml: two opsi mocks as config servers Test and Production, see Test Environment
--lang de or --lang enlanguage of the messages for this run (otherwise OPBWEB_LANG from .env)
-h, --helpshows the help

Sequence: the script aborts if .env is missing („.env fehlt – bitte ./setup-opbweb.sh ausführen (im Entwickler-Checkout: ./setup-opbweb.sh –dev).“ – .env is missing, please run ./setup-opbweb.sh, in a developer checkout ./setup-opbweb.sh –dev), calls scripts/init-secrets.sh, runs docker compose up -d and shows the status with docker compose ps. Running containers whose configuration has not changed are left untouched. start.sh does not pull new third-party images; for that, see Updating .

stop.sh

./stop.sh

The script has no options. It runs docker compose down with docker-compose.yml and the overlays present (docker-compose.dev.yml, docker-compose.test.yml) and thus stops production, dev and test services alike. Containers and networks are removed; the volumes with database and object storage are kept.

Restarting individual services

docker restart opbweb-worker
docker restart opbweb-frontend   # e.g. after a certificate change

Restarting the worker interrupts running jobs. Check under Jobs beforehand whether installations are currently running.

Containers

ContainerServiceNote
opbweb-frontendnginxweb interface, manual, proxy
opbweb-apiAPIruns the migrations at startup
opbweb-workerworkerjobs, cron, opsi connection
opbweb-dbMariaDB
opbweb-redisRedis
opbweb-garageGarageobject storage
opbweb-garage-initGarage setupruns briefly on every start and exits with exit code 0
opbweb-opsi-mockdev onlysimulated opsiconfd (first opsi system)
opbweb-opsi-mock2dev onlysimulated opsiconfd (second opsi system)
opbweb-frontend-devdev onlyVite dev server
opbweb-manual-devdev onlyHugo preview of the manual
opbweb-launcher-builddev onlybuilds the launcher zips at startup and exits with exit code 0
opbweb-launcher-devdev onlyserves the launcher zips

docker compose ps shows the status (production stack); docker compose ps -a also includes exited containers such as garage-init.

Logs

docker logs -f --tail=100 opbweb-api
docker logs -f --tail=100 opbweb-worker
docker compose logs -f --tail=50          # all services
ContainerWhat you find there
opbweb-apimigrations and uvicorn start, creation of the first admin, HTTP requests (one line per request), problems with LDAP sign-in
opbweb-workerjob execution, opsi check (every 5 minutes by default), errors in connections to opsiconfd
opbweb-frontendchoice of HTTP or HTTPS, requests and nginx errors
opbweb-garage-initsetup of layout, key and bucket

LOG_LEVEL in .env controls the verbosity of api and worker (DEBUG, INFO, WARNING, ERROR; default INFO). A change takes effect with the next ./start.sh.

For LDAP sign-in, the API logs why a sign-in fails: user not found (zero hits for the search filter) and wrong password at level INFO, no group mapped to a role at level WARNING. With LOG_LEVEL=WARNING or ERROR you therefore do not see the first two cases. For example:

docker logs opbweb-api 2>&1 | grep "LDAP login"

python-opsi and the HTTP client httpx only output warnings and errors. Their debug output is enabled only by DEBUG=true; this also starts the API with hot reload and is meant for development only, see Configuration .

The complete log of every job is also available in the user interface under Jobs, see Jobs .

Checking the status

EndpointSign-inResponse
/healthnostatic healthy from nginx; only checks whether nginx responds
/api/healthno{"status":"ok","version":"…"}; the API is reachable through nginx
/api/health/detailsyes (bearer token)checks database, redis, storage, worker, opsi with ok, details and duration
/api/system/databaseyes, adminrows and size per database table
/api/system/storageyes, adminobject storage usage per area, deduplication, unused and missing blobs
/api/system/storage/objectsyes, adminobjects of an area with the package versions using them (basis of the bucket browser)
System status pageyes, all rolesthe same checks as a table, refreshed every 10 seconds

/api/health is suitable for simple monitoring:

curl -fsS https://opbweb.example.local/api/health

The detailed check evaluates as follows (the details follow the interface language):

CheckError if …
Databaseno connection; otherwise the details show the MariaDB version and schema revision
Redisno connection
Object storageno connection or bucket missing (“Bucket ‘…’ is missing”)
Workerno heartbeat (“no sign of life”) or last heartbeat older than 90 seconds
opsi servernot checked yet, no config server configured, or for at least one config server: connection failed, no admin rights, new CA certificate not yet accepted, or expiry warning

Administrators also see the database table sizes, the object storage usage and the bucket browser on the system status page, see System status .

Scheduled tasks

The worker runs these tasks itself; you do not need a cron entry on the host.

TaskScheduleEffect
Heartbeatevery 30 secondssign of life for the system status
opsi checkevery 5 minutes (default)connection, depot sync, CA comparison, expiry warning 60 days in advance
Cleanup of temporary filesdaily at 03:17 (default)deletes uploads/ and downloads/ older than 24 hours

The times of the opsi check and the cleanup are set under Settings → Worker ; they apply without a restart.

The automatic task does not remove unused package files (blobs) or build results. An administrator does that when needed with Clean up … on the System status page: first a dry run, then the confirmed deletion.

Updating

  1. Back up the database and object storage, see Backup and Restore . Database migrations cannot easily be undone.

  2. Check under Jobs that no installations are running.

  3. Get the new version, for example with git pull or with a new stack ZIP unpacked over the installation. The ZIP contains no .env, secrets/ or certs/, they are kept.

  4. Bring .env up to date:

    ./setup-opbweb.sh --updateonly
    

    The script keeps all existing values, adds new variables with their default and creates new secrets; existing secrets stay unchanged. If you want to change values at the same time, run ./setup-opbweb.sh without an option.

  5. Update the images and start the stack:

    • Locally built images:

      ./start.sh --build
      

      --build uses the Docker build cache. Security updates of the base images and the system packages do not necessarily arrive that way. For a security update, build without cache and with fresh base images, then run ./start.sh:

      docker compose build --pull --no-cache
      
    • Images from a registry: Enter the new image versions, either interactively with ./setup-opbweb.sh (questions about the image versions; the default is the previous version) or directly in BACKEND_IMAGE, FRONTEND_IMAGE and GARAGE_INIT_IMAGE in .env. --updateonly keeps the previous versions. With a versions.env next to it (stack ZIP), its versions are the defaults, for --updateonly too; unpacking a new ZIP and running --updateonly thus switches the images to its versions. Then:

      docker compose pull
      ./start.sh
      

      If you use the latest tags, docker compose pull and ./start.sh are enough.

  6. The API runs alembic upgrade head automatically at startup. Check the log afterwards:

    docker logs opbweb-api 2>&1 | head -50
    
  7. In the user interface, check the versions in the status bar (“Frontend …”, “Backend …”) and the System status page.

start.sh does not automatically pull new versions of the third-party images (MariaDB 11.4, Redis 7, Garage v2.2.0) within their tags. Example, run when needed:

docker compose pull db redis garage
./start.sh

To also update the base images of your own images (node, nginx, python), build with docker compose build --pull and then start with ./start.sh. This only applies to locally built images.

Versioning

Backend and frontend have separate version numbers following the scheme MAJOR.MINOR.PATCH (milestone, phase, bug fix). The user interface shows both in the status bar; /api/health returns the backend version.

Versions are set with set-version.sh; the affected files are listed in version.yaml (backend: backend/src/core/version.py and backend/pyproject.toml, frontend: frontend/package.json via npm version).

CallEffect
./set-version.shshows the current versions
./set-version.sh 0.14.0sets all components (with confirmation; --force without)
./set-version.sh --backend 0.13.1sets the backend only
./set-version.sh --frontend 0.13.1sets the frontend only
./set-version.sh --component <name> <version>sets one component from version.yaml
./set-version.sh --config <file>uses a different configuration file

A changed version file on the default branch triggers the build of the matching image in GitLab CI, see Build and Delivery .

opsiconfd: admin-networks

The worker calls opsiconfd with admin rights. If the option admin-networks is set in the opsiconfd configuration, the worker’s source address must be within it; otherwise opsiconfd responds with HTTP 403.

Which address opsiconfd sees depends on where oPBWeb runs (derived from Docker’s network behavior; please verify in the opsiconfd log):

./stop.sh removes the Docker networks; on the next start, opbweb_opsi-net may get a different subnet. Therefore either enter the entire address range from which Docker allocates its networks, or check the subnet after every restart. The opsi manual describes the syntax of the option; opsiconfd must be restarted after the change.

Development stack

The development stack is meant for further development, not for production. In a fresh developer checkout, first create .env (from .env.example, without questions) and then start with locally built images:

./setup-opbweb.sh --dev
./start.sh --dev --build

It adds hot reload (sources as bind mounts, DEBUG=true), two simulated opsiconfd instances, a preview of the manual and the launcher zips to the production stack. API and worker run with the image opbweb-backend:dev.

WhatAddress
Vite dev server (user interface with hot reload)http://localhost:3100
Manual preview (manual-dev, via the Vite proxy)http://localhost:3100/manual/
Launcher zips (launcher-dev, via the Vite proxy)http://localhost:3100/downloads/
API with OpenAPI interfacehttp://localhost:8100/api/docs
nginx frontendport from FRONTEND_HTTP_PORT or FRONTEND_HTTPS_PORT
MariaDBport 3307
Redisport 6380
Garage S3port 3910
opsiconfd mock (opsi-mock)opsi-config.mock and depot1.mock, only in the Docker network
second opsiconfd mock (opsi-mock2)opsi-config2.mock and depot2.mock, only in the Docker network

Each mock simulates a configserver and a depot, including install, uninstall and WebDAV. opbweb-opsi-mock is the first opsi system: as long as no config server is configured, the API registers it at startup as “opsiconfd-Mock” with the stage Test (URL http://opsi-config.mock:4447, user opsiadm, password mock, without TLS verification). opbweb-opsi-mock2 is a second, independent opsi system for tests with several config servers; it is not registered automatically. To use it in the user interface, add it under Depots with + (URL http://opsi-config2.mock:4447, user opsiadm, password mock, TLS verification off).

The end-to-end tests register both mocks as config servers themselves (as “t-Mock 1” and “t-Mock 2”, if not present yet), remove them again afterwards and work only on their depots. They do not assume that the mock is the only or the default config server, and they also run when a real opsi server is configured in addition. The manual preview does not refresh by itself; reload the page after a change.

Backup and Restore

What you need to back up, how to back it up and in which order to restore

Audience: Technical administration

Overview

oPBWeb’s data lives in two places that belong together: the database holds all metadata, the object storage holds the file contents. The database refers to objects in the storage via SHA-256 checksums and keys (blobs/…, artifacts/…). A backup is only usable if both parts come from the same point in time and the matching secrets are included.

What to back up

WhatWhereBack upNote
Databasevolume opbweb_mariadb-datayes, as a dumppackages, versions, users, jobs, audit log, snippets, config servers and depots, settings, encrypted opsi passwords
Object storage metadatavolume opbweb_garage-metayes, with the stack stoppedbucket, keys, object index
Object storage datavolume opbweb_garage-datayes, with the stack stoppedcontents of package files and built packages
Secretssecrets/yeswithout opsi_key_private.pem the opsi password is lost; without the other secrets, database and object storage no longer fit together
Configuration.envyes
Certificatescerts/ (incl. certs/ldap/)yesTLS certificate and key, LDAP CA
Redisvolume opbweb_redis-datanoonly queue and short-lived status
S3 credentialsvolume opbweb_garage-credentialsnogarage-init rewrites them

Also note the oPBWeb version (Git commit or the versions from the status bar). A database can only be used with the same or a newer oPBWeb version.

Creating a backup

To make sure database and object storage have the same state, first stop the services that change data, then back up the database, and then back up the object storage with the stack stopped.

  1. Check under Jobs that no jobs are running or waiting.

  2. Create a protected backup directory:

    mkdir -p -m 700 backup
    
  3. Stop frontend, worker and API. Database and object storage keep running:

    docker stop opbweb-frontend opbweb-worker opbweb-api
    
  4. Back up the database:

    docker exec opbweb-db sh -c 'mariadb-dump -uroot -p"$(cat /run/secrets/db_root_password)" --single-transaction opbweb' > backup/opbweb.sql
    

    The command reads the root password inside the container from the Docker secret; it does not appear on the host’s command line.

  5. Stop the stack:

    ./stop.sh
    
  6. Back up the two Garage volumes (example with an Alpine helper container):

    docker run --rm -v opbweb_garage-meta:/v:ro -v "$PWD/backup":/b alpine tar czf /b/garage-meta.tgz -C /v .
    docker run --rm -v opbweb_garage-data:/v:ro -v "$PWD/backup":/b alpine tar czf /b/garage-data.tgz -C /v .
    
  7. Back up secrets, configuration and certificates:

    tar czf backup/config.tgz secrets .env certs
    
  8. Start the stack again:

    ./start.sh
    

Then transfer the contents of backup/ to your backup target. The downtime depends mainly on the size of the object storage.

If a short interruption of the whole stack is acceptable anyway, you can back up the volume opbweb_mariadb-data with the stack stopped, just like the Garage volumes, instead of creating a dump. In that case, the passwords from secrets/db_password.txt and secrets/db_root_password.txt, which are stored in the volume, must be kept together with the database.

Restoring

The order matters: first secrets and configuration, then the object storage, then the database, and finally the start.

  1. Stop the stack.

    ./stop.sh
    
  2. Provide the program version. Check out the same oPBWeb version that belongs to the backup (or a newer one). If you use images from a registry, their versions are in the backed-up .env (BACKEND_IMAGE etc.).

  3. Restore secrets, .env and certificates. Use the original files from the backup, not newly generated ones:

    tar xzf backup/config.tgz
    chmod 750 secrets && chmod 444 secrets/*
    

    The files need permissions 444 so that the processes in the containers can read them; start.sh sets the permissions via init-secrets.sh on every start anyway.

  4. Restore the object storage. Remove the existing Garage volumes including the S3 credentials and restore the backup:

    docker volume rm opbweb_garage-meta opbweb_garage-data opbweb_garage-credentials
    docker volume create opbweb_garage-meta
    docker volume create opbweb_garage-data
    docker run --rm -v opbweb_garage-meta:/v -v "$PWD/backup":/b alpine tar xzf /b/garage-meta.tgz -C /v
    docker run --rm -v opbweb_garage-data:/v -v "$PWD/backup":/b alpine tar xzf /b/garage-data.tgz -C /v
    

    You must remove the volume garage-credentials so that garage-init rewrites the credentials from the restored key. If an old volume remains, the credentials do not match the object storage. On the next start, the garage-init log shows “Credential files missing, writing from existing key…”.

  5. Restore the database. Remove the database volume, start only the database and import the dump:

    docker volume rm opbweb_mariadb-data
    docker compose up -d db
    docker compose ps db        # wait until the status is "healthy"
    docker exec -i opbweb-db sh -c 'mariadb -uroot -p"$(cat /run/secrets/db_root_password)" opbweb' < backup/opbweb.sql
    

    On the first start with the empty volume, MariaDB creates the database opbweb and the application user with the passwords from secrets/. If you backed up the volume instead of a dump, restore it like the Garage volumes.

  6. Start the stack.

    ./start.sh
    

    The API runs any missing migrations if the program version is newer than the backup. It does not create a first administrator account, because the user table from the backup is populated.

  7. Verify.

    • System status: all services OK. As an administrator, check under Object storage that no missing blobs are reported; missing blobs indicate that database and object storage come from different points in time.
    • Depots → config server: Test connection for each config server. If sign-in fails, opsi_key_private.pem does not match the database; enter the password of the technical user again.
    • Open a package and download a file or a built package.

Jobs that were waiting or running at the time of the backup are not resumed by oPBWeb after the restore. Start them again if needed.

Consistency

SituationConsequence
Database older than object storagesurplus objects; harmless, Clean up … on the system status page removes them
Database newer than object storagemissing blobs or build results; affected files cannot be opened, affected versions must be rebuilt
New key pair opsi_key_*.pemopsi password unreadable; enter it again
New db_password.txt or db_root_password.txt with a backed-up database volumeAPI and worker cannot reach the database
Old volume garage-credentials with a restored object storageaccess to the object storage fails; remove the volume and restart

For regular backups, a fixed order in a script that runs all steps of the section Creating a backup one after another helps.

Troubleshooting

Typical problems with cause and fix, plus an overview of important error messages

Audience: Technical administration

Overview

This chapter sorts typical problems by symptom. For each symptom you find the likely cause and the fix. At the end there is a table of important error messages. Messages of the server follow the interface language; this chapter quotes them as shown in the English interface. Messages of the shell scripts and container logs are German and are quoted with an English translation.

First places to look

SourceContent
System status pagestatus of database, Redis, object storage, worker and opsi server, see System status
Jobs pagecomplete log of every job with error message, see Jobs
docker compose ps -awhich containers are running and which have exited (with exit code)
docker logs opbweb-apistartup, migrations, sign-in, LDAP
docker logs opbweb-workerjobs, opsi connection
docker logs opbweb-garage-initobject storage setup
Status bar of the user interface“Backend not connected” if the API does not respond; orange notice for problems with the opsi server

Stack does not start

SymptomCauseFix
start.sh reports „.env fehlt – bitte ./setup-opbweb.sh ausführen (im Entwickler-Checkout: ./setup-opbweb.sh –dev).“ (.env is missing, run the setup script)no .env./setup-opbweb.sh (or cp .env.example .env)
API does not start, log: “Secret file not found: …”secret file missing, for example because the stack was started with docker compose instead of ./start.shuse ./start.sh or run ./scripts/init-secrets.sh
API exits after about 60 seconds, log: “Database not reachable: …”MariaDB not ready, or the password does not match the volumecheck docker logs opbweb-db; if db_password.txt was changed, see Configuration
api, worker and frontend do not start, garage-init has a non-zero exit codeobject storage setup faileddocker logs opbweb-garage-init; “ERROR: No admin token found” means secrets/garage_admin_token.txt is missing
garage-init keeps reporting “Not ready yet, retrying in 2s…”Garage does not start or the admin token does not matchdocker logs opbweb-garage; do not change the token in secrets/ afterwards
API log: “No users and no ADMIN_PASSWORD configured - no initial admin created”secrets/admin_password.txt is empty or missing while the user table is emptyfill the file or let init-secrets.sh create it, restart the stack

Sign-in

“Wrong user name or password.” For local accounts, every failed attempt counts. After 5 failed attempts from the same address the account is locked for that address for 15 minutes: “Account locked until HH:MM.” The user can still sign in from other addresses; the account is locked for everyone only after 50 failed attempts. After 20 failed attempts from one address (across all accounts) oPBWeb answers “Too many failed sign-ins from this address.” An administrator removes the lock under Users and roles with Unlock; Set password also unlocks the account. See Users and roles .

Password of the only administrator forgotten. There is no tool for resetting it without signing in. Another administrator (local or via an LDAP group) can set the password. So create a second administrator account early on. secrets/admin_password.txt only applies on the very first start.

“Account is disabled.” The account is deactivated in oPBWeb. An administrator reactivates it under Users and roles.

LDAP messages (“Directory service not reachable.”, “No permission for oPBWeb (no matching group).”): see LDAP Sign-in .

Signed out again after reloading the page or after about 15 minutes. The session is renewed via the cookie opbweb_refresh. If COOKIE_SECURE=true is set while oPBWeb is only reachable via HTTP, the browser does not store the cookie. Set COOKIE_SECURE to match the protocol and restart with ./start.sh.

“Session expired.” The token is invalid or was revoked, for example after signing out, after 7 days without renewal, or after secrets/jwt_secret_key.txt was changed. Sign in again.

Connection to the opsi server

The opsi server row on the system status page shows the status, with one entry per config server; in addition, there is the colored dot in front of each config server in the Depots view. The worker checks all config servers every 5 minutes; after a change you can check immediately under Depots → config server with Test connection. The setup is described in Config servers .

SymptomCauseFix
“not checked yet”worker has just startedwait for the next check (default 5 minutes) or use Test connection
“No config server configured (Depots → add config server).”no config server storedadd a config server under Depots with +
“No CA certificate is stored for the opsi server (Depots → config server).”TLS verification without CAFetch from server, compare the fingerprint, Trust, Save
TLS or certificate error despite CAURL with an IP address instead of a host name; the opsiconfd certificate is issued for the FQDNenter the URL with the FQDN, e.g. https://opsi.example.local:4447
“The server delivers a different CA certificate than the stored one (rotation?)”, on the system status page “Server delivers a new CA certificate, please accept it under Depots”opsi has renewed its CAAccept new certificate for the affected config server under Depots; until then, connections fail as soon as the server uses the new CA
“CA certificate expires in N days”warning 60 days before expiryhave the CA renewed on the opsi server, then accept it
“… has no opsi admin rights”, on the system status page “Login without admin rights”technical user is not an opsi adminadd the user to the opsi admin group
HTTP 403 on calls despite correct passwordadmin-networks in opsiconfd excludes the worker’s addresssee admin-networks
“Config server not reachable: …”network, DNS or firewall between worker and port 4447check reachability from the worker (see below)
“A config server with this URL is already configured: …”the same URL is to be added a second timeselect the existing config server in the list
in the job log “Depot … already belongs to config server … and is skipped”the same opsi server is configured under two URLsremove the duplicate config server
Sign-in to the opsi server fails after a restoreopsi_key_private.pem does not match the databaseenter the password of the technical user again

Check reachability from the worker container (example; -k only for this test, because the opsi CA is not known inside the container):

docker exec opbweb-worker curl -sk -o /dev/null -w '%{http_code}\n' https://opsi.example.local:4447/

An HTTP status number shows that name resolution and connection work. An error such as “Could not resolve host” or a timeout points to DNS or the firewall.

Building

Build errors are shown in the log of the Build job under Jobs.

MessageFix
“Script file (action) is missing in CLIENT_DATA.”The script entered in the product data is missing from the package. Upload the file or correct the entry.
“A build is already running for this version.”Wait for the running build to finish.
“The version was changed during the build. Please build again.”Something was saved during the build. Click Build again.
“Version X is released; use “Back to draft” to rebuild.”Released versions are read-only. Use Back to draft or New version.
“Archive could not be created: …”Check the details in the message; often there is no free space on the host or the object storage is not reachable.
“The package cannot be built like this.”The check before building found errors; each error is listed separately in the job log. Correct them and click Build again.

Installing and uninstalling

Symptom or messageCauseFix
Install … is grayed out, or “The version has not been built yet or was changed since the last build. Please build it first.”no current buildBuild again
“The depot depot (stage) requires the role Releaser.”production depot, Packager roleask a Releaser, or check the depot stage, see Depot management
“Depot depot is disabled or no longer exists.”depot deactivated in oPBWeb or no longer reported by the configserverSync under Depots, activate the depot if needed
“Installation failed on: …”opsiconfd error during upload or depot_installPackagecheck the job log and the opsiconfd log
Product marked as locked on the depotan aborted installation left the lock behindunder Depots → Installed products, select it and click Unlock (n), or in the install dialog use Force installation, even if the product is locked
“[depot] Depot has been busy with another job for too long.”another job has held the depot for more than 6 hourscheck the running job under Jobs; if the worker hangs, docker restart opbweb-worker
Upload of large packages abortstime limit for upload and installation reachedincrease OPSI_INSTALL_TIMEOUT_SECONDS; with many depots also JOB_TIMEOUT_SECONDS
Job ends with “Cancelled: job time limit exceeded or worker stopped.”job ran longer than JOB_TIMEOUT_SECONDS or the worker was restartedraise the limit or start the job again; a version released by it and installed nowhere is a draft again
Version is released although the installation failedoPBWeb releases the version when the installation job startsintended; for changes use Back to draft or New version

Worker not reachable

Symptoms: On the system status page, the Worker row reports “no sign of life” or “last sign of life N s ago”; jobs stay in the waiting state; the file manager under Depots reports “Worker not reachable.” or “The worker does not respond.”

Causes and fix:

  1. Check whether the container is running: docker compose ps worker.
  2. Read the log: docker logs --tail=200 opbweb-worker. Common causes are missing secrets, no connection to Redis, or an error at startup.
  3. Restart the worker: docker restart opbweb-worker.
  4. “The worker does not respond.” can also mean that the worker is busy. It runs at most 10 jobs at the same time; file manager calls wait at most 60 seconds. Wait for running jobs to finish and try again.

Object storage

SymptomCauseFix
System status, object storage: “Bucket ‘packages’ is missing”garage-init did not run or did not succeeddocker logs opbweb-garage-init, then ./start.sh
Access to the object storage denied, for example after a restoreS3 credentials in the volume garage-credentials do not match the key in Garage./stop.sh, docker volume rm opbweb_garage-credentials, ./start.sh
Files cannot be opened, system status reports missing blobsdatabase and object storage come from different points in timerestore a consistent backup, see Backup and Restore ; rebuild affected versions
Disk space on the host is running lowold blobs and build results of deleted packagesClean up … on the System status page; check volume usage with docker system df -v

Uploads and time limits

SymptomCauseFix
Upload aborts with HTTP 413file larger than 20 GB (client_max_body_size)make the file smaller or get it into the package another way, for example via the workbench
“Network error while uploading”connection between browser and server dropped, or a proxy in between has its own limitcheck upstream proxies and firewalls for size and time limits
Long requests abort after about 11 minutesnginx timeout of 660 seconds without data transfer to the APIlong operations run as jobs; set similar timeouts on your own proxies in front

The worker deletes uploaded files that were not processed after 24 hours.

HTTPS and certificates

SymptomCauseFix
HTTPS does not respond, only HTTPcerts/fullchain.pem or certs/key.pem is missing or misnamedcheck the file names; the opbweb-frontend log shows „Kein TLS-Zertifikat – HTTP auf Port 80“ (no TLS certificate – HTTP on port 80)
Frontend does not start after a certificate changecertificate and key do not match or are damageddocker logs opbweb-frontend; check the pair
New certificate is not servednginx reads certificates only at startupdocker restart opbweb-frontend
HTTP no longer works in the browser after switching back from HTTPSHSTS header (one year) stored in the browserstay with HTTPS or delete the HSTS entry in the browser
Signed out after reloadCOOKIE_SECURE does not match the protocolsee Sign-in

Important error messages

MessageMeaningFix
Not signed in.no token sentsign in again
Session expired.token invalid or revokedsign in again
Account not available.account deleted or deactivatedask an administrator
No permission for this action.role is insufficient (HTTP 403)check the role
Wrong user name or password.wrong credentials or unknown usercheck the input; lock after 5 failed attempts from the same address
Too many failed sign-ins from this address.20 failed attempts from one address within 15 minuteswait
Account locked until HH:MM.local lock after failed attemptswait or Unlock
Account is disabled.account deactivated in oPBWebactivate the account
Directory service not reachable.LDAP connection or search failedLDAP Sign-in
No permission for oPBWeb (no matching group).LDAP sign-in without a matching groupcheck the group mapping
The last active administrator cannot be removed. / … cannot be deleted.protection of the last administratorcreate another administrator first
Version X is released (installed) and read-only. …editing a released versionBack to draft or New version
Script file (action) is missing in CLIENT_DATA.script missing during buildadd the file
The version has not been built yet or was changed since the last build. Please build it first.installing without a current buildBuild
The depot depot (stage) requires the role Releaser/Packager.depot stage rulecheck the role or the depot stage
[depot] Depot has been busy with another job for too long.depot lock longer than 6 hcheck for a hanging job or worker
Installation failed on: …opsiconfd errorjob log, opsiconfd log
Config server not reachable: …network to the opsi servercheck reachability from the worker
No config server configured (Depots → add config server).no config server configuredadd a config server under Depots
No CA certificate is stored for the opsi server (Depots → config server).CA missingFetch from server
A config server with this URL is already configured: nameduplicate URLuse the existing config server
The server delivers a different CA certificate than the stored one (rotation?)CA rotationAccept new certificate
user has no opsi admin rightstechnical user without admin rightsgrant the rights in opsi
{method}: HTTP {code} …opsiconfd refuses (text from opsiconfd, not translated); for 403 usually admin-networksadmin-networks
Worker not reachable. / The worker does not respond.worker stopped or busyWorker not reachable
Download link invalid or expired.link older than 2 minutesstart the download again
Deleting requires a dry run. / The dry run is missing or older than one hour. Please check again.cleanup without a valid dry runstart a new dry run
Imports or builds are running. Please start again after they have finished.cleanup blockedstart again later
Unexpected error: …unhandled error in the jobcheck the job log and the worker log
Secret file not found: path (API log)secret file missing at startup./start.sh or init-secrets.sh
Database not reachable: … (API log)database not reachable after 60 sopbweb-db log

Build and Delivery

Building images, GitLab CI pipeline, container scanning and generating the manual as website and PDF

Audience: Technical administration

Overview

oPBWeb consists of three images of its own: backend (for api and worker), frontend (nginx with user interface and manual) and garage-init. You either build them locally with ./start.sh --build or let GitLab CI build them and push them to the project registry. The CI also generates the manual as PDF. This chapter describes the images, the pipeline and how to build the manual locally.

Images

All Dockerfiles use the project directory as build context. Among other things, .dockerignore keeps secrets/, certs/, .env, node_modules/ as well as the Hugo output and manual/.doc-state/ out of the context.

Backend

backend/Dockerfile, one image for api and worker:

StageContent
basepython:3.13-slim, system packages (including msitools for MSI information), Python dependencies from requirements.txt, plus python-opsi directly from the GitHub tag 4.3.32.1 (build argument PYTHON_OPSI_REF), because it is not on PyPI
developmentadditional development tools; in the dev stack, the sources come in as a bind mount
productionwithout compiler and Git, application under /app, runs as user appuser (UID 1000), port 8000

Locally, this produces opbweb-backend:latest (production) or opbweb-backend:dev (dev stack).

Frontend

frontend/Dockerfile:

StageContent
developmentnode:22-alpine with the Vite dev server (dev stack only)
buildernode:22-alpine; npm ci and npm run build produce the Vue user interface
manualhugomods/hugo:exts-0.154.5; builds this manual from manual/ with hugo --minify --baseURL /manual/
launchergolang:1.24-alpine; builds the launcher for opsi-setup-detector (tools/opb-launcher/build.sh), one zip file per platform
productionnginx:stable-alpine (with apk upgrade), nginx configuration from frontend/nginx/, user interface under /usr/share/nginx/html, manual under /usr/share/nginx/html/manual, launcher zips under /usr/share/nginx/html/downloads (address /downloads/)

The manual is therefore an integral part of the frontend image. A change to the manual appears in the web interface only with a newly built frontend image.

garage-init

garage/Dockerfile.garage-init: alpine:3.21 with bash, jq and curl plus the script garage/init-garage.sh. Locally, the image is called opbweb-garage-init:1.0.0. The version is fixed and only changes when the Dockerfile or the script changes.

Network access during the build

For a build, the host needs access to Docker Hub (base images), GitHub (python-opsi, Hugo modules), PyPI, the npm registry and the Go module proxy proxy.golang.org (Docsy). The launcher needs no third-party Go modules.

GitLab CI pipeline

The pipeline is defined in .gitlab-ci.yml. The jobs run on runners with the tag dind and build with Docker-in-Docker.

Stages and jobs

StageJobResult
buildbuild-backendbackend:<backend version> and backend:latest
buildbuild-frontendfrontend:<frontend version> and frontend:latest (with manual)
buildbuild-garage-initgarage-init:1.0.0, garage-init:latest and garage-init:<short commit SHA>
scancontainer_scanningvulnerability report for backend:<version>
scancontainer_scanning-frontendvulnerability report for frontend:<version>
releasebuild-manualsPDFs of the manual and the release “manuals-latest”
releasebuild-launcherlauncher zips as generic package opb-launcher/<launcher version> (after go vet and go test)
releasestack-releasestack ZIP oPBWeb-Stack-<pipeline no>.zip as generic package stack/<pipeline no> and release “stack-latest”

The pipeline reads the versions from backend/src/core/version.py and frontend/package.json. The images end up in the project registry under <registry>/<project path>/backend, …/frontend and …/garage-init (path in lowercase). The registry address is set as CI_REGISTRY in .gitlab-ci.yml.

Stack ZIP

The job stack-release packs everything a Docker host needs to set up and start with the registry images into oPBWeb-Stack-<pipeline no>.zip, without sources:

ContentPurpose
setup-opbweb.sh, start.sh, stop.sh, scripts/init-secrets.shsetup, start and stop
docker-compose.yml, garage/garage.tomlthe stack
versions.envregistry prefix and image versions of this pipeline; setup-opbweb.sh takes them as defaults
.env.exampleoverview of all variables (the setup does not need it)
docker-compose.test.yml, tools/opsi-mock/, START-TEST-ENVIRONMENT_de.md/_en.mdtest environment with two opsi mocks
QUICKSTART_de.md/_en.md, README.mdshort guide to putting it into operation

docker-compose.dev.yml is left out on purpose: the development stack mounts the sources. Without sources, setup-opbweb.sh offers no local build and start.sh --dev stops with a message. The ZIP is stored in the generic package stack/<pipeline no> of the project registry and linked from the release oPBWeb Stack (tag stack-latest), which every run recreates.

Using the images from the registry

The target host then does not need to build the images. In ./setup-opbweb.sh, choose r at the images question and enter the registry prefix (default: the registry of this CI, a different one via OPBWEB_REGISTRY) and the versions you want. The script writes BACKEND_IMAGE, FRONTEND_IMAGE and GARAGE_INIT_IMAGE to .env. Then pull the images (sign in with docker login first if the registry requires it) with docker compose pull and start with ./start.sh without --build. Details are in Installation and Configuration .

When each job runs

On the default branch of the project (set in GitLab under Settings → Repository, e.g. master or main), the jobs start automatically when their triggers change:

JobTrigger on the default branch
build-backend, container_scanningbackend/src/core/version.py
build-frontend, container_scanning-frontendfrontend/package.json
build-garage-initgarage/Dockerfile.garage-init, garage/init-garage.sh
build-manualsmanual/content/, manual/hugo.toml, manual/layouts/, manual/assets/, manual/static/, manual/scripts/, manual/build-pdf.sh
build-launchertools/opb-launcher/version.json
stack-releasebackend or frontend version, docker-compose.yml, docker-compose.test.yml, setup-opbweb.sh, start.sh, stop.sh, scripts/init-secrets.sh, garage/garage.toml, tools/opsi-mock/opsi_mock.py, START-TEST-ENVIRONMENT_*.md, QUICKSTART_*.md

A new backend or frontend image is therefore typically created by setting a new version with set-version.sh, see Operation and Updates . A change to the manual alone produces new PDFs but no new frontend image; for that you need a new frontend version or a manual run of build-frontend.

Starting the pipeline manually

When you start a pipeline manually in GitLab, you choose what should run with the input run_job:

run_jobEffect
all (default)each job follows its rules for the default branch (see above); on other branches the pipeline stays empty
build-coreall three image builds
security-scanboth container scans
build-backendbackend image only
build-frontendfrontend image only
build-garage-initgarage-init image only
build-manualsmanual PDFs only
build-launcherlauncher zips only
stack-releasestack ZIP only

The scans check the image with the current version number from the registry. It must therefore have been built and pushed beforehand.

Container scanning

The scan jobs use the GitLab template Jobs/Container-Scanning.gitlab-ci.yml with the threshold HIGH. The reports are available as job artifacts (gl-container-scanning-report-backend.json, gl-container-scanning-report-frontend.json) and appear in GitLab’s security reports.

Manual PDFs

The job build-manuals runs in the image python:3, installs WeasyPrint, Hugo extended version 0.154.5 (variable HUGO_VERSION), Go, Node.js and the GitLab release-cli, and then calls manual/build-pdf.sh output. It produces one PDF file per part and language:

PartGermanEnglish
InstallationoPBWeb-Installation-<Version>.pdfoPBWeb-Installation-Guide-<Version>.pdf
User GuideoPBWeb-Benutzerhandbuch-<Version>.pdfoPBWeb-User-Guide-<Version>.pdf
Functional AdministrationoPBWeb-Fachl-Administration-<Version>.pdfoPBWeb-Functional-Administration-<Version>.pdf
Technical AdministrationoPBWeb-Techn-Administration-<Version>.pdfoPBWeb-Technical-Administration-<Version>.pdf

<Version> is the frontend version. The job uploads the PDFs to the generic package manuals/latest in the project registry; it deletes files with the same name first. It then recreates the release manuals-latest („oPBWeb Handbücher“ – oPBWeb manuals) with one link per PDF („DE: Installation“, „EN: User Guide“ and so on) and the build date, pipeline number and frontend version in the description. In addition, the PDFs are kept as job artifacts for 30 days.

Building the manual locally

The sources are in manual/ (Hugo with the Docsy theme as a Hugo module). German texts are in manual/content/de/, English texts in manual/content/en/, images under manual/static/images/<language>/. You do not need to install anything on the host; all methods run in containers.

Preview in the dev stack

The development stack starts the service manual-dev (hugo server in the image hugomods/hugo:exts-0.154.5). You reach the preview via the Vite proxy:

http://localhost:3100/manual/

Live reload is disabled; reload the page after a change. If there are problems, docker logs opbweb-manual-dev helps. The dev stack is described in Operation and Updates .

Building the website as in the image

The frontend image builds the manual itself. To check it without building an image, you can run the same command in a container (example):

docker run --rm -v "$PWD/manual":/src -w /src hugomods/hugo:exts-0.154.5 \
  sh -c 'npm ci --no-audit --no-fund && hugo --minify --baseURL /manual/'

The result is in manual/public/. The directory is excluded from Git and from the Docker build.

Building the PDFs

manual/build-pdf.sh needs Hugo extended, Go, npm and Python with WeasyPrint. Run it in a container, as the CI does. The script reads the versions from backend/ and frontend/, so mount the whole project directory. Example for an x86-64 host, based on the CI job:

docker run --rm -v "$PWD":/work -w /work python:3 bash -c '
  pip install --quiet weasyprint &&
  apt-get update -qq && apt-get install -y -qq wget git golang nodejs npm > /dev/null &&
  wget -q https://github.com/gohugoio/hugo/releases/download/v0.154.5/hugo_extended_0.154.5_linux-amd64.deb &&
  dpkg -i hugo_extended_0.154.5_linux-amd64.deb > /dev/null &&
  manual/build-pdf.sh output'

The PDFs are then in output/. On an ARM machine, use the matching Hugo package (linux-arm64) or start the container with --platform linux/amd64. The files created in the container are owned by root.

For PDF printing, Hugo also generates Docsy’s print output; manual/scripts/combine_print_html.py combines it into one HTML file with a title page per part and language, which WeasyPrint converts into a PDF.

Hugo and Docsy versions

Docsy is pinned to v0.15.0 in manual/go.mod, matching Hugo 0.154.5, which the CI uses for the PDFs. Update Docsy only together with a matching Hugo version, and check the website and PDFs afterwards.