Gunicorn is replaced with nginx-unit

We now serve Netbox with an nginx-unit instance instead of Gunicorn.
This allows us to get rid of the extra Nginx container because Unit is
also serving the static files. The static files are now collected at container
buildtime instead of every startup.
This commit is contained in:
Tobias Genannt 2020-11-10 15:23:07 +01:00
parent 380cb77080
commit d273391773
15 changed files with 135 additions and 136 deletions

View File

@ -65,13 +65,3 @@ If your log is very long, create a Gist instead (and post the link to it): https
```text ```text
LOG LOG LOG LOG LOG LOG
``` ```
The output of `docker-compose logs nginx`:
<!--
Only if you have gotten a 5xx http error, else delete this section.
If your log is very long, create a Gist instead (and post the link to it): https://gist.github.com
-->
```text
LOG LOG LOG
```

View File

@ -19,7 +19,7 @@ jobs:
- ./build.sh develop - ./build.sh develop
docker_from: docker_from:
- '' # use the default of the build script - '' # use the default of the build script
# - python:3.10-rc-alpine # disable until dependencies work - alpine:edge
fail-fast: false fail-fast: false
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Builds new Netbox Docker Images name: Builds new Netbox Docker Images

View File

@ -12,27 +12,15 @@ RUN apk add --no-cache \
libffi-dev \ libffi-dev \
libxslt-dev \ libxslt-dev \
openldap-dev \ openldap-dev \
postgresql-dev postgresql-dev \
py3-pip \
WORKDIR /install python3-dev \
&& python3 -m venv /opt/netbox/venv \
RUN pip install --prefix="/install" --no-warn-script-location \ && /opt/netbox/venv/bin/python3 -m pip install --upgrade pip setuptools
# gunicorn is used for launching netbox
gunicorn \
greenlet \
eventlet \
# napalm is used for gathering information from network devices
napalm \
# ruamel is used in startup_scripts
'ruamel.yaml>=0.15,<0.16' \
# django_auth_ldap is required for ldap
django_auth_ldap \
# django-storages was introduced in 2.7 and is optional
django-storages
ARG NETBOX_PATH ARG NETBOX_PATH
COPY ${NETBOX_PATH}/requirements.txt / COPY ${NETBOX_PATH}/requirements.txt requirements-container.txt /
RUN pip install --prefix="/install" --no-warn-script-location -r /requirements.txt RUN /opt/netbox/venv/bin/pip install -r /requirements.txt -r /requirements-container.txt
### ###
# Main stage # Main stage
@ -44,6 +32,7 @@ FROM ${FROM} as main
RUN apk add --no-cache \ RUN apk add --no-cache \
bash \ bash \
ca-certificates \ ca-certificates \
curl \
graphviz \ graphviz \
libevent \ libevent \
libffi \ libffi \
@ -51,35 +40,38 @@ RUN apk add --no-cache \
libressl \ libressl \
libxslt \ libxslt \
postgresql-libs \ postgresql-libs \
ttf-ubuntu-font-family python3 \
py3-pip \
ttf-ubuntu-font-family \
unit \
unit-python3
WORKDIR /opt WORKDIR /opt
COPY --from=builder /install /usr/local COPY --from=builder /opt/netbox/venv /opt/netbox/venv
ARG NETBOX_PATH ARG NETBOX_PATH
COPY ${NETBOX_PATH} /opt/netbox COPY ${NETBOX_PATH} /opt/netbox
COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py
COPY docker/gunicorn_config.py /etc/netbox/
COPY docker/nginx.conf /etc/netbox-nginx/nginx.conf
COPY docker/docker-entrypoint.sh /opt/netbox/docker-entrypoint.sh COPY docker/docker-entrypoint.sh /opt/netbox/docker-entrypoint.sh
COPY docker/launch-netbox.sh /opt/netbox/launch-netbox.sh
COPY startup_scripts/ /opt/netbox/startup_scripts/ COPY startup_scripts/ /opt/netbox/startup_scripts/
COPY initializers/ /opt/netbox/initializers/ COPY initializers/ /opt/netbox/initializers/
COPY configuration/ /etc/netbox/config/ COPY configuration/ /etc/netbox/config/
COPY docker/nginx-unit.json /etc/unit/
WORKDIR /opt/netbox/netbox WORKDIR /opt/netbox/netbox
# Must set permissions for '/opt/netbox/netbox/static' directory
# to g+w so that `./manage.py collectstatic` can be executed during
# container startup.
# Must set permissions for '/opt/netbox/netbox/media' directory # Must set permissions for '/opt/netbox/netbox/media' directory
# to g+w so that pictures can be uploaded to netbox. # to g+w so that pictures can be uploaded to netbox.
RUN mkdir static && chmod -R g+w static media RUN mkdir -p static /opt/unit/state/ /opt/unit/tmp/ \
&& chmod -R g+w media /opt/unit/ \
&& SECRET_KEY="dummy" /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py collectstatic --no-input
ENTRYPOINT [ "/opt/netbox/docker-entrypoint.sh" ] ENTRYPOINT [ "/opt/netbox/docker-entrypoint.sh" ]
CMD ["gunicorn", "-c /etc/netbox/gunicorn_config.py", "netbox.wsgi"] CMD [ "/opt/netbox/launch-netbox.sh" ]
LABEL ORIGINAL_TAG="" \ LABEL ORIGINAL_TAG="" \
NETBOX_GIT_BRANCH="" \ NETBOX_GIT_BRANCH="" \

