From 07fcb205b4f6bd00d98c39fe84629ca7f1f76325 Mon Sep 17 00:00:00 2001 From: Tobias Genannt Date: Wed, 12 Nov 2025 13:57:50 +0100 Subject: [PATCH] Switch server to Granian (#1561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Switch server to Granian Because the development of Nginx Unit has been stopped, switch the server to Granian which still allows to serve the Python application and the static files from one server. * Addressed review comments * Update docker/launch-netbox.sh Co-authored-by: Christian Mäder * Update docker/launch-netbox.sh Co-authored-by: Christian Mäder --------- Co-authored-by: Christian Mäder --- .github/workflows/push.yml | 1 + Dockerfile | 24 ++++----- docker-compose.override.yml.example | 4 -- docker-compose.test.yml | 2 +- docker-compose.yml | 2 +- docker/granian.py | 13 +++++ docker/launch-netbox.sh | 73 +++++++------------------ docker/nginx-unit.json | 82 ----------------------------- docker/unit.list | 1 - env/netbox.env | 2 + requirements-container.txt | 1 + 11 files changed, 47 insertions(+), 158 deletions(-) create mode 100644 docker/granian.py delete mode 100644 docker/nginx-unit.json delete mode 100644 docker/unit.list diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index f4b6305..fa6bab1 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -42,6 +42,7 @@ jobs: VALIDATE_GITHUB_ACTIONS_ZIZMOR: false VALIDATE_GITLEAKS: false VALIDATE_JSCPD: false + VALIDATE_PYTHON_PYLINT: false VALIDATE_TRIVY: false FILTER_REGEX_EXCLUDE: (.*/)?(LICENSE|configuration/.*) EDITORCONFIG_FILE_NAME: .editorconfig-checker.json diff --git a/Dockerfile b/Dockerfile index b398e9c..753200a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ ARG NETBOX_PATH COPY ${NETBOX_PATH}/requirements.txt requirements-container.txt / ENV VIRTUAL_ENV=/opt/netbox/venv RUN \ - # Gunicorn is not needed because we use Nginx Unit + # Gunicorn is not needed because we use Granian sed -i -e '/gunicorn/d' /requirements.txt && \ # We need 'social-auth-core[all]' in the Docker image. But if we put it in our own requirements-container.txt # we have potential version conflicts and the build will fail. @@ -46,8 +46,6 @@ RUN \ ARG FROM FROM ${FROM} AS main -COPY docker/unit.list /etc/apt/sources.list.d/unit.list -ADD --chmod=444 --chown=0:0 https://unit.nginx.org/keys/nginx-keyring.gpg /usr/share/keyrings/nginx-keyring.gpg RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get update -qq \ && apt-get upgrade \ @@ -64,8 +62,6 @@ RUN export DEBIAN_FRONTEND=noninteractive \ openssl \ python3 \ tini \ - unit-python3.12=1.34.2-1~noble \ - unit=1.34.2-1~noble \ && rm -rf /var/lib/apt/lists/* # Copy the modified 'requirements*.txt' files, to have the files actually used during installation @@ -81,21 +77,21 @@ COPY docker/ldap_config.docker.py /opt/netbox/netbox/netbox/ldap_config.py COPY docker/docker-entrypoint.sh /opt/netbox/docker-entrypoint.sh COPY docker/launch-netbox.sh /opt/netbox/launch-netbox.sh COPY configuration/ /etc/netbox/config/ -COPY docker/nginx-unit.json /etc/unit/ +COPY docker/granian.py /opt/netbox/netbox/netbox/granian.py COPY VERSION /opt/netbox/VERSION WORKDIR /opt/netbox/netbox # Must set permissions for '/opt/netbox/netbox/media' directory # to g+w so that pictures can be uploaded to netbox. -RUN mkdir -p static media /opt/unit/state/ /opt/unit/tmp/ \ - && chown -R unit:root /opt/unit/ media reports scripts \ - && chmod -R g+w /opt/unit/ media reports scripts \ - && cd /opt/netbox/ && SECRET_KEY="dummyKeyWithMinimumLength-------------------------" /opt/netbox/venv/bin/python -m mkdocs build \ - --config-file /opt/netbox/mkdocs.yml --site-dir /opt/netbox/netbox/project-static/docs/ \ - && DEBUG="true" SECRET_KEY="dummyKeyWithMinimumLength-------------------------" /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py collectstatic --no-input \ - && mkdir /opt/netbox/netbox/local \ - && echo "build: Docker-$(cat /opt/netbox/VERSION)" > /opt/netbox/netbox/local/release.yaml +RUN useradd --home-dir /opt/netbox/ --no-create-home --no-user-group --system --shell /bin/false --uid 999 --gid 0 netbox \ + && mkdir -p static media local \ + && chown -R netbox:root media reports scripts \ + && chmod -R g+w media reports scripts \ + && cd /opt/netbox/ && SECRET_KEY="dummyKeyWithMinimumLength-------------------------" /opt/netbox/venv/bin/python -m mkdocs build \ + --config-file /opt/netbox/mkdocs.yml --site-dir /opt/netbox/netbox/project-static/docs/ \ + && DEBUG="true" SECRET_KEY="dummyKeyWithMinimumLength-------------------------" /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py collectstatic --no-input \ + && echo "build: Docker-$(cat /opt/netbox/VERSION)" > /opt/netbox/netbox/local/release.yaml ENV LANG=C.utf8 PATH=/opt/netbox/venv/bin:$PATH VIRTUAL_ENV=/opt/netbox/venv UV_NO_CACHE=1 ENTRYPOINT [ "/usr/bin/tini", "--" ] diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example index d7ef961..1394814 100644 --- a/docker-compose.override.yml.example +++ b/docker-compose.override.yml.example @@ -2,9 +2,6 @@ services: netbox: ports: - "8000:8080" - # If you want the Nginx unit status page visible from the - # outside of the container add the following port mapping: - # - "8001:8081" # healthcheck: # Time for which the health check can fail after the container is started. # This depends mostly on the performance of your database. On the first start, @@ -19,4 +16,3 @@ services: # SUPERUSER_EMAIL: "" # SUPERUSER_NAME: "" # SUPERUSER_PASSWORD: "" - diff --git a/docker-compose.test.yml b/docker-compose.test.yml index d5acd95..8e22aa6 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -9,7 +9,7 @@ services: redis-cache: condition: service_healthy env_file: env/netbox.env - user: "unit:root" + user: "netbox:root" volumes: - ./test-configuration/test_config.py:/etc/netbox/config/test_config.py:z,ro healthcheck: diff --git a/docker-compose.yml b/docker-compose.yml index 2049c33..86586c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: - redis - redis-cache env_file: env/netbox.env - user: "unit:root" + user: "netbox:root" healthcheck: test: curl -f http://localhost:8080/login/ || exit 1 start_period: 90s diff --git a/docker/granian.py b/docker/granian.py new file mode 100644 index 0000000..651b7da --- /dev/null +++ b/docker/granian.py @@ -0,0 +1,13 @@ +from granian.utils.proxies import wrap_wsgi_with_proxy_headers +from netbox.wsgi import application + +application = wrap_wsgi_with_proxy_headers( + application, + trusted_hosts=[ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "fc00::/7", + "fe80::/10", + ], +) diff --git a/docker/launch-netbox.sh b/docker/launch-netbox.sh index 11da1de..a61e076 100755 --- a/docker/launch-netbox.sh +++ b/docker/launch-netbox.sh @@ -1,57 +1,20 @@ #!/bin/bash -UNIT_CONFIG="${UNIT_CONFIG-/etc/unit/nginx-unit.json}" -# Also used in "nginx-unit.json" -UNIT_SOCKET="/opt/unit/unit.sock" - -load_configuration() { - MAX_WAIT=10 - WAIT_COUNT=0 - while [ ! -S $UNIT_SOCKET ]; do - if [ $WAIT_COUNT -ge $MAX_WAIT ]; then - echo "⚠️ No control socket found; configuration will not be loaded." - return 1 - fi - - WAIT_COUNT=$((WAIT_COUNT + 1)) - echo "⏳ Waiting for control socket to be created... (${WAIT_COUNT}/${MAX_WAIT})" - - sleep 1 - done - - # even when the control socket exists, it does not mean unit has finished initialisation - # this curl call will get a reply once unit is fully launched - curl --silent --output /dev/null --request GET --unix-socket $UNIT_SOCKET http://localhost/ - - echo "⚙️ Applying configuration from $UNIT_CONFIG" - - RESP_CODE=$( - curl \ - --silent \ - --output /dev/null \ - --write-out '%{http_code}' \ - --request PUT \ - --data-binary "@${UNIT_CONFIG}" \ - --unix-socket $UNIT_SOCKET \ - http://localhost/config - ) - if [ "$RESP_CODE" != "200" ]; then - echo "⚠️ Could not load Unit configuration" - kill "$(cat /opt/unit/unit.pid)" - return 1 - fi - - echo "✅ Unit configuration loaded successfully" -} - -load_configuration & - -exec unitd \ - --no-daemon \ - --control unix:$UNIT_SOCKET \ - --pid /opt/unit/unit.pid \ - --log /dev/stdout \ - --statedir /opt/unit/state/ \ - --tmpdir /opt/unit/tmp/ \ - --user unit \ - --group root +exec granian \ + --host "::" \ + --port "8080" \ + --interface "wsgi" \ + --no-ws \ + --workers "${GRANIAN_WORKERS:-4}" \ + --respawn-failed-workers \ + --backpressure "${GRANIAN_BACKPRESSURE:-${GRANIAN_WORKERS:-4}}" \ + --loop "uvloop" \ + --log \ + --log-level "info" \ + --access-log \ + --working-dir "/opt/netbox/netbox/" \ + --static-path-route "/static" \ + --static-path-mount "/opt/netbox/netbox/static/" \ + --pid-file "/tmp/granian.pid" \ + "${GRANIAN_EXTRA_ARGS[@]}" \ + "netbox.granian:application" diff --git a/docker/nginx-unit.json b/docker/nginx-unit.json deleted file mode 100644 index 95fa1ef..0000000 --- a/docker/nginx-unit.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "listeners": { - "0.0.0.0:8080": { - "pass": "routes/main", - "forwarded": { - "client_ip": "X-Forwarded-For", - "protocol": "X-Forwarded-Proto", - "source": ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] - } - }, - "0.0.0.0:8081": { - "pass": "routes/status", - "forwarded": { - "client_ip": "X-Forwarded-For", - "protocol": "X-Forwarded-Proto", - "source": ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] - } - }, - "[::]:8080": { - "pass": "routes/main", - "forwarded": { - "client_ip": "X-Forwarded-For", - "protocol": "X-Forwarded-Proto", - "source": ["fc00::/7", "fe80::/10"] - } - }, - "[::]:8081": { - "pass": "routes/status", - "forwarded": { - "client_ip": "X-Forwarded-For", - "protocol": "X-Forwarded-Proto", - "source": ["fc00::/7", "fe80::/10"] - } - } - }, - "routes": { - "main": [ - { - "match": { - "uri": "/static/*" - }, - "action": { - "share": "/opt/netbox/netbox${uri}" - } - }, - { - "action": { - "pass": "applications/netbox" - } - } - ], - "status": [ - { - "match": { - "uri": "/status/*" - }, - "action": { - "proxy": "http://unix:/opt/unit/unit.sock" - } - } - ] - }, - "applications": { - "netbox": { - "type": "python 3", - "path": "/opt/netbox/netbox/", - "module": "netbox.wsgi", - "home": "/opt/netbox/venv", - "processes": { - "max": 4, - "spare": 1, - "idle_timeout": 120 - } - } - }, - "access_log": "/dev/stdout", - "settings": { - "http": { - "max_body_size": 104857600 - } - } -} diff --git a/docker/unit.list b/docker/unit.list deleted file mode 100644 index 6193723..0000000 --- a/docker/unit.list +++ /dev/null @@ -1 +0,0 @@ -deb [signed-by=/usr/share/keyrings/nginx-keyring.gpg] http://packages.nginx.org/unit/ubuntu/ noble unit diff --git a/env/netbox.env b/env/netbox.env index 52fca3b..88c8705 100644 --- a/env/netbox.env +++ b/env/netbox.env @@ -15,6 +15,8 @@ EMAIL_USERNAME=netbox # EMAIL_USE_SSL and EMAIL_USE_TLS are mutually exclusive, i.e. they can't both be `true`! EMAIL_USE_SSL=false EMAIL_USE_TLS=false +GRANIAN_BACKPRESSURE=4 +GRANIAN_WORKERS=4 GRAPHQL_ENABLED=true MEDIA_ROOT=/opt/netbox/netbox/media METRICS_ENABLED=false diff --git a/requirements-container.txt b/requirements-container.txt index 65dc789..74b4b1e 100644 --- a/requirements-container.txt +++ b/requirements-container.txt @@ -1,5 +1,6 @@ django-auth-ldap==5.2.0 dulwich==0.24.10 +granian[uvloop]==2.5.7 python3-saml==1.16.0 --no-binary lxml --no-binary xmlsec