I can find many examples of using Django, Gunicorn, and Nginx for an application, and also ways to implement HTTPS on Nginx, but not all together. This post will document my final configurations and explain my decisions.
First, the code:
nginx.conf:
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
## Rewrite http to https
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
## Use https
server {
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; ";
listen 443 ssl;
server_name your.domain.xxx;
ssl_certificate <path-to-certificate>;
ssl_certificate_key <path-to-private-key>;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
location = /favicon.ico { access_log off; log_not_found off; }
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host:443;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 300s;
proxy_pass http://localhost:8000;
}
}
}
gunicorn.py:
##bind = ":8000"
workers = 3
timeout = 300
proc_name = "dupe"
user = "<your user>"
group = "<your group>"
raw_env = []
capture_output = True
loglevel = "debug"
errorlog = "<path-and-name-for-log-file>"
gunicorn.service:
[Unit]
Description=gunicorn daemon
After=network.target
[Service]
Environment=DJANGO_DEBUG=False
Environment=DJANGO_SECRET_KEY=<your SECRET_KEY>
Environment=DJANGO_ALLOWED_HOSTS=[*]
Environment=DJANGO_STATIC_ROOT=<application.path>/staticfiles
Environment=DJANGO_SETTINGS_MODULE=<project>.settings.devl
User=<your user>
Group=<your group>
WorkingDirectory=<application.path>
ExecStart=<virtualenv.path>/bin/gunicorn -c <application.path>/gunicorn.py --bind 0.0.0.0:8000 <project>.wsgi:application
[Install]
WantedBy=multi-user.target
Notes:
nginx.conf holds all of the interesting stuff
The first server section grabs any http traffic coming in on port 80 and redirects it to the https URL on port 443.
Handling https traffic was trickier. We need to pass all of the header settings from the incoming request on to gunicorn/Django at localhost. All of the proxy-set
attribute values in the location section came from a little bit of trial and error.
The setting for proxy-read-timeout
is admittedly high – five minutes! This application has one longer running transaction that was exceeding the 60 second default. This value needs to match the timeout
setting in the gunicorn settings. I’ll probably lower it to a more respectable two minutes before hitting production.
Finally, astute readers will notice that there is no code to handle requests for static resources. This is because I am using Whitenoise to route those calls through Django. This works only because my app just has eight users and Django can handle the load. Anything that is public facing should use Nginx to serve static files directly (or look into using a CDN, etc.)
Not much in gunicorn.py
I’m using 3 workers – plenty for my expected load. The timeout
is set to 300 seconds, which is the same found in nginx.conf above. These should match.
The bind
setting is commented and moved to gunicorn.service so that I can use this file for more than one instance of the application. A second service file can specify a unique port for its copy of gunicorn.
gunicorn.service – Instance specific settings
Since my app is stored in a git repo (on a company server, not github), I don’t want to include any security information like the SECRET_KEY in a versioned file. Therefore, this guy is not included there – although an example file is.
The Environment variables set here are read by Django at startup.
The ExecStart line starts by running the version of gunicorn found in the virtualenv for this instance. Setting up a virtualenv is beyond this scope of this post, but I hope that if you’ve gotten this far you know all about them.
Note that the bind parameter includes the port number used for this instance of gunicorn as mentioned above.
Questions? Comments?
I hope this helps someone setting up a Django app. Please drop a note in the comments with your experience. I’d also invite questions, as well as comments on how I can improve my configuration. Thanks all!