├── .github └── workflows │ ├── create-update-pr.yml │ ├── push-to-docker-registry.yml │ └── verify.yml ├── .gitignore ├── 3.2 ├── Dockerfile ├── gunicorn.conf.py └── start.sh ├── 4.1 ├── Dockerfile ├── gunicorn.conf.py └── start.sh ├── 4.2 ├── Dockerfile ├── gunicorn.conf.py └── start.sh ├── README.md ├── build.sh ├── template ├── Dockerfile ├── gunicorn.conf.py └── start.sh ├── update.sh └── variables.sh /.github/workflows/create-update-pr.yml: -------------------------------------------------------------------------------- 1 | name: create-update-pr 2 | 3 | on: 4 | schedule: 5 | # Once a day, when doesn't matter, let's be nice and not do it at 00:00 like everybody else does. 6 | - cron: '22 2 * * *' 7 | 8 | jobs: 9 | create-pull-request-for-updated-dockerfiles: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: update_dockerfiles 14 | run: ./update.sh 15 | - name: create_pull_request 16 | uses: peter-evans/create-pull-request@v3 17 | with: 18 | title: auto-update 19 | body: updated Django, Gunicorn or pytz version 20 | commit-message: updated Django, Gunicorn or pytz version 21 | -------------------------------------------------------------------------------- /.github/workflows/push-to-docker-registry.yml: -------------------------------------------------------------------------------- 1 | name: push-to-docker-registry 2 | 3 | on: 4 | schedule: 5 | # Once a day, when doesn't matter, let's be nice and not do it at 00:00 like everybody else does. 6 | - cron: '33 3 * * *' 7 | push: 8 | branches: [ master ] 9 | 10 | jobs: 11 | update-base-image-and-push-to-docker-registry: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: azure/docker-login@v1 16 | with: 17 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 18 | password: ${{ secrets.DOCKER_HUB_PASSWORD }} 19 | - name: update_base_image_build_and_push_containers 20 | run: PUSH_TO_REGISTRY=1 ./build.sh 21 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: verify 2 | 3 | on: 4 | pull_request: 5 | types: ['opened', 'edited', 'reopened', 'synchronize'] 6 | 7 | jobs: 8 | verify-by-building-docker-image: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: update_base_image_and_build_containers 13 | run: PUSH_TO_REGISTRY=0 ./build.sh 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | -------------------------------------------------------------------------------- /3.2/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine 2 | 3 | # add gunicorn user 4 | # -D = Don't assign a password 5 | # using root group for OpenShift compatibility 6 | ENV GUNICORN_USER_NAME=gunicorn 7 | ENV GUNICORN_USER_UID=1001 8 | ENV GUNICORN_USER_GROUP=root 9 | RUN adduser -D -u $GUNICORN_USER_UID -G $GUNICORN_USER_GROUP $GUNICORN_USER_NAME 10 | 11 | # set default port for gunicorn 12 | ENV PORT=8000 13 | 14 | # add gunicorn config 15 | ENV GUNICORN_CONFIG_ROOT=/etc/gunicorn 16 | RUN mkdir -p $GUNICORN_CONFIG_ROOT 17 | COPY gunicorn.conf.py $GUNICORN_CONFIG_ROOT 18 | 19 | # setup working directory 20 | ENV WORKDIR=/usr/django 21 | RUN mkdir -p $WORKDIR 22 | WORKDIR $WORKDIR 23 | 24 | # install tini to ensure that gunicorn processes will receive signals 25 | # install gettext and bash (required by start.sh) 26 | RUN apk add --no-cache tini gettext bash 27 | 28 | # run start.sh on container start 29 | COPY start.sh $WORKDIR 30 | ENTRYPOINT ["/sbin/tini", "--"] 31 | CMD ["./start.sh"] 32 | 33 | # create directories for generated static content, user-uploaded files and application source code 34 | ENV STATIC_ROOT=/var/www/static 35 | ENV MEDIA_ROOT=/var/www/media 36 | ENV SOURCE_ROOT=$WORKDIR/app 37 | RUN mkdir -p $STATIC_ROOT $MEDIA_ROOT $SOURCE_ROOT 38 | 39 | # making sure the gunicorn user has access to all these resources 40 | RUN chown -R $GUNICORN_USER_NAME $GUNICORN_CONFIG_ROOT $WORKDIR $STATIC_ROOT $MEDIA_ROOT && \ 41 | chgrp -R $GUNICORN_USER_GROUP $GUNICORN_CONFIG_ROOT $WORKDIR $STATIC_ROOT $MEDIA_ROOT && \ 42 | chmod -R 770 $GUNICORN_CONFIG_ROOT $WORKDIR $STATIC_ROOT $MEDIA_ROOT 43 | 44 | # install gunicorn & django 45 | ENV GUNICORN_VERSION=23.0.0 46 | ENV DJANGO_VERSION=3.2.25 47 | RUN pip install \ 48 | gunicorn==$GUNICORN_VERSION \ 49 | django==$DJANGO_VERSION 50 | 51 | # install pytz for Django 3.x 52 | RUN if [[ "$DJANGO_VERSION" == 3.* ]]; then pip install pytz==2025.2; fi 53 | 54 | # switch to non-root user 55 | USER $GUNICORN_USER_UID 56 | -------------------------------------------------------------------------------- /3.2/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | workers = multiprocessing.cpu_count() * 2 + 1 4 | loglevel = "warning" 5 | -------------------------------------------------------------------------------- /3.2/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # change into app directory (start.sh cannot be there because this could be inside the volume) 5 | cd app 6 | 7 | # set internal field separator to split commands 8 | IFS=';' 9 | DJANGO_MANAGEMENT_JOB_ARRAY=(${DJANGO_MANAGEMENT_JOB}) 10 | DJANGO_MANAGEMENT_ON_START_ARRAY=(${DJANGO_MANAGEMENT_ON_START}) 11 | unset IFS 12 | 13 | executeManagementCommands() { 14 | COMMAND_ARRAY=("$@") 15 | for COMMAND in "${COMMAND_ARRAY[@]}"; do 16 | echo "executing python manage.py ${COMMAND}" 17 | python manage.py ${COMMAND} 18 | done 19 | } 20 | 21 | # run django management commands without starting gunicorn afterwards 22 | if [ ${#DJANGO_MANAGEMENT_JOB_ARRAY[@]} -ne 0 ]; then 23 | executeManagementCommands "${DJANGO_MANAGEMENT_JOB_ARRAY[@]}" 24 | exit 0 25 | fi 26 | 27 | # run django management commands before starting gunicorn 28 | if [ ${#DJANGO_MANAGEMENT_ON_START_ARRAY[@]} -ne 0 ]; then 29 | executeManagementCommands "${DJANGO_MANAGEMENT_ON_START_ARRAY[@]}" 30 | fi 31 | 32 | # start gunicorn 33 | echo "starting gunicorn (PORT=${PORT}, RELOAD=${GUNICORN_RELOAD:-false}, APP=${DJANGO_APP})" 34 | if [ "$GUNICORN_RELOAD" == "true" ]; then 35 | gunicorn -c /etc/gunicorn/gunicorn.conf.py --bind 0.0.0.0:${PORT} --reload ${DJANGO_APP}.wsgi 36 | else 37 | gunicorn -c /etc/gunicorn/gunicorn.conf.py --bind 0.0.0.0:${PORT} --preload ${DJANGO_APP}.wsgi 38 | fi 39 | -------------------------------------------------------------------------------- /4.1/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine 2 | 3 | # add gunicorn user 4 | # -D = Don't assign a password 5 | # using root group for OpenShift compatibility 6 | ENV GUNICORN_USER_NAME=gunicorn 7 | ENV GUNICORN_USER_UID=1001 8 | ENV GUNICORN_USER_GROUP=root 9 | RUN adduser -D -u $GUNICORN_USER_UID -G $GUNICORN_USER_GROUP $GUNICORN_USER_NAME 10 | 11 | # set default port for gunicorn 12 | ENV PORT=8000 13 | 14 | # add gunicorn config 15 | ENV GUNICORN_CONFIG_ROOT=/etc/gunicorn 16 | RUN mkdir -p $GUNICORN_CONFIG_ROOT 17 | COPY gunicorn.conf.py $GUNICORN_CONFIG_ROOT 18 | 19 | # setup working directory 20 | ENV WORKDIR=/usr/django 21 | RUN mkdir -p $WORKDIR 22 | WORKDIR $WORKDIR 23 | 24 | # install tini to ensure that gunicorn processes will receive signals 25 | # install gettext and bash (required by start.sh) 26 | RUN apk add --no-cache tini gettext bash 27 | 28 | # run start.sh on container start 29 | COPY start.sh $WORKDIR 30 | ENTRYPOINT ["/sbin/tini", "--"] 31 | CMD ["./start.sh"] 32 | 33 | # create directories for generated static content, user-uploaded files and application source code 34 | ENV STATIC_ROOT=/var/www/static 35 | ENV MEDIA_ROOT=/var/www/media 36 | ENV SOURCE_ROOT=$WORKDIR/app 37 | RUN mkdir -p $STATIC_ROOT $MEDIA_ROOT $SOURCE_ROOT 38 | 39 | # making sure the gunicorn user has access to all these resources 40 | RUN chown -R $GUNICORN_USER_NAME $GUNICORN_CONFIG_ROOT $WORKDIR $STATIC_ROOT $MEDIA_ROOT && \ 41 | chgrp -R $GUNICORN_USER_GROUP $GUNICORN_CONFIG_ROOT $WORKDIR $STATIC_ROOT $MEDIA_ROOT && \ 42 | chmod -R 770 $GUNICORN_CONFIG_ROOT $WORKDIR $STATIC_ROOT $MEDIA_ROOT 43 | 44 | # install gunicorn & django 45 | ENV GUNICORN_VERSION=23.0.0 46 | ENV DJANGO_VERSION=4.1.13 47 | RUN pip install \ 48 | gunicorn==$GUNICORN_VERSION \ 49 | django==$DJANGO_VERSION 50 | 51 | # install pytz for Django 3.x 52 | RUN if [[ "$DJANGO_VERSION" == 3.* ]]; then pip install pytz==2025.2; fi 53 | 54 | # switch to non-root user 55 | USER $GUNICORN_USER_UID 56 | -------------------------------------------------------------------------------- /4.1/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | workers = multiprocessing.cpu_count() * 2 + 1 4 | loglevel = "warning" 5 | -------------------------------------------------------------------------------- /4.1/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # change into app directory (start.sh cannot be there because this could be inside the volume) 5 | cd app 6 | 7 | # set internal field separator to split commands 8 | IFS=';' 9 | DJANGO_MANAGEMENT_JOB_ARRAY=(${DJANGO_MANAGEMENT_JOB}) 10 | DJANGO_MANAGEMENT_ON_START_ARRAY=(${DJANGO_MANAGEMENT_ON_START}) 11 | unset IFS 12 | 13 | executeManagementCommands() { 14 | COMMAND_ARRAY=("$@") 15 | for COMMAND in "${COMMAND_ARRAY[@]}"; do 16 | echo "executing python manage.py ${COMMAND}" 17 | python manage.py ${COMMAND} 18 | done 19 | } 20 | 21 | # run django management commands without starting gunicorn afterwards 22 | if [ ${#DJANGO_MANAGEMENT_JOB_ARRAY[@]} -ne 0 ]; then 23 | executeManagementCommands "${DJANGO_MANAGEMENT_JOB_ARRAY[@]}" 24 | exit 0 25 | fi 26 | 27 | # run django management commands before starting gunicorn 28 | if [ ${#DJANGO_MANAGEMENT_ON_START_ARRAY[@]} -ne 0 ]; then 29 | executeManagementCommands "${DJANGO_MANAGEMENT_ON_START_ARRAY[@]}" 30 | fi 31 | 32 | # start gunicorn 33 | echo "starting gunicorn (PORT=${PORT}, RELOAD=${GUNICORN_RELOAD:-false}, APP=${DJANGO_APP})" 34 | if [ "$GUNICORN_RELOAD" == "true" ]; then 35 | gunicorn -c /etc/gunicorn/gunicorn.conf.py --bind 0.0.0.0:${PORT} --reload ${DJANGO_APP}.wsgi 36 | else 37 | gunicorn -c /etc/gunicorn/gunicorn.conf.py --bind 0.0.0.0:${PORT} --preload ${DJANGO_APP}.wsgi 38 | fi 39 | -------------------------------------------------------------------------------- /4.2/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine 2 | 3 | # add gunicorn user 4 | # -D = Don't assign a password 5 | # using root group for OpenShift compatibility 6 | ENV GUNICORN_USER_NAME=gunicorn 7 | ENV GUNICORN_USER_UID=1001 8 | ENV GUNICORN_USER_GROUP=root 9 | RUN adduser -D -u $GUNICORN_USER_UID -G $GUNICORN_USER_GROUP $GUNICORN_USER_NAME 10 | 11 | # set default port for gunicorn 12 | ENV PORT=8000 13 | 14 | # add gunicorn config 15 | ENV GUNICORN_CONFIG_ROOT=/etc/gunicorn 16 | RUN mkdir -p $GUNICORN_CONFIG_ROOT 17 | COPY gunicorn.conf.py $GUNICORN_CONFIG_ROOT 18 | 19 | # setup working directory 20 | ENV WORKDIR=/usr/django 21 | RUN mkdir -p $WORKDIR 22 | WORKDIR $WORKDIR 23 | 24 | # install tini to ensure that gunicorn processes will receive signals 25 | # install gettext and bash (required by start.sh) 26 | RUN apk add --no-cache tini gettext bash 27 | 28 | # run start.sh on container start 29 | COPY start.sh $WORKDIR 30 | ENTRYPOINT ["/sbin/tini", "--"] 31 | CMD ["./start.sh"] 32 | 33 | # create directories for generated static content, user-uploaded files and application source code 34 | ENV STATIC_ROOT=/var/www/static 35 | ENV MEDIA_ROOT=/var/www/media 36 | ENV SOURCE_ROOT=$WORKDIR/app 37 | RUN mkdir -p $STATIC_ROOT $MEDIA_ROOT $SOURCE_ROOT 38 | 39 | # making sure the gunicorn user has access to all these resources 40 | RUN chown -R $GUNICORN_USER_NAME $GUNICORN_CONFIG_ROOT $WORKDIR $STATIC_ROOT $MEDIA_ROOT && \ 41 | chgrp -R $GUNICORN_USER_GROUP $GUNICORN_CONFIG_ROOT $WORKDIR $STATIC_ROOT $MEDIA_ROOT && \ 42 | chmod -R 770 $GUNICORN_CONFIG_ROOT $WORKDIR $STATIC_ROOT $MEDIA_ROOT 43 | 44 | # install gunicorn & django 45 | ENV GUNICORN_VERSION=23.0.0 46 | ENV DJANGO_VERSION=4.2.22 47 | RUN pip install \ 48 | gunicorn==$GUNICORN_VERSION \ 49 | django==$DJANGO_VERSION 50 | 51 | # install pytz for Django 3.x 52 | RUN if [[ "$DJANGO_VERSION" == 3.* ]]; then pip install pytz==2025.2; fi 53 | 54 | # switch to non-root user 55 | USER $GUNICORN_USER_UID 56 | -------------------------------------------------------------------------------- /4.2/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | workers = multiprocessing.cpu_count() * 2 + 1 4 | loglevel = "warning" 5 | -------------------------------------------------------------------------------- /4.2/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # change into app directory (start.sh cannot be there because this could be inside the volume) 5 | cd app 6 | 7 | # set internal field separator to split commands 8 | IFS=';' 9 | DJANGO_MANAGEMENT_JOB_ARRAY=(${DJANGO_MANAGEMENT_JOB}) 10 | DJANGO_MANAGEMENT_ON_START_ARRAY=(${DJANGO_MANAGEMENT_ON_START}) 11 | unset IFS 12 | 13 | executeManagementCommands() { 14 | COMMAND_ARRAY=("$@") 15 | for COMMAND in "${COMMAND_ARRAY[@]}"; do 16 | echo "executing python manage.py ${COMMAND}" 17 | python manage.py ${COMMAND} 18 | done 19 | } 20 | 21 | # run django management commands without starting gunicorn afterwards 22 | if [ ${#DJANGO_MANAGEMENT_JOB_ARRAY[@]} -ne 0 ]; then 23 | executeManagementCommands "${DJANGO_MANAGEMENT_JOB_ARRAY[@]}" 24 | exit 0 25 | fi 26 | 27 | # run django management commands before starting gunicorn 28 | if [ ${#DJANGO_MANAGEMENT_ON_START_ARRAY[@]} -ne 0 ]; then 29 | executeManagementCommands "${DJANGO_MANAGEMENT_ON_START_ARRAY[@]}" 30 | fi 31 | 32 | # start gunicorn 33 | echo "starting gunicorn (PORT=${PORT}, RELOAD=${GUNICORN_RELOAD:-false}, APP=${DJANGO_APP})" 34 | if [ "$GUNICORN_RELOAD" == "true" ]; then 35 | gunicorn -c /etc/gunicorn/gunicorn.conf.py --bind 0.0.0.0:${PORT} --reload ${DJANGO_APP}.wsgi 36 | else 37 | gunicorn -c /etc/gunicorn/gunicorn.conf.py --bind 0.0.0.0:${PORT} --preload ${DJANGO_APP}.wsgi 38 | fi 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![push-to-docker-registry](https://github.com/alesandroLang/docker-django/workflows/push-to-docker-registry/badge.svg) 2 | 3 | # Supported tags 4 | - latest, 4.2 5 | - 4.2 6 | - 4.1 7 | - 3.2 8 | 9 | More detailed information about the tags can be found in the *Image Update* section. 10 | 11 | # About this image 12 | This image can be used as a starting point to run django applications. 13 | 14 | It uses [gunicorn](http://gunicorn.org/) in the latest version to serve the wsgi application. 15 | The container picks up the wsgi entry point based on the environment variable `DJANGO_APP`. 16 | Gunicorn uses the port defined by the environment variable `PORT` (default port is `8000`). 17 | The environment variable `GUNICORN_RELOAD` can be set to `true` to activate hot reload for python files. 18 | To support more files, such as HTML templates or CSS, override the gunicorn configuration and use the `reload_extra_files` setting. 19 | 20 | Django is already installed within the version specified by the image. 21 | For example `4.2` will contain the latest django version of `4.2.x`. 22 | The image does also ship with `gettext` (and for Django 3.x `pytz`) installed. 23 | Using the latest supported python version for the corresponding django release. 24 | 25 | You can use volumes to access generated static and user-uploaded files (`/var/www/static` and `/var/www/media`). 26 | Create a volume for `/usr/django/app` for live reload during development. 27 | 28 | To execute django management commands like for example `collectstatic` the environment variable `DJANGO_MANAGEMENT_ON_START` can 29 | be set to a semicolon separated list of commands (e.g. `migrate --noinput;collectstatic --noinput`). These commands will be 30 | executed in the specified order before django will be started. This docker image can also be used to execute certain management 31 | commands without starting django afterwards. This is useful if you are running multiple django containers and want to schedule a 32 | job only once. Therefore use the environment variable `DJANGO_MANAGEMENT_JOB`. 33 | 34 | Gunicorn starts with the non-root user `gunicorn`. 35 | This user is member of the `root` group for OpenShift compatibility. 36 | 37 | # How to use this image 38 | 39 | ## Basic Setup 40 | 41 | FROM alang/django 42 | ENV DJANGO_APP=demo # will start /usr/django/app/demo/wsgi.py 43 | COPY src /usr/django/app 44 | 45 | ## Create new django project 46 | 47 | Bootstrap a new project called `demo` within the `src` folder: 48 | 49 | docker run --rm -v "$PWD/src:/usr/django/app" alang/django django-admin startproject demo app 50 | 51 | ## Executing one off commands 52 | 53 | How to execute one off django commands like `makemigrations`: 54 | 55 | docker run --rm -v "$PWD/src:/usr/django/app" -e DJANGO_MANAGEMENT_JOB=makemigrations alang/django 56 | 57 | ## Gunicorn Configuration 58 | 59 | A custom gunicorn config can be included: 60 | 61 | COPY gunicorn.conf.py /etc/gunicorn/ 62 | 63 | ## Install System Packages 64 | 65 | The image is based on [Alpine Linux](https://alpinelinux.org/). 66 | 67 | Therefore `apk` must be used to install additional packages: 68 | 69 | # install system packages required by psycopg2 70 | USER root 71 | RUN apk add --no-cache gcc postgresql-dev musl-dev 72 | USER $GUNICORN_USER_UID 73 | 74 | # Image Update 75 | 76 | Once a day the base image is updated, and a new image will be build based on it to pick up updates within the base image. 77 | 78 | To simplify the update process when using this image, in addition to the stable tags (e.g. `3.0`), unique tags containing the git 79 | commit of this repository plus the current date will be created (e.g. `3.0-c68d547-20200529`). 80 | 81 | This means that the stable tags always represent the most recent version, whereby the unique tags allow changes in this 82 | repository, or the base image to be imported in a controlled manner. 83 | 84 | # User Feedback 85 | 86 | ## Issues 87 | If you have any problems with or questions about this image, please contact me through a GitHub issue. 88 | 89 | ## Contributing 90 | You are invited to contribute new features, fixes, or updates, large or small. 91 | Please send me a pull request. 92 | 93 | # CI 94 | 95 | The following GitHub workflows do exist: 96 | 97 | - **push-to-docker-registry** 98 | Does build the image with an updated base image and store it within the [official docker registry](https://hub.docker.com/r/alang/django). 99 | 100 | - **verify** 101 | Verifies each pull request by building the docker image. 102 | 103 | - **create-update-pr** 104 | Create a pull request whenever a new version of Django, Gunicorn or pytz is available. 105 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | source variables.sh 6 | 7 | # default to 0 (do not push to registry) if the environment variable is not set 8 | PUSH_TO_REGISTRY=${PUSH_TO_REGISTRY:-0} 9 | 10 | buildImage() { 11 | local STABLE_TAG="${IMAGE}:$1" 12 | 13 | local GIT_COMMIT=$(git describe --always --dirty) 14 | local TODAY=$(date '+%Y%m%d') 15 | local UNIQUE_TAG="${IMAGE}:$1-${GIT_COMMIT}-${TODAY}" 16 | 17 | echo -e "\nbuilding image ${UNIQUE_TAG} ..." 18 | docker build --quiet --tag "${UNIQUE_TAG}" "$1" 19 | 20 | echo -e "\ntagging image ${UNIQUE_TAG} as ${STABLE_TAG} ..." 21 | docker tag "${UNIQUE_TAG}" "${STABLE_TAG}" 22 | 23 | if [[ $PUSH_TO_REGISTRY -eq 1 ]]; then 24 | echo -e "\npushing image ${STABLE_TAG} + ${UNIQUE_TAG} to registry ..." 25 | docker push "${UNIQUE_TAG}" 26 | docker push "${STABLE_TAG}" 27 | fi 28 | } 29 | 30 | echo "updating base image $BASE_IMAGE ..." 31 | docker pull "$BASE_IMAGE" 32 | 33 | for VERSION in "${VERSIONS[@]}"; do 34 | buildImage "$VERSION" 35 | done 36 | 37 | echo -e "\ntagging image ${IMAGE}:${VERSION_FOR_TAG_LATEST} as latest ..." 38 | docker tag "${IMAGE}:${VERSION_FOR_TAG_LATEST}" "$IMAGE:latest" 39 | 40 | if [[ $PUSH_TO_REGISTRY -eq 1 ]]; then 41 | echo -e "\npushing image ${IMAGE}:${VERSION_FOR_TAG_LATEST} + $IMAGE:latest to registry ..." 42 | docker push "$IMAGE:latest" 43 | fi 44 | -------------------------------------------------------------------------------- /template/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM {{BASE_IMAGE}} 2 | 3 | # add gunicorn user 4 | # -D = Don't assign a password 5 | # using root group for OpenShift compatibility 6 | ENV GUNICORN_USER_NAME=gunicorn 7 | ENV GUNICORN_USER_UID=1001 8 | ENV GUNICORN_USER_GROUP=root 9 | RUN adduser -D -u $GUNICORN_USER_UID -G $GUNICORN_USER_GROUP $GUNICORN_USER_NAME 10 | 11 | # set default port for gunicorn 12 | ENV PORT=8000 13 | 14 | # add gunicorn config 15 | ENV GUNICORN_CONFIG_ROOT=/etc/gunicorn 16 | RUN mkdir -p $GUNICORN_CONFIG_ROOT 17 | COPY gunicorn.conf.py $GUNICORN_CONFIG_ROOT 18 | 19 | # setup working directory 20 | ENV WORKDIR=/usr/django 21 | RUN mkdir -p $WORKDIR 22 | WORKDIR $WORKDIR 23 | 24 | # install tini to ensure that gunicorn processes will receive signals 25 | # install gettext and bash (required by start.sh) 26 | RUN apk add --no-cache tini gettext bash 27 | 28 | # run start.sh on container start 29 | COPY start.sh $WORKDIR 30 | ENTRYPOINT ["/sbin/tini", "--"] 31 | CMD ["./start.sh"] 32 | 33 | # create directories for generated static content, user-uploaded files and application source code 34 | ENV STATIC_ROOT=/var/www/static 35 | ENV MEDIA_ROOT=/var/www/media 36 | ENV SOURCE_ROOT=$WORKDIR/app 37 | RUN mkdir -p $STATIC_ROOT $MEDIA_ROOT $SOURCE_ROOT 38 | 39 | # making sure the gunicorn user has access to all these resources 40 | RUN chown -R $GUNICORN_USER_NAME $GUNICORN_CONFIG_ROOT $WORKDIR $STATIC_ROOT $MEDIA_ROOT && \ 41 | chgrp -R $GUNICORN_USER_GROUP $GUNICORN_CONFIG_ROOT $WORKDIR $STATIC_ROOT $MEDIA_ROOT && \ 42 | chmod -R 770 $GUNICORN_CONFIG_ROOT $WORKDIR $STATIC_ROOT $MEDIA_ROOT 43 | 44 | # install gunicorn & django 45 | ENV GUNICORN_VERSION={{GUNICORN_VERSION}} 46 | ENV DJANGO_VERSION={{DJANGO_VERSION}} 47 | RUN pip install \ 48 | gunicorn==$GUNICORN_VERSION \ 49 | django==$DJANGO_VERSION 50 | 51 | # install pytz for Django 3.x 52 | RUN if [[ "$DJANGO_VERSION" == 3.* ]]; then pip install pytz=={{PYTZ_VERSION}}; fi 53 | 54 | # switch to non-root user 55 | USER $GUNICORN_USER_UID 56 | -------------------------------------------------------------------------------- /template/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | workers = multiprocessing.cpu_count() * 2 + 1 4 | loglevel = "warning" 5 | -------------------------------------------------------------------------------- /template/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # change into app directory (start.sh cannot be there because this could be inside the volume) 5 | cd app 6 | 7 | # set internal field separator to split commands 8 | IFS=';' 9 | DJANGO_MANAGEMENT_JOB_ARRAY=(${DJANGO_MANAGEMENT_JOB}) 10 | DJANGO_MANAGEMENT_ON_START_ARRAY=(${DJANGO_MANAGEMENT_ON_START}) 11 | unset IFS 12 | 13 | executeManagementCommands() { 14 | COMMAND_ARRAY=("$@") 15 | for COMMAND in "${COMMAND_ARRAY[@]}"; do 16 | echo "executing python manage.py ${COMMAND}" 17 | python manage.py ${COMMAND} 18 | done 19 | } 20 | 21 | # run django management commands without starting gunicorn afterwards 22 | if [ ${#DJANGO_MANAGEMENT_JOB_ARRAY[@]} -ne 0 ]; then 23 | executeManagementCommands "${DJANGO_MANAGEMENT_JOB_ARRAY[@]}" 24 | exit 0 25 | fi 26 | 27 | # run django management commands before starting gunicorn 28 | if [ ${#DJANGO_MANAGEMENT_ON_START_ARRAY[@]} -ne 0 ]; then 29 | executeManagementCommands "${DJANGO_MANAGEMENT_ON_START_ARRAY[@]}" 30 | fi 31 | 32 | # start gunicorn 33 | echo "starting gunicorn (PORT=${PORT}, RELOAD=${GUNICORN_RELOAD:-false}, APP=${DJANGO_APP})" 34 | if [ "$GUNICORN_RELOAD" == "true" ]; then 35 | gunicorn -c /etc/gunicorn/gunicorn.conf.py --bind 0.0.0.0:${PORT} --reload ${DJANGO_APP}.wsgi 36 | else 37 | gunicorn -c /etc/gunicorn/gunicorn.conf.py --bind 0.0.0.0:${PORT} --preload ${DJANGO_APP}.wsgi 38 | fi 39 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | source variables.sh 6 | 7 | copyTemplateCodeByVersion() { 8 | rm -rf "$1" 9 | mkdir -p "$1" 10 | cp -R template/* "$1" 11 | } 12 | 13 | replaceBaseImageWithinTemplate() { 14 | echo " using base image ${BASE_IMAGE}" 15 | 16 | sed "s/{{BASE_IMAGE}}/${BASE_IMAGE}/g" "$1/Dockerfile" > "$1/Dockerfile.tmp" 17 | mv "$1/Dockerfile.tmp" "$1/Dockerfile" 18 | } 19 | 20 | getPyPiPackageVersions() { 21 | curl --silent "https://pypi.org/pypi/$1/json" | 22 | jq -r '.releases | keys | .[]' | 23 | grep -vE '[0-9]([a-z]|rc)[0-9]' | 24 | sort --version-sort 25 | } 26 | 27 | replaceGunicornVersionWithinTemplate() { 28 | local GUNICORN_VERSION 29 | GUNICORN_VERSION=$(getPyPiPackageVersions 'gunicorn' | tail -1) 30 | 31 | echo " using gunicorn ${GUNICORN_VERSION}" 32 | 33 | sed "s/{{GUNICORN_VERSION}}/${GUNICORN_VERSION}/g" "$1/Dockerfile" > "$1/Dockerfile.tmp" 34 | mv "$1/Dockerfile.tmp" "$1/Dockerfile" 35 | } 36 | 37 | replaceDjangoVersionWithinTemplate() { 38 | local DJANGO_VERSION 39 | DJANGO_VERSION=$(getPyPiPackageVersions 'Django' | grep "^$1" | tail -1) 40 | 41 | echo " using django ${DJANGO_VERSION}" 42 | 43 | sed "s/{{DJANGO_VERSION}}/${DJANGO_VERSION}/g" "$1/Dockerfile" > "$1/Dockerfile.tmp" 44 | mv "$1/Dockerfile.tmp" "$1/Dockerfile" 45 | } 46 | 47 | replacePytzVersionWithinTemplate() { 48 | local PYTZ_VERSION 49 | PYTZ_VERSION=$(getPyPiPackageVersions 'pytz' | tail -1) 50 | 51 | echo " using pytz ${PYTZ_VERSION}" 52 | 53 | sed "s/{{PYTZ_VERSION}}/${PYTZ_VERSION}/g" "$1/Dockerfile" > "$1/Dockerfile.tmp" 54 | mv "$1/Dockerfile.tmp" "$1/Dockerfile" 55 | } 56 | 57 | updateImageByVersion() { 58 | echo "updating image $1" 59 | copyTemplateCodeByVersion "$1" 60 | replaceBaseImageWithinTemplate "$1" 61 | replaceGunicornVersionWithinTemplate "$1" 62 | replaceDjangoVersionWithinTemplate "$1" 63 | replacePytzVersionWithinTemplate "$1" 64 | } 65 | 66 | for VERSION in "${VERSIONS[@]}"; do 67 | updateImageByVersion "${VERSION}" 68 | done 69 | -------------------------------------------------------------------------------- /variables.sh: -------------------------------------------------------------------------------- 1 | # variables used by update.sh and build.sh 2 | 3 | VERSIONS=() 4 | VERSIONS+=('3.2') 5 | VERSIONS+=('4.1') 6 | VERSIONS+=('4.2') 7 | 8 | VERSION_FOR_TAG_LATEST='4.2' 9 | 10 | IMAGE='alang/django' 11 | BASE_IMAGE='python:3.11-alpine' 12 | --------------------------------------------------------------------------------