feat: add NetBox Cloudron package (Infrastructure) [#648]

Official-image wrapper of netboxcommunity/netbox v4.6.10
(netbox-docker 5.0.2, Granian), digest-pinned. OIDC via
python-social-auth wired to the platform provider; RQ worker
backgrounded behind a web-port gate; addon waits cover the 30s
upstream DB timeout. Full-stack verified: migrations, Granian,
worker, login page + SSO button. 16/~57.

Detail + verification: https://projects.knownelement.com/issues/648#note-4976
This commit is contained in:
2026-09-06 06:26:54 -05:00
parent ee5344f5a5
commit cead1523ad
11 changed files with 392 additions and 14 deletions
@@ -0,0 +1 @@
repo
@@ -0,0 +1,11 @@
# NetBox operator knobs (all optional; defaults shown)
# Copy values into the Cloudron app's custom env config as needed.
# Django TIME_ZONE (IANA name)
NETBOX_TIME_ZONE=UTC
# Granian web workers (default 4; lower on small installs)
NETBOX_GRANIAN_WORKERS=4
# Expose Prometheus metrics at /metrics
NETBOX_METRICS_ENABLED=false
@@ -0,0 +1,13 @@
# Changelog — NetBox Cloudron Package
## 1.0.0 — 2026-09-06
- Initial package: NetBox 4.6.10 (netboxcommunity/netbox v4.6.10, from
netbox-docker 5.0.2), digest-pinned amd64 manifest
`sha256:5ee780bd…`
- Official-image wrapper; Granian web on :8080, RQ worker backgrounded by
start.sh after migrations complete
- postgresql + redis + localstorage addons; tasks DB 0 / caching DB 1
- Platform OIDC wired via `REMOTE_AUTH_BACKEND` (OpenIdConnectAuth) +
`SOCIAL_AUTH_OIDC_*`; local Django login retained for admin bootstrap
- Persistence: media / reports / scripts / .secret_key under /app/data
@@ -0,0 +1,23 @@
{
"manifestVersion": 2,
"type": "app",
"id": "io.cloudron.netbox",
"title": "NetBox",
"description": "Open-source IPAM and DCIM web application: IP address and prefix management, VLANs, circuits, racks, devices, virtualization, tenancy, plus REST and GraphQL APIs. PostgreSQL stores all state; Redis backs caching and background jobs (webhooks, scripts, reports, scheduled tasks). SSO via the Cloudron OIDC provider; local Django login remains available for admin bootstrap.",
"author": "NetBox Community / NetBox-Docker contributors",
"website": "https://netbox.readthedocs.io/",
"contactEmail": "cloudron@tsys.dev",
"tagline": "IP address management (IPAM) and data center infrastructure management (DCIM)",
"version": "4.6.10",
"healthCheckPath": "/login/",
"httpPort": 8080,
"memoryLimit": 2048,
"addons": {
"localstorage": {},
"postgresql": {},
"redis": {}
},
"mediaLinks": [],
"changelog": "Initial Cloudron package for NetBox 4.6.10 (official-image wrapper of netboxcommunity/netbox v4.6.10, built from netbox-docker 5.0.2, digest pinned). start.sh maps the Cloudron postgresql/redis addons and the platform OIDC provider onto the image's env-driven configuration, persists the Django SECRET_KEY under /app/data, rebinds media/reports/scripts into localstorage, and backgrounds the RQ worker (gated on the web port so migrations finish first). First admin is created via the Cloudron terminal (manage.py createsuperuser); SSO users register without privileges.",
"icon": "file://logo.png"
}
@@ -0,0 +1,60 @@
# NetBox Cloudron Package
#
# NetBox is an open-source IPAM / DCIM web application (Django): circuits,
# devices, racks, IP address management, prefixes, VLANs, virtualization,
# with a REST + GraphQL API. PostgreSQL for storage, Redis for caching and
# background tasks (RQ webhooks/scripts/jobs).
#
# Upstream: https://github.com/netbox-community/netbox (Apache-2.0), v4.6.10
# - Built/published by the separate netbox-docker repo; v4.6.10 images
# come from netbox-docker 5.0.2 (tag alias v4.6.10-5.0.2, pushed
# 2026-09-02). 5.x serves HTTP via Granian on port 8080 (nginx-unit is
# gone since 4.0), runs as user netbox (uid 999, gid 0), and the
# default container is WEB-ONLY: upstream compose runs the RQ worker
# as a separate service. Cloudron is one-container-per-app, so
# start.sh backgrounds `manage.py rqworker` (gated on the web port so
# it only starts once the entrypoint's migrations have finished).
# - The image configuration is env-driven (netbox-docker
# configuration/configuration.py): DB_*, REDIS[_CACHE]_*, SECRET_KEY,
# ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS, REMOTE_AUTH_BACKEND +
# SOCIAL_AUTH_OIDC_* all map straight onto Cloudron addon/platform env.
# - docker-entrypoint.sh waits for the DB, migrates, and creates the
# superuser unless SKIP_SUPERUSER=true (our default; the README covers
# creating the admin via the Cloudron terminal).
#
# Authentication: NATIVE OIDC (preferred). NetBox 4.6 uses python-social-
# auth directly: REMOTE_AUTH_BACKEND=OpenIdConnectAuth + the
# SOCIAL_AUTH_OIDC_{OIDC_ENDPOINT,KEY,SECRET} trio, wired in start.sh from
# the Cloudron platform provider (CLOUDRON_OIDC_*). The local Django login
# form remains available alongside SSO for admin bootstrap; SSO-registered
# users arrive with no privileges (README).
#
# Pattern: official-image wrapper. Image pinned by tag AND digest (amd64
# manifest digest of the v4.6.10 tag, from docker manifest inspect — the
# Hub API's index digest is NOT usable as tag@digest, see JOURNAL #668).
FROM netboxcommunity/netbox:v4.6.10@sha256:5ee780bdc63b5a5f7ad5073e05e265f74d1a1c52ca0057facc9e4279fcfdb8cc
# Persistence rebinds must happen at BUILD time: the container runs as the
# unprivileged netbox user (999) and could not replace these directories
# later. Symlink targets are created by start.sh under /app/data.
USER root
RUN rm -rf /opt/netbox/netbox/media /opt/netbox/netbox/reports /opt/netbox/netbox/scripts \
&& ln -s /app/data/media /opt/netbox/netbox/media \
&& ln -s /app/data/reports /opt/netbox/netbox/reports \
&& ln -s /app/data/scripts /opt/netbox/netbox/scripts
# start.sh (runs as the netbox user): persists the SECRET_KEY, maps the
# Cloudron addon/platform env onto the image's env-driven configuration,
# backgrounds the RQ worker, then execs the upstream entrypoint chain
# (docker-entrypoint.sh launch-netbox.sh) under tini.
# Made executable on the host, not at build time (Cloudron builds hit
# permission errors on RUN chmod).
COPY start.sh /app/start.sh
# Granian binds :: (dual-stack) on 8080; Cloudron proxies TLS here.
EXPOSE 8080
USER 999:0
# Keep upstream's tini as PID 1; start.sh execs the upstream CMD chain.
ENTRYPOINT ["/usr/bin/tini", "--", "/bin/bash", "/app/start.sh"]
@@ -0,0 +1,67 @@
# NetBox — Cloudron Package
[NetBox](https://github.com/netbox-community/netbox) v4.6.10 — open-source
IPAM/DCIM: IP addresses, prefixes, VLANs, sites, racks, devices, circuits,
virtualization, tenancy, REST + GraphQL APIs. Packaged as an
**official-image wrapper** around `netboxcommunity/netbox:v4.6.10`
(built from netbox-docker 5.0.2, served by Granian on :8080), digest-pinned.
## Authentication (auth gate: ✅ OIDC preferred)
NetBox 4.6 authenticates via **python-social-auth** with no legacy
`SOCIAL_AUTH_TYPE` involved:
- `REMOTE_AUTH_BACKEND = social_core.backends.open_id_connect.OpenIdConnectAuth`
- `SOCIAL_AUTH_OIDC_OIDC_ENDPOINT / KEY / SECRET` ← wired by `start.sh`
from the Cloudron platform OIDC provider (`CLOUDRON_OIDC_*`,
manifestVersion 2), scopes `openid profile email`.
Two consequences that matter on first boot:
1. **SSO-registered users are created without privileges.** An admin must
exist first — create it from the Cloudron **terminal**:
```sh
cd /opt/netbox/netbox
python3 manage.py createsuperuser
```
Then log in on `/login/` with the local form (it stays available
alongside the SSO button) and grant groups/permissions as needed.
2. The platform OIDC client must register the standard redirect:
`https://<app-domain>/complete/oidc/` (the Cloudron provider handles
this when the app is installed).
## Runtime layout
| Concern | How |
|---------|-----|
| Database | Cloudron `postgresql` addon (PG 16 ≥ NetBox 4.6's PG 14 floor; PG 15+ required from 4.7) |
| Cache + tasks | Cloudron `redis` addon — one instance, two logical DBs (tasks=0, caching=1, upstream convention) |
| Migrations | Run automatically by the upstream `docker-entrypoint.sh` (waits for PostgreSQL first) |
| RQ worker | Backgrounded by `start.sh`, started only after the web port answers (i.e. post-migration) — webhooks, scripts, reports, scheduled/housekeeping jobs |
| Persistence | `/app/data` (localstorage): `media/` (attachments), `reports/`, `scripts/` (custom code), `.secret_key` |
| SECRET_KEY | Generated once (64 hex chars), persisted; rotating it invalidates sessions |
| First boot | Allow up to ~10-15 min — `migrate` + `reindex --lazy` run before Granian binds :8080 (observed: ~24 min on a 3x-oversubscribed dev box, minutes on an idle host; subsequent boots are fast) |
## Operator knobs (`.env.example`)
| Variable | Default | Purpose |
|----------|---------|---------|
| `NETBOX_TIME_ZONE` | `UTC` | Django `TIME_ZONE` |
| `NETBOX_GRANIAN_WORKERS` | `4` | Web workers (lower on small installs) |
| `NETBOX_METRICS_ENABLED` | `false` | Prometheus endpoint at `/metrics` |
## Upgrades
The image is pinned by tag **and** digest in the `Dockerfile`. To upgrade,
bump both (verify the new amd64 digest with
`docker manifest inspect netboxcommunity/netbox:<tag>`) and rebuild —
migrations run automatically on first boot.
## Package pattern
Official-image wrapper + `start.sh` env mapping — see the repo
`JOURNAL.md` NetBox section for the full write-up (worker-gating trick,
build-time persistence symlinks, OIDC wiring).
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

+106
View File
@@ -0,0 +1,106 @@
#!/bin/bash
set -euo pipefail
# NetBox runtime setup for Cloudron:
# 1. create the persistent data dirs (media / reports / scripts — the
# image paths are build-time symlinks into /app/data)
# 2. persist the Django SECRET_KEY (>= 50 chars; rotating it invalidates
# sessions and encrypted values)
# 3. map the Cloudron addon + platform env onto the image's env-driven
# configuration (netbox-docker configuration/configuration.py):
# DB_* from postgresql, REDIS[_CACHE]_* from redis, OIDC from the
# platform OIDC provider
# 4. background the RQ worker (gated on the web port so it only starts
# after the entrypoint's migrations), then exec the upstream
# entrypoint chain, which waits for the DB, migrates, and hands over
# to Granian on :8080
#
# Authentication: SSO via the Cloudron platform OIDC provider
# (REMOTE_AUTH_BACKEND = OpenIdConnectAuth + SOCIAL_AUTH_OIDC_*). The
# local Django login stays available on /login/ for admin bootstrap;
# SSO-registered users are created WITHOUT privileges (see README).
DATA_DIR="/app/data"
SECRET_FILE="${DATA_DIR}/.secret_key"
# --- 1. persistent data directories -------------------------------------------
mkdir -p "${DATA_DIR}/media" "${DATA_DIR}/reports" "${DATA_DIR}/scripts"
# --- 2. persistent SECRET_KEY --------------------------------------------------
# hex on purpose: 64 chars, comfortably over NetBox's 50-char minimum
if [[ ! -s "${SECRET_FILE}" ]]; then
( umask 077; head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > "${SECRET_FILE}" )
echo "Generated new NetBox secret key"
fi
export SECRET_KEY="$(cat "${SECRET_FILE}")"
# --- 3. Cloudron -> netbox-docker environment -----------------------------------
export DB_HOST="${CLOUDRON_POSTGRESQL_HOST:-127.0.0.1}"
export DB_PORT="${CLOUDRON_POSTGRESQL_PORT:-5432}"
export DB_NAME="${CLOUDRON_POSTGRESQL_DATABASE:-netbox}"
export DB_USER="${CLOUDRON_POSTGRESQL_USERNAME:-netbox}"
export DB_PASSWORD="${CLOUDRON_POSTGRESQL_PASSWORD:-}"
# one Cloudron redis instance, two logical databases (upstream convention:
# tasks on 0, caching on 1)
export REDIS_HOST="${CLOUDRON_REDIS_HOST:-127.0.0.1}"
export REDIS_PORT="${CLOUDRON_REDIS_PORT:-6379}"
export REDIS_PASSWORD="${CLOUDRON_REDIS_PASSWORD:-}"
export REDIS_DATABASE=0
export REDIS_CACHE_HOST="${CLOUDRON_REDIS_HOST:-127.0.0.1}"
export REDIS_CACHE_PORT="${CLOUDRON_REDIS_PORT:-6379}"
export REDIS_CACHE_PASSWORD="${CLOUDRON_REDIS_PASSWORD:-}"
export REDIS_CACHE_DATABASE=1
# hosts / CSRF: Cloudron terminates TLS at the platform proxy
export ALLOWED_HOSTS="${CLOUDRON_APP_DOMAIN:-localhost} localhost"
export CSRF_TRUSTED_ORIGINS="${CLOUDRON_APP_ORIGIN:-http://localhost}"
# first admin is created by the operator via the Cloudron terminal
# (README): the entrypoint's own superuser seeding is skipped
export SKIP_SUPERUSER=true
# operator knobs (see .env.example)
export TIME_ZONE="${NETBOX_TIME_ZONE:-UTC}"
export GRANIAN_WORKERS="${NETBOX_GRANIAN_WORKERS:-4}"
export METRICS_ENABLED="${NETBOX_METRICS_ENABLED:-false}"
# --- 3b. platform OIDC provider -> python-social-auth ---------------------------
# social-core needs the issuer base URL WITH a trailing slash for
# .well-known/openid-configuration discovery
OIDC_ISSUER="${CLOUDRON_OIDC_ISSUER:-}"
OIDC_ISSUER="${OIDC_ISSUER%/}/"
export REMOTE_AUTH_BACKEND='social_core.backends.open_id_connect.OpenIdConnectAuth'
export SOCIAL_AUTH_OIDC_OIDC_ENDPOINT="${OIDC_ISSUER}"
export SOCIAL_AUTH_OIDC_KEY="${CLOUDRON_OIDC_CLIENT_ID:-}"
export SOCIAL_AUTH_OIDC_SECRET="${CLOUDRON_OIDC_CLIENT_SECRET:-}"
export SOCIAL_AUTH_OIDC_SCOPE='openid profile email'
export LOGOUT_REDIRECT_URL="${CLOUDRON_APP_ORIGIN:-/}"
# --- 4. RQ worker + upstream entrypoint -----------------------------------------
# wait for the addons FIRST: netbox-docker's entrypoint gives up on the DB
# after only DB_WAIT_TIMEOUT=30s, which can lose the race with a cold
# Cloudron postgres addon
wait_tcp() {
local host="$1" port="$2" name="$3"
echo "Waiting for ${name} at ${host}:${port} ..."
until (exec 3<>"/dev/tcp/${host}/${port}") 2>/dev/null; do
echo "${name} is unavailable - sleeping"
sleep 2
done
echo "${name} is up"
}
wait_tcp "${DB_HOST}" "${DB_PORT}" "PostgreSQL"
wait_tcp "${REDIS_HOST}" "${REDIS_PORT}" "Redis"
# The worker must not run before the schema exists; Granian answering on
# 8080 is the signal that docker-entrypoint.sh finished migrating.
(
until (exec 3<>/dev/tcp/127.0.0.1/8080) 2>/dev/null; do sleep 2; done
echo "web port is up - starting RQ worker"
exec python3 /opt/netbox/netbox/manage.py rqworker
) &
echo "Starting NetBox (migrations run automatically, then Granian binds :8080) ..."
exec /opt/netbox/docker-entrypoint.sh /opt/netbox/launch-netbox.sh