├── py310-bullseye-runtime ├── onbuild │ ├── manage.py │ ├── django-init │ ├── Dockerfile │ └── uwsgi.ini └── Dockerfile ├── py312-bookworm-runtime ├── onbuild │ ├── manage.py │ ├── django-init │ ├── Dockerfile │ └── uwsgi.ini └── Dockerfile ├── py310-bullseye-build ├── onbuild │ └── Dockerfile └── Dockerfile ├── py312-bookworm-build ├── onbuild │ └── Dockerfile └── Dockerfile ├── Makefile └── README.md /py310-bullseye-runtime/onbuild/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | # Make sure manage.py can be called from the $PATH 7 | from django.core.management import execute_from_command_line 8 | os.chdir('/app/src') 9 | sys.path.insert(0, '/app/src') 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /py312-bookworm-runtime/onbuild/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | # Make sure manage.py can be called from the $PATH 7 | from django.core.management import execute_from_command_line 8 | os.chdir('/app/src') 9 | sys.path.insert(0, '/app/src') 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /py310-bullseye-runtime/onbuild/django-init: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Only check when the container is started with the default CMD. 4 | if [ "$*" = "/usr/local/bin/uwsgi --ini /usr/local/etc/uwsgi.ini" ]; then 5 | # For local development, avoid having to exec into the container for a test db. 6 | # For production, migrate the database using an AWS single task definition or Kubernetes initContainer. 7 | if [ "$DATABASE_URL" = "sqlite:////tmp/demo.db" ]; then 8 | echo '** No $DATABASE_URL is configured, generating example data for local development' >&2 9 | manage.py migrate 10 | fi 11 | 12 | manage.py check --deploy 13 | fi 14 | 15 | 16 | exec "$@" 17 | -------------------------------------------------------------------------------- /py312-bookworm-runtime/onbuild/django-init: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Only check when the container is started with the default CMD. 4 | if [ "$*" = "/usr/local/bin/uwsgi --ini /usr/local/etc/uwsgi.ini" ]; then 5 | # For local development, avoid having to exec into the container for a test db. 6 | # For production, migrate the database using an AWS single task definition or Kubernetes initContainer. 7 | if [ "$DATABASE_URL" = "sqlite:////tmp/demo.db" ]; then 8 | echo '** No $DATABASE_URL is configured, generating example data for local development' >&2 9 | manage.py migrate 10 | fi 11 | 12 | manage.py check --deploy 13 | fi 14 | 15 | 16 | exec "$@" 17 | -------------------------------------------------------------------------------- /py310-bullseye-build/onbuild/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build environment has gcc and develop header files. 2 | # The installed files are copied to the smaller runtime container. 3 | FROM edoburu/django-base-images:py310-bullseye-build 4 | 5 | # Install (and compile) all dependencies 6 | ONBUILD RUN mkdir -p /app/src/requirements 7 | ONBUILD COPY src/requirements/*.txt /app/src/requirements/ 8 | ONBUILD ARG PIP_REQUIREMENTS=/app/src/requirements/docker.txt 9 | 10 | ONBUILD RUN pip install --no-binary=Pillow -r $PIP_REQUIREMENTS 11 | 12 | # Remove unneeded locale files 13 | ONBUILD RUN find /usr/local/lib/python3.10/site-packages/ -name '*.po' -delete 14 | # find /usr/local/lib/python3.10/site-packages/babel/locale-data/ -not -name 'en*' -not -name 'nl*' -name '*.dat' -delete && \ 15 | # find /usr/local/lib/python3.10/site-packages/tinymce/ -regextype posix-egrep -not -regex '.*/langs/(en|nl).*\.js' -wholename '*/langs/*.js' -delete 16 | -------------------------------------------------------------------------------- /py312-bookworm-build/onbuild/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build environment has gcc and develop header files. 2 | # The installed files are copied to the smaller runtime container. 3 | FROM edoburu/django-base-images:py312-bookworm-build 4 | 5 | # Install (and compile) all dependencies 6 | ONBUILD RUN mkdir -p /app/src/requirements 7 | ONBUILD COPY src/requirements/*.txt /app/src/requirements/ 8 | ONBUILD ARG PIP_REQUIREMENTS=/app/src/requirements/docker.txt 9 | 10 | ONBUILD RUN pip install --no-binary=Pillow -r $PIP_REQUIREMENTS 11 | 12 | # Remove unneeded locale files 13 | ONBUILD RUN find /usr/local/lib/python3.12/site-packages/ -name '*.po' -delete 14 | # find /usr/local/lib/python3.12/site-packages/babel/locale-data/ -not -name 'en*' -not -name 'nl*' -name '*.dat' -delete && \ 15 | # find /usr/local/lib/python3.12/site-packages/tinymce/ -regextype posix-egrep -not -regex '.*/langs/(en|nl).*\.js' -wholename '*/langs/*.js' -delete 16 | -------------------------------------------------------------------------------- /py310-bullseye-runtime/onbuild/Dockerfile: -------------------------------------------------------------------------------- 1 | # Start runtime container 2 | # Default DATABASE_URL is useful for local testing, and avoids connect timeouts for `manage.py`. 3 | FROM edoburu/django-base-images:py310-bullseye-runtime 4 | ENV UWSGI_PROCESSES=1 \ 5 | UWSGI_THREADS=10 \ 6 | UWSGI_OFFLOAD_THREADS=%k \ 7 | DATABASE_URL=sqlite:////tmp/demo.db 8 | 9 | # System config (done early, avoid running on every code change) 10 | EXPOSE 8080 1717 11 | CMD ["/usr/local/bin/uwsgi", "--ini", "/usr/local/etc/uwsgi.ini"] 12 | WORKDIR /app/src 13 | ONBUILD VOLUME /app/web/media 14 | 15 | RUN mkdir -p /app/web/media /app/web/static/CACHE \ 16 | && chown -R app:app /app/web/media/ /app/web/static/CACHE \ 17 | && chmod -R go+rw /app/web/media/ /app/web/static/CACHE \ 18 | && echo -e "[uwsgi]\nfoo = 1" > /usr/local/etc/uwsgi-local.ini 19 | 20 | # Install dependencies 21 | COPY manage.py /usr/local/bin/ 22 | COPY uwsgi.ini /usr/local/etc/uwsgi.ini 23 | COPY django-init /django-init 24 | ENTRYPOINT ["/django-init"] 25 | ONBUILD COPY --from=build-image /usr/local/bin/ /usr/local/bin/ 26 | ONBUILD COPY --from=build-image /usr/local/lib/python3.10/site-packages/ /usr/local/lib/python3.10/site-packages/ 27 | 28 | # Insert application code. 29 | # - Set a default database URL for accidental DB requests 30 | # - Prepare gzipped versions of static files for uWSGI to use 31 | # - Create a default database inside the container (as demo), 32 | # when caller doesn't define DATABASE_URL 33 | # - Give full permissions, so Kubernetes can run the image as different user 34 | ONBUILD COPY web /app/web 35 | ONBUILD COPY src /app/src 36 | 37 | ONBUILD RUN rm /app/src/*/settings/local.py* \ 38 | && find . -name '*.pyc' -delete \ 39 | && python -mcompileall -q */ 40 | -------------------------------------------------------------------------------- /py312-bookworm-runtime/onbuild/Dockerfile: -------------------------------------------------------------------------------- 1 | # Start runtime container 2 | # Default DATABASE_URL is useful for local testing, and avoids connect timeouts for `manage.py`. 3 | FROM edoburu/django-base-images:py312-bookworm-runtime 4 | ENV UWSGI_PROCESSES=1 \ 5 | UWSGI_THREADS=10 \ 6 | UWSGI_OFFLOAD_THREADS=%k \ 7 | DATABASE_URL=sqlite:////tmp/demo.db 8 | 9 | # System config (done early, avoid running on every code change) 10 | EXPOSE 8080 1717 11 | CMD ["/usr/local/bin/uwsgi", "--ini", "/usr/local/etc/uwsgi.ini"] 12 | WORKDIR /app/src 13 | ONBUILD VOLUME /app/web/media 14 | 15 | RUN mkdir -p /app/web/media /app/web/static/CACHE \ 16 | && chown -R app:app /app/web/media/ /app/web/static/CACHE \ 17 | && chmod -R go+rw /app/web/media/ /app/web/static/CACHE \ 18 | && echo -e "[uwsgi]\nfoo = 1" > /usr/local/etc/uwsgi-local.ini 19 | 20 | # Install dependencies 21 | COPY manage.py /usr/local/bin/ 22 | COPY uwsgi.ini /usr/local/etc/uwsgi.ini 23 | COPY django-init /django-init 24 | ENTRYPOINT ["/django-init"] 25 | ONBUILD COPY --from=build-image /usr/local/bin/ /usr/local/bin/ 26 | ONBUILD COPY --from=build-image /usr/local/lib/python3.12/site-packages/ /usr/local/lib/python3.12/site-packages/ 27 | 28 | # Insert application code. 29 | # - Set a default database URL for accidental DB requests 30 | # - Prepare gzipped versions of static files for uWSGI to use 31 | # - Create a default database inside the container (as demo), 32 | # when caller doesn't define DATABASE_URL 33 | # - Give full permissions, so Kubernetes can run the image as different user 34 | ONBUILD COPY web /app/web 35 | ONBUILD COPY src /app/src 36 | 37 | ONBUILD RUN rm /app/src/*/settings/local.py* \ 38 | && find . -name '*.pyc' -delete \ 39 | && python -mcompileall -q */ 40 | -------------------------------------------------------------------------------- /py310-bullseye-runtime/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM edoburu/django-base-images:py310-bullseye-build AS build-image 2 | 3 | FROM python:3.10-slim-bullseye 4 | # Make sure `pip` doesn't cache, and `uv` installs into the system dir 5 | ENV PYTHONUNBUFFERED=1 \ 6 | PIP_NO_CACHE_DIR=off \ 7 | UV_PYTHON_DOWNLOADS=never \ 8 | UV_PYTHON=python3.12 \ 9 | UV_PROJECT_ENVIRONMENT=/usr/local/ \ 10 | UV_NO_BINARY_PACKAGE=Pillow \ 11 | UV_NO_CACHE=1 \ 12 | UV_NO_DEV=1 \ 13 | UV_NO_EDITABLE=1 \ 14 | UV_NO_MANAGED_PYTHON=1 \ 15 | UV_COMPILE_BYTECODE=1 \ 16 | UV_FROZEN=1 17 | 18 | # Install runtime dependencies. 19 | # - curl for healthchecks 20 | # - gettext for `manage.py compilemessages` (run here to avoid busting build-container cache layers) 21 | # - postgresql-client for `manage.py dbshell` 22 | # - mime-support for `uwsgi --http` 23 | # - libxml for lxml 24 | # - libpng, freetype, libtiff and jpeg2000 support for Pillow 25 | # - skipped libjpeg62-turbo, as mozjpeg is used instead. 26 | # Performing dist-upgrade in case base image is outdated. 27 | RUN apt-get update \ 28 | && apt-get dist-upgrade -y \ 29 | && mkdir -p /usr/share/man/man1 /usr/share/man/man7 \ 30 | && apt-get install --no-install-recommends -y \ 31 | libxml2 \ 32 | libpng16-16 \ 33 | libopenjp2-7 \ 34 | libfreetype6 \ 35 | libtiff5 \ 36 | libwebp6 \ 37 | libwebpmux3 \ 38 | libwebpdemux2 \ 39 | curl \ 40 | gettext \ 41 | mime-support \ 42 | postgresql-client \ 43 | && rm -rf /var/lib/apt/lists/* /var/cache/debconf/*-old \ 44 | && echo "font/woff2 woff2" >> /etc/mime.types \ 45 | && echo "image/webp webp" >> /etc/mime.types \ 46 | && useradd --system --user-group app 47 | 48 | COPY --from=build-image /opt/mozjpeg /opt/mozjpeg 49 | -------------------------------------------------------------------------------- /py312-bookworm-runtime/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM edoburu/django-base-images:py312-bookworm-build AS build-image 2 | 3 | FROM python:3.12-slim-bookworm 4 | # Make sure `pip` doesn't cache, and `uv` installs into the system dir 5 | ENV PYTHONUNBUFFERED=1 \ 6 | PIP_NO_CACHE_DIR=off \ 7 | UV_PYTHON_DOWNLOADS=never \ 8 | UV_PYTHON=python3.12 \ 9 | UV_PROJECT_ENVIRONMENT=/usr/local/ \ 10 | UV_NO_BINARY_PACKAGE=Pillow \ 11 | UV_NO_CACHE=1 \ 12 | UV_NO_DEV=1 \ 13 | UV_NO_EDITABLE=1 \ 14 | UV_NO_MANAGED_PYTHON=1 \ 15 | UV_COMPILE_BYTECODE=1 \ 16 | UV_FROZEN=1 17 | 18 | # Install runtime dependencies. 19 | # - curl for healthchecks 20 | # - gettext for `manage.py compilemessages` (run here to avoid busting build-container cache layers) 21 | # - postgresql-client for `manage.py dbshell` 22 | # - mime-support for `uwsgi --http` 23 | # - libxml for lxml 24 | # - libpng, freetype, libtiff and jpeg2000 support for Pillow 25 | # - skipped libjpeg62-turbo, as mozjpeg is used instead. 26 | # Performing dist-upgrade in case base image is outdated. 27 | RUN apt-get update \ 28 | && apt-get dist-upgrade -y \ 29 | && mkdir -p /usr/share/man/man1 /usr/share/man/man7 \ 30 | && apt-get install --no-install-recommends -y \ 31 | libxml2 \ 32 | libpng16-16 \ 33 | libopenjp2-7 \ 34 | libfreetype6 \ 35 | libtiff6 \ 36 | libwebp7 \ 37 | libwebpmux3 \ 38 | libwebpdemux2 \ 39 | curl \ 40 | gettext \ 41 | mime-support \ 42 | postgresql-client \ 43 | && rm -rf /var/lib/apt/lists/* /var/cache/debconf/*-old \ 44 | && echo "font/woff2 woff2" >> /etc/mime.types \ 45 | && echo "image/webp webp" >> /etc/mime.types \ 46 | && useradd --system --user-group app 47 | 48 | COPY --from=build-image /opt/mozjpeg /opt/mozjpeg 49 | -------------------------------------------------------------------------------- /py310-bullseye-build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-bullseye 2 | # Make sure `pip` doesn't cache, and `uv` installs into the system dir 3 | ENV PYTHONUNBUFFERED=1 \ 4 | PIP_NO_CACHE_DIR=off \ 5 | UV_PYTHON_DOWNLOADS=never \ 6 | UV_PYTHON=python3.12 \ 7 | UV_PROJECT_ENVIRONMENT=/usr/local/ \ 8 | UV_NO_BINARY_PACKAGE=Pillow \ 9 | UV_NO_CACHE=1 \ 10 | UV_NO_DEV=1 \ 11 | UV_NO_EDITABLE=1 \ 12 | UV_NO_MANAGED_PYTHON=1 \ 13 | UV_COMPILE_BYTECODE=1 \ 14 | UV_FROZEN=1 15 | 16 | # Make sure optimized libjpeg is used for smaller thumbnail images 17 | # Based on https://engineeringblog.yelp.com/2017/06/making-photos-smaller.html 18 | # and https://github.com/danbooru/danbooru/blob/master/Dockerfile 19 | ARG MOZJPEG_VERSION=4.1.5 20 | RUN apt-get update \ 21 | && apt-get install --yes cmake nasm libpng-dev zlib1g-dev libwebp-dev \ 22 | && rm /usr/lib/x86_64-linux-gnu/pkgconfig/xcb.pc \ 23 | /usr/lib/x86_64-linux-gnu/libxcb.* \ 24 | /usr/lib/x86_64-linux-gnu/pkgconfig/libjpeg.pc \ 25 | /usr/lib/x86_64-linux-gnu/libjpeg.* \ 26 | /usr/include/xcb/*.h \ 27 | /usr/include/jpegint.h \ 28 | && mkdir /root/build \ 29 | && curl -L -s https://github.com/mozilla/mozjpeg/archive/v$MOZJPEG_VERSION.tar.gz \ 30 | | tar --directory=/root/build -zxf - \ 31 | && cd /root/build/mozjpeg-$MOZJPEG_VERSION \ 32 | && cmake -DCMAKE_INSTALL_PREFIX=/opt/mozjpeg -DENABLE_STATIC=0 -DWITH_ARITH_ENC=1 -DWITH_ARITH_DEC=1 . \ 33 | && make -j install/strip \ 34 | && rm -rf * /opt/mozjpeg/share /opt/mozjpeg/man \ 35 | && /opt/mozjpeg/bin/cjpeg -version 36 | 37 | ENV LIBDIR=/opt/mozjpeg/lib64 \ 38 | LD_RUN_PATH=/opt/mozjpeg/lib64:/usr/lib/x86_64-linux-gnu 39 | 40 | # Add caching layer for latest pillow 41 | RUN pip install -U pip wheel uv \ 42 | && pip install --no-binary=Pillow -C xcb=disable Pillow \ 43 | && pip install uwsgi 44 | -------------------------------------------------------------------------------- /py312-bookworm-build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-bookworm 2 | # Make sure `pip` doesn't cache, and `uv` installs into the system dir 3 | ENV PYTHONUNBUFFERED=1 \ 4 | PIP_NO_CACHE_DIR=off \ 5 | UV_PYTHON_DOWNLOADS=never \ 6 | UV_PYTHON=python3.12 \ 7 | UV_PROJECT_ENVIRONMENT=/usr/local/ \ 8 | UV_NO_BINARY_PACKAGE=Pillow \ 9 | UV_NO_CACHE=1 \ 10 | UV_NO_DEV=1 \ 11 | UV_NO_EDITABLE=1 \ 12 | UV_NO_MANAGED_PYTHON=1 \ 13 | UV_COMPILE_BYTECODE=1 \ 14 | UV_FROZEN=1 15 | 16 | # Make sure optimized libjpeg is used for smaller thumbnail images 17 | # Based on https://engineeringblog.yelp.com/2017/06/making-photos-smaller.html 18 | # and https://github.com/danbooru/danbooru/blob/master/Dockerfile 19 | ARG MOZJPEG_VERSION=4.1.5 20 | RUN apt-get update \ 21 | && apt-get install --yes cmake nasm libpng-dev zlib1g-dev libwebp-dev cmake nasm \ 22 | && rm /usr/lib/x86_64-linux-gnu/pkgconfig/xcb.pc \ 23 | /usr/lib/x86_64-linux-gnu/libxcb.* \ 24 | /usr/lib/x86_64-linux-gnu/pkgconfig/libjpeg.pc \ 25 | /usr/lib/x86_64-linux-gnu/libjpeg.* \ 26 | /usr/include/xcb/*.h \ 27 | /usr/include/jpegint.h \ 28 | && mkdir /root/build \ 29 | && curl -L -s https://github.com/mozilla/mozjpeg/archive/v$MOZJPEG_VERSION.tar.gz \ 30 | | tar --directory=/root/build -zxf - \ 31 | && cd /root/build/mozjpeg-$MOZJPEG_VERSION \ 32 | && cmake -DCMAKE_INSTALL_PREFIX=/opt/mozjpeg -DENABLE_STATIC=0 -DWITH_ARITH_ENC=1 -DWITH_ARITH_DEC=1 . \ 33 | && make -j install/strip \ 34 | && rm -rf * /opt/mozjpeg/share /opt/mozjpeg/man \ 35 | && /opt/mozjpeg/bin/cjpeg -version 36 | 37 | ENV LIBDIR=/opt/mozjpeg/lib64 \ 38 | LD_RUN_PATH=/opt/mozjpeg/lib64:/usr/lib/x86_64-linux-gnu 39 | 40 | # Add caching layer for latest pillow 41 | RUN pip install -U pip wheel uv \ 42 | && pip install --no-binary=Pillow -C xcb=disable Pillow \ 43 | && pip install uwsgi 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build runtime 2 | 3 | export DOCKER_BUILDKIT=0 4 | 5 | all: all312 all310 6 | all312: pull312 build312 runtime312 7 | all310: pull310 build310 runtime310 8 | 9 | pull: pull312 pull310 10 | build: build312 build310 11 | runtime: runtime312 runtime310 12 | push: push312 push310 13 | clean: clean312 clean310 14 | 15 | # Ensure builds stack on top of the latest version of the base image. 16 | # A manual pull is used instead of `docker build --pull` to avoid repulling our own images. 17 | pull312: 18 | docker pull python:3.12-bookworm 19 | docker pull python:3.12-slim-bookworm 20 | 21 | pull310: 22 | docker pull python:3.10-bullseye 23 | docker pull python:3.10-slim-bullseye 24 | 25 | ## Building build container 26 | build312: 27 | docker build -t edoburu/django-base-images:py312-bookworm-build ./py312-bookworm-build/ 28 | docker build -t edoburu/django-base-images:py312-bookworm-build-onbuild ./py312-bookworm-build/onbuild/ 29 | 30 | build310: 31 | docker build -t edoburu/django-base-images:py310-bullseye-build ./py310-bullseye-build/ 32 | docker build -t edoburu/django-base-images:py310-bullseye-build-onbuild ./py310-bullseye-build/onbuild/ 33 | 34 | ## Building runtime container 35 | runtime312: 36 | docker build -t edoburu/django-base-images:py312-bookworm-runtime ./py312-bookworm-runtime/ 37 | docker build -t edoburu/django-base-images:py312-bookworm-runtime-onbuild ./py312-bookworm-runtime/onbuild/ 38 | 39 | runtime310: 40 | docker build -t edoburu/django-base-images:py310-bullseye-runtime ./py310-bullseye-runtime/ 41 | docker build -t edoburu/django-base-images:py310-bullseye-runtime-onbuild ./py310-bullseye-runtime/onbuild/ 42 | 43 | push312: 44 | docker push edoburu/django-base-images:py312-bookworm-build 45 | docker push edoburu/django-base-images:py312-bookworm-build-onbuild 46 | docker push edoburu/django-base-images:py312-bookworm-runtime 47 | docker push edoburu/django-base-images:py312-bookworm-runtime-onbuild 48 | 49 | push310: 50 | docker push edoburu/django-base-images:py310-bullseye-build 51 | docker push edoburu/django-base-images:py310-bullseye-build-onbuild 52 | docker push edoburu/django-base-images:py310-bullseye-runtime 53 | docker push edoburu/django-base-images:py310-bullseye-runtime-onbuild 54 | 55 | clean312: 56 | docker rmi edoburu/django-base-images:py312-bookworm-build edoburu/django-base-images:py312-bookworm-build-onbuild edoburu/django-base-images:py312-bookworm-runtime edoburu/django-base-images:py312-bookworm-runtime-onbuild 57 | 58 | clean310: 59 | docker rmi edoburu/django-base-images:py310-bullseye-build edoburu/django-base-images:py310-bullseye-build-onbuild edoburu/django-base-images:py310-bullseye-runtime edoburu/django-base-images:py310-bullseye-runtime-onbuild 60 | -------------------------------------------------------------------------------- /py310-bullseye-runtime/onbuild/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | strict = true 3 | module = $(UWSGI_MODULE) 4 | processes = $(UWSGI_PROCESSES) 5 | threads = $(UWSGI_THREADS) 6 | procname-prefix-spaced = uwsgi: $(UWSGI_MODULE) ($(ALLOWED_HOSTS)) 7 | 8 | # HTTP serving avoids the need for an Nginx container 9 | http-socket = :8080 10 | http-enable-proxy-protocol = true 11 | http-auto-chunked = true 12 | http-keepalive = 75 13 | http-timeout = 75 14 | honour-range = true 15 | offload-threads = $(UWSGI_OFFLOAD_THREADS) 16 | 17 | # Stats exposure 18 | stats = :1717 19 | stats-http = true 20 | 21 | # Better startup/shutdown in docker: 22 | die-on-term = true 23 | lazy-apps = false 24 | need-app = true 25 | no-defer-accept = true 26 | 27 | # Better behavior 28 | # https://www.techatbloomberg.com/blog/configuring-uwsgi-production-deployment/ 29 | master = true 30 | single-interpreter = true 31 | enable-threads = true 32 | py-call-uwsgi-fork-hooks = true 33 | py-call-osafterfork = true 34 | thunder-lock = true 35 | vacuum = true 36 | 37 | # Logging 38 | log-x-forwarded-for = true 39 | #memory-report = true 40 | #disable-logging = true 41 | #log-slow = 200 42 | #log-date = true 43 | 44 | # Avoid errors on aborted client connections 45 | ignore-sigpipe = true 46 | ignore-write-errors = true 47 | disable-write-exception = true 48 | 49 | # Limits, and kill requests after 120 seconds 50 | harakiri = 120 51 | harakiri-verbose = true 52 | post-buffering = 4096 53 | buffer-size = 65535 54 | #listen=1000 55 | #max-fd=120000 56 | 57 | # Reduce memory usage (Linux default is 8MB stack), 58 | # 512k should even be sufficient for Python 59 | thread-stacksize = 1024 60 | 61 | # Custom headers for all files, not only those served by Django 62 | add-header = X-Content-Type-Options: nosniff 63 | 64 | # Static file serving with caching headers and gzip 65 | static-map = /static=/app/web/static 66 | static-map = /media=/app/web/media 67 | static-safe = /usr/local/lib/python3.10/site-packages/ 68 | static-safe = /app/src/frontend/static/ 69 | static-gzip-dir = /app/web/static/ 70 | route-uri = ^/static/ addheader:Vary: Accept-Encoding 71 | 72 | # Only add default far-future expires for URLs that have cache busting paths 73 | # More rules can be added via uwsgi-local.ini 74 | #static-expires-uri = ^/media/cache/ 2592000 75 | #static-expires-uri = ^/static/CACHE/ 2592000 76 | 77 | # Cache stat() calls 78 | cache2 = name=statcalls,items=2000,keysize=200,blocksize=50 79 | static-cache-paths = 86400 80 | 81 | # Redirect http -> https 82 | if-not-env = UWSGI_ALLOW_HTTP=true 83 | add-header = Strict-Transport-Security: max-age=16070400 84 | route-if = equal:${HTTP_X_FORWARDED_PROTO};http redirect-permanent:https://${HTTP_HOST}${REQUEST_URI} 85 | endif = 86 | 87 | # Avoid caching static files with a 404, as another docker container might be serving it. 88 | # This also avoids forwarding the request to the Python app. 89 | error-route-status = 404 goto:error404 90 | error-route = .* last: 91 | 92 | error-route-label = error404 93 | error-route-if = startswith:${PATH_INFO};/static/ remheader:Expires 94 | error-route-if = startswith:${PATH_INFO};/static/ addheader:Cache-Control: no-cache 95 | error-route = .* last: 96 | 97 | # Allow hot reloading (e.g. skaffold) 98 | if-env = UWSGI_HOT_RELOAD=true 99 | exec-asap = manage.py compilemessages 100 | exec-post-app = manage.py migrate --noinput 101 | py-auto-reload = 1 102 | endif = 103 | 104 | # Allow to extend this configuration 105 | ini = /usr/local/etc/uwsgi-local.ini 106 | -------------------------------------------------------------------------------- /py312-bookworm-runtime/onbuild/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | strict = true 3 | module = $(UWSGI_MODULE) 4 | processes = $(UWSGI_PROCESSES) 5 | threads = $(UWSGI_THREADS) 6 | procname-prefix-spaced = uwsgi: $(UWSGI_MODULE) ($(ALLOWED_HOSTS)) 7 | 8 | # HTTP serving avoids the need for an Nginx container 9 | http-socket = :8080 10 | http-enable-proxy-protocol = true 11 | http-auto-chunked = true 12 | http-keepalive = 75 13 | http-timeout = 75 14 | honour-range = true 15 | offload-threads = $(UWSGI_OFFLOAD_THREADS) 16 | 17 | # Stats exposure 18 | stats = :1717 19 | stats-http = true 20 | 21 | # Better startup/shutdown in docker: 22 | die-on-term = true 23 | lazy-apps = false 24 | need-app = true 25 | no-defer-accept = true 26 | 27 | # Better behavior 28 | # https://www.techatbloomberg.com/blog/configuring-uwsgi-production-deployment/ 29 | master = true 30 | single-interpreter = true 31 | enable-threads = true 32 | py-call-uwsgi-fork-hooks = true 33 | py-call-osafterfork = true 34 | thunder-lock = true 35 | vacuum = true 36 | 37 | # Logging 38 | log-x-forwarded-for = true 39 | #memory-report = true 40 | #disable-logging = true 41 | #log-slow = 200 42 | #log-date = true 43 | 44 | # Avoid errors on aborted client connections 45 | ignore-sigpipe = true 46 | ignore-write-errors = true 47 | disable-write-exception = true 48 | 49 | # Limits, and kill requests after 120 seconds 50 | harakiri = 120 51 | harakiri-verbose = true 52 | post-buffering = 4096 53 | buffer-size = 65535 54 | #listen=1000 55 | #max-fd=120000 56 | 57 | # Reduce memory usage (Linux default is 8MB stack), 58 | # 512k should even be sufficient for Python 59 | thread-stacksize = 1024 60 | 61 | # Custom headers for all files, not only those served by Django 62 | add-header = X-Content-Type-Options: nosniff 63 | 64 | # Static file serving with caching headers and gzip 65 | static-map = /static=/app/web/static 66 | static-map = /media=/app/web/media 67 | static-safe = /usr/local/lib/python3.12/site-packages/ 68 | static-safe = /app/src/frontend/static/ 69 | static-gzip-dir = /app/web/static/ 70 | route-uri = ^/static/ addheader:Vary: Accept-Encoding 71 | 72 | # Only add default far-future expires for URLs that have cache busting paths 73 | # More rules can be added via uwsgi-local.ini 74 | #static-expires-uri = ^/media/cache/ 2592000 75 | #static-expires-uri = ^/static/CACHE/ 2592000 76 | 77 | # Cache stat() calls 78 | cache2 = name=statcalls,items=2000,keysize=200,blocksize=50 79 | static-cache-paths = 86400 80 | 81 | # Redirect http -> https 82 | if-not-env = UWSGI_ALLOW_HTTP=true 83 | add-header = Strict-Transport-Security: max-age=16070400 84 | route-if = equal:${HTTP_X_FORWARDED_PROTO};http redirect-permanent:https://${HTTP_HOST}${REQUEST_URI} 85 | endif = 86 | 87 | # Avoid caching static files with a 404, as another docker container might be serving it. 88 | # This also avoids forwarding the request to the Python app. 89 | error-route-status = 404 goto:error404 90 | error-route = .* last: 91 | 92 | error-route-label = error404 93 | error-route-if = startswith:${PATH_INFO};/static/ remheader:Expires 94 | error-route-if = startswith:${PATH_INFO};/static/ addheader:Cache-Control: no-cache 95 | error-route = .* last: 96 | 97 | # Allow hot reloading (e.g. skaffold) 98 | if-env = UWSGI_HOT_RELOAD=true 99 | exec-asap = manage.py compilemessages 100 | exec-post-app = manage.py migrate --noinput 101 | py-auto-reload = 1 102 | endif = 103 | 104 | # Allow to extend this configuration 105 | ini = /usr/local/etc/uwsgi-local.ini 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Base docker images for Django 2 | ============================= 3 | 4 | These images are suitable as base for running Django inside a Docker Swarm or Kubernetes environment. 5 | 6 | Features: 7 | 8 | * Nessesairy base libraries to install Django. 9 | * Multi-stage builds for smaller runtime images (below 300MB). 10 | * No separate Nginx container is needed thanks to `uwsgi --http-socket`. 11 | * [MozJPEG](https://github.com/mozilla/mozjpeg) installed. 12 | * Pillow generated JPEG images pass [Google PageSpeed](https://developers.google.com/speed/pagespeed/insights/). 13 | * Run `manage.py check --deploy` on startup. 14 | 15 | The default ports are 8080 (HTTP) and 1717 (uWSGI stats). These higher ports allow the container to run as unprivileged user. 16 | 17 | Static files are efficiently served from uWSGI using offload threads. You may use [WhiteNoise](http://whitenoise.evans.io/) to generate cache-busting file names, given that you have an edge-node that caches these files (e.g. AWS CloudFront). Delegating static file serving to a separate Nginx container is pretty pointless, given that everything likely runs behind an HTTP proxy already to route virtual hosts (e.g. Kubernetes Nginx ingress). 18 | 19 | The standard libjpeg and libjpeg-turbo are optimized to quickly encode JPEG files. MozJPEG is a drop-in replacement which is slower for encoding, but faster for downloading and decoding. The resuling images are much slower and pass [Google PageSpeed](https://developers.google.com/speed/pagespeed/insights/). All images generated by Pillow, including those by [sorl-thumbnail](https://github.com/jazzband/sorl-thumbnail), use MozJPEG for rendering. 20 | 21 | Linking Pillow against MozJPEG does require require Pillow to be installed from the source tarball using `pip install --no-binary=Pillow ...`. Otherwise, the Pillow wheel package is installed, which bundles it's own copy of libjpeg. The build image contains a caching layer for the latest Pillow image to avoid recompiling Pillow in most use-cases. 22 | 23 | **Tips:** When using `sorl-thumbnail`, add the setting `THUMBNAIL_QUALITY = 75` to match the behavior of the default `cjpeg` tool. A quality of 80 is also sufficient. The default value for `THUMBNAIL_QUALITY` is 95, which generates overly large thumbnail files. 24 | 25 | **NOTE:** uwsgi doesn't build yet on Python 3.12, hence it's not provided in the py312-* images. 26 | 27 | Available tags 28 | -------------- 29 | 30 | Base images: 31 | 32 | - `py312-bookworm-build` ([Dockerfile](https://github.com/edoburu/docker-django-base-image/blob/master/py312-bookworm-build/Dockerfile)) - Build-time container with [mozjpeg](https://github.com/mozilla/mozjpeg), and [Pillow](https://python-pillow.org/) 9.2.0 linked to it. 33 | - `py312-bookworm-runtime` ([Dockerfile](https://github.com/edoburu/docker-django-base-image/blob/master/py312-bookworm-runtime/Dockerfile)) - Run-time container with [mozjpeg](https://github.com/mozilla/mozjpeg), and default run-time libraries. 34 | - `py310-bullseye-build` ([Dockerfile](https://github.com/edoburu/docker-django-base-image/blob/master/py310-bullseye-build/Dockerfile)) - Build-time container with [mozjpeg](https://github.com/mozilla/mozjpeg), and [Pillow](https://python-pillow.org/) 9.2.0 linked to it. 35 | - `py310-bullseye-runtime` ([Dockerfile](https://github.com/edoburu/docker-django-base-image/blob/master/py310-bullseye-runtime/Dockerfile)) - Run-time container with [mozjpeg](https://github.com/mozilla/mozjpeg), and default run-time libraries. 36 | 37 | Onbuild images: 38 | 39 | - `py312-bookworm-build-onbuild` ([Dockerfile](https://github.com/edoburu/docker-django-base-image/blob/master/py312-bookworm-build/onbuild/Dockerfile)) - Pre-scripted build container that assumes `src/requirements/docker.txt` is available. Supports `PIP_REQUIREMENTS` build arg. 40 | - `py312-bookworm-runtime-onbuild` ([Dockerfile](https://github.com/edoburu/docker-django-base-image/blob/master/py312-bookworm-runtime/onbuild/Dockerfile)) - Pre-scripted runtime container that assumes `src/`, `web/media` and `web/static` are available. Supports `GIT_VERSION` build arg. 41 | - `py310-bullseye-build-onbuild` ([Dockerfile](https://github.com/edoburu/docker-django-base-image/blob/master/py310-bullseye-build/onbuild/Dockerfile)) - Pre-scripted build container that assumes `src/requirements/docker.txt` is available. Supports `PIP_REQUIREMENTS` build arg. 42 | - `py310-bullseye-runtime-onbuild` ([Dockerfile](https://github.com/edoburu/docker-django-base-image/blob/master/py310-bullseye-runtime/onbuild/Dockerfile)) - Pre-scripted runtime container that assumes `src/`, `web/media` and `web/static` are available. Supports `GIT_VERSION` build arg. 43 | 44 | 45 | Onbuild Usage 46 | ------------- 47 | 48 | The "onbuild" images contain pre-scripted and opinionated assumptions about the application layout. 49 | Using these images result in a very small ``Dockerfile``: 50 | 51 | ```dockerfile 52 | FROM edoburu/django-base-images:py312-bookworm-build-onbuild AS build-image 53 | 54 | # Remove more unneeded locale files 55 | RUN find /usr/local/lib/python3.12/site-packages/babel/locale-data/ -not -name 'en*' -not -name 'nl*' -name '*.dat' -delete && \ 56 | find /usr/local/lib/python3.12/site-packages/tinymce/ -regextype posix-egrep -not -regex '.*/langs/(en|nl).*\.js' -wholename '*/langs/*.js' -delete 57 | 58 | # Start runtime container 59 | FROM edoburu/django-base-images:py312-bookworm-runtime-onbuild 60 | ENV DJANGO_SETTINGS_MODULE=mysite.settings.docker \ 61 | UWSGI_MODULE=mysite.wsgi.docker:application 62 | 63 | # Collect static files as root, with gzipped files for uwsgi to serve 64 | RUN manage.py compilemessages && \ 65 | manage.py collectstatic --settings=$DJANGO_SETTINGS_MODULE --noinput && \ 66 | gzip --keep --best --force --recursive /app/web/static/ 67 | 68 | # Add extra uwsgi settings 69 | COPY deployment/docker/uwsgi-local.ini /usr/local/etc/uwsgi-local.ini 70 | 71 | # Reduce default permissions 72 | USER app 73 | ``` 74 | 75 | You may add `SILENCED_SYSTEM_CHECKS = ['security.W001']` since the `SecurityMiddleware` headers are sent by uWSGI already. 76 | 77 | 78 | Base image usage 79 | ---------------- 80 | 81 | While the "onbuild" images are opinionated, the base images only contain what is absolutely needed. By using these images, your custom `Dockerfile` can break with all assumptions that the onbuild images make. This example is equivalent to the onbuild example above: 82 | 83 | ```dockerfile 84 | # Build environment has gcc and develop header files. 85 | # The installed files are copied to the smaller runtime container. 86 | FROM edoburu/django-base-images:py312-bookworm-build AS build-image 87 | 88 | # Install (and compile) all dependencies 89 | RUN mkdir -p /app/src/requirements 90 | COPY src/requirements/*.txt /app/src/requirements/ 91 | ARG PIP_REQUIREMENTS=/app/src/requirements/docker.txt 92 | RUN pip install --no-binary=Pillow -r $PIP_REQUIREMENTS 93 | 94 | # Remove unneeded locale files 95 | RUN find /usr/local/lib/python3.12/site-packages/ -name '*.po' -delete && \ 96 | find /usr/local/lib/python3.12/site-packages/babel/locale-data/ -not -name 'en*' -not -name 'nl*' -name '*.dat' -delete && \ 97 | find /usr/local/lib/python3.12/site-packages/tinymce/ -regextype posix-egrep -not -regex '.*/langs/(en|nl).*\.js' -wholename '*/langs/*.js' -delete 98 | 99 | # Start runtime container 100 | # Default DATABASE_URL is useful for local testing, and avoids connect timeouts for `manage.py`. 101 | FROM edoburu/django-base-images:py312-bookworm-runtime 102 | ENV UWSGI_PROCESSES=1 \ 103 | UWSGI_THREADS=20 \ 104 | UWSGI_OFFLOAD_THREADS=%k \ 105 | UWSGI_MODULE=mysite.wsgi.docker:application \ 106 | DJANGO_SETTINGS_MODULE=mysite.settings.docker \ 107 | DATABASE_URL=sqlite:////tmp/demo.db 108 | 109 | # System config (done early, avoid running on every code change) 110 | EXPOSE 8080 1717 111 | CMD ["/usr/local/bin/uwsgi", "--ini", "/usr/local/etc/uwsgi.ini"] 112 | WORKDIR /app/src 113 | VOLUME /app/web/media 114 | 115 | # Install dependencies 116 | COPY --from=build-image /usr/local/bin/ /usr/local/bin/ 117 | COPY --from=build-image /usr/local/lib/python3.12/site-packages/ /usr/local/lib/python3.12/site-packages/ 118 | COPY deployment/docker/manage.py /usr/local/bin/ 119 | COPY deployment/docker/uwsgi.ini /usr/local/etc/uwsgi.ini 120 | 121 | # Insert application code. 122 | # - Set a default database URL for accidental DB requests 123 | # - Prepare gzipped versions of static files for uWSGI to use 124 | # - Create a default database inside the container (as demo), 125 | # when caller doesn't define DATABASE_URL 126 | # - Give full permissions, so Kubernetes can run the image as different user 127 | COPY web /app/web 128 | COPY src /app/src 129 | RUN rm /app/src/*/settings/local.py* && \ 130 | find . -name '*.pyc' -delete && \ 131 | python -mcompileall -q */ && \ 132 | manage.py compilemessages && \ 133 | manage.py collectstatic --noinput && \ 134 | gzip --keep --best --force --recursive /app/web/static/ && \ 135 | chown -R app:app /app/web/media/ /app/web/static/CACHE && \ 136 | chmod -R go+rw /app/web/media/ /app/web/static/CACHE 137 | 138 | # Reduce default permissions 139 | USER app 140 | ``` 141 | 142 | A `.dockerignore` with at least the following exclusions is recommended: 143 | 144 | ``` 145 | *.pyc 146 | *.mo 147 | *.db 148 | *.css.map 149 | .cache 150 | .sass-cache 151 | .idea 152 | .vagrant 153 | .git 154 | .DS_Store 155 | __pycache__ 156 | src/myproject/settings/local.py 157 | src/node_modules 158 | web/media 159 | web/static/CACHE 160 | ``` 161 | 162 | Overriding UWSGI config 163 | ----------------------- 164 | 165 | The onbuild image contains a default [uwsgi.ini](https://github.com/edoburu/docker-django-base-images/blob/master/py37-stretch-runtime/onbuild/uwsgi.ini) that is fully functional, but is slightly opinionated about static file paths. It can be easily extended by adding a different version: 166 | 167 | ``` 168 | COPY uwsgi-local.ini /usr/local/etc/uwsgi-local.ini 169 | ``` 170 | 171 | Or overwritten all together: 172 | 173 | ``` 174 | COPY uwsgi.ini /usr/local/etc/uwsgi.ini 175 | ``` 176 | 177 | The default [uwsgi.ini](https://github.com/edoburu/docker-django-base-images/blob/master/py37-stretch-runtime/onbuild/uwsgi.ini) serves static files entirely from uWSGI, with HTTP cache headers set. [WhiteNoise](http://whitenoise.evans.io/) can still be used to generate cache-busing file names, but serving files performs better using `uwsgi --static-map` since it can cache ``stat()`` calls and use offload threads. 178 | 179 | 180 | Other recommendations 181 | --------------------- 182 | 183 | Don't forget to include a `HEALTHCHECK` in your docker file: 184 | 185 | ```dockerfile 186 | HEALTHCHECK --interval=5m --timeout=3s CMD curl -f http://localhost:8080/api/healthcheck/ || exit 1 187 | ``` 188 | 189 | Consider adding `localhost` to `ALLOWED_HOSTS`, to make healthhecks easy to implement. 190 | 191 | Consider including the latest git version: 192 | 193 | ```dockerfile 194 | ARG GIT_VERSION 195 | LABEL git-version=$GIT_VERSION 196 | RUN echo $GIT_VERSION > .docker-git-version 197 | ``` 198 | --------------------------------------------------------------------------------