Baumann Works logo
Baumann Works Tools, code, and hardware
Baumann Works

LabHelper

Feb 2026 ·Active ·source

LabHelper

A Django web app for keeping track of what’s in the physical storage boxes in a lab or workshop. Boxes hold things, things are described, pictured, tagged, and linked to datasheets and resources, and everything is searchable from one box. Authentication is handled by Keycloak (OIDC), and the app is packaged for Kubernetes deployment via ArgoCD.

Features

Tech stack

AreaChoice
FrameworkDjango 5.2 (Python 3.13/3.14)
DatabaseSQLite (file-backed, persisted on a PVC in production)
Authmozilla-django-oidc against Keycloak
Static filesWhiteNoise (compressed manifest storage)
ImagesPillow + sorl-thumbnail
Markdownmarkdown rendered then sanitised with bleach
Tree datadjango-mptt (facet/type hierarchies)
WSGI serverGunicorn (3 workers)
DeploymentDocker image → Gitea registry → ArgoCD → Kubernetes (Traefik ingress)

Data model

BoxType ──1:N──> Box ──1:N──> Thing ──M:N──> Tag <──1:N── Facet
                                 │
                                 ├──1:N──> ThingFile
                                 └──1:N──> ThingLink

Local development

# 1. Activate the virtualenv
source .venv/bin/activate

# 2. Apply migrations (SQLite DB lives at data/db.sqlite3)
python manage.py migrate

# 3. Run the dev server
python manage.py runserver              # http://localhost:8000
python manage.py runserver 0.0.0.0:8000 # all interfaces

For local development you’ll typically want DEBUG=True (the default) and to reach the app from an IP inside ALLOWED_CIDR_NETS, which bypasses the login requirement (see Authentication). Otherwise you need a working Keycloak realm configured through the environment variables below.

Common commands

# Database
python manage.py makemigrations boxes   # after editing boxes/models.py
python manage.py migrate
python manage.py showmigrations

# Tests
python manage.py test                   # everything
python manage.py test boxes             # the boxes app only

# Static files (after CSS changes)
python manage.py collectstatic

# Custom management commands
python manage.py list_things            # print all things with box IDs
python manage.py clean_orphaned_files --dry-run    # remove unreferenced files
python manage.py clean_orphaned_images --dry-run   # remove unreferenced images

Configuration

All configuration is read from environment variables (see labhelper/settings.py). Sensible defaults exist for local development; production values come from the Kubernetes ConfigMap (argocd/configmap.yaml) and Secret.

VariablePurposeDefault
DJANGO_SECRET_KEYDjango secret keydev key (do not use in prod)
DEBUGDebug modeTrue
ALLOWED_HOSTSComma-separated allowed hosts*
ALLOWED_CIDR_NETSCIDR ranges that skip login10.0.0.0/16,192.168.0.0/16
CSRF_TRUSTED_ORIGINSTrusted origins for CSRFprod URL + 127.0.0.1:8000
STATIC_URL / MEDIA_URLStatic/media URL prefixes/static/ / /media/
TIME_ZONE, LANGUAGE_CODE, USE_I18N, USE_TZLocalisationUTC / en-us / True
LOGIN_URLWhere unauthenticated users gooidc_authentication_init
LOGIN_REDIRECT_URL / LOGOUT_REDIRECT_URLPost-auth redirectsindex / login
OIDC_OP_BASE_URLKeycloak realm URL (endpoints derived from it)
OIDC_RP_CLIENT_ID / OIDC_RP_CLIENT_SECRETOIDC client credentials
GUNICORN_OPTSExtra gunicorn flags

OIDC_OP_BASE_URL should be the realm URL, e.g. https://sso.example.com/realms/homelab. The individual OIDC endpoints (auth, token, userinfo, JWKS, logout) are derived from it automatically but can each be overridden with their own OIDC_OP_*_ENDPOINT variable.

Authentication

Login is handled by Keycloak through mozilla-django-oidc. Two access paths exist:

  1. OIDC login — the normal path. KeycloakOIDCBackend (labhelper/auth_backend.py) syncs the user’s name, is_staff flag, and Django group membership from Keycloak group claims on every login. The mapping:

    Keycloak groupDjango groupis_staff
    LabHelper AdministratorsLabHelper Administratorsyes
    LabHelper StaffLabHelper Staffno
    LabHelper ViewersLabHelper Viewersno
  2. Trusted networks — every view is wrapped in conditional_login_required (boxes/decorators.py), which skips authentication entirely for clients whose IP falls within ALLOWED_CIDR_NETS. This is what lets the app run open on a trusted LAN while still requiring SSO from outside.

The ModelBackend is retained as a fallback so the Django admin (/admin/) remains reachable for emergency access.

See Keycloak-installation.md for notes on setting up the realm, client, and groups.

Deployment

The app ships as two container images to a Gitea registry (git.baumann.gr/adebaumann/…) and is reconciled onto Kubernetes by ArgoCD from the manifests in argocd/:

Images are not built on every push. The Gitea Actions workflow (.gitea/workflows/build-containers-on-demand.yml) triggers only when argocd/deployment.yaml, the Dockerfile, or data-loader/** change, reads the exact image tag out of the deployment manifest, and builds + pushes only if that tag isn’t already in the registry. So deploying = bumping the image tag in the manifest.

Two helper scripts prepare a release:

# Full deploy: bump BOTH image versions (+0.001) and snapshot the DB into the loader
./scripts/full_deploy.sh

# Partial deploy: bump only the main web container (no DB snapshot)
./scripts/partial_deploy.sh

After running a script, commit and push the changed manifests — Gitea Actions builds the images and ArgoCD rolls them out.

Kubernetes resources (argocd/)

FileResource
deployment.yamlDeployment (web + loader init container) and Service
ingress.yamlTraefik Ingress for labhelper.adebaumann.com
configmap.yamlNon-secret environment configuration
001_pvc.yamlPersistentVolumeClaim for the SQLite DB and media
nfs-pv.yamlNFS-backed PersistentVolume

Secrets (DJANGO_SECRET_KEY, oidc-client-secret) are supplied via a Kubernetes Secret; see k8s-templates/secret.yaml and scripts/deploy_secret.sh. Liveness and readiness probes hit the /health/ endpoint.

Project layout

labhelper/
├── boxes/                  # main app: models, views, forms, templates, static, commands
│   ├── models.py           # BoxType, Box, Facet, Tag, Thing, ThingFile, ThingLink
│   ├── views.py            # all views (function-based)
│   ├── decorators.py       # conditional_login_required
│   └── management/commands # list_things, clean_orphaned_files, clean_orphaned_images
├── labhelper/              # project config
│   ├── settings.py         # env-driven settings
│   ├── urls.py             # URL routing
│   ├── auth_backend.py     # Keycloak OIDC backend + group mapping
│   └── templates/          # base.html, login.html
├── data/                   # SQLite DB + uploaded media (mounted from a PVC in prod)
├── data-loader/            # init-container image that seeds the DB
├── argocd/                 # Kubernetes manifests (ArgoCD-managed)
├── k8s-templates/          # secret templates
├── scripts/                # deploy helpers
├── Dockerfile              # web container build
├── gunicorn.conf.py        # gunicorn configuration
└── manage.py

Notes & gotchas

← ThinkCentre Watchdog Rotary Dial →