{"id":495,"date":"2016-11-06T09:47:47","date_gmt":"2016-11-06T14:47:47","guid":{"rendered":"https:\/\/dashdrum.com\/blog\/?p=495"},"modified":"2016-11-23T14:26:59","modified_gmt":"2016-11-23T19:26:59","slug":"django-gunicorn-nginx-https","status":"publish","type":"post","link":"https:\/\/dashdrum.com\/blog\/2016\/11\/django-gunicorn-nginx-https\/","title":{"rendered":"Django, Gunicorn, Nginx, &#038; HTTPS"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/dashdrum.com\/blog\/wp-content\/uploads\/2016\/11\/dguh.png\" alt=\"Django, gunicorn, nginx, and https logos\" width=\"800\" height=\"800\" class=\"alignnone size-full wp-image-505\" srcset=\"https:\/\/dashdrum.com\/blog\/wp-content\/uploads\/2016\/11\/dguh.png 800w, https:\/\/dashdrum.com\/blog\/wp-content\/uploads\/2016\/11\/dguh-150x150.png 150w, https:\/\/dashdrum.com\/blog\/wp-content\/uploads\/2016\/11\/dguh-300x300.png 300w, https:\/\/dashdrum.com\/blog\/wp-content\/uploads\/2016\/11\/dguh-768x768.png 768w\" sizes=\"auto, (max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px\" \/>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. \u00a0This post will document my final configurations and explain my decisions.<\/p>\n<h2>First, the code:<\/h2>\n<h3>nginx.conf:<\/h3>\n<pre><code>worker_processes  auto;\r\n\r\nevents {\r\n    worker_connections  1024;\r\n}\r\n\r\n\r\nhttp {\r\n    include       mime.types;\r\n    default_type  application\/octet-stream;\r\n    sendfile        on;\r\n    keepalive_timeout  65;\r\n\r\n    ## Rewrite http to https\r\n\r\n    server {\r\n        listen 80 default_server;\r\n        listen [::]:80 default_server;\r\n        server_name _;\r\n        return 301 https:\/\/$host$request_uri;\r\n    }\r\n\r\n    ## Use https\r\n\r\n    server {\r\n        add_header Strict-Transport-Security \"max-age=63072000; includeSubdomains; \";\r\n\r\n        listen              443 ssl;\r\n        server_name         your.domain.xxx;\r\n        ssl_certificate     &lt;path-to-certificate&gt;;\r\n        ssl_certificate_key &lt;path-to-private-key&gt;;\r\n        ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;\r\n        ssl_ciphers         HIGH:!aNULL:!MD5;\r\n\r\n        location = \/favicon.ico { access_log off; log_not_found off; }\r\n\r\n\r\n        location \/ {\r\n            proxy_set_header Host               $host;\r\n            proxy_set_header X-Real-IP          $remote_addr;\r\n            proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;\r\n            proxy_set_header X-Forwarded-Host   $host:443;\r\n            proxy_set_header X-Forwarded-Server $host;\r\n            proxy_set_header X-Forwarded-Port   443;\r\n            proxy_set_header X-Forwarded-Proto  https;\r\n            proxy_read_timeout 300s; \r\n            proxy_pass http:\/\/localhost:8000;\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<h3>gunicorn.py:<\/h3>\n<pre><code>##bind = \":8000\"\r\nworkers = 3\r\ntimeout = 300\r\nproc_name = \"dupe\"\r\nuser = \"&lt;your user&gt;\"\r\ngroup = \"&lt;your group&gt;\"\r\nraw_env = []\r\ncapture_output = True\r\nloglevel = \"debug\"\r\nerrorlog = \"&lt;path-and-name-for-log-file&gt;\"<\/code><\/pre>\n<h3>gunicorn.service:<\/h3>\n<pre><code>[Unit]\r\nDescription=gunicorn daemon\r\nAfter=network.target\r\n\r\n[Service]\r\nEnvironment=DJANGO_DEBUG=False\r\nEnvironment=DJANGO_SECRET_KEY=&lt;your SECRET_KEY&gt;\r\nEnvironment=DJANGO_ALLOWED_HOSTS=[*]\r\nEnvironment=DJANGO_STATIC_ROOT=&lt;application.path&gt;\/staticfiles\r\nEnvironment=DJANGO_SETTINGS_MODULE=&lt;project&gt;.settings.devl\r\nUser=&lt;your user&gt;\r\nGroup=&lt;your group&gt;\r\nWorkingDirectory=&lt;application.path&gt;\r\nExecStart=&lt;virtualenv.path&gt;\/bin\/gunicorn -c &lt;application.path&gt;\/gunicorn.py  --bind 0.0.0.0:8000 &lt;project&gt;.wsgi:application\r\n\r\n[Install]\r\nWantedBy=multi-user.target<\/code><\/pre>\n<h2>Notes:<\/h2>\n<h3>nginx.conf holds all of the interesting stuff<\/h3>\n<p>The first server section grabs any http traffic coming in on port 80 and redirects it to the https URL on port 443.  <\/p>\n<p>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 <code>proxy-set<\/code> attribute values in the location section came from a little bit of trial and error.<\/p>\n<p>The setting for <code>proxy-read-timeout<\/code> is admittedly high &#8211; five minutes! This application has one longer running transaction that was exceeding the 60 second default. This value needs to match the <code>timeout<\/code> setting in the gunicorn settings. I&#8217;ll probably lower it to a more respectable two minutes before hitting production.<\/p>\n<p>Finally, astute readers will notice that there is no code to handle requests for static resources. This is because I am using <a href=\"http:\/\/whitenoise.evans.io\/en\/stable\/\" title=\"Whitenoise Docs\">Whitenoise<\/a> 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.)<\/p>\n<h3>Not much in gunicorn.py<\/h3>\n<p>I&#8217;m using 3 workers &#8211; plenty for my expected load.  The <code>timeout<\/code> is set to 300 seconds, which is the same found in nginx.conf above.  These should match.<\/p>\n<p>The <code>bind<\/code> 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.<\/p>\n<h3>gunicorn.service &#8211; Instance specific settings<\/h3>\n<p>Since my app is stored in a git repo (on a company server, not github), I don&#8217;t want to include any security information like the SECRET_KEY in a versioned file. Therefore, this guy is not included there &#8211; although an example file is.<\/p>\n<p>The Environment variables set here are read by Django at startup.<\/p>\n<p>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&#8217;ve gotten this far you know all about them.<\/p>\n<p>Note that the bind parameter includes the port number used for this instance of gunicorn as mentioned above.<\/p>\n<h2>Questions? Comments?<\/h2>\n<p>I hope this helps someone setting up a Django app. Please drop a note in the comments with your experience.  I&#8217;d also invite questions, as well as comments on how I can improve my configuration.  Thanks all!<br \/>\n&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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. \u00a0This 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 &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/dashdrum.com\/blog\/2016\/11\/django-gunicorn-nginx-https\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Django, Gunicorn, Nginx, &#038; HTTPS&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-495","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/dashdrum.com\/blog\/wp-json\/wp\/v2\/posts\/495","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dashdrum.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dashdrum.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dashdrum.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dashdrum.com\/blog\/wp-json\/wp\/v2\/comments?post=495"}],"version-history":[{"count":10,"href":"https:\/\/dashdrum.com\/blog\/wp-json\/wp\/v2\/posts\/495\/revisions"}],"predecessor-version":[{"id":513,"href":"https:\/\/dashdrum.com\/blog\/wp-json\/wp\/v2\/posts\/495\/revisions\/513"}],"wp:attachment":[{"href":"https:\/\/dashdrum.com\/blog\/wp-json\/wp\/v2\/media?parent=495"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dashdrum.com\/blog\/wp-json\/wp\/v2\/categories?post=495"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dashdrum.com\/blog\/wp-json\/wp\/v2\/tags?post=495"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}