View File

@ -56,7 +56,7 @@ cd netbox-docker
tee docker-compose.override.yml <<EOF tee docker-compose.override.yml <<EOF
version: '3.4' version: '3.4'
services: services:
nginx: netbox:
ports: ports:
- 8000:8080 - 8000:8080
EOF EOF

View File

@ -49,7 +49,7 @@ if [ "${1}x" == "x" ] || [ "${1}" == "--help" ] || [ "${1}" == "-h" ]; then
echo " DOCKERFILE The name of Dockerfile to use." echo " DOCKERFILE The name of Dockerfile to use."
echo " Default: Dockerfile" echo " Default: Dockerfile"
echo " DOCKER_FROM The base image to use." echo " DOCKER_FROM The base image to use."
echo " Default: 'python:3.9-alpine'" echo " Default: 'alpine:3.13'"
echo " DOCKER_TARGET A specific target to build." echo " DOCKER_TARGET A specific target to build."
echo " It's currently not possible to pass multiple targets." echo " It's currently not possible to pass multiple targets."
echo " Default: main ldap" echo " Default: main ldap"
@ -106,7 +106,7 @@ else
fi fi
### ###
# Variables for fetching the source # Variables for fetching the Netbox source
### ###
SRC_ORG="${SRC_ORG-netbox-community}" SRC_ORG="${SRC_ORG-netbox-community}"
SRC_REPO="${SRC_REPO-netbox}" SRC_REPO="${SRC_REPO-netbox}"
@ -115,10 +115,10 @@ URL="${URL-https://github.com/${SRC_ORG}/${SRC_REPO}.git}"
NETBOX_PATH="${NETBOX_PATH-.netbox}" NETBOX_PATH="${NETBOX_PATH-.netbox}"
### ###
# Fetching the source # Fetching the Netbox source
### ###
if [ "${2}" != "--push-only" ] && [ -z "${SKIP_GIT}" ] ; then if [ "${2}" != "--push-only" ] && [ -z "${SKIP_GIT}" ] ; then
echo "🌐 Checking out '${NETBOX_BRANCH}' of netbox from the url '${URL}' into '${NETBOX_PATH}'" echo "🌐 Checking out '${NETBOX_BRANCH}' of Netbox from the url '${URL}' into '${NETBOX_PATH}'"
if [ ! -d "${NETBOX_PATH}" ]; then if [ ! -d "${NETBOX_PATH}" ]; then
$DRY git clone -q --depth 10 -b "${NETBOX_BRANCH}" "${URL}" "${NETBOX_PATH}" $DRY git clone -q --depth 10 -b "${NETBOX_BRANCH}" "${URL}" "${NETBOX_PATH}"
fi fi
@ -135,7 +135,7 @@ if [ "${2}" != "--push-only" ] && [ -z "${SKIP_GIT}" ] ; then
$DRY git checkout -qf FETCH_HEAD $DRY git checkout -qf FETCH_HEAD
$DRY git prune $DRY git prune
) )
echo "✅ Checked out netbox" echo "✅ Checked out Netbox"
fi fi
### ###
@ -157,7 +157,7 @@ fi
# Determining the value for DOCKER_FROM # Determining the value for DOCKER_FROM
### ###
if [ -z "$DOCKER_FROM" ]; then if [ -z "$DOCKER_FROM" ]; then
DOCKER_FROM="python:3.9-alpine" DOCKER_FROM="alpine:3.13"
fi fi
### ###
@ -271,7 +271,7 @@ for DOCKER_TARGET in "${DOCKER_TARGETS[@]}"; do
if ! printf '%s\n' "${IMAGES_LAYERS_OLD[@]}" | grep -q -P "^${PYTHON_LAST_LAYER}\$"; then if ! printf '%s\n' "${IMAGES_LAYERS_OLD[@]}" | grep -q -P "^${PYTHON_LAST_LAYER}\$"; then
SHOULD_BUILD="true" SHOULD_BUILD="true"
BUILD_REASON="${BUILD_REASON} python" BUILD_REASON="${BUILD_REASON} alpine"
fi fi
if [ "${NETBOX_GIT_REF}" != "${NETBOX_GIT_REF_OLD}" ]; then if [ "${NETBOX_GIT_REF}" != "${NETBOX_GIT_REF_OLD}" ]; then
SHOULD_BUILD="true" SHOULD_BUILD="true"

