Setting up Docker can sometimes be confusing. There are many little pieces that need to come together for everything to work as expected. Outlined in this post is a simple Docker setup you can use for your Django projects in development and production environments.
TL;DR: Sample project
You can check out the code on GitHub.
Dockerfile
FROM python:3.6-alpine
RUN apk --update add \
build-base \
postgresql \
postgresql-dev \
libpq \
# pillow dependencies
jpeg-dev \
zlib-dev
RUN mkdir /www
WORKDIR /www
COPY requirements.txt /www/
RUN pip install -r requirements.txt
ENV PYTHONUNBUFFERED 1
COPY . /www/
This Dockerfile is pretty straightforward. It starts from a Python-Alpine base image, then installs the dependencies that Django needs, notably postgresql
and postgresql-dev
. This Django project is setup to use PostgreSQL. If you want to use another database engine like MySQL or MongoDB, you need to install the required adapters/dependencies. Also, if you’re dealing with images (ImageField), you need to install jpeg-dev
and zlib-dev
as well (see Pillow dependencies).
Afterwards, it installs the packages in requirements.txt, then copies everything in the project root to /www/
where the image would be executed from. ENV PYTHONUNBUFFERED 1
causes all output to stdout
to be flushed immediately, so that you can easily see what’s going on inside your Python app from a terminal.
docker-compose.yml
I almost always use Docker Compose with Docker. With Compose, you can run multiple containers at once, and you don’t have to memorize long Docker commands.
version: "3"
services:
web:
build: .
restart: on-failure
env_file:
- ./.env
command: python manage.py runserver 0.0.0.0:8000
volumes:
- .:/www
ports:
- "8000:8000"
depends_on:
- db
db:
image: "postgres:10.3-alpine"
restart: unless-stopped
env_file:
- ./.env
ports:
- "5432:5432"
volumes:
- ./postgres/data:/var/lib/postgresql/data
There are two services in this Compose file — web and db. The web service builds the Django app using the Dockerfile in the previous section. A volume is created to map the project directory in the host to the one in the container (- .:/www
) so that any changes made on the host are mirrored in the container. The command
parameter uses Django’s dev server to run the app (python manage.py runserver 0.0.0.0:8000
).
The db service uses a PostgreSQL image and maps PostgreSQL’s data directory to ./postgres/data
in the host, so that DB data is persisted even if the container gets destroyed. This very PostgreSQL image (the official postgreSQL image) uses several environment variables to configure PostgreSQL, including POSTGRES_USER
, POSTGRES_PASSWORD
and POSTGRES_DB
.
# .env
DEBUG=1
ALLOWED_HOSTS=*
SECRET_KEY=secret123
POSTGRES_HOST=db
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=secret123
POSTGRES_DB=mypgsqldb
The above variables are made available to the services using the env_file
parameter. The postgres variables are used by both the web and db service (see the settings.py file in the sample project).
docker-compose.prod.yml
My production configs usually differ a bit from their development counterpart. I use Gunicorn to serve the Django app, and NGINX as a reverse proxy and static/media file server.
This setup, while good for production, is not very convenient in development where the goal is often to break things and move fast. Sometimes, I decide to use a managed database service so there’s no need for a Dockerized database server. All these are reflected in my docker-compose.prod.yml file.
version: "3"
services:
web:
build: .
restart: on-failure
env_file:
- ./.env
command: gunicorn --bind 0.0.0.0:8080 dockerizeddjango.wsgi
ports:
- "8080:8080"
depends_on:
- nginx
nginx:
image: "nginx"
restart: always
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./staticfiles:/static
- ./mediafiles:/media
ports:
- "80:80"
The web service in the production Compose file looks identical to one in the development file, except for the command
parameter (majorly). It uses Gunicorn (which is more suitable) to serve the application (gunicorn — bind 0.0.0.0:8080 dockerizeddjango.wsgi
).
There’s also an NGINX service. Notice the volume definitions in this service. The first volume maps /etc/nginx/conf.d
to the ./nginx/conf.d
folder on the host which contains dockerizeddjango.conf.
# dockerizeddjango.conf
server {
listen 80;
server_name localhost;
# serve static files
location /static/ {
alias /static/;
}
# serve media files
location /media/ {
alias /media/;
}
# pass requests for dynamic content to gunicorn
location / {
proxy_pass http://web:8080;
}
}
This is a very basic Nginx config file. It’s also pretty self-explanatory. It passes requests with /static
(e.g example.com/static/virus.js) and /media
(e.g example.com/media/anonymous.jpg) to the /static
and /media
directories on the web container respectively. These directories are mapped to the /staticfiles
and /mediafiles
directories on the host where staticfiles are collected and media files uploaded. Other requests are proxied to Gunicorn.
Did you observe the use of service names in some files? e.g POSTGRES_HOST=db
and proxy_pass http://web:8080;
. db and web resolve to the IP addresses of their containers. This is handled by Docker Compose so that we don’t have to worry about what the IP address of a container is or hardcode IP addresses that might change later.
Use the following command to run the Compose file:
docker-compose -f docker-compose.prod.yml up --build -d
The -f
parameter specifies the production Compose file, docker-compose.prod.yml (Docker Compose defaults to docker-compose.yml). --build
tells Compose to rebuild the images each time the command is run, and the -d
flag runs the containers in detached mode so that they keep running in the background even when your terminal is closed.
You can run migrations and create an admin user with the following commands:
docker-compose -f docker-compose.prod.yml run web python manage.py migrate
docker-compose -f docker-compose.prod.yml run web python manage.py collectstatic --noinput
Last modified on 2023-03-14