deploy Django project with docker
Category: programming
Docker is a very popular container and a lot of companies are using it – it provides isolation and flexibility to application development and deployment. Especially when you do not have sudo access and you want to deploy your web application, docker can be very handy.
To get started, you have to have to be able to connect docker daemon and run docker commands – so docker has to be installed and running and you have to be added to the docker group. You could test your access to docker via docker ps
to see the output.
Now let’s say you have a sample Django web application to deploy. The traditional way is to have Nginx + uwsgi + Django + RDBMS (for example, PostgreSQL) and now we are going to do the same with docker and once configured properly, the same script can be just moved to another machine and boot web application with just a command.
To manage things with greater ease, I recommend installing docker-compose which will help to bring up a few connected containers in one way. So pip install docker-compose
. Notice this command does not require sodo access if a virtual environment is created and used.
First let’s understand the structure: Python code will live in a Python container and run by uwsgi; Nginx is responsible for serving static files and forward requests to uwsgi – so Nginx container should be able to access uwsgi port and be able to access static files in Django, and if needed, uploaded files need to be shared between Nginx and Django as well; for data storage and caching, PostgreSQL and redis should be linked to Django as well. Following graph illustrate this process:
Let’s get started with redis and PostgreSQL since they do not have any dependencies.
# postgres Dockerfile
# I recommend created a separate deployment folder and postgres sub folder
# you can change the version of postgres if needed
FROM postgres:9.5
MAINTAINER franklingu
# redis Dockerfile
FROM redis:3.2
MAINTAINER franklingu
And we are done with caching and database – that is 2 out of 4 done!
Let’s move on to Django container. So for production deployment, there are a few things to modified to make sure your app is secure and the most important thing is to turn debug off. Let’s modify settings.py then.
# additional part, placed at bottom of settings.py, not to be overriden
DEBUG = True
DEBUG = False if os.environ.get('DJANGO_DEPLOYMENT') else True
if not DEBUG:
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
# currently no ssl support at the moment
CSRF_COOKIE_SECURE = False
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_HTTPONLY = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'
SITE_URL = 'production_url'
ALLOWED_HOSTS = ['production_url']
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ['POSTGRES_DB'],
'USER': os.environ['POSTGRES_USER'],
'PASSWORD': os.environ['POSTGRES_PASSWORD'],
'HOST': 'postgres',
'PORT': '5432',
'CONN_MAX_AGE': 3600,
}
}
MEDIA_ROOT = '/usr/src/uploads/'
STATIC_ROOT = '/usr/src/static/'
Of course there could be other things you want to do differently in production env, for example, you may want to remove django-extensions and django-debug-toolbar from INSTALLED_APPS.
Notice that in the above script, some os environments variables are used – these need to be set by docker container – we will get back to this later.
Let’s create Dockerfile for Django container:
# Dockerfile for deployment
FROM ubuntu:16.04
# Replace shell with bash so we can source files
RUN rm /bin/sh && ln -s /bin/bash /bin/sh
ENV DEBIAN_FRONTEND noninteractive
# Install all required packages here and do a clean to reduce image size
RUN apt-get update && apt-get install -y build-essential \
libpq-dev libssl-dev libffi-dev python3-pip \
apt-get clean
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY requirements.txt /usr/src/app/
RUN pip3 install -U pip setuptools
RUN pip3 install -r requirements.txt
RUN pip3 install uwsgi
# Copy source code to container
COPY . /usr/src/app/
RUN mkdir -p /var/run/uwsgi \
/var/log/uwsgi \
/var/log/app \
/usr/src/app/run \
/var/data/app
So basically there we are installing necessary system packages, Python packages and create necessary runtime directories for application.
We also need to prepare script and configuration files for uwsgi.
# uwsgi.ini file
[uwsgi]
# Django-related settings
# the base directory (full path)
chdir = /usr/src/app
# Django's wsgi file
module = webapp.wsgi
# process-related settings
# master
master = True
# maximum number of worker processes
processes = 10
# the socket (use the full path to be safe
socket = :8000
# ... with appropriate permissions
chmod-socket = 664
# clear environment on exit
vacuum = True
# set an environment variable
env = DJANGO_SETTINGS_MODULE=doc_store.settings
# create a pidfile
safe-pidfile = /var/run/uwsgi/doc_store.pid
# respawn processes taking more than 20 seconds
harakiri = 20
# limit the project to 128 MB, inactive
# limit-as = 128
# respawn processes after serving 5000 requests
max-requests = 5000
single-interpreter=True
enable-threads=True
#!/usr/bin/env bash
# uwsgi start.sh script
python3 manage.py migrate --noinput
python3 manage.py collectstatic --noinput
/usr/local/bin/uwsgi --ini /usr/src/app/deployment/uwsgi/uwsgi.ini
And for nginx, we need to make the link to Django container is done well, and access to static files and uploaded files are done. So the last step is a bit heavier than previous steps:
# Nginx container Dockerfile
FROM nginx:latest
MAINTAINER franklingu
WORKDIR /
ADD website.conf /
ADD ssl/*.crt /etc/ssl/certs/
ADD ssl/*.key /etc/ssl/private/
ADD start.sh /
RUN chmod +x start.sh
# nginx configuration file. in this case it is named as website.conf
# normally you leave this at the default of 1024
events {
worker_connections 1024;
}
http {
# cf http://blog.maxcdn.com/accept-encoding-its-vary-important/
gzip_vary on;
gzip_proxied any;
gzip_types *;
# http://nginx.org/en/docs/http/configuring_https_servers.html#optimization
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
server_tokens off;
# the upstream component nginx needs to connect to
upstream django {
server webapp:8000;
}
server {
listen 80 default_server;
# the domain name it will serve for
server_name ${NGINX_SERVER_NAME};
charset utf-8;
# max upload size
client_max_body_size 75M;
# Django media
location /media {
alias /usr/src/uploads;
}
location /static {
alias /usr/src/static;
# http://stackoverflow.com/q/19213510/1346257
include /etc/nginx/mime.types;
}
location = /robots.txt { return 200 "User-agent: *\nAllow: /"; }
location = /favicon.ico { access_log off; log_not_found off; return 404; }
#Prevent serving of sysfiles / vim backup files
location ~ /\. { access_log off; log_not_found off; deny all; }
location ~ ~$ { access_log off; log_not_found off; deny all; }
# Finally, send all non-media requests to the Django server.
location / {
uwsgi_pass django;
include uwsgi_params; # the uwsgi_params file you installed
}
}
# configuration of the server
server {
# the port your site will be served on
listen 433 ssl default_server;
# the domain name it will serve for
server_name ${NGINX_SERVER_NAME};
charset utf-8;
ssl_certificate /etc/ssl/certs/${NGINX_CRT_NAME}.crt;
ssl_certificate_key /etc/ssl/private/${NGINX_KEY_NAME}.key;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # ie defaults minus SSLv3
# max upload size
client_max_body_size 75M;
# Django media
location /media {
alias /usr/src/app/uploads;
}
location /static {
alias /usr/src/app/staticfiles;
# http://stackoverflow.com/q/19213510/1346257
include /etc/nginx/mime.types;
}
location = /robots.txt { return 200 "User-agent: *\nAllow: /"; }
location = /favicon.ico { access_log off; log_not_found off; return 404; }
#Prevent serving of sysfiles / vim backup files
location ~ /\. { access_log off; log_not_found off; deny all; }
location ~ ~$ { access_log off; log_not_found off; deny all; }
# Finally, send all non-media requests to the Django server.
location / {
uwsgi_pass django;
include uwsgi_params; # the uwsgi_params file you installed
}
}
}
#!/usr/bin/env bash
# start.sh
envsubst '${NGINX_CRT_NAME} ${NGINX_KEY_NAME} ${NGINX_SERVER_NAME}' < website.conf > /etc/nginx/nginx.conf
nginx -g "daemon off;"
And finally, a docker-compose file to stick all of the things together:
version: '2'
services:
webapp_db:
container_name: webapp_db
build: ./deployment/psql
mem_limit: 1024m
volumes:
- /export/scratch/data/webapp/pg_data:/var/lib/postgresql/data
ports:
- "0.0.0.0:5433:5432"
env_file:
- ./deployment/environ/deployment.env
webapp_redis:
container_name: webapp_redis
build: ./deployment/redis
mem_limit: 2048m
expose:
- "6379"
env_file:
- ./deployment/environ/deployment.env
webapp:
container_name: webapp
build:
context: .
dockerfile: ./Dockerfile.webapp
command: /usr/src/app/deployment/uwsgi/start.sh
mem_limit: 1024m
depends_on:
- webapp_db
- webapp_redis
volumes:
- /usr/src/static
- /usr/src/uploads
expose:
- "8000"
env_file:
- ./deployment/environ/deployment.env
webapp_nginx:
container_name: webapp_nginx
build: ./deployment/nginx
command: /bin/bash start.sh
mem_limit: 1024m
links:
- webapp
depends_on:
- webapp
volumes_from:
- webapp
ports:
- "0.0.0.0:80:80"
- "0.0.0.0:433:433"
env_file:
- ./deployment/environ/deployment.env