View File

@ -14,19 +14,9 @@ services:
- ./configuration:/etc/netbox/config:z,ro - ./configuration:/etc/netbox/config:z,ro
- ./reports:/etc/netbox/reports:z,ro - ./reports:/etc/netbox/reports:z,ro
- ./scripts:/etc/netbox/scripts:z,ro - ./scripts:/etc/netbox/scripts:z,ro
- netbox-nginx-config:/etc/netbox-nginx:z
- netbox-static-files:/opt/netbox/netbox/static:z
- netbox-media-files:/opt/netbox/netbox/media:z - netbox-media-files:/opt/netbox/netbox/media:z
nginx:
command: nginx -c /etc/netbox-nginx/nginx.conf
image: nginx:1.19-alpine
depends_on:
- netbox
ports: ports:
- 8080 - 8080
volumes:
- netbox-static-files:/opt/netbox/netbox/static:ro
- netbox-nginx-config:/etc/netbox-nginx/:ro
postgres: postgres:
image: postgres:12-alpine image: postgres:12-alpine
env_file: env/postgres.env env_file: env/postgres.env
@ -45,9 +35,5 @@ services:
- redis-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose - redis-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
env_file: env/redis-cache.env env_file: env/redis-cache.env
volumes: volumes:
netbox-static-files:
driver: local
netbox-nginx-config:
driver: local
netbox-media-files: netbox-media-files:
driver: local driver: local

View File

@ -15,30 +15,19 @@ services:
- ./configuration:/etc/netbox/config:z,ro - ./configuration:/etc/netbox/config:z,ro
- ./reports:/etc/netbox/reports:z,ro - ./reports:/etc/netbox/reports:z,ro
- ./scripts:/etc/netbox/scripts:z,ro - ./scripts:/etc/netbox/scripts:z,ro
- netbox-nginx-config:/etc/netbox-nginx:z
- netbox-static-files:/opt/netbox/netbox/static:z
- netbox-media-files:/opt/netbox/netbox/media:z - netbox-media-files:/opt/netbox/netbox/media:z
ports:
- "8080"
netbox-worker: netbox-worker:
<<: *netbox <<: *netbox
depends_on: depends_on:
- redis - redis
entrypoint: entrypoint:
- python3 - /opt/netbox/venv/bin/python
- /opt/netbox/netbox/manage.py - /opt/netbox/netbox/manage.py
command: command:
- rqworker - rqworker
ports: []
# nginx
nginx:
command: nginx -c /etc/netbox-nginx/nginx.conf
image: nginx:1.19-alpine
depends_on:
- netbox
ports:
- 8080
volumes:
- netbox-static-files:/opt/netbox/netbox/static:ro
- netbox-nginx-config:/etc/netbox-nginx/:ro
# postgres # postgres
postgres: postgres:
@ -66,10 +55,6 @@ services:
env_file: env/redis-cache.env env_file: env/redis-cache.env
volumes: volumes:
netbox-static-files:
driver: local
netbox-nginx-config:
driver: local
netbox-media-files: netbox-media-files:
driver: local driver: local
netbox-postgres-data: netbox-postgres-data:

