Docker Image for Django Application (Debian Based)
Aug. 20, 2022Docker is most reliable and flexible way to distribute your Django applications, it able to include entire environment settings, dependencies and application files to deploy it to the server or any cloud provider.
This post show you a few examples how to build Docker image for your Django application, this will be based on Debian base images as it recommended option for Python applications, of course it could be build based on Alpine images but it mostly suitable for small apps because in most cases build time and final image size based on Alpine would be bigger than Debian based, more about issues with Alpine based images you can read here and here.
Django requires at least two containers to run in production mode, one serve application itself, second one is nginx container which proxy requests to app container and serves static files. To make it easier we will build basic images with minimal requirements first and then improve it gradually.
Building Basic Images
I propose you place docker related files under the .docker
directory to keep it sorted. Example project structure looks as follows.
|-- .docker
| |-- app
| | |-- Dockerfile
| | `-- entrypoint.sh
| `-- nginx
| |-- Dockerfile
| `-- nginx.conf
|-- app
| |-- __init__.py
| |-- asgi.py
| |-- settings.py
| |-- urls.py
| `-- wsgi.py
|-- docker-compose-app.yml
|-- docker-compose-nginx.yml
|-- docker-compose.yml
|-- manage.py
`-- requirements.txt
Application Image
First example is minimal image which may host Django in the out-of-box state with sqlite database and automatic migrations applying on container startup.
Dockerfile
FROM python:3.9-slim
ENV PYTHONUNBUFFERED=1 \
PYTHONFAULTHANDLER=1 \
PYTHONHASHSEED=random \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100
# Copy app source
WORKDIR /app
COPY . .
# Install requirements
RUN pip install --no-cache-dir gunicorn gevent
RUN pip install --no-cache-dir -r requirements.txt
# Copy and enable
COPY .docker/app/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ]
EXPOSE 8000
# 'app' in the 'app.wsgi:application' - it is directory name where placed settings.py file
CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "--error-logfile", "-", "--access-logfile", "-", "app.wsgi:application"]
entrypoint.sh
#!/bin/bash
set -e
echo "Applying database migrations..."
python manage.py migrate
exec "$@"
docker-compose-app.yml
Use this docker compose to build image with given files structure.
Build command: docker-compose -f docker-compose-app.yml build
version: '3.8'
services:
django_app:
container_name: django_app
build:
context: .
dockerfile: .docker/app/Dockerfile
image: django_app:latest
restart: always
ports:
- "80:8000"
Nginx image
Nginx serves staticfiles and proxying http requests to the WSGI server which is required in produciton mode (DEBUG=False
)
staticfiles configuration
Make sure you’re configured staticfiles in the Django’s settings.py
first, as it uses to build nginx container image.STATIC_ROOT = BASE_DIR / 'staticfiles'
Dockerfile
FROM django_app:latest as app_image
# Generate staticfiles bundle
RUN python manage.py collectstatic --noinput
FROM nginx:alpine
# Copy staticfiles bundle to the image
COPY --from=app_image /app/staticfiles /var/www/static
COPY .docker/nginx/nginx.conf /etc/nginx/conf.d/default.conf
nginx.conf
server {
listen 80;
root /var/www/static;
error_page 500 502 503 504 /nginx_50x.html;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
client_max_body_size 64M;
location / {
try_files $uri @proxy;
}
location @proxy {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://django_app:8000; # << Application container name
proxy_connect_timeout 900;
proxy_send_timeout 900;
proxy_read_timeout 900;
send_timeout 900;
}
location /static {
autoindex off;
alias /var/www/static;
}
}
Nginx Dockerfile depends on application image because app image used to extract staticfiles from it. So make sure you build application image first.
docker-compose-nginx.yml
Use this docker compose to build image with given files structure.
Build command: docker-compose -f docker-compose-nginx.yml build
version: '3.8'
services:
django_nginx:
container_name: django_nginx
restart: always
build:
context: .
dockerfile: .docker/nginx/Dockerfile
image: django_nginx
ports:
- "80:80"
Full project example could be found on Github
Postgres Ready Application Image
This example shows how to install psycopg2 dependencies, this is single change from basic image, everything else remains same. You can find full example on Github
Dockerfile
FROM python:3.9-slim
ENV PYTHONUNBUFFERED=1 \
PYTHONFAULTHANDLER=1 \
PYTHONHASHSEED=random \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100
# Install dependencies
RUN apt update && apt install -y \
# psycopg2 dependencies
libpq-dev gcc \
# Clean apt cache
&& apt clean && rm -rf /var/lib/apt/lists/*
# Copy app source
WORKDIR /app
COPY . .
# Install requirements
RUN pip install --no-cache-dir gunicorn gevent
RUN pip install --no-cache-dir -r requirements.txt
# Copy and enable
COPY .docker/app/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ]
EXPOSE 8000
# 'app' in the 'app.wsgi:application' - it is directory name where placed settings.py file
CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "--error-logfile", "-", "--access-logfile", "-", "app.wsgi:application"]
MySQL Ready Application Image
Same as previous but with MySQL dependencies installation. Full example available on Github
FROM python:3.9-slim
ENV PYTHONUNBUFFERED=1 \
PYTHONFAULTHANDLER=1 \
PYTHONHASHSEED=random \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100
# Install dependencies
RUN apt update && apt install -y \
# mysqlclient dependencies
default-libmysqlclient-dev gcc \
# Clean apt cache
&& apt clean && rm -rf /var/lib/apt/lists/*
# Copy app source
WORKDIR /app
COPY . .
# Install requirements
RUN pip install --no-cache-dir gunicorn gevent
RUN pip install --no-cache-dir -r requirements.txt
# Copy and enable
COPY .docker/app/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ]
EXPOSE 8000
# 'app' in the 'app.wsgi:application' - it is directory name where placed settings.py file
CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "--error-logfile", "-", "--access-logfile", "-", "app.wsgi:application"]
Conclusion
This is good start to your Docker environment for the Django application. In case if your database would be dockerized too (as it in the example repository’s docker-compose.yml) would be good to add “database wait” script to make application’s image entrypoint.sh
more flexible to wait until database will be ready to accept incoming connections before entrypoint.sh
executes migrations, it avoid startup errors while database isn’t started yet, I’ll tell about this in next post.