Deploying to Production

TODO: Document Jenkins CI/CD setup.

Serving Static Files

When developing (settings.DEBUG = True), django serves up static content. If we are only running our API service, there are no static files to serve as the only content is JSON, however, with the addition of the openapi documentation and Swagger-UI we will need to collect and serve up the static files with a web server.

Following are some examples of how to do this with a Docker setup and with Apache httpd. TODO: Also describe how to do this with, e.g., AWS Elastic Beanstalk, Lambda, etc.


Making a Docker Image

We’ll make a Docker image for myapp with:

docker build -t myapp:latest .

But there are a number of extra steps, so I’ve added them to the tox.ini. Just do a tox -e docker and the right thing will happen. Here’s what I’ve added which:

  1. bundles myapp.json

  2. builds a new wheel.

  3. builds the docker image.

  4. saves the image as a tar file so it can be docker image loaded elsewhere if needed.

+deps =
+whitelist_externals =
+    docker
+    npm
+    swagger-ui-watcher
+setenv =
+    DJANGO_SETTINGS_MODULE = training.settings
+commands =
+    npm install swagger-ui-watcher
+    /bin/cp docs/schemas/jsonapi.yaml .
+    swagger-ui-watcher -b myapp/static/openapi/myapp.json docs/schemas/myapp.yaml
+    /bin/rm -f jsonapi.yaml
+    /bin/rm -rf dist
+    python bdist_wheel
+    docker build -t myapp:latest .
+    docker image save -o myapp-docker.tar myapp:latest
+    /usr/bin/printf '\n\033[0;31m copy the tar to the docker host and do docker image load -i myapp-docker.tar\033[0m\n'


We’ll use docker-compose to combine an nginx web server and our django app (served by gunicorn) using traefik as the “edge router” to allow them to both listen on various paths below https://localhost/ (technically this could be done with Apache httpd or nginx too, but traefik does some docker magic).

See docker-compose.yml for the gory details.

Just do a docker-compose up and away you go (I’ve used –no-start –force-recreate and start/stop):

(env) django-training$ docker-compose up --no-start --force-recreate
Recreating django-training_nginx_1 ... done
Recreating django-training_demoapp_1 ... done
Recreating django-training_traefik_1 ... done
(env) django-training$ docker-compose start
Starting nginx   ... done
Starting demoapp ... done
Starting traefik ... done
(env) django-training$ docker-compose stop
Stopping django-training_traefik_1 ... done
Stopping django-training_demoapp_1 ... done
Stopping django-training_nginx_1   ... done
(env) django-training$ 

Apache httpd mod_wsgi

Our preferred environment for running Django apps is with mod_wsgi under Apache httpd. At CUIT, this is generally done with Puppet. For simplicity, I’ll just show some sample files:


# The WSGI Apache module configuration file is being
# managed by Puppet. Any changes will be overwritten.
<IfModule mod_wsgi.c>
  WSGISocketPrefix /var/run/wsgi
  WSGIPythonHome "/var/www/django-jsonapi-training/env"
  WSGIPythonPath "/var/www/django-jsonapi-training/env/lib/python3.6/site-packages"


LoadModule wsgi_module modules/


# ************************************
# Vhost template in module puppetlabs-apache
# Managed by Puppet
# ************************************

<VirtualHost *:8443>

  ## Vhost docroot
  DocumentRoot "/var/www/django-jsonapi-training"

  ## Directories, there should at least be a declaration for /var/www/django-jsonapi-training

  <Directory "/var/www/django-jsonapi-training">
    Options Indexes FollowSymLinks MultiViews
    AllowOverride All
    Order allow,deny
    Allow from all

  ## Logging
  ErrorLog "/var/log/httpd/"
  ServerSignature Off
  CustomLog "/var/log/httpd/" "virtualhost_snat" 

  ## SSL directives
  SSLEngine on
  SSLCertificateFile      "/etc/pki/tls/certs/localhost.crt"
  SSLCertificateKeyFile   "/etc/pki/tls/private/localhost.key"
  SSLCertificateChainFile "/etc/pki/tls/certs/localhost.csr"
  SSLCACertificatePath    "/etc/pki/tls/certs"
  SSLCACertificateFile    "/etc/pki/tls/certs/intermediateCAbundle.crt"
  WSGIDaemonProcess django-jsonapi-training
  WSGIProcessGroup django-jsonapi-training
  WSGIScriptAlias / "/var/www/django-jsonapi-training/"
  WSGIPassAuthorization On
  WSGIChunkedRequest On

and /var/www/django-jsonapi-training/

Generated by Puppet. DO NOT EDIT.

WSGI config for django-jsonapi-training project.

It exposes the WSGI callable as a module-level variable named ``application``.

import sys
import site
import os

# Calculate path to site-packages directory.
python_home = "/var/www/django-jsonapi-training/env"
python_version = ".".join(map(str, sys.version_info[:2]))
site_packages = python_home + "/lib/python%s/site-packages" % python_version

# Add the site-packages directory.

from django.core.wsgi import get_wsgi_application

os.environ["DJANGO_SETTINGS_MODULE"] = "training.settings"
os.environ["DJANGO_SECRET_KEY"] = "123456789012345687890"
os.environ["DJANGO_DEBUG"] = "false"
os.environ["DJANGO_SQLSERVER"] = "true"
os.environ["DJANGO_SQLSERVER_DB"] = "mydb"
os.environ["DJANGO_SQLSERVER_USER"] = "myuser"
os.environ["DJANGO_SQLSERVER_PASS"] = "mypass"
os.environ["DJANGO_SQLSERVER_HOST"] = "mydbhost"
os.environ["OAUTH2_SERVER"] = ""
os.environ["RESOURCE_SERVER_ID"] = "demo_resource_server"
os.environ["RESOURCE_SERVER_SECRET"] = "wL0pgS5RcNOgdOSSmejzZNA605d3MtkoXMVSDaJxmaTU70XnYQPOabBAYtfkWXay"

application = get_wsgi_application()

Make careful note to have WSGIPassAuthorization On or the Authorization header will not be passed through to the Django app. Alternatively, look into mod_auth_openidc and use the REMOTE_USER. I have not tested this approach. It depends on whether your backend server needs to introspect the Bearer token.