From b38ce536b0a997c9168b8be3f19003a9089240d7 Mon Sep 17 00:00:00 2001 From: Tobias Genannt Date: Fri, 12 Sep 2025 07:40:24 +0200 Subject: [PATCH] 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. --- .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 | 71 ++++++------------------- docker/nginx-unit.json | 82 ----------------------------- docker/unit.list | 1 - requirements-container.txt | 1 + 10 files changed, 43 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..7054f43 100755 --- a/docker/launch-netbox.sh +++ b/docker/launch-netbox.sh @@ -1,57 +1,18 @@ #!/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 "4" \ + --backpressure "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" \ + "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/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