From c536a16a2a9ff53eca6a268a93f477bb0da7b39d Mon Sep 17 00:00:00 2001 From: mr-forust Date: Fri, 25 Sep 2026 23:24:21 +0200 Subject: [PATCH] feat(netbox): add enterprise-grade server documenting app w/ shared postgres --- netbox/.env.example | 32 ++++ netbox/README.md | 96 ++++++++++++ netbox/compose.yaml | 137 +++++++++++++++++ netbox/configuration/configuration.py | 48 ++++++ netbox/k8s/certificates.yaml | 28 ++++ netbox/k8s/config.yaml | 21 +++ netbox/k8s/ingress.yaml | 36 +++++ netbox/k8s/namespace.yaml | 4 + netbox/k8s/netbox.yaml | 204 +++++++++++++++++++++++++ netbox/k8s/secrets.yaml.example | 18 +++ netbox/k8s/settings.yaml | 56 +++++++ netbox/k8s/valkey.yaml | 82 ++++++++++ postgres/README.md | 3 +- postgres/initdb/01-create-databases.sh | 2 + postgres/k8s/network-policy.yaml | 3 + postgres/k8s/postgres.yaml | 2 + postgres/k8s/secrets.yaml.example | 1 + 17 files changed, 772 insertions(+), 1 deletion(-) create mode 100644 netbox/.env.example create mode 100644 netbox/README.md create mode 100644 netbox/compose.yaml create mode 100644 netbox/configuration/configuration.py create mode 100644 netbox/k8s/certificates.yaml create mode 100644 netbox/k8s/config.yaml create mode 100644 netbox/k8s/ingress.yaml create mode 100644 netbox/k8s/namespace.yaml create mode 100644 netbox/k8s/netbox.yaml create mode 100644 netbox/k8s/secrets.yaml.example create mode 100644 netbox/k8s/settings.yaml create mode 100644 netbox/k8s/valkey.yaml diff --git a/netbox/.env.example b/netbox/.env.example new file mode 100644 index 0000000..0fbc3b2 --- /dev/null +++ b/netbox/.env.example @@ -0,0 +1,32 @@ +POSTGRES_DB=netbox +POSTGRES_USER=netbox +POSTGRES_PASSWORD=CHANGE_ME_POSTGRES_PASSWORD + +DB_NAME=netbox +DB_USER=netbox +DB_PASSWORD=CHANGE_ME_POSTGRES_PASSWORD +DB_HOST=postgres +DB_PORT=5432 +DB_SSLMODE=disable + +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD=CHANGE_ME_REDIS_PASSWORD +REDIS_DATABASE=0 +REDIS_CACHE_HOST=redis-cache +REDIS_CACHE_PORT=6379 +REDIS_CACHE_PASSWORD=CHANGE_ME_REDIS_CACHE_PASSWORD +REDIS_CACHE_DATABASE=1 + +ALLOWED_HOSTS=localhost,127.0.0.1,[::1],netbox.forust.xyz,netbox.workstation.internal +CSRF_TRUSTED_ORIGINS=https://netbox.forust.xyz,https://netbox.workstation.internal + +SECRET_KEY=CHANGE_ME_DJANGO_SECRET_KEY +API_TOKEN_PEPPER_1=CHANGE_ME_API_TOKEN_PEPPER +TIME_ZONE=Europe/Bratislava +TZ=Europe/Bratislava + +SKIP_SUPERUSER=false +SUPERUSER_NAME=admin +SUPERUSER_EMAIL=admin@example.com +SUPERUSER_PASSWORD=CHANGE_ME_SUPERUSER_PASSWORD diff --git a/netbox/README.md b/netbox/README.md new file mode 100644 index 0000000..bbada74 --- /dev/null +++ b/netbox/README.md @@ -0,0 +1,96 @@ +# NetBox + +NetBox for homelab documentation and visualization. Two runtimes are available: + +| Runtime | Manifest | Purpose | +| ------- | -------------- | -------------------------------------------------------------- | +| Docker | `compose.yaml` | Local stand on `127.0.0.1:8000` (no public exposure) | +| k8s | `k8s/` | Homelab service on `netbox.forust.xyz` (and the internal name) | + +Both use the same image (`netboxcommunity/netbox:v4.7-5.1.1`) and Valkey for tasks +plus a second logical database for caching. The Docker stand keeps its own +PostgreSQL container, while the k8s deployment uses the shared `database` cluster +(`postgres.database.svc.cluster.local:5432`, role/database `netbox`); only Valkey +stays a per-service StatefulSet. + +## Docker Compose + +```bash +cp .env.example .env +# replace CHANGE_ME +docker compose up -d +``` + +The UI is available at . The port is bound to `127.0.0.1` +intentionally, so this stand is not exposed on the LAN or public interfaces. + +The `netbox` service is also attached to the external `proxy` network and carries +Traefik labels for `netbox.forust.xyz` and `netbox.workstation.internal`. Those +labels only take effect while the Docker Traefik stack is running; it is currently +stopped, and the live ingress path in this homelab is the k8s Traefik. + +Inspect startup and health with: + +```bash +docker compose ps +docker compose logs -f netbox +``` + +Stop it with `docker compose down`; data is kept in the named volumes +`netbox-postgres`, `netbox-media-files`, `netbox-reports-files`, +`netbox-scripts-files` and `netbox-redis-data`. + +## Kubernetes + +`k8s/` is deployed in the homelab cluster and serves `netbox.forust.xyz` publicly +plus `netbox.workstation.internal` / `netbox.gigaforust.internal` internally. To +rebuild it from scratch: + +```bash +# 1. shared PostgreSQL: the password lives in the shared secret, NetBox keeps a copy +kubectl -n database patch secret postgres-shared-secrets \ + --type merge -p '{"stringData":{"NETBOX_DB_PASSWORD":""}}' +kubectl -n database exec postgres17-0 -- psql -U postgres -d postgres \ + -c 'CREATE ROLE netbox LOGIN PASSWORD ...' -c 'CREATE DATABASE netbox OWNER netbox' + +# 2. secrets first: the deploy workflow never applies *secret*.yaml +cp k8s/secrets.yaml.example k8s/secrets.yaml # replace CHANGE_ME +kubectl apply -f k8s/secrets.yaml + +# 3. manifests +kubectl apply -f k8s/ +``` + +The shared cluster is reached at `postgres.database.svc.cluster.local:5432`. Its +NetworkPolicy (`postgres/k8s/network-policy.yaml`) must list the `netbox` namespace +or connections are dropped, and `postgres/initdb/01-create-databases.sh` already +creates the role and database on a fresh data directory. NetBox has no PostgreSQL +StatefulSet of its own — only `netbox-valkey`. + +`netbox.forust.xyz` resolves to this host (`78.98.72.122`) through the `DOMAINS` +list in the `default/cfddns` secret. cert-manager issues `netbox-prod-tls` with the +`letsencrypt-prod` issuer, the internal route uses `internal-wildcard-tls`. + +Resources are permanent again now that the first-boot migrations are complete: +the web container reserves `100m`/`512Mi` and is capped at `2` CPU/`2Gi`, the +worker reserves `50m`/`256Mi` and is capped at `1` CPU/`1Gi`, and Valkey reserves +`25m`/`64Mi` and is capped at `250m`/`256Mi`. The deliberately generous CPU caps +leave enough headroom for future schema migrations without letting one process +consume the whole node. + +The first start applies ~810 migrations, each in its own transaction with DDL and +a commit; every later start is a no-op. The startup probe allows 15 minutes and +`progressDeadlineSeconds` is 1800 for the same reason. Probes run inside the pod +and explicitly set `Host: netbox.forust.xyz`; a kubelet `httpGet.host` field would +replace the probe destination with that public hostname and bypass the pod. + +## Secrets + +- `netbox/.env` (compose) and `netbox/k8s/secrets.yaml` (k8s) are gitignored. Only + `.env.example` and `k8s/secrets.yaml.example` are committed. +- `netbox/configuration/configuration.py` is env-driven: hosts, database, Redis and + the Django keys all come from the environment, so the same settings file works in + both runtimes. The k8s copy lives in the `netbox-settings` ConfigMap + (`k8s/settings.yaml`) and must be kept in sync with the file. +- Rotating `SECRET_KEY` invalidates all sessions; rotating `API_TOKEN_PEPPER_1` + invalidates every API token. diff --git a/netbox/compose.yaml b/netbox/compose.yaml new file mode 100644 index 0000000..1b2f312 --- /dev/null +++ b/netbox/compose.yaml @@ -0,0 +1,137 @@ +services: + netbox: + image: docker.io/netboxcommunity/netbox:v4.7-5.1.1 + container_name: netbox + restart: unless-stopped + user: "netbox:root" + ports: + - "127.0.0.1:8000:8080" + env_file: + - .env + environment: + GRANIAN_WORKERS: "2" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + redis-cache: + condition: service_healthy + volumes: + - ./configuration:/etc/netbox/config:z,ro + - netbox-media-files:/opt/netbox/netbox/media + - netbox-reports-files:/opt/netbox/netbox/reports + - netbox-scripts-files:/opt/netbox/netbox/scripts + networks: + - default + - proxy + labels: + - "traefik.enable=true" + - "traefik.http.services.netbox.loadbalancer.server.port=8080" + + # Prod Router + - "traefik.http.routers.netbox.rule=Host(`netbox.forust.xyz`)" + - "traefik.http.routers.netbox.entrypoints=websecure" + - "traefik.http.routers.netbox.middlewares=security-headers@file" + - "traefik.http.routers.netbox.tls.certresolver=letsencrypt" + # Local Router + - "traefik.http.routers.netbox-local.rule=Host(`netbox.workstation.internal`)" + - "traefik.http.routers.netbox-local.entrypoints=websecure" + - "traefik.http.routers.netbox-local.tls=true" + healthcheck: + test: ["CMD", "/opt/netbox/health.sh"] + start_period: 600s + timeout: 5s + interval: 15s + retries: 10 + + netbox-worker: + image: docker.io/netboxcommunity/netbox:v4.7-5.1.1 + container_name: netbox-worker + restart: unless-stopped + user: "netbox:root" + command: + - /opt/netbox/venv/bin/python + - /opt/netbox/netbox/manage.py + - rqworker + env_file: + - .env + depends_on: + netbox: + condition: service_healthy + volumes: + - ./configuration:/etc/netbox/config:z,ro + - netbox-media-files:/opt/netbox/netbox/media + - netbox-reports-files:/opt/netbox/netbox/reports + - netbox-scripts-files:/opt/netbox/netbox/scripts + healthcheck: + test: ["CMD-SHELL", "ps -ef | grep -q '[r]qworker'"] + start_period: 30s + timeout: 5s + interval: 15s + retries: 10 + + postgres: + image: docker.io/postgres:18.6-alpine + container_name: netbox-postgres + restart: unless-stopped + environment: + POSTGRES_DB: "${POSTGRES_DB:?POSTGRES_DB must be set}" + POSTGRES_USER: "${POSTGRES_USER:?POSTGRES_USER must be set}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}" + volumes: + - netbox-postgres:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", 'pg_isready -q -t 2 -d "$${POSTGRES_DB}" -U "$${POSTGRES_USER}"'] + start_period: 20s + timeout: 5s + interval: 10s + retries: 10 + + redis: + image: docker.io/valkey/valkey:9.1.2-alpine + container_name: netbox-redis + restart: unless-stopped + command: + - sh + - -c + - valkey-server --appendonly yes --requirepass "$$REDIS_PASSWORD" + environment: + REDIS_PASSWORD: "${REDIS_PASSWORD:?REDIS_PASSWORD must be set}" + volumes: + - netbox-redis-data:/data + healthcheck: + test: ["CMD-SHELL", 'valkey-cli --pass "$${REDIS_PASSWORD}" ping | grep -q PONG'] + start_period: 5s + timeout: 5s + interval: 5s + retries: 10 + + redis-cache: + image: docker.io/valkey/valkey:9.1.2-alpine + container_name: netbox-redis-cache + restart: unless-stopped + command: + - sh + - -c + - valkey-server --requirepass "$$REDIS_CACHE_PASSWORD" + environment: + REDIS_CACHE_PASSWORD: "${REDIS_CACHE_PASSWORD:?REDIS_CACHE_PASSWORD must be set}" + healthcheck: + test: ["CMD-SHELL", 'valkey-cli --pass "$${REDIS_CACHE_PASSWORD}" ping | grep -q PONG'] + start_period: 5s + timeout: 5s + interval: 5s + retries: 10 + +volumes: + netbox-media-files: + netbox-reports-files: + netbox-scripts-files: + netbox-postgres: + netbox-redis-data: + +networks: + default: + proxy: + external: true diff --git a/netbox/configuration/configuration.py b/netbox/configuration/configuration.py new file mode 100644 index 0000000..9a3a552 --- /dev/null +++ b/netbox/configuration/configuration.py @@ -0,0 +1,48 @@ +import os + + +def _csv(name, default=""): + return [item.strip() for item in os.environ.get(name, default).split(",") if item.strip()] + + +ALLOWED_HOSTS = _csv("ALLOWED_HOSTS", "localhost,127.0.0.1,[::1]") +CSRF_TRUSTED_ORIGINS = _csv("CSRF_TRUSTED_ORIGINS") +USE_X_FORWARDED_HOST = True +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +DATABASES = { + "default": { + "NAME": os.environ["DB_NAME"], + "USER": os.environ["DB_USER"], + "PASSWORD": os.environ["DB_PASSWORD"], + "HOST": os.environ["DB_HOST"], + "PORT": os.environ.get("DB_PORT", "5432"), + "OPTIONS": {"sslmode": os.environ.get("DB_SSLMODE", "disable")}, + "CONN_MAX_AGE": int(os.environ.get("DB_CONN_MAX_AGE", "300")), + } +} + +REDIS = { + "tasks": { + "HOST": os.environ["REDIS_HOST"], + "PORT": int(os.environ.get("REDIS_PORT", "6379")), + "PASSWORD": os.environ["REDIS_PASSWORD"], + "DATABASE": int(os.environ.get("REDIS_DATABASE", "0")), + "SSL": False, + }, + "caching": { + "HOST": os.environ["REDIS_CACHE_HOST"], + "PORT": int(os.environ.get("REDIS_CACHE_PORT", "6379")), + "PASSWORD": os.environ["REDIS_CACHE_PASSWORD"], + "DATABASE": int(os.environ.get("REDIS_CACHE_DATABASE", "1")), + "SSL": False, + }, +} + +SECRET_KEY = os.environ["SECRET_KEY"] +API_TOKEN_PEPPERS = {1: os.environ["API_TOKEN_PEPPER_1"]} +TIME_ZONE = os.environ.get("TIME_ZONE", "UTC") +MEDIA_ROOT = "/opt/netbox/netbox/media" +REPORTS_ROOT = "/opt/netbox/netbox/reports" +SCRIPTS_ROOT = "/opt/netbox/netbox/scripts" +CENSUS_REPORTING_ENABLED = False diff --git a/netbox/k8s/certificates.yaml b/netbox/k8s/certificates.yaml new file mode 100644 index 0000000..b26909b --- /dev/null +++ b/netbox/k8s/certificates.yaml @@ -0,0 +1,28 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: netbox-prod-tls + namespace: netbox +spec: + secretName: netbox-prod-tls + dnsNames: + - netbox.forust.xyz + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: internal-wildcard-tls + namespace: netbox +spec: + secretName: internal-wildcard-tls + dnsNames: + - "*.workstation.internal" + - "*.gigaforust.internal" + - workstation.internal + - gigaforust.internal + issuerRef: + name: internal-ca + kind: ClusterIssuer diff --git a/netbox/k8s/config.yaml b/netbox/k8s/config.yaml new file mode 100644 index 0000000..9d9dc65 --- /dev/null +++ b/netbox/k8s/config.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: netbox-config + namespace: netbox +data: + DB_HOST: "postgres.database.svc.cluster.local" + DB_PORT: "5432" + DB_SSLMODE: "disable" + REDIS_HOST: "netbox-valkey" + REDIS_PORT: "6379" + REDIS_DATABASE: "0" + REDIS_CACHE_HOST: "netbox-valkey" + REDIS_CACHE_PORT: "6379" + REDIS_CACHE_DATABASE: "1" + TIME_ZONE: "Europe/Bratislava" + TZ: "Europe/Bratislava" + GRANIAN_WORKERS: "2" + ALLOWED_HOSTS: "netbox.forust.xyz,netbox.workstation.internal,netbox.gigaforust.internal" + CSRF_TRUSTED_ORIGINS: "https://netbox.forust.xyz,https://netbox.workstation.internal,https://netbox.gigaforust.internal" + SKIP_SUPERUSER: "false" diff --git a/netbox/k8s/ingress.yaml b/netbox/k8s/ingress.yaml new file mode 100644 index 0000000..bb4b7ea --- /dev/null +++ b/netbox/k8s/ingress.yaml @@ -0,0 +1,36 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: netbox-prod + namespace: netbox +spec: + entryPoints: + - websecure + routes: + - match: Host(`netbox.forust.xyz`) + kind: Rule + middlewares: + - name: crowdsec-bouncer + namespace: crowdsec + services: + - name: netbox-service + port: 8080 + tls: + secretName: netbox-prod-tls +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: netbox-local + namespace: netbox +spec: + entryPoints: + - websecure + routes: + - match: Host(`netbox.workstation.internal`) || Host(`netbox.gigaforust.internal`) + kind: Rule + services: + - name: netbox-service + port: 8080 + tls: + secretName: internal-wildcard-tls diff --git a/netbox/k8s/namespace.yaml b/netbox/k8s/namespace.yaml new file mode 100644 index 0000000..1a63822 --- /dev/null +++ b/netbox/k8s/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: netbox diff --git a/netbox/k8s/netbox.yaml b/netbox/k8s/netbox.yaml new file mode 100644 index 0000000..95f778c --- /dev/null +++ b/netbox/k8s/netbox.yaml @@ -0,0 +1,204 @@ +apiVersion: v1 +kind: Service +metadata: + name: netbox-service + namespace: netbox +spec: + selector: + app: netbox + ports: + - name: http + port: 8080 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: netbox-deployment + namespace: netbox + labels: + app: netbox +spec: + replicas: 1 + progressDeadlineSeconds: 300 + selector: + matchLabels: + app: netbox + strategy: + # ReadWriteOnce PVC + type: Recreate + template: + metadata: + labels: + app: netbox + spec: + containers: + - name: netbox + image: docker.io/netboxcommunity/netbox:v4.7-5.1.1 + ports: + - name: http + containerPort: 8080 + envFrom: + - configMapRef: + name: netbox-config + - secretRef: + name: netbox-secrets + volumeMounts: + - name: netbox-config + mountPath: /etc/netbox/config + readOnly: true + - name: netbox-media + mountPath: /opt/netbox/netbox/media + - name: netbox-reports + mountPath: /opt/netbox/netbox/reports + - name: netbox-scripts + mountPath: /opt/netbox/netbox/scripts + startupProbe: + exec: + command: + - /opt/netbox/venv/bin/python + - -c + - >- + exec /usr/bin/curl --fail --silent --show-error --max-time 4 + --header 'Host: netbox.forust.xyz' + http://127.0.0.1:8080/login/ >/dev/null + failureThreshold: 90 + periodSeconds: 10 + readinessProbe: + exec: + command: + - /opt/netbox/venv/bin/python + - -c + - >- + exec /usr/bin/curl --fail --silent --show-error --max-time 4 + --header 'Host: netbox.forust.xyz' + http://127.0.0.1:8080/login/ >/dev/null + periodSeconds: 10 + livenessProbe: + exec: + command: + - /opt/netbox/venv/bin/python + - -c + - >- + exec /usr/bin/curl --fail --silent --show-error --max-time 4 + --header 'Host: netbox.forust.xyz' + http://127.0.0.1:8080/login/ >/dev/null + initialDelaySeconds: 30 + periodSeconds: 30 + resources: + requests: + cpu: "100m" + memory: "512Mi" + limits: + cpu: "2" + memory: "2Gi" + volumes: + - name: netbox-config + configMap: + name: netbox-settings + - name: netbox-media + persistentVolumeClaim: + claimName: netbox-media-pvc + - name: netbox-reports + persistentVolumeClaim: + claimName: netbox-reports-pvc + - name: netbox-scripts + persistentVolumeClaim: + claimName: netbox-scripts-pvc +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: netbox-worker-deployment + namespace: netbox + labels: + app: netbox-worker +spec: + replicas: 1 + progressDeadlineSeconds: 300 + selector: + matchLabels: + app: netbox-worker + strategy: + type: Recreate + template: + metadata: + labels: + app: netbox-worker + spec: + containers: + - name: netbox-worker + image: docker.io/netboxcommunity/netbox:v4.7-5.1.1 + command: + - /opt/netbox/venv/bin/python + - netbox/manage.py + - rqworker + workingDir: /opt/netbox + envFrom: + - configMapRef: + name: netbox-config + - secretRef: + name: netbox-secrets + volumeMounts: + - name: netbox-config + mountPath: /etc/netbox/config + readOnly: true + - name: netbox-media + mountPath: /opt/netbox/netbox/media + - name: netbox-reports + mountPath: /opt/netbox/netbox/reports + - name: netbox-scripts + mountPath: /opt/netbox/netbox/scripts + resources: + requests: + cpu: "50m" + memory: "256Mi" + limits: + cpu: "1" + memory: "1Gi" + volumes: + - name: netbox-config + configMap: + name: netbox-settings + - name: netbox-media + persistentVolumeClaim: + claimName: netbox-media-pvc + - name: netbox-reports + persistentVolumeClaim: + claimName: netbox-reports-pvc + - name: netbox-scripts + persistentVolumeClaim: + claimName: netbox-scripts-pvc +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: netbox-media-pvc + namespace: netbox +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 2Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: netbox-reports-pvc + namespace: netbox +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: netbox-scripts-pvc + namespace: netbox +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi diff --git a/netbox/k8s/secrets.yaml.example b/netbox/k8s/secrets.yaml.example new file mode 100644 index 0000000..dabfabe --- /dev/null +++ b/netbox/k8s/secrets.yaml.example @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Secret +metadata: + name: netbox-secrets + namespace: netbox +type: Opaque +stringData: + DB_NAME: "netbox" + DB_USER: "netbox" + DB_PASSWORD: "CHANGE_ME_POSTGRES_PASSWORD" + REDIS_PASSWORD: "CHANGE_ME_VALKEY_PASSWORD" + REDIS_CACHE_PASSWORD: "CHANGE_ME_VALKEY_PASSWORD" + VALKEY_PASSWORD: "CHANGE_ME_VALKEY_PASSWORD" + SECRET_KEY: "CHANGE_ME_DJANGO_SECRET_KEY" + API_TOKEN_PEPPER_1: "CHANGE_ME_API_TOKEN_PEPPER" + SUPERUSER_NAME: "admin" + SUPERUSER_EMAIL: "admin@example.com" + SUPERUSER_PASSWORD: "CHANGE_ME_SUPERUSER_PASSWORD" diff --git a/netbox/k8s/settings.yaml b/netbox/k8s/settings.yaml new file mode 100644 index 0000000..fbd67d8 --- /dev/null +++ b/netbox/k8s/settings.yaml @@ -0,0 +1,56 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: netbox-settings + namespace: netbox +data: + # Sync wit netbox/configuration/configuration.py (the Docker mounts that file). + configuration.py: | + import os + + + def _csv(name, default=""): + return [item.strip() for item in os.environ.get(name, default).split(",") if item.strip()] + + + ALLOWED_HOSTS = _csv("ALLOWED_HOSTS", "localhost,127.0.0.1,[::1]") + CSRF_TRUSTED_ORIGINS = _csv("CSRF_TRUSTED_ORIGINS") + USE_X_FORWARDED_HOST = True + SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + + DATABASES = { + "default": { + "NAME": os.environ["DB_NAME"], + "USER": os.environ["DB_USER"], + "PASSWORD": os.environ["DB_PASSWORD"], + "HOST": os.environ["DB_HOST"], + "PORT": os.environ.get("DB_PORT", "5432"), + "OPTIONS": {"sslmode": os.environ.get("DB_SSLMODE", "disable")}, + "CONN_MAX_AGE": int(os.environ.get("DB_CONN_MAX_AGE", "300")), + } + } + + REDIS = { + "tasks": { + "HOST": os.environ["REDIS_HOST"], + "PORT": int(os.environ.get("REDIS_PORT", "6379")), + "PASSWORD": os.environ["REDIS_PASSWORD"], + "DATABASE": int(os.environ.get("REDIS_DATABASE", "0")), + "SSL": False, + }, + "caching": { + "HOST": os.environ["REDIS_CACHE_HOST"], + "PORT": int(os.environ.get("REDIS_CACHE_PORT", "6379")), + "PASSWORD": os.environ["REDIS_CACHE_PASSWORD"], + "DATABASE": int(os.environ.get("REDIS_CACHE_DATABASE", "1")), + "SSL": False, + }, + } + + SECRET_KEY = os.environ["SECRET_KEY"] + API_TOKEN_PEPPERS = {1: os.environ["API_TOKEN_PEPPER_1"]} + TIME_ZONE = os.environ.get("TIME_ZONE", "UTC") + MEDIA_ROOT = "/opt/netbox/netbox/media" + REPORTS_ROOT = "/opt/netbox/netbox/reports" + SCRIPTS_ROOT = "/opt/netbox/netbox/scripts" + CENSUS_REPORTING_ENABLED = False diff --git a/netbox/k8s/valkey.yaml b/netbox/k8s/valkey.yaml new file mode 100644 index 0000000..fc8cef9 --- /dev/null +++ b/netbox/k8s/valkey.yaml @@ -0,0 +1,82 @@ +apiVersion: v1 +kind: Service +metadata: + name: netbox-valkey + namespace: netbox + labels: + app: netbox-valkey +spec: + clusterIP: None + selector: + app: netbox-valkey + ports: + - name: valkey + port: 6379 + targetPort: valkey +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: netbox-valkey + namespace: netbox + labels: + app: netbox-valkey +spec: + serviceName: netbox-valkey + replicas: 1 + selector: + matchLabels: + app: netbox-valkey + template: + metadata: + labels: + app: netbox-valkey + spec: + containers: + - name: valkey + image: docker.io/valkey/valkey:9.1.2-alpine + command: + - sh + - -c + - valkey-server --appendonly yes --save 30 1 --loglevel warning --requirepass "$VALKEY_PASSWORD" + env: + - name: VALKEY_PASSWORD + valueFrom: + secretKeyRef: + name: netbox-secrets + key: VALKEY_PASSWORD + ports: + - name: valkey + containerPort: 6379 + volumeMounts: + - name: valkey-data + mountPath: /data + startupProbe: + exec: + command: ["sh", "-c", 'valkey-cli --pass "$VALKEY_PASSWORD" ping | grep -q PONG'] + failureThreshold: 20 + periodSeconds: 5 + readinessProbe: + exec: + command: ["sh", "-c", 'valkey-cli --pass "$VALKEY_PASSWORD" ping | grep -q PONG'] + periodSeconds: 10 + livenessProbe: + exec: + command: ["sh", "-c", 'valkey-cli --pass "$VALKEY_PASSWORD" ping | grep -q PONG'] + initialDelaySeconds: 20 + periodSeconds: 20 + resources: + requests: + cpu: "25m" + memory: "64Mi" + limits: + cpu: "250m" + memory: "256Mi" + volumeClaimTemplates: + - metadata: + name: valkey-data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi diff --git a/postgres/README.md b/postgres/README.md index 982bd41..db166ac 100644 --- a/postgres/README.md +++ b/postgres/README.md @@ -1,7 +1,7 @@ # Shared PostgreSQL This directory contains the shared PostgreSQL 17 deployment for Authentik, -Gitea, Netronome, and Statuspage. It creates one database and one login role +Gitea, NetBox, Netronome, and Statuspage. It creates one database and one login role per service. Per-service standalone databases were removed after the migration (Sep 2026); Penpot stays on its own compose PostgreSQL (archived, not part of the shared instance). @@ -12,6 +12,7 @@ not part of the shared instance). | ---------- | ------------------- | -------------------------------------- | | Authentik | 2025.10.x | Supported (Authentik requires 14+) | | Gitea | 1.27.3 | Supported (Gitea requires 12+) | +| NetBox | 4.7.x | Supported (NetBox 4.x requires 13+) | | Netronome | 0.14.0 | Supported (upstream's example uses 17) | | Statuspage | custom | Supported | diff --git a/postgres/initdb/01-create-databases.sh b/postgres/initdb/01-create-databases.sh index 4da0148..fbcba0f 100755 --- a/postgres/initdb/01-create-databases.sh +++ b/postgres/initdb/01-create-databases.sh @@ -3,6 +3,7 @@ set -euo pipefail : "${AUTHENTIK_DB_PASSWORD:?AUTHENTIK_DB_PASSWORD is required}" : "${GITEA_DB_PASSWORD:?GITEA_DB_PASSWORD is required}" +: "${NETBOX_DB_PASSWORD:?NETBOX_DB_PASSWORD is required}" : "${NETRONOME_DB_PASSWORD:?NETRONOME_DB_PASSWORD is required}" : "${PENPOT_DB_PASSWORD:?PENPOT_DB_PASSWORD is required}" : "${STATUSPAGE_DB_PASSWORD:?STATUSPAGE_DB_PASSWORD is required}" @@ -23,6 +24,7 @@ SQL create_role_and_database authentik authentik "$AUTHENTIK_DB_PASSWORD" create_role_and_database gitea gitea "$GITEA_DB_PASSWORD" +create_role_and_database netbox netbox "$NETBOX_DB_PASSWORD" create_role_and_database netronome netronome "$NETRONOME_DB_PASSWORD" create_role_and_database penpot penpot "$PENPOT_DB_PASSWORD" create_role_and_database statuspage statuspage "$STATUSPAGE_DB_PASSWORD" diff --git a/postgres/k8s/network-policy.yaml b/postgres/k8s/network-policy.yaml index b4ec4dd..17203de 100644 --- a/postgres/k8s/network-policy.yaml +++ b/postgres/k8s/network-policy.yaml @@ -17,6 +17,9 @@ spec: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: gitea + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: netbox - namespaceSelector: matchLabels: kubernetes.io/metadata.name: netronome diff --git a/postgres/k8s/postgres.yaml b/postgres/k8s/postgres.yaml index e452d52..ae83540 100644 --- a/postgres/k8s/postgres.yaml +++ b/postgres/k8s/postgres.yaml @@ -113,6 +113,7 @@ data: : "${AUTHENTIK_DB_PASSWORD:?AUTHENTIK_DB_PASSWORD is required}" : "${GITEA_DB_PASSWORD:?GITEA_DB_PASSWORD is required}" + : "${NETBOX_DB_PASSWORD:?NETBOX_DB_PASSWORD is required}" : "${NETRONOME_DB_PASSWORD:?NETRONOME_DB_PASSWORD is required}" : "${PENPOT_DB_PASSWORD:?PENPOT_DB_PASSWORD is required}" : "${STATUSPAGE_DB_PASSWORD:?STATUSPAGE_DB_PASSWORD is required}" @@ -133,6 +134,7 @@ data: create_role_and_database authentik authentik "$AUTHENTIK_DB_PASSWORD" create_role_and_database gitea gitea "$GITEA_DB_PASSWORD" + create_role_and_database netbox netbox "$NETBOX_DB_PASSWORD" create_role_and_database netronome netronome "$NETRONOME_DB_PASSWORD" create_role_and_database penpot penpot "$PENPOT_DB_PASSWORD" create_role_and_database statuspage statuspage "$STATUSPAGE_DB_PASSWORD" diff --git a/postgres/k8s/secrets.yaml.example b/postgres/k8s/secrets.yaml.example index 4768d78..7311069 100644 --- a/postgres/k8s/secrets.yaml.example +++ b/postgres/k8s/secrets.yaml.example @@ -8,6 +8,7 @@ stringData: POSTGRES_ADMIN_PASSWORD: "" AUTHENTIK_DB_PASSWORD: "" GITEA_DB_PASSWORD: "" + NETBOX_DB_PASSWORD: "" NETRONOME_DB_PASSWORD: "" PENPOT_DB_PASSWORD: "" STATUSPAGE_DB_PASSWORD: ""