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.