commit 136ee48052ae3aba3722759626febc132731728a Author: Sven Luethi Date: Sat Jul 18 14:53:27 2026 +0200 new file: .env new file: Containerfile new file: README.md new file: compose.yml new file: configuration.py new file: entrypoint.sh new file: local_requirements.txt new file: named.conf new file: nginx.conf new file: rndc.key diff --git a/.env b/.env new file mode 100644 index 0000000..1b5fd26 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +NETBOX_VERSION=4.6.5 +PYTHON_VERSION=3.14 diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..5b31e99 --- /dev/null +++ b/Containerfile @@ -0,0 +1,99 @@ +# ============================================================ +# NetBox 4.5 – Python 3.12 / Alpine +# Custom validators → baked in via COPY +# Custom scripts → mounted as volume at runtime +# Plugins & extra deps → baked in via pip +# configuration.py → mounted as volume at runtime +# ============================================================ + +# ── Stage 1: build wheel cache ──────────────────────────────── +ARG PYTHON_VERSION + +FROM python:${PYTHON_VERSION}-alpine AS builder + +ARG NETBOX_VERSION +LABEL org.opencontainers.image.version=$NETBOX_VERSION + +RUN apk update && apk add --no-cache \ + build-base \ + cargo \ + curl \ + git \ + libffi-dev \ + openssl-dev \ + postgresql-dev \ + xmlsec-dev \ + zlib-dev + +WORKDIR /build + +# Download NetBox source +RUN curl -fsSL \ + "https://github.com/netbox-community/netbox/archive/refs/tags/v${NETBOX_VERSION}.tar.gz" \ + | tar -xz --strip-components=1 + +# Install NetBox core requirements +RUN pip install --upgrade pip && \ + pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt + +# ── Install plugins & extra deps here ──────────────────────── +COPY local_requirements.txt /build/local_requirements.txt +RUN pip wheel --no-cache-dir --wheel-dir /wheels -r /build/local_requirements.txt + +# ── Stage 2: runtime image ──────────────────────────────────── +FROM python:${PYTHON_VERSION}-alpine AS runtime + +LABEL org.opencontainers.image.title="netbox" \ + org.opencontainers.image.version="${NETBOX_VERSION}" \ + org.opencontainers.image.description="NetBox network source of truth" + +# Runtime OS deps only (no build tools) +RUN apk add --no-cache \ + bash \ + curl \ + libffi \ + libpq \ + libxslt \ + openssl \ + xmlsec \ + zlib && \ + addgroup -S netbox && \ + adduser -S -G netbox -h /opt/netbox netbox + +WORKDIR /opt/netbox + +# Copy NetBox source from builder +COPY --from=builder /build . + +# Install all pre-built wheels (core + plugins) +COPY --from=builder /wheels /wheels +RUN pip install --no-cache-dir --no-index --find-links=/wheels /wheels/*.whl && \ + rm -rf /wheels + +# Create directories NetBox expects to own +RUN mkdir -p \ + netbox/media \ + netbox/reports \ + netbox/scripts \ + netbox/static && \ + chown -R netbox:netbox /opt/netbox + +# ── Volume mount points ─────────────────────────────────────── +# +# /opt/netbox/netbox/netbox/configuration.py +# → your configuration.py +# +# /opt/netbox/netbox/media/ +# → uploaded media (persist across restarts) +# +VOLUME ["/opt/netbox/netbox/media"] +VOLUME ["/opt/netbox/netbox/static"] + +# Entrypoint script handles: collectstatic, migrate, then gunicorn/worker +COPY --chown=netbox:netbox ./entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +USER netbox + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["gunicorn"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..35006c5 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Netbox Stack +This project provides a fully working Netbox Installation connected to a BIND9 +Server for DNS distribution. Further components like acme certificate management +will be added eventually. + +## Build netbox image +You can either build the netbox container yourself with following command or let +compose pull it from my own repository at git.luethi.co. Note that the reason +i pushed all images to my repo that compose relies on is simply convenience and +reducing load on dockerhub. You can modify `compose.yml` to source all but the +`netbox` images from dockerhub instead. + +```bash +# Build the image (bakes in plugins) +podman-compose --podman-build-args '\-\-network=host' build +``` + +## Start the Stack via compose +```bash +# Start the full stack +podman compose up -d +``` + +--- + +## Building the image directly without compose +```bash +podman build \ + --build-arg PYTHON_VERSION=3.14 \ + --build-arg NETBOX_VERSION=4.6.5 \ + --tag localhost/netbox:4.6.5 \ + . +``` + +--- + +## Updating NetBox + +1. Bump `PYTHON_VERSION` and `NETBOX_VERSION` in `.env` +2. Rebuild: `podman-compose --podman-build-args '\-\-network=host build --no-cache` +3. Restart: `podman-compose up -d` + +Migrations run automatically on startup via the entrypoint. diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..6e3aa0b --- /dev/null +++ b/compose.yml @@ -0,0 +1,128 @@ +name: netbox + +services: + + # __ PostgreSQL ______________________________________________ + postgres: + image: git.luethi.co/sven/netbox-stack/postgres:16-alpine + restart: unless-stopped + network_mode: host + environment: + POSTGRES_DB: netbox + POSTGRES_USER: netbox + POSTGRES_PASSWORD: netbox + volumes: + - postgres_data:/var/lib/postgresql/data:U + healthcheck: + test: ["CMD-SHELL", "pg_isready -U netbox"] + interval: 10s + timeout: 5s + retries: 5 + + # __ Redis ___________________________________________________ + redis: + image: git.luethi.co/sven/netbox-stack/redis:7-alpine + restart: unless-stopped + network_mode: host + command: redis-server --save 60 1 + volumes: + - redis_data:/data:U + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 5 + + # __ NetBox web (Gunicorn) ___________________________________ + netbox: + image: git.luethi.co/sven/netbox-stack/netbox:${NETBOX_VERSION} + build: + context: . + dockerfile: Containerfile + network: host + args: + NETBOX_VERSION: ${NETBOX_VERSION} + PYTHON_VERSION: ${PYTHON_VERSION} + HTTP_PROXY: ${HTTP_PROXY} + HTTPS_PROXY: ${HTTPS_PROXY} + NO_PROXY: ${NO_PROXY} + restart: unless-stopped + network_mode: host + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - ./configuration.py:/opt/netbox/netbox/netbox/configuration.py:ro + - netbox_custom_scripts:/opt/netbox/netbox/scripts:ro + - netbox_media:/opt/netbox/netbox/media:U + - netbox_static:/opt/netbox/netbox/static:U + environment: + GUNICORN_BIND: "localhost:8085" + GUNICORN_WORKERS: 4 + GUNICORN_THREADS: 3 + GUNICORN_TIMEOUT: 120 + GUNICORN_MAX_REQUESTS: 5000 + GUNICORN_MAX_REQUESTS_JITTER: 500 + command: gunicorn + healthcheck: + test: ["CMD", "python", "/opt/netbox/netbox/manage.py", "check"] + interval: 60s + timeout: 30s + retries: 5 + + + # __ NetBox RQ worker ________________________________________ + netbox-worker: + image: git.luethi.co/sven/netbox-stack/netbox:${NETBOX_VERSION} + restart: unless-stopped + network_mode: host + depends_on: + - netbox + volumes: + - ./configuration.py:/opt/netbox/netbox/netbox/configuration.py:ro + - netbox_custom_scripts:/opt/netbox/netbox/scripts:ro + - netbox_media:/opt/netbox/netbox/media:U + - netbox_static:/opt/netbox/netbox/static:U + command: worker + + # __ NetBox DNS transfer endpoint ____________________________ + netbox-dns-transfer-endpoint: + image: git.luethi.co/sven/netbox-stack/netbox:${NETBOX_VERSION} + restart: unless-stopped + network_mode: host + depends_on: + - netbox + volumes: + - ./configuration.py:/opt/netbox/netbox/netbox/configuration.py:ro + command: dns-transfer-endpoint + + # __ NGINX reverse proxy _____________________________________ + nginx: + image: git.luethi.co/sven/netbox-stack/nginx:alpine + restart: unless-stopped + network_mode: host + depends_on: + - netbox + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + - netbox_static:/static:ro + + # __ BIND9 ___________________________________________________ + bind9: + image: git.luethi.co/sven/netbox-stack/bind9:9.20 + restart: unless-stopped + depends_on: + - netbox-dns-transfer-endpoint + network_mode: host + volumes: + - ./named.conf:/etc/bind/named.conf:ro + - ./rndc.key:/etc/bind/rndc.key:ro + +volumes: + postgres_data: + redis_data: + netbox_media: + netbox_static: + netbox_custom_scripts: diff --git a/configuration.py b/configuration.py new file mode 100644 index 0000000..cd9238f --- /dev/null +++ b/configuration.py @@ -0,0 +1,115 @@ +# ============================================================ +# configuration.py – NetBox local configuration +# Mount this file into the container at: +# /opt/netbox/netbox/netbox/configuration.py +# ============================================================ + +# ── Required settings ───────────────────────────────────────── + +ALLOWED_HOSTS = [ + "*", +] + +CSRF_TRUSTED_ORIGINS = [ + "http://localhost:8084", + "http://127.0.0.1:8084", + "http://[::1]:8084" +] + +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +DATABASE = { + "NAME": "netbox", + "USER": "netbox", + "PASSWORD": "netbox", + "HOST": "localhost", + "PORT": "5432", + "CONN_MAX_AGE": 300, +} + +REDIS = { + "tasks": { + "HOST": "localhost", # service name in your pod / compose + "PORT": 6379, + "PASSWORD": "", + "DATABASE": 0, + "SSL": False, + }, + "caching": { + "HOST": "localhost", + "PORT": 6379, + "PASSWORD": "", + "DATABASE": 1, + "SSL": False, + }, +} + +SECRET_KEY = "CuUxP6Vg5(f^#Tk+QRgZ)Wt9x1NG##yv(c7BSmbZ+4v@Sd(ha0" + +PLUGINS = [ + "netbox_dns", + "netbox_dns_bridge" +] + +# Plugin-specific configuration (optional) +PLUGINS_CONFIG = { + "netbox_dns_bridge": { + "notify_clients": True, + "tsig_keys": { + "_default_": { + "keyname": "_default_", + "algorithm": "hmac-sha256", + "secret": "yJrOo4c3rDnv3mIrNDfSpGXePsypavsUSpNGxSojcYM=", + } + } + }, +} + +# ── Custom validators ───────────────────────────────────────── +# Reference Python classes mounted via volume into the container. +# The classes must be importable (i.e. their directory must be on +# PYTHONPATH or placed inside netbox/netbox/). + +CUSTOM_VALIDATORS = { + # "dcim.site": ["my_validators.SiteValidator"], +} + +# ── Optional / commonly changed settings ────────────────────── + +TIME_ZONE = "UTC" +DATE_FORMAT = "N j, Y" +TIME_FORMAT = "g:i a" +DATETIME_FORMAT = "N j, Y g:i a" + +# Email +#EMAIL = { +# "SERVER": "localhost", +# "PORT": 25, +# "USERNAME": "", +# "PASSWORD": "", +# "USE_SSL": False, +# "USE_TLS": False, +# "TIMEOUT": 10, +# "FROM_EMAIL": "netbox@example.com", +#} + +# Media & static files are stored inside the container (media volume) +MEDIA_ROOT = "/opt/netbox/netbox/media" + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "level": "DEBUG", + }, + }, + "root": { + "handlers": ["console"], + #"level": "WARNING", + "level": "DEBUG", + }, +} + +API_TOKEN_PEPPERS = {1: '2QLPiNVBUPaZtvYrVNdRAdqPVBFeM+zkHcOExnBVG6O53xK2FBoewbVRYvS+ygsrMwB69j5qulkl'} diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..1521dd9 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# ============================================================ +# /entrypoint.sh – NetBox container entrypoint +# ============================================================ +set -euo pipefail + +NETBOX_ROOT=/opt/netbox/netbox +MANAGE="${NETBOX_ROOT}/manage.py" + +#RETRIES=10 +wait_for_db() { + #until python "${MANAGE}" migrate --check &>/dev/null; do + until python "${MANAGE}" check --database default &>/dev/null; do + echo "[entrypoint] Waiting for database …" +# if (( RETRIES == 0 )); then +# echo "[entrypoint] Database did not become ready." +# exit 1 +# fi +# ((RETRIES--)) + sleep 2 + done + echo "[entrypoint] Database is ready." +} + +run_migrations() { + echo "[entrypoint] Applying database migrations …" + python "${MANAGE}" migrate --no-input +} + +run_collectstatic() { + echo "[entrypoint] Collecting static files …" + python "${MANAGE}" collectstatic --no-input --clear +} + +case "${1:-gunicorn}" in + gunicorn) + wait_for_db + run_migrations + run_collectstatic + echo "[entrypoint] Starting Gunicorn …" + cd $NETBOX_ROOT + exec gunicorn \ + --bind "${GUNICORN_BIND:-localhost:8080}" \ + --workers "${GUNICORN_WORKERS:-4}" \ + --worker-class "${GUNICORN_WORKER_CLASS:-gthread}" \ + --threads "${GUNICORN_THREADS:-3}" \ + --timeout "${GUNICORN_TIMEOUT:-120}" \ + --max-requests "${GUNICORN_MAX_REQUESTS:-5000}" \ + --max-requests-jitter "${GUNICORN_MAX_REQUESTS_JITTER:-500}" \ + --log-level "${GUNICORN_LOG_LEVEL:-info}" \ + --access-logfile - \ + --error-logfile - \ + netbox.wsgi + ;; + + worker) + wait_for_db + echo "[entrypoint] Starting RQ worker …" + exec python "${MANAGE}" rqworker \ + high default low + ;; + + dns-transfer-endpoint) + wait_for_db + echo "[entrypoint] Starting DNS Transfer Endpoint …" + exec python "${MANAGE}" dns-transfer-endpoint --address ::1 + ;; + + housekeeping) + wait_for_db + echo "[entrypoint] Running housekeeping …" + exec python "${MANAGE}" housekeeping + ;; + + shell) + exec python "${MANAGE}" shell + ;; + + *) + # Pass-through for arbitrary commands (e.g. manage.py subcommands) + exec python "${MANAGE}" "$@" + ;; +esac diff --git a/local_requirements.txt b/local_requirements.txt new file mode 100644 index 0000000..5d4af83 --- /dev/null +++ b/local_requirements.txt @@ -0,0 +1,2 @@ +netbox-plugin-dns==1.5.10 +netbox-plugin-dns-bridge==1.5.7 diff --git a/named.conf b/named.conf new file mode 100644 index 0000000..7456ead --- /dev/null +++ b/named.conf @@ -0,0 +1,96 @@ +options { + listen-on port 5355 { any; }; + listen-on-v6 port 5355 { any; }; + directory "/var/cache/bind"; + recursion no; + allow-recursion { none; }; + allow-query { any; }; +}; + +logging { + channel stderr_log { + stderr; + severity dynamic; + print-time yes; + print-category yes; + print-severity yes; + }; + + category default { stderr_log; }; + category general { stderr_log; }; + category queries { stderr_log; }; + category resolver { stderr_log; }; + category client { stderr_log; }; + category security { stderr_log; }; + category config { stderr_log; }; + category dnssec { stderr_log; }; + category lame-servers { stderr_log; }; + category network { stderr_log; }; + category notify { stderr_log; }; + category xfer-in { stderr_log; }; + category xfer-out { stderr_log; }; + category update { stderr_log; }; + category update-security { stderr_log; }; + category dispatch { stderr_log; }; + category rpz { stderr_log; }; + category rate-limit { stderr_log; }; +}; + +controls { + inet 127.0.0.1 port 9953 + allow { 127.0.0.1; } keys { "rndc-key"; }; + + inet ::1 port 9953 + allow { ::1; } keys { "rndc-key"; }; +}; + +########## ACLs ########## + +acl public { + !10.0.0.0/8; + !172.16.0.0/12; + !192.168.0.0/16; + any; +}; + +acl private { + 10.0.0.0/8; + 172.16.0.0/12; + 192.168.0.0/16; +}; + +######## TSIG Keys ######## +key "rndc-key" { + algorithm hmac-sha256; + secret "yJrOo4c3rDnv3mIrNDfSpGXePsypavsUSpNGxSojcYM="; +}; + +key "_default_" { + algorithm hmac-sha256; + secret "yJrOo4c3rDnv3mIrNDfSpGXePsypavsUSpNGxSojcYM="; +}; + +########################### + + +########## ZONES ########## +view "_default_" { + match-clients { key "_default_"; public; }; + allow-notify { key "_default_"; }; + allow-transfer { key "_default_"; }; + + catalog-zones { + zone "catz" + default-masters { ::1 port 5354 key "_default_"; } + zone-directory "/var/lib/bind" + min-update-interval 1; + }; + + zone "catz" { + type slave; + file "/var/lib/bind/catz_default"; + masters { ::1 port 5354 key "_default_"; }; + notify no; + }; +}; + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..831fff1 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,21 @@ +server { + listen 8084; + server_name _; + + location /static/ { + alias /static/; + expires 30d; + access_log off; + } + + location / { + proxy_pass http://localhost:8085; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_redirect off; + } +} diff --git a/rndc.key b/rndc.key new file mode 100644 index 0000000..132970c --- /dev/null +++ b/rndc.key @@ -0,0 +1,4 @@ +key "rndc-key" { + algorithm hmac-sha256; + secret "yJrOo4c3rDnv3mIrNDfSpGXePsypavsUSpNGxSojcYM="; +};