View File

@ -9,6 +9,7 @@ from os import scandir
import importlib.util import importlib.util
import sys import sys
def _filename(f): def _filename(f):
return f.name return f.name

View File

@ -7,6 +7,9 @@ set -e
# Allows Netbox to be run as non-root users # Allows Netbox to be run as non-root users
umask 002 umask 002
# Load correct Python3 env
source /opt/netbox/venv/bin/activate
# Try to connect to the DB # Try to connect to the DB
DB_WAIT_TIMEOUT=${DB_WAIT_TIMEOUT-3} DB_WAIT_TIMEOUT=${DB_WAIT_TIMEOUT-3}
MAX_DB_WAIT_TIME=${MAX_DB_WAIT_TIME-30} MAX_DB_WAIT_TIME=${MAX_DB_WAIT_TIME-30}
@ -60,9 +63,6 @@ else
echo "import runpy; runpy.run_path('../startup_scripts')" | ./manage.py shell --interface python echo "import runpy; runpy.run_path('../startup_scripts')" | ./manage.py shell --interface python
fi fi
# Copy static files
./manage.py collectstatic --no-input
echo "✅ Initialisation is done." echo "✅ Initialisation is done."
# Launch whatever is passed by docker # Launch whatever is passed by docker

View File

@ -1,8 +0,0 @@
command = '/usr/bin/gunicorn'
pythonpath = '/opt/netbox/netbox'
bind = '0.0.0.0:8001'
workers = 3
errorlog = '-'
accesslog = '-'
capture_output = False
loglevel = 'info'

53
docker/launch-netbox.sh Executable file
View File

@ -0,0 +1,53 @@
#!/bin/bash
UNIT_CONFIG="${UNIT_CONFIG-/etc/unit/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 -gte $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 no 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 \
--state /opt/unit/state/ \
--tmp /opt/unit/tmp/

40
docker/nginx-unit.json Normal file
View File

@ -0,0 +1,40 @@
{
"listeners": {
"*:8080": {
"pass": "routes"
}
},
"routes": [
{
"match": {
"uri": "/static/*"
},
"action": {
"share": "/opt/netbox/netbox"
}
},
{
"action": {
"pass": "applications/netbox"
}
}
],
"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"
}

View File

@ -1,44 +0,0 @@
daemon off;
worker_processes 1;
error_log /dev/stderr info;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
gzip on;
server_tokens off;
client_max_body_size 10M;
server {
listen 8080;
access_log off;
location /static/ {
alias /opt/netbox/netbox/static/;
}
location / {
proxy_pass http://netbox:8001;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
}
}
server {
listen 8081;
access_log off;
location = /stub_status {
stub_status;
}
}
}

View File

@ -0,0 +1,4 @@
napalm==3.2.0
ruamel.yaml>=0.15,<0.16
django-auth-ldap==2.2.0
django-storages==1.10.1

View File

@ -35,7 +35,7 @@ if [ -z "${IMAGE}" ]; then
fi fi
# The docker compose command to use # The docker compose command to use
doco="docker-compose -f docker-compose.test.yml" doco="docker-compose --file docker-compose.test.yml --project-name netbox_docker_test_${1}"
INITIALIZERS_DIR=".initializers" INITIALIZERS_DIR=".initializers"