NetBox¶
Purpose¶
NetBox provides DCIM (data centre infrastructure
management) and IPAM (IP address management): the source of truth for devices, racks, prefixes, and
IP allocations. It runs internal-only in the default namespace: a web frontend and an RQ
background worker (webhooks, scripts, reports, and the daily housekeeping system job).
Design decisions¶
NetBox is one of the few apps here that does not use the bjw-s app-template chart. It uses the
official netbox-chart (oci://ghcr.io/netbox-community/netbox-chart/netbox) via an
OCIRepository + chartRef, because NetBox's multi-workload topology (web + worker) and its
existingSecret contract are already modelled by the upstream chart.
- Namespace
default, modelled on the in-repopaperlessapp (its closest twin). - PostgreSQL: shared CNPG cluster (
postgres18-rw.database.svc.cluster.local,sslmode=require); bundled Postgres disabled. Thenetboxrole and database are declared as CNPGDatabaseRoleandDatabaseresources inkubernetes/apps/database/cloudnative-pg/tenants/netbox.yaml; no init container and no superuser credential in the app Secret. - Redis: external Dragonfly (
dragonfly.database.svc.cluster.local:6379); the chart's bundled cache (valkey.enabled) is disabled. Separate logical DB indices for the tasks queue (0) vs the cache (1). - Media: a local
ceph-blockPVC consumed viapersistence.existingClaimand backed up by the VolSync component (VOLSYNC_CAPACITY). NetBox uses no object storage; the cluster's S3 tier is Garage (storagenamespace), not Ceph RGW (which was removed). - Auth / exposure: superuser-only first pass (pocket-id OIDC is a fast-follow); internal-only
HTTPRoute on
envoy-internal, never publicly exposed. - Health monitored automatically by the gatus-sidecar (it auto-discovers the HTTPRoute);
reloader.stakater.com/autorolls pods on secret/config change; a ServiceMonitor exposes metrics to kube-prometheus-stack.
Deploy gotchas¶
- One shared
netbox-secretmust carry the chart's EXACT key names. All of the chart's secret references (globalexistingSecret,superuser.existingSecret,externalDatabase/tasksDatabase/cachingDatabaseexistingSecretName) point at the singlenetbox-secret, so that Secret must contain every key the chart projects, spelled exactly as the chart expects: secret_key,email_password(the chart's projectedsecretsvolume requires the key to exist even when email is unused, so set it to an empty string).passwordandapi_token(superuser).db_password(the value ofexternalDatabase.existingSecretKey).tasks_passwordandcache_password(the Dragonfly tasks/caching DB keys).-
These mismatches do not show up in a
flate(local) or Konflate (in-cluster PR render): the manifest is valid. They only fail at runtime as podFailedMount(references non-existent secret key) orCreateContainerConfigError(couldn't find key …). Before wiring the ExternalSecret, render the chart and grep the output for everysecretKeyRefkey:and projected-volumeitems[].key: -
Web and worker share one RWO
mediaPVC, so they must colocate. With a ReadWriteOnceceph-blockclaim, scheduling the pods onto different nodes deadlocks onMulti-Attachduring a rollout. Pin them together with podAffinity (anchor on the worker) or hit a stuck rollout. - The chart's housekeeping CronJob is disabled, deliberately. v4.7.0 removed
manage.py housekeeping, the command that CronJob runs, and moved the work into an internal RQ system job thatmanage.py rqworkerschedules itself, so the worker is now the only component housekeeping needs. The chart has not caught up and still defaults it on, which costs a dailyKubeJobFailed. Do not "fix" this by deploying anrqscheduler— see KB-032, which also covers how the internal job wedges permanently and how to un-wedge it. - Granian web OOMs at the chart's default 4 workers (peaks ~956Mi against a 1Gi limit). Set
GRANIAN_WORKERS=2and give the web pod request 512Mi / limit 1.5Gi. - Django host guarding rejects unlisted Host headers, including kubelet probes.
ALLOWED_HOSTSmust include both the route host(s) and the in-cluster service name (the chart'sallowedHostsIncludesPodIPcovers the pod IP), and CSRF trusted origins must listhttps://netbox.${SECRET_DOMAIN}. Miss this and probes (and the UI) get a 400. SECRET_KEYmust be 50+ characters. Generate it long enough (e.g.openssl rand -base64 60) or NetBox refuses to start.- The 1Password source item lives in the
Talosvault and is created via theopCLI (never the UI), with values generated locally.
Operational notes¶
- Reconcile lives in the
defaultnamespace; itdependsOncloudnative-pg-tenantsanddragonfly-clusterin thedatabasenamespace, so reconcile those first if NetBox shows "dependency not ready".
- Hostname is
netbox.${SECRET_DOMAIN}(internal DNS via external-dns). The HTTPRoute's only hostname sits on theenvoy-internallistener, which is what keeps it internal-only. It does not expose NetBox publicly. - First-boot check: confirm
kubectl -n database get databaserole,database netboxshowsAPPLIED truefor both, then log in as the superuser (admin). - Backups: the
mediaPVC is snapshotted by VolSync. Thenetboxdatabase lives on the shared CNPG cluster and is backed up by CNPG's own path, not VolSync. - Teardown:
prune: trueremoves NetBox's Kubernetes resources on revert, but thenetboxdatabase on the shared CNPG cluster and the 1Password item persist and must be dropped manually for a clean teardown.