Shipping containers.
©Unsplash

Category: Hacks

Ship Django project as Docker image.

Recently I needed to deploy inherited Django project. My usual approach would be to ship it as docker container. Since python is not exactly my thing, I googled how to. Unfortunately there was no simple and clean tutorial. So I wrote it (with Gunincorn and Nginx to be production ready).

Tags: DIY.

Let's ship a Django project as a Docker image.

A couple of prerequisites before we start:
- docker installed and running on your machine,
- since my tutorial is minimalistic, it is addressed to people with some experience.

You can begin with existing code, or create a new Django project:
django-admin startproject dockerdjango
In my case file structure looks like this (before adding docker related code):
dockerdjango
├─appsample 
│ └...
├─dockerdjango
│ ├...
│ ├settings.py
│ ├urls.py
│ ├views.py
│ └wsgi.py
├─static
│ └...
├─templates
│ └...
├─db.sglite3
├─manage.py
└─requirements.txt
In my approach, to build a Docker image we need 3 more files:
1. /Dockerfile (/ represents root of your project next to manage.py)
FROM python:3.11.3-slim-bullseye

# install nginx
RUN apt-get update && apt-get install nginx vim -y --no-install-recommends
COPY nginx.site.conf /etc/nginx/sites-available/default
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
    && ln -sf /dev/stderr /var/log/nginx/error.log

# copy source and install dependencies
RUN mkdir -p /opt/app
RUN mkdir -p /opt/app/pip_cache
RUN mkdir -p /opt/app/dockerdjango
COPY requirements.txt server-up.sh /opt/app/
COPY .pip_cache /opt/app/pip_cache/
COPY dockerdjango /opt/app/dockerdjango/
COPY appsample /opt/app/appsample/
COPY static /opt/app/static/
# not default django templates folder (setup in settings.py) 
COPY templates /opt/app/templates/
WORKDIR /opt/app
RUN pip install -r requirements.txt --cache-dir /opt/app/pip_cache
RUN chown -R www-data:www-data /opt/app
RUN chmod 755 server-up.sh

# start server
EXPOSE 8020
STOPSIGNAL SIGTERM
CMD ["/opt/app/server-up.sh"]
2. /nginx.site.conf
# nginx default site config for /etc/nginx/sites-available/default

server {
    listen 8020;
    server_name example.org;

    location / {
        proxy_pass http://127.0.0.1:8010;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    location /static {
        root /opt/app;
    }
}
and bash script to run it
3. /server-up.sh
#!/usr/bin/env bash
# start-up.sh

# Start Gunicorn processes & start the Nginx process and put it in the background
(gunicorn dockerdjango.wsgi --user www-data --bind 0.0.0.0:8010 --workers 3) &
nginx -g "daemon off;"

# more on gunicon https://docs.gunicorn.org/en/stable/deploy.html
# more on nginx in container https://docs.nginx.com/nginx-management-suite/nim/previous-versions/v1/tutorials/containers/
And a few updates to /djangodocker/settings.py
ALLOWED_HOSTS = ['*'] # not default
STATICFILES_DIRS = [  # not default
    BASE_DIR / "static",
]
CSRF_TRUSTED_ORIGINS = ['http://127.0.0.1:8020']  # not default
Your /requirements.txt should contain at least:
django == 4.2.1
gunicorn == 20.1.0
Now you're ready to build the image (type in terminal in root of your project where Dockerfile is located):
docker build -t yourfancytag .
and run a container build out of the image:
docker run -it -p 8020:8020 --name dockerdjango yourfancytag
You should be able to access http://127.0.0.1:8020, in my project: /GitHub link below/ Container ship Later you can fire it without interactive terminal (-it)
docker run dockerdjango

Any issues?

First check your Dockerfile, it happens you're trying to copy or run something that does not exist in your project. Audit static and templates folders, they are not default ones.

Still failing? Compare with my project, it is published on Github: https://github.com/msatbsx/dockerdjango or clone it (due to Cloudflare email obfuscation being unable to switch off in the line below please replace USER with git [at] github.com):
git clone USER:msatbsx/dockerdjango.git
You can also build from my image and explore (it can be pulled from hub.docker.com):
docker run -d -p 8020:8020 --name dd msatbsx/dockerdjango:1
When the container is already created, you can run it interactive and explore inside:
docker exec -it dd bash

Michal

The Ace.
Michal's assistant eye