<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Matt's Dev Blog - DevOps</title><link href="https://mattsegal.dev/" rel="alternate"></link><link href="https://mattsegal.dev/feeds/devops.atom.xml" rel="self"></link><id>https://mattsegal.dev/</id><updated>2020-07-31T12:00:00+10:00</updated><entry><title>A breakdown of how NGINX is configured with Django</title><link href="https://mattsegal.dev/nginx-django-reverse-proxy-config.html" rel="alternate"></link><published>2020-07-31T12:00:00+10:00</published><updated>2020-07-31T12:00:00+10:00</updated><author><name>Matthew Segal</name></author><id>tag:mattsegal.dev,2020-07-31:/nginx-django-reverse-proxy-config.html</id><summary type="html">&lt;p&gt;You are trying to deploy your Django web app to the internet.
You have never done this before, so you follow a guide like &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu-16-04"&gt;this one&lt;/a&gt;.
The guide gives you many instructions, which includes installing and configuring an "NGINX reverse proxy".
At some point you mutter to yourself:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What-the-hell is …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;You are trying to deploy your Django web app to the internet.
You have never done this before, so you follow a guide like &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu-16-04"&gt;this one&lt;/a&gt;.
The guide gives you many instructions, which includes installing and configuring an "NGINX reverse proxy".
At some point you mutter to yourself:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What-the-hell is an NGINX? Eh, whatever, let's keep reading.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You will have to copy-paste some weird gobbledygook into a file, which looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# NGINX site config file at /etc/nginx/sites-available/myproject&lt;/span&gt;
&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;server_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;foo.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_redirect&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://foo.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/static/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;root&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/home/myuser/myproject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;What is all this stuff? What is it supposed to do?&lt;/p&gt;
&lt;p&gt;Most people do their first Django deployment as a learning exercise.
You want to understand what you are doing, so that you can fix problems if you get stuck
and so you don't need to rely on guides in the future.
In this post I'll break down the elements of this NGINX config and how it ties in with Django,
so that you can confidently debug, update and extend it in the future.&lt;/p&gt;
&lt;h2&gt;What is this file supposed to achieve?&lt;/h2&gt;
&lt;p&gt;This scary-looking config file sets up NGINX so that it acts as the entrypoint to your Django application.
Explaining &lt;em&gt;why&lt;/em&gt; you might choose to use NGINX is a topic too expansive for this post, so I'm just going to stick to explaining
how it works.&lt;/p&gt;
&lt;p&gt;NGINX is completely separate program to your Django app.
It is running inside its own process, while Django is running inside a &lt;a href="https://mattsegal.dev/simple-django-deployment-2.html#wsgi"&gt;WSGI server&lt;/a&gt; process, such as Gunicorn.
In this post I will sometimes refer to Gunicorn and Django interchangeably.&lt;/p&gt;
&lt;p&gt;&lt;img alt="nginx as a separate process" src="https://mattsegal.dev/nginx-separate-process.png"&gt;&lt;/p&gt;
&lt;p&gt;All HTTP requests that hit your Django app have to go through NGINX first.&lt;/p&gt;
&lt;p&gt;&lt;img alt="nginx proxy" src="https://mattsegal.dev/nginx-proxy.png"&gt;&lt;/p&gt;
&lt;p&gt;NGINX listens for incoming HTTP requests on port 80 and HTTPS requests on port 443. 
When a new request comes in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NGINX looks at the request, checks some rules, and sends it on to your WSGI server, which is usually listening on localhost, port 8000&lt;/li&gt;
&lt;li&gt;Your Django app will process the request and eventually produce a response&lt;/li&gt;
&lt;li&gt;Your WSGI server will send the response back to NGINX; and then&lt;/li&gt;
&lt;li&gt;NGINX will send the response back out to the original requesting client&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also configure NGINX to serve static files, like images, directly from the filesystem, so that requests for these assets don't need to go through Django&lt;/p&gt;
&lt;p&gt;&lt;img alt="nginx proxy with static files" src="https://mattsegal.dev/nginx-static-proxy.png"&gt;&lt;/p&gt;
&lt;p&gt;You can adjust the rules in NGINX so that it selectively routes requests to multiple app servers. You could, for example, run a Wordpress site and a Django app from the same server:&lt;/p&gt;
&lt;p&gt;&lt;img alt="nginx multi proxy" src="https://mattsegal.dev/nginx-multi-proxy.png"&gt;&lt;/p&gt;
&lt;p&gt;Now that you have a general idea of what NGINX is supposed to do, let's go over the config file that makes this happen.&lt;/p&gt;
&lt;h2&gt;Server block&lt;/h2&gt;
&lt;p&gt;The top level block in the NGINX config file is the &lt;a href="https://docs.nginx.com/nginx/admin-guide/web-server/web-server/#setting-up-virtual-servers"&gt;virtual server&lt;/a&gt;.
The main utility of virtual servers is that they allow you to sort incoming requests based on the port and hostname. 
Let's start by looking at a basic server block:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Listen on port 80 for incoming requests.&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Return status code 200 with text &amp;quot;Hello World&amp;quot;.&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;Hello&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;World&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let me show you some example requests. Say we're on the same server as NGINX and we send a GET request using the command line tool &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;curl localhost
&lt;span class="c1"&gt;# Hello World&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This &lt;code&gt;curl&lt;/code&gt; command sends the following &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages"&gt;HTTP request&lt;/a&gt; to localhost, port 80:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/&lt;/span&gt; &lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;localhost&lt;/span&gt;
&lt;span class="na"&gt;User-Agent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;curl/7.58.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We will get the following HTTP response back from NGINX, with a 200 OK status code and "Hello World" in the body:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;application/octet-stream&lt;/span&gt;
&lt;span class="na"&gt;Content-Length&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;11&lt;/span&gt;

Hello World
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We can also request some random path and we get the same result:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;curl localhost/some/path/on/website
&lt;span class="c1"&gt;# Hello World&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With &lt;code&gt;curl&lt;/code&gt; sending this HTTP request: &lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/some/path/on/website&lt;/span&gt; &lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;localhost&lt;/span&gt;
&lt;span class="na"&gt;User-Agent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;curl/7.58.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and we get back the same response as before:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;application/octet-stream&lt;/span&gt;
&lt;span class="na"&gt;Content-Length&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;11&lt;/span&gt;

Hello World
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Simple so far, but not very interesting, let's start to mix it up with multiple server blocks.&lt;/p&gt;
&lt;h2&gt;Multiple virtual servers&lt;/h2&gt;
&lt;p&gt;You can add more than one virtual server in NGINX:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# All requests to foo.com return a 200 OK status code&lt;/span&gt;
&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;server_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;foo.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;Welcome&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;foo.com!&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;

&lt;span class="c1"&gt;# Any other requests get a 404 Not Found page&lt;/span&gt;
&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;default_server&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;NGINX uses the &lt;code&gt;server_name&lt;/code&gt; directive to check the &lt;code&gt;Host&lt;/code&gt; header of incoming requests and match the request to a virtual server. Your web browser will usually set this header automatically for you.
You can set up a particular virtual server to be the default choice (&lt;code&gt;default_server&lt;/code&gt;) if no other ones match the incoming request. You can use this feature to host multiple
Django apps on a single server. All you need to do is &lt;a href="https://mattsegal.dev/dns-for-noobs.html"&gt;set up your DNS&lt;/a&gt; to get multiple domain names to point to a single server, and then add a virtual server for each Django app.&lt;/p&gt;
&lt;p&gt;Let's test out the config above. If send a request to &lt;code&gt;localhost&lt;/code&gt;, we'll get a 404 status code from the default server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;curl localhost
&lt;span class="c1"&gt;# &amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;#   &amp;lt;head&amp;gt;&amp;lt;title&amp;gt;404 Not Found&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;#   ...&lt;/span&gt;
&lt;span class="c1"&gt;# &amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is the request that gets sent:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/&lt;/span&gt; &lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;localhost&lt;/span&gt;
&lt;span class="na"&gt;User-Agent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;curl/7.58.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Our request was matched to the default server because the &lt;code&gt;Host&lt;/code&gt; header we sent didn't match &lt;code&gt;foo.com&lt;/code&gt;. Let's try setting the &lt;code&gt;Host&lt;/code&gt; header to &lt;code&gt;foo.com&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;curl localhost --header &lt;span class="s2"&gt;&amp;quot;Host: foo.com&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# Welcome to foo.com!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is the request that gets sent:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/&lt;/span&gt; &lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;foo.com&lt;/span&gt;
&lt;span class="na"&gt;User-Agent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;curl/7.58.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now are directed to the &lt;code&gt;foo.com&lt;/code&gt; virtual server because we sent the correct &lt;code&gt;Host&lt;/code&gt; header in our request.
Finally, we can see that setting a random &lt;code&gt;Host&lt;/code&gt; header sends us to the default server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;curl localhost --header &lt;span class="s2"&gt;&amp;quot;Host: fasfsadfs.com&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# &amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;#   &amp;lt;head&amp;gt;&amp;lt;title&amp;gt;404 Not Found&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;#   ...&lt;/span&gt;
&lt;span class="c1"&gt;# &amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There's &lt;a href="https://docs.nginx.com/nginx/admin-guide/web-server/web-server/#setting-up-virtual-servers"&gt;more&lt;/a&gt; that you can do with virtual servers in NGINX,
but what we've covered so far should be enough for you to understand their typical usage with Django. &lt;/p&gt;
&lt;div class="ui divider" style="margin: 1.5em 0;"&gt;&lt;/div&gt;
&lt;form action="https://dev.us19.list-manage.com/subscribe/post?u=e7a1ec466f7bb1732dbd23fc7&amp;amp;id=ec345473bd" method="post" name="mc-embedded-subscribe-form" target="_blank" style="text-align: center; padding-bottom: 1em;" novalidate&gt;
  &lt;h3 class="subscribe-cta"&gt;Get alerted when I publish new blog posts&lt;/h3&gt;
  &lt;div class="ui fluid action input subscribe"&gt;
    &lt;input
      type="email"
      value=""
      name="EMAIL"
      placeholder="Enter your email address"
    /&gt;
    &lt;button class="ui primary button" type="submit" name="subscribe"&gt;
      Subscribe
    &lt;/button&gt;
  &lt;/div&gt;
  &lt;div style="position: absolute; left: -5000px;" aria-hidden="true"&gt;
    &lt;input
      type="text"
      name="b_e7a1ec466f7bb1732dbd23fc7_ec345473bd"
      tabindex="-1"
      value=""
    /&gt;
  &lt;/div&gt;
&lt;/form&gt;
&lt;div class="ui divider" style="margin: 1.5em 0;"&gt;&lt;/div&gt;

&lt;h2&gt;Location blocks&lt;/h2&gt;
&lt;p&gt;Within a virtual server you can route the request based on the path.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Requests to the root path get a 200 OK response&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;Cool!&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Requests to /forbidden get 403 Forbidden response&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/forbidden&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Under this configuration, any requested path that matches &lt;code&gt;/forbidden&lt;/code&gt; will return a 403 Forbidden status code, and everything else will return &lt;em&gt;Cool!&lt;/em&gt; Let's try it out:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;curl localhost
&lt;span class="c1"&gt;# Cool!&lt;/span&gt;
curl localhost/blah/blah/blah
&lt;span class="c1"&gt;# Cool!&lt;/span&gt;
curl localhost/forbidden
&lt;span class="c1"&gt;# &amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;# &amp;lt;head&amp;gt;&amp;lt;title&amp;gt;403 Forbidden&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="c1"&gt;# &amp;lt;/html&amp;gt;&lt;/span&gt;

curl localhost/forbidden/blah/blah/blah
&lt;span class="c1"&gt;# &amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;# &amp;lt;head&amp;gt;&amp;lt;title&amp;gt;403 Forbidden&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="c1"&gt;# &amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now that we've covered &lt;code&gt;server&lt;/code&gt; and &lt;code&gt;location&lt;/code&gt; blocks it should be easier to make sense of some of the config that I showed you at the start of this post:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;server_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;foo.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# Do something...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/static/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# Do something...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next we'll dig into the connection between NGINX and our WSGI server.&lt;/p&gt;
&lt;h2&gt;Reverse proxy location&lt;/h2&gt;
&lt;p&gt;As mentioned earlier, NGINX acts as a &lt;a href="https://en.wikipedia.org/wiki/Reverse_proxy#:~:text=In%20computer%20networks%2C%20a%20reverse,from%20the%20proxy%20server%20itself."&gt;reverse proxy&lt;/a&gt; for Django:&lt;/p&gt;
&lt;p&gt;&lt;img alt="nginx proxy" src="https://mattsegal.dev/nginx-proxy.png"&gt;&lt;/p&gt;
&lt;p&gt;This reverse proxy setup is configured within this location block:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;proxy_pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;proxy_redirect&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://foo.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In the next few sections I will break down the directives in this block so that you understand what is going on. 
You might also find the NGINX documentation on &lt;a href="https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/"&gt;reverse proxies&lt;/a&gt; helpful for understanding this config.&lt;/p&gt;
&lt;h2&gt;Proxy pass&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;proxy_pass&lt;/code&gt; directive tells NGINX to send all requests for that location to the specified address.
For example, if your WSGI server was running on localhost (which has IP 127.0.0.1), port 8000, then you would use this config:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can also point &lt;code&gt;proxy_pass&lt;/code&gt; at a &lt;a href="https://en.wikipedia.org/wiki/Unix_domain_socket#:~:text=A%20Unix%20domain%20socket%20or,the%20same%20host%20operating%20system."&gt;Unix domain socket&lt;/a&gt;, with Gunicorn listening on that socket, which is very similar to using localhost except it doesn't use up a port number and it's a bit faster:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://unix:/home/user/my-socket-file.sock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Seems simple enough - you just point NGINX at your WSGI server, so... what was all that other crap? Why do you set &lt;code&gt;proxy_set_header&lt;/code&gt; and &lt;code&gt;proxy_redirect&lt;/code&gt;? That's what we'll discuss next.&lt;/p&gt;
&lt;h2&gt;NGINX is lying to you&lt;/h2&gt;
&lt;p&gt;As a reverse proxy, NGINX will receive HTTP requests from clients and then send those requests to our Gunicorn WSGI server.
The problem is that NGINX hides information from our WSGI server. The HTTP request that Gunicorn receives is not the same as the one that NGINX received from the client.&lt;/p&gt;
&lt;p&gt;&lt;img alt="nginx hiding info" src="https://mattsegal.dev/nginx-hide-info.png"&gt;&lt;/p&gt;
&lt;p&gt;Let me give you an example, which is illustrated above. You, the client, have an IP of &lt;code&gt;12.34.56.78&lt;/code&gt; and you go to &lt;code&gt;https://foo.com&lt;/code&gt; in your web browser and try to load the page. The request hits the server on port 443 and is read by NGINX. At this stage, NGINX knows that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the protocol is &lt;a href="https://www.cloudflare.com/learning/ssl/what-is-https/"&gt;HTTPS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;the client has an IP address of &lt;code&gt;12.34.56.78&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;the request is for the host &lt;code&gt;foo.com&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;NGINX then sends the request onwards to Gunicorn. When Gunicorn receives this request, it thinks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the protocol is HTTP, not HTTPS, because the connection between NGINX and Gunicorn is not encrypted&lt;/li&gt;
&lt;li&gt;the client has the IP address &lt;code&gt;127.0.0.1&lt;/code&gt;, because that's the address NGINX is using&lt;/li&gt;
&lt;li&gt;the host is &lt;code&gt;127.0.0.1:8000&lt;/code&gt; because NGINX said so&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some of this lost information is useful, and we want to force NGINX to send it to our WSGI server. That's what these lines are for:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="k"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="k"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next, I will explain each line in more detail.&lt;/p&gt;
&lt;h2&gt;Setting the Host header&lt;/h2&gt;
&lt;p&gt;Django would like to know the value of the &lt;code&gt;Host&lt;/code&gt; header so that various bits of the framework, like &lt;a href="https://docs.djangoproject.com/en/3.0/ref/settings/#allowed-hosts"&gt;ALLOWED_HOSTS&lt;/a&gt; or &lt;a href="https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.HttpRequest.get_host"&gt;HttpRequest.get_host&lt;/a&gt; can work. The problem is that NGINX does not pass the &lt;code&gt;Host&lt;/code&gt; header to proxied servers by default.&lt;/p&gt;
&lt;p&gt;For example, when I'm using &lt;code&gt;proxy_pass&lt;/code&gt; like I did in the previous section, and I send a request with the &lt;code&gt;Host&lt;/code&gt; header to NGINX like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;curl localhost --header &lt;span class="s2"&gt;&amp;quot;Host: foo.com&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then NGINX receives the HTTP request, which looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/&lt;/span&gt; &lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;foo.com&lt;/span&gt;
&lt;span class="na"&gt;User-Agent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;curl/7.58.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and then NGINX sends a HTTP request to your WSGI server, like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/&lt;/span&gt; &lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.0&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;127.0.0.1:8000&lt;/span&gt;
&lt;span class="na"&gt;User-Agent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;curl/7.58.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notice something? That rat-fuck-excuse-for-a-webserver sent different headers to our WSGI server!
I'm sure there is a good reason for this behaviour, but it's not what we want because it breaks some Django functionality.
We can fix this by using the &lt;code&gt;proxy_set_header&lt;/code&gt; as follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# Ensure original Host header is forwarded to our Django app.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now NGINX will send the desired headers to Django:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/&lt;/span&gt; &lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.0&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;foo.com&lt;/span&gt;
&lt;span class="na"&gt;User-Agent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;curl/7.58.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Gunicorn will read this &lt;code&gt;Host&lt;/code&gt; header and provide it to you in your Django views via the &lt;code&gt;request.META&lt;/code&gt; object:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# views.py&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;my_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;META&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;HTTP_HOST&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Eg. &amp;quot;foo.com&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Got host &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Setting the X-Forwarded-Whatever headers&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;Host&lt;/code&gt; header isn't the only useful information that NGINX does not pass to Gunicorn. We would also like the protocol and source IP address of the client request
to be passed to our WSGI server. We achieve this with these two lines:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="k"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I just want to point out that these header names are completely arbitrary. You can send any header you want with the format &lt;code&gt;X-Insert-Words-Here&lt;/code&gt; to Gunicorn and it will parse it and send it onwards to Django. For example, you could set the header to be &lt;code&gt;X-Matt-Is-Cool&lt;/code&gt; as follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Matt-Is-Cool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;it&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;true&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now NGINX will include this header with every request it sends to Gunicorn. When Gunicorn parses the HTTP request it reads &lt;strong&gt;any&lt;/strong&gt; header with the format &lt;code&gt;X-Insert-Words-Here&lt;/code&gt; into a Python dictionary, which ends up in the &lt;code&gt;HttpRequest&lt;/code&gt; object that Django passes to your view. So in this case, &lt;code&gt;X-Matt-Is-Cool&lt;/code&gt; gets turned into the key &lt;code&gt;HTTP_X_MATT_IS_COOL&lt;/code&gt; in your request object. For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# views.py&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;my_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Prints value of X-Matt-Is-Cool header included by NGINX&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;META&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;HTTP_X_MATT_IS_COOL&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# it is true&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Hello World&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This means you can add in whatever custom headers you like to your NGINX config, but for now let's focus on getting the protocol and client IP address to your Django app.&lt;/p&gt;
&lt;h2&gt;Setting the X-Forwarded-Proto header&lt;/h2&gt;
&lt;p&gt;Django sometimes needs to know whether the incoming request is secure (HTTPS) or not (HTTP). For example, some features of the &lt;a href="https://docs.djangoproject.com/en/3.0/ref/middleware/#http-strict-transport-security"&gt;SecurityMiddleware&lt;/a&gt; class checks for HTTPS. The problem is, of course, that NGINX is &lt;em&gt;always&lt;/em&gt; telling Django that the client's request to the sever is not secure, even when it is.  This problem always crops up for me when I'm implementing pagination, and the "next" URL has &lt;code&gt;http://&lt;/code&gt; instead of &lt;code&gt;https://&lt;/code&gt; like it should.  &lt;/p&gt;
&lt;p&gt;Our fix for this is to put the client request protocol into a header called &lt;code&gt;X-Forwarded-Proto&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then you need to set up the &lt;a href="https://docs.djangoproject.com/en/3.0/ref/settings/#secure-proxy-ssl-header"&gt;SECURE_PROXY_SSL_HEADER&lt;/a&gt; setting to read this header in your &lt;code&gt;settings.py&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;SECURE_PROXY_SSL_HEADER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;HTTP_X_FORWARDED_PROTO&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;https&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now Django can tell the difference between incoming HTTP requests and HTTPS requests. &lt;/p&gt;
&lt;h2&gt;Setting the X-Forwarded-For header&lt;/h2&gt;
&lt;p&gt;Now let's talk about determining the client's IP address. As mentioned before, NGINX will always lie to you and say that the client IP address is &lt;code&gt;127.0.0.1&lt;/code&gt;.
If you don't care about client IP addresses, then you don't care about this header. You don't need to set it if you don't want to. Knowing the client IP might be useful sometimes. For example, if you want to guess at where they are located, or if you are building one of those &lt;a href="https://www.expressvpn.com/what-is-my-ip"&gt;&lt;em&gt;What's My IP?&lt;/em&gt;&lt;/a&gt; websites:&lt;/p&gt;
&lt;p&gt;&lt;img alt="some website knows my ip address" src="https://mattsegal.dev/my-ip.png"&gt;&lt;/p&gt;
&lt;p&gt;You can set the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For"&gt;X-Forwarded-For&lt;/a&gt; header to tell Gunicorn the original IP address of the client: &lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;As described earlier, the header &lt;code&gt;X-Forwarded-For&lt;/code&gt; gets turned into the key &lt;code&gt;HTTP_X_FORWARDED_FOR&lt;/code&gt; in your request object. For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# views.py&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;my_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Prints client IP address: &amp;quot;12.34.56.78&amp;quot;&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;META&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;HTTP_X_FORWARDED_FOR&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="c1"&gt;# Prints NGINX IP address: &amp;quot;127.0.0.1&amp;quot;, ie. localhost&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;META&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;REMOTE_ADDR&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Hello World&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Does this seem kind of underwhelming? Maybe a little pointless? As I said before, if you don't care about client IP addresses, then this header isn't for you.&lt;/p&gt;
&lt;h2&gt;Proxy redirect&lt;/h2&gt;
&lt;p&gt;Let's cover the final line of the Django reverse proxy config: &lt;code&gt;proxy_redirect&lt;/code&gt;.
The NGINX docs for this directive are &lt;a href="http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_redirect"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_redirect&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://foo.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This directive is used when handling redirects that are issued by Django.
For example, you might have a webpage that used to live at path &lt;code&gt;old/page/&lt;/code&gt;, but you moved it to &lt;code&gt;new/page/&lt;/code&gt;.
You want to send any user that asked for &lt;code&gt;old/page/&lt;/code&gt; to &lt;code&gt;new/page/&lt;/code&gt;. 
To achieve this you could write a Django view like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# view.py&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;redirect_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;HttpResponseRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;new/page/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When a user asks for &lt;code&gt;old/page/&lt;/code&gt;, this view will send them a HTTP response with a 302 redirect status code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;302&lt;/span&gt; &lt;span class="ne"&gt;Found&lt;/span&gt;
&lt;span class="na"&gt;Location&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;new/page/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Your web browser will follow the &lt;code&gt;Location&lt;/code&gt; response header to the new page.
A problem occurs when your Django app includes the WSGI server's address and port in the &lt;code&gt;Location&lt;/code&gt; header:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;302&lt;/span&gt; &lt;span class="ne"&gt;Found&lt;/span&gt;
&lt;span class="na"&gt;Location&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;http://127.0.0.1:8000/new/page/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is a problem because the client's browser will try to go to that address, and it will fail because the WSGI server is not
on the same server as the client.&lt;/p&gt;
&lt;p&gt;Here's the thing: I have never actually seen this happen, and I'm having trouble thinking of a common scenario where this would happen.
Send me an email if you know where this issue crops up. Anyway, using &lt;code&gt;proxy_redirect&lt;/code&gt; helps in the hypothetical case where Django does include the WSGI address
in a redirect's &lt;code&gt;Location&lt;/code&gt; header.&lt;/p&gt;
&lt;p&gt;The directive rewrites the header using the syntax:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_redirect&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;redirect&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;replacement&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;So, for example, if there was a redirect response like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;302&lt;/span&gt; &lt;span class="ne"&gt;Found&lt;/span&gt;
&lt;span class="na"&gt;Location&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;http://127.0.0.1:8000/new/page/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and you set up your &lt;code&gt;proxy_redirect&lt;/code&gt; like this &lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_redirect&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://foo.com/blog/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;then the outgoing response would be re-written to this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;302&lt;/span&gt; &lt;span class="ne"&gt;Found&lt;/span&gt;
&lt;span class="na"&gt;Location&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;https://foo.com/blog/new/page/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I guess this directive might be useful in some situations? I'm not really sure.&lt;/p&gt;
&lt;h2&gt;Static block&lt;/h2&gt;
&lt;p&gt;Earlier I mentioned that NGINX can serve static files directly from the filesystem.&lt;/p&gt;
&lt;p&gt;&lt;img alt="nginx proxy with static files" src="https://mattsegal.dev/nginx-static-proxy.png"&gt;&lt;/p&gt;
&lt;p&gt;This is a good idea because NGINX is much more efficient at doing this than your WSGI server will be.
It means that your server will be able to respond faster to static file request and handle more traffic.
You can use &lt;a href="https://docs.djangoproject.com/en/3.0/howto/static-files/deployment/#serving-static-files-in-production"&gt;this technique&lt;/a&gt; to put all of your
Django app's static files into a folder like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;/home/myuser/myproject 
└─ static               Your static files
    ├─ styles.css       CSS file
    ├─ main.js          JavaScript file
    └─ cat.png          A picture of a cat
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then you can set the &lt;code&gt;/static/&lt;/code&gt; location to serve files directly from this folder: &lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/static/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;root&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/home/myuser/myproject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now a request to &lt;code&gt;http://localhost/static/cat.png&lt;/code&gt; will cause NGINX to read from &lt;code&gt;/home/myuser/myproject/static/cat.png&lt;/code&gt;, without sending a request to the WSGI server.&lt;/p&gt;
&lt;h2&gt;Next steps&lt;/h2&gt;
&lt;p&gt;Now you know what every line of your Django app's NGINX config is doing.
Hopefully you will be able to use this knowledge to debug issues faster and customise your existing setup.
If you have specific questions that weren't covered by this post, I recommend looking at the official NGINX documentation &lt;a href="https://docs.nginx.com/nginx/admin-guide/web-server/web-server/"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you liked this post then you might also like reading some other stuff I've written:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://mattsegal.dev/simple-django-deployment.html"&gt;A simple guide to deploying a Django app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mattsegal.dev/django-prod-architectures.html"&gt;An overview of Django server setups&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mattsegal.dev/django-gunicorn-nginx-logging.html"&gt;How to manage logs with Django, Gunicorn and NGINX&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A mini rant on Django performance: &lt;a href="https://mattsegal.dev/is-django-too-slow.html"&gt;Is Django too slow?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A little series on Postgres database backups &lt;a href="https://mattsegal.dev/postgres-backup-and-restore.html"&gt;1&lt;/a&gt;, &lt;a href="https://mattsegal.dev/postgres-backup-automate.html"&gt;2&lt;/a&gt;, &lt;a href="https://mattsegal.dev/restore-django-local-database.html"&gt;3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you found some of the stuff about HTTP in this post confusing, I heartily recommend checking out Brian Will's "The Internet" videos to learn more about what HTTP, TCP, and ports are: &lt;a href="https://www.youtube.com/watch?v=DTQV7_HwF58"&gt;part 1&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=3fvUc2Dzr04&amp;amp;t=167s"&gt;part 2&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=_55PyDw0lGU"&gt;part 3&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=yz3lkSqioyU"&gt;part 4&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And, of course, if you want to get updates on any new posts I write, you can subscribe to my blog's mailing list below.&lt;/p&gt;</content><category term="DevOps"></category></entry><entry><title>How to automate your Postgres database backups</title><link href="https://mattsegal.dev/postgres-backup-automate.html" rel="alternate"></link><published>2020-06-05T12:00:00+10:00</published><updated>2020-06-05T12:00:00+10:00</updated><author><name>Matthew Segal</name></author><id>tag:mattsegal.dev,2020-06-05:/postgres-backup-automate.html</id><summary type="html">&lt;p&gt;If you've got a web app running in production, then you'll want to take &lt;a href="https://mattsegal.dev/postgres-backup-and-restore.html"&gt;regular database backups&lt;/a&gt;, or else you risk losing all your data. Taking these backups manually is fine, but it's easy to forget to do it. It's better to remove the chance of human error and automate …&lt;/p&gt;</summary><content type="html">&lt;p&gt;If you've got a web app running in production, then you'll want to take &lt;a href="https://mattsegal.dev/postgres-backup-and-restore.html"&gt;regular database backups&lt;/a&gt;, or else you risk losing all your data. Taking these backups manually is fine, but it's easy to forget to do it. It's better to remove the chance of human error and automate the whole process. To automate your backup and restore you will need three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A safe place to store your backup files&lt;/li&gt;
&lt;li&gt;A script that creates the backups and uploads them to the safe place&lt;/li&gt;
&lt;li&gt;A method to automatically run the backup script every day&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;A safe place for your database backup files&lt;/h3&gt;
&lt;p&gt;You don't want to store your backup files on the same server as your database. If your database server gets deleted, then you'll lose your backups as well. Instead, you should store your backups somewhere else, like a hard drive, your PC, or in the cloud.&lt;/p&gt;
&lt;p&gt;I like using cloud object storage for this kind of use-case. If you haven't heard of "object storage" before: it's just a kind of cloud service where you can store a bunch of files. All major cloud providers offer this service:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Amazon's AWS has the &lt;a href="https://aws.amazon.com/s3/"&gt;Simple Storage Service (S3)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Microsoft's Azure has &lt;a href="https://azure.microsoft.com/en-us/services/storage/"&gt;Storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Google Cloud also has &lt;a href="https://cloud.google.com/storage"&gt;Storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;DigitalOcean has &lt;a href="https://www.digitalocean.com/products/spaces/"&gt;Spaces&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These object storage services are &lt;em&gt;very&lt;/em&gt; cheap at around 2c/GB/month, you'll never run out of disk space, they're easy to access from command line tools and they have very fast upload/download speeds, especially to/from other services hosted with the same cloud provider. I use these services a lot: this blog is being served from AWS S3.&lt;/p&gt;
&lt;p&gt;I like using S3 simply because I'm quite familiar with it, so that's what we're going to use for the rest of this post. If you're not already familiar with using the AWS command-line, then check out this post I wrote about &lt;a href="https://mattsegal.dev/aws-s3-intro.html"&gt;getting started with AWS S3&lt;/a&gt; before you continue.&lt;/p&gt;
&lt;h3&gt;Creating a database backup script&lt;/h3&gt;
&lt;p&gt;In my &lt;a href="https://mattsegal.dev/postgres-backup-and-restore.html"&gt;previous post on database backups&lt;/a&gt; I showed you a small script to automatically take a backup using PostgreSQL:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c1"&gt;# Backs up mydatabase to a file.&lt;/span&gt;
&lt;span class="nv"&gt;TIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date &lt;span class="s2"&gt;&amp;quot;+%s&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;BACKUP_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;postgres_&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PGDATABASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TIME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pgdump&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Backing up &lt;/span&gt;&lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
pg_dump --format&lt;span class="o"&gt;=&lt;/span&gt;custom &amp;gt; &lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Backup completed for &lt;/span&gt;&lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I'm going to assume you have set up your Postgres database environment variables (&lt;code&gt;PGHOST&lt;/code&gt;, etc) either in the script, or elsewhere, as mentioned in the previous post.
Next we're going to get our script to upload all backups to AWS S3.&lt;/p&gt;
&lt;h3&gt;Uploading backups to AWS Simple Storage Service (S3)&lt;/h3&gt;
&lt;p&gt;We will be uploading our backups to S3 with the &lt;code&gt;aws&lt;/code&gt; command line (CLI) tool. To get this tool to work, we need to set up our AWS credentials on the server by either using &lt;code&gt;aws configure&lt;/code&gt; or by setting the environment variables &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; and &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;. Once that's done we can use &lt;code&gt;aws s3 cp&lt;/code&gt; to upload our backup files. Let's say we're using a bucket called "&lt;code&gt;mydatabase-backups&lt;/code&gt;":&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c1"&gt;# Backs up mydatabase to a file and then uploads it to AWS S3.&lt;/span&gt;
&lt;span class="c1"&gt;# First, dump database backup to a file&lt;/span&gt;
&lt;span class="nv"&gt;TIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date &lt;span class="s2"&gt;&amp;quot;+%s&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;BACKUP_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;postgres_&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PGDATABASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TIME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pgdump&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Backing up &lt;/span&gt;&lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
pg_dump --format&lt;span class="o"&gt;=&lt;/span&gt;custom &amp;gt; &lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt;

&lt;span class="c1"&gt;# Second, copy file to AWS S3&lt;/span&gt;
&lt;span class="nv"&gt;S3_BUCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;s3://mydatabase-backups
&lt;span class="nv"&gt;S3_TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$S3_BUCKET&lt;/span&gt;/&lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Copying &lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="nv"&gt;$S3_TARGET&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
aws s3 cp &lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt; &lt;span class="nv"&gt;$S3_TARGET&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Backup completed for &lt;/span&gt;&lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You should be able to run this multiple times and see a new backup appear in your S3 bucket's webpage every time you do it. As a bonus, you can add a little one liner at the end of your script that checks for the last uploaded file to the S3 bucket:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;BACKUP_RESULT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;aws s3 ls &lt;span class="nv"&gt;$S3_BUCKET&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; tail -n &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Latest S3 backup: &lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_RESULT&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Once you're confident that your backup script works, we can move on to getting it to run every day.&lt;/p&gt;
&lt;h3&gt;Running cron jobs&lt;/h3&gt;
&lt;p&gt;Now we need to get our server to run this script every day, even when we're not around. The simplest way to do this is on a Linux server is with &lt;a href="https://en.wikipedia.org/wiki/Cron"&gt;cron&lt;/a&gt;. Cron can automatically run scripts for us on a schedule. We'll be using the &lt;code&gt;crontab&lt;/code&gt; tool to set up our backup job.&lt;/p&gt;
&lt;p&gt;You can read more about how to use crontab &lt;a href="https://linuxize.com/post/scheduling-cron-jobs-with-crontab/"&gt;here&lt;/a&gt;. If you find that you're having issues setting up cron, you might also find this &lt;a href="https://serverfault.com/questions/449651/why-is-my-crontab-not-working-and-how-can-i-troubleshoot-it"&gt;StackOverflow post&lt;/a&gt; useful.&lt;/p&gt;
&lt;p&gt;Before we set up our daily database backup job, I suggest trying out a test script to make sure that your cron setup is working. For example, this script prints the current time when it is run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;$(&lt;/span&gt;date&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Using &lt;code&gt;nano&lt;/code&gt;, you can create a new file called &lt;code&gt;~/test.sh&lt;/code&gt;, save it, then make it executable as follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;nano ~/test.sh
&lt;span class="c1"&gt;# Write out the time printing script in nano, save the file.&lt;/span&gt;
chmod +x ~/test.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then you can test it out a little by running it a couple of times to check that it is printing the time:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;~/test.sh
&lt;span class="c1"&gt;# Sat Jun  6 08:05:14 UTC 2020&lt;/span&gt;
~/test.sh
&lt;span class="c1"&gt;# Sat Jun  6 08:05:14 UTC 2020&lt;/span&gt;
~/test.sh
&lt;span class="c1"&gt;# Sat Jun  6 08:05:14 UTC 2020&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Once you're confident that your test script works, you can create a cron job to run it every minute. Cron uses a special syntax to specifiy how often a job runs. These "cron expressions" are a pain to write by hand, so I use &lt;a href="https://crontab.cronhub.io/"&gt;this tool&lt;/a&gt; to generate them. The cron expression for "every minute" is the inscrutable string "&lt;code&gt;* * * * *&lt;/code&gt;". This is the crontab entry that we're going to use:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Test crontab entry&lt;/span&gt;
&lt;span class="nv"&gt;SHELL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/bin/bash
* * * * * ~/test.sh &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&amp;gt;&amp;gt; ~/time.log
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;SHELL&lt;/code&gt; setting tells crontab to use bash to execute our command&lt;/li&gt;
&lt;li&gt;The "&lt;code&gt;* * * * *&lt;/code&gt;" entry tells cron to execute our command every minute&lt;/li&gt;
&lt;li&gt;The command &lt;code&gt;~/test.sh &amp;amp;&amp;gt;&amp;gt; ~/time.log&lt;/code&gt; runs our test script &lt;code&gt;~/test.sh&lt;/code&gt; and then appends all output to a log file called &lt;code&gt;~/time.log&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Enter the text above into your user's crontab file using the crontab editor:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;crontab -e
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Once you've saved your entry, you should then be able to view your crontab entry using the list command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;crontab -l
&lt;span class="c1"&gt;# SHELL=/bin/bash&lt;/span&gt;
&lt;span class="c1"&gt;# * * * * * ~/test.sh &amp;amp;&amp;gt;&amp;gt; ~/time.log&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can check that cron is actually trying to run your script by watching the system log:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;tail -f /var/log/syslog &lt;span class="p"&gt;|&lt;/span&gt; grep CRON
&lt;span class="c1"&gt;# Jun  6 11:17:01 swarm CRON[6908]: (root) CMD (~/test.sh &amp;amp;&amp;gt;&amp;gt; ~/time.log)&lt;/span&gt;
&lt;span class="c1"&gt;# Jun  6 11:17:01 swarm CRON[6908]: (root) CMD (~/test.sh &amp;amp;&amp;gt;&amp;gt; ~/time.log)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can also watch your logfile to see that time is being written every minute:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;tail -f time.log
&lt;span class="c1"&gt;# Sat Jun 6 11:34:01 UTC 2020&lt;/span&gt;
&lt;span class="c1"&gt;# Sat Jun 6 11:35:01 UTC 2020&lt;/span&gt;
&lt;span class="c1"&gt;# Sat Jun 6 11:36:01 UTC 2020&lt;/span&gt;
&lt;span class="c1"&gt;# Sat Jun 6 11:37:01 UTC 2020&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Once you're happy that you can run a test script every minute with cron, we can move on to running your database backup script daily.&lt;/p&gt;
&lt;h3&gt;Running our backup script daily&lt;/h3&gt;
&lt;p&gt;Now we're nearly ready to run our backup script using a cron job. There are a few changes that we'll need to make to our existing setup. First we need to write our database backup script to &lt;code&gt;~/backup.sh&lt;/code&gt; and make sure it is executable:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;chmod +x ~/backup.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then we need to crontab entry to run every day, which will be "&lt;a href="https://crontab.cronhub.io/"&gt;&lt;code&gt;0 0 * * *&lt;/code&gt;&lt;/a&gt;", and update our cron command to run our backup script. Our new crontab entry should be:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Database backup crontab entry&lt;/span&gt;
&lt;span class="nv"&gt;SHELL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/bin/bash
&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; * * * ~/backup.sh &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&amp;gt;&amp;gt; ~/backup.log
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Update your crontab with &lt;code&gt;crontab -e&lt;/code&gt;. Now we wait! This script should run every night at midnight (server time) to take your database backups and upload them to AWS S3. If this isn't working, then change your cron expression so that it runs the script every minute, and use the steps I showed above to try and debug the problem.&lt;/p&gt;
&lt;p&gt;Hopefully it all runs OK and you will have plenty of daily database backups to roll back to if anything ever goes wrong.&lt;/p&gt;
&lt;h3&gt;Automatic restore from the latest backup&lt;/h3&gt;
&lt;p&gt;When disaster strikes and you need your backups, you could manually view your S3 bucket, download the backup file, upload it to the server and manual run the restore, which I documented in my &lt;a href="https://mattsegal.dev/postgres-backup-and-restore.html"&gt;previous post&lt;/a&gt;. This is totally fine, but as a bonus I thought it would be nice to include a script that automatically downloads the latest backup file and uses it to restore your database. This kind of script would be ideal for dumping production data into a test server. First I'll show you the script, then I'll explain how it works:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;quot;\nRestoring database &lt;/span&gt;&lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt;&lt;span class="s2"&gt; from S3 backups&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Find the latest backup file&lt;/span&gt;
&lt;span class="nv"&gt;S3_BUCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;s3://mydatabase-backups
&lt;span class="nv"&gt;LATEST_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;aws s3 ls &lt;span class="nv"&gt;$S3_BUCKET&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; awk &lt;span class="s1"&gt;&amp;#39;{print $4}&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sort &lt;span class="p"&gt;|&lt;/span&gt; tail -n &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;quot;\nFound file &lt;/span&gt;&lt;span class="nv"&gt;$LATEST_FILE&lt;/span&gt;&lt;span class="s2"&gt; in bucket &lt;/span&gt;&lt;span class="nv"&gt;$S3_BUCKET&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Restore from the latest backup file&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;quot;\nRestoring &lt;/span&gt;&lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt;&lt;span class="s2"&gt; from &lt;/span&gt;&lt;span class="nv"&gt;$LATEST_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;S3_TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$S3_BUCKET&lt;/span&gt;/&lt;span class="nv"&gt;$LATEST_FILE&lt;/span&gt;
aws s3 cp &lt;span class="nv"&gt;$S3_TARGET&lt;/span&gt; - &lt;span class="p"&gt;|&lt;/span&gt; pg_restore --dbname &lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt; --clean --no-owner
&lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;quot;\nRestore completed&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I've assumed that all the Postgres environment variables (&lt;code&gt;PGHOST&lt;/code&gt;, etc) are already set elsewhere.&lt;/p&gt;
&lt;p&gt;There are three tasks that are done in this script:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;finding the latest backup file in S3&lt;/li&gt;
&lt;li&gt;downloading the backup file&lt;/li&gt;
&lt;li&gt;restoring from the backup file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the first part of this script is finding the latest database backup file. The way we know which file is the latest is because of the Unix timestamp which we added to the filename. The first command we use is &lt;code&gt;aws s3 ls&lt;/code&gt;, which shows us all the files in our backup bucket:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;aws s3 ls &lt;span class="nv"&gt;$S3_BUCKET&lt;/span&gt;
&lt;span class="c1"&gt;# 2019-04-04 10:04:58     112309 postgres_mydatabase_1554372295.pgdump&lt;/span&gt;
&lt;span class="c1"&gt;# 2019-04-06 07:48:53     112622 postgres_mydatabase_1554536929.pgdump&lt;/span&gt;
&lt;span class="c1"&gt;# 2019-04-14 07:24:02     113484 postgres_mydatabase_1555226638.pgdump&lt;/span&gt;
&lt;span class="c1"&gt;# 2019-05-06 11:37:39     115805 postgres_mydatabase_1557142655.pgdump&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We then use &lt;code&gt;awk&lt;/code&gt; to isolate the filename. &lt;code&gt;awk&lt;/code&gt; is a text processing tool which I use occasionally, along with &lt;code&gt;cut&lt;/code&gt; and &lt;code&gt;sed&lt;/code&gt; to mangle streams of text into the shape I want. I hate them all, but they can be useful.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;aws s3 ls &lt;span class="nv"&gt;$S3_BUCKET&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; awk &lt;span class="s1"&gt;&amp;#39;{print $4}&amp;#39;&lt;/span&gt;
&lt;span class="c1"&gt;# postgres_mydatabase_1554372295.pgdump&lt;/span&gt;
&lt;span class="c1"&gt;# postgres_mydatabase_1554536929.pgdump&lt;/span&gt;
&lt;span class="c1"&gt;# postgres_mydatabase_1555226638.pgdump&lt;/span&gt;
&lt;span class="c1"&gt;# postgres_mydatabase_1557142655.pgdump&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We then run &lt;code&gt;sort&lt;/code&gt; over this output to ensure that each line is sorted by the time. The aws CLI tool seems to sort this data by the uploaded time, but we want to use &lt;em&gt;our&lt;/em&gt; timestamp, just in case a file was manually uploaded out-of-order:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;aws s3 ls &lt;span class="nv"&gt;$S3_BUCKET&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; awk &lt;span class="s1"&gt;&amp;#39;{print $4}&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sort
&lt;span class="c1"&gt;# postgres_mydatabase_1554372295.pgdump&lt;/span&gt;
&lt;span class="c1"&gt;# postgres_mydatabase_1554536929.pgdump&lt;/span&gt;
&lt;span class="c1"&gt;# postgres_mydatabase_1555226638.pgdump&lt;/span&gt;
&lt;span class="c1"&gt;# postgres_mydatabase_1557142655.pgdump&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We use &lt;code&gt;tail&lt;/code&gt; to grab the last line of the output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;aws s3 ls &lt;span class="nv"&gt;$S3_BUCKET&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; awk &lt;span class="s1"&gt;&amp;#39;{print $4}&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sort &lt;span class="p"&gt;|&lt;/span&gt; tail -n &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="c1"&gt;# postgres_mydatabase_1557142655.pgdump&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And there's our filename! We use the &lt;code&gt;$()&lt;/code&gt; &lt;a href="http://www.tldp.org/LDP/abs/html/commandsub.html"&gt;command-substituation&lt;/a&gt; thingy to capture the command output and store it in a variable:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;LATEST_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;aws s3 ls &lt;span class="nv"&gt;$S3_BUCKET&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; awk &lt;span class="s1"&gt;&amp;#39;{print $4}&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sort &lt;span class="p"&gt;|&lt;/span&gt; tail -n &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$LATEST_FILE&lt;/span&gt;
&lt;span class="c1"&gt;# postgres_mydatabase_1557142655.pgdump&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And that's part one of our script done: find the latest backup file. Now we need to download that file and use it to restore our database. We use the &lt;code&gt;aws&lt;/code&gt; CLI to copy backup file from S3 and stream the bytes into stdout. This literally prints out your whole backup file into the terminal:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;S3_TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$S3_BUCKET&lt;/span&gt;/&lt;span class="nv"&gt;$LATEST_FILE&lt;/span&gt;
aws s3 cp &lt;span class="nv"&gt;$S3_TARGET&lt;/span&gt; -
&lt;span class="c1"&gt;# xtshirt9.5.199.5.19k0ENCODINENCODING&lt;/span&gt;
&lt;span class="c1"&gt;# SET client_encoding = &amp;#39;UTF8&amp;#39;;&lt;/span&gt;
&lt;span class="c1"&gt;# false00&lt;/span&gt;
&lt;span class="c1"&gt;# ... etc ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;-&lt;/code&gt; symbol is commonly used in shell scripting to mean "write to stdout". This isn't very useful on it's own, but we can send that data to the &lt;code&gt;pg_restore&lt;/code&gt; command via a pipe:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;S3_TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$S3_BUCKET&lt;/span&gt;/&lt;span class="nv"&gt;$LATEST_FILE&lt;/span&gt;
aws s3 cp &lt;span class="nv"&gt;$S3_TARGET&lt;/span&gt; - &lt;span class="p"&gt;|&lt;/span&gt; pg_restore --dbname &lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt; --clean --no-owner
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And that's the whole script!&lt;/p&gt;
&lt;h3&gt;Next steps&lt;/h3&gt;
&lt;p&gt;Now you can set up automated backups for your Postgres database. Hopefully having these daily backups this will take a weight off your mind. Don't forget to do a test restore every now and then, because backups are worthless if you aren't confident that they actually work.&lt;/p&gt;
&lt;p&gt;If you want to learn more about the Unix shell tools I used in this post, then I recommend having a go at the &lt;a href="https://overthewire.org/"&gt;Over the Wire Wargames&lt;/a&gt;, which teaches you about bash scripting and hacking at the same time.&lt;/p&gt;</content><category term="DevOps"></category></entry><entry><title>An introduction to cloud file storage</title><link href="https://mattsegal.dev/aws-s3-intro.html" rel="alternate"></link><published>2020-06-05T11:00:00+10:00</published><updated>2020-06-05T11:00:00+10:00</updated><author><name>Matthew Segal</name></author><id>tag:mattsegal.dev,2020-06-05:/aws-s3-intro.html</id><summary type="html">&lt;p&gt;Sometimes when you're running a web app you will find that you have a lot of files on your server. All these files will start to feel like a burden. You might worry about losing them all if the server fails, or you might be concerned about running out of …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Sometimes when you're running a web app you will find that you have a lot of files on your server. All these files will start to feel like a burden. You might worry about losing them all if the server fails, or you might be concerned about running out of disk space. You might even have multiple servers that all need to access these files.&lt;/p&gt;
&lt;p&gt;Wouldn't it be nice if solving all these issues were someone else's problem? You would pay a few cents a month so that you never need to think about this again, right? I like using cloud object storage for hosting most of my web app's files and backups. If you haven't heard of "object storage" before: it's just a kind of cloud service where you can store a bunch of files. All major cloud providers offer this service:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Amazon's AWS has the &lt;a href="https://aws.amazon.com/s3/"&gt;Simple Storage Service (S3)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Microsoft's Azure has &lt;a href="https://azure.microsoft.com/en-us/services/storage/"&gt;Storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Google Cloud also has &lt;a href="https://cloud.google.com/storage"&gt;Storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;DigitalOcean has &lt;a href="https://www.digitalocean.com/products/spaces/"&gt;Spaces&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These object storage services are &lt;em&gt;very&lt;/em&gt; cheap at around 2c/GB/month, you'll never run out of disk space, they're easy to access from command line tools and they have very fast upload/download speeds, especially to/from other services hosted with the same cloud provider. I use these services a lot: this blog is being served from AWS S3.&lt;/p&gt;
&lt;p&gt;I like using S3 simply because I'm quite familiar with it, so that's what we're going to use for the rest of this post. The other services are probably great as well. This video will take you through how to get started with AWS S3.&lt;/p&gt;
&lt;div class="yt-embed"&gt;
    &lt;iframe 
        src="https://www.youtube.com/embed/b-icwbsGZkc" 
        frameborder="0" 
        allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" 
        allowfullscreen
    &gt;
    &lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;As an update to this video: AWS also ships a self-contained CLI tool that doesn't need to be installed in a virtual environment, which you can read about &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html"&gt;here&lt;/a&gt;. Eg:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip&amp;quot;&lt;/span&gt;
curl &lt;span class="nv"&gt;$URL&lt;/span&gt; -o &lt;span class="s2"&gt;&amp;quot;awscliv2.zip&amp;quot;&lt;/span&gt;
unzip awscliv2.zip
sudo ./aws/install
aws --version
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;One great use-case for object storage like AWS S3 is hosting your &lt;a href="https://mattsegal.dev/postgres-backup-automate.html"&gt;database backups&lt;/a&gt;.&lt;/p&gt;</content><category term="DevOps"></category></entry><entry><title>How to backup and restore a Postgres database</title><link href="https://mattsegal.dev/postgres-backup-and-restore.html" rel="alternate"></link><published>2020-06-04T12:00:00+10:00</published><updated>2020-06-04T12:00:00+10:00</updated><author><name>Matthew Segal</name></author><id>tag:mattsegal.dev,2020-06-04:/postgres-backup-and-restore.html</id><summary type="html">&lt;p&gt;You've deployed your Django web app to to the internet. Grats! Now you have a fun new problem: your app's database is full of precious "live" data, and if you lose that data, it's gone forever. If your database gets blown away or corrupted, then you will need backups to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;You've deployed your Django web app to to the internet. Grats! Now you have a fun new problem: your app's database is full of precious "live" data, and if you lose that data, it's gone forever. If your database gets blown away or corrupted, then you will need backups to restore your data. This post will go over how to backup and restore PostgreSQL, which is the database most commonly deployed with Django.&lt;/p&gt;
&lt;p&gt;Not everyone needs backups. If your Django app is just a hobby project then losing all your data might not be such a big deal. That said, if your app is a critical part of a business, then losing your app's data could literally mean the end of the business - people losing their jobs and going bankrupt. So, at least some of time, you don't want to lose all your data.&lt;/p&gt;
&lt;p&gt;The good news is that backing up and restoring Postgres is pretty easy, you only need two commands: &lt;code&gt;pg_dump&lt;/code&gt; and &lt;code&gt;pg_restore&lt;/code&gt;. If you're using MySQL instead of Postgres, then you can do something very similar to the instructions in this post using &lt;a href="https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html"&gt;&lt;code&gt;mysqldump&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Taking database backups&lt;/h3&gt;
&lt;p&gt;I'm going to assume that you've already got a Postgres database running somewhere. You'll need to run the following code from a &lt;code&gt;bash&lt;/code&gt; shell on a Linux machine that can access the database. In this example, let's say you're logged into the database server with &lt;code&gt;ssh&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The first thing to do is set some &lt;a href="https://www.postgresql.org/docs/current/libpq-envars.html"&gt;Postgres-specifc environment variables&lt;/a&gt; to specify your target database and login credentials. This is mostly for our convenience later on.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# The server Postgres is running on&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PGHOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;localhost
&lt;span class="c1"&gt;# The port Postgres is listening on&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PGPORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;5432&lt;/span&gt;
&lt;span class="c1"&gt;# The database you want to back up&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PGDATABASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mydatabase
&lt;span class="c1"&gt;# The database user you are logging in as&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PGUSER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myusername
&lt;span class="c1"&gt;# The database user&amp;#39;s password&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mypassw0rd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can test these environment variables by running a &lt;a href="https://www.postgresql.org/docs/current/app-psql.html"&gt;&lt;code&gt;psql&lt;/code&gt;&lt;/a&gt; command to list all the tables in your app's database.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;psql -c &lt;span class="s2"&gt;&amp;quot;\dt&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Output:&lt;/span&gt;
&lt;span class="c1"&gt;# List of relations&lt;/span&gt;
&lt;span class="c1"&gt;# Schema | Name          | Type  | Owner&lt;/span&gt;
&lt;span class="c1"&gt;#--------+---------------+-------+--------&lt;/span&gt;
&lt;span class="c1"&gt;# public | auth_group    | table | myusername&lt;/span&gt;
&lt;span class="c1"&gt;# public | auth_group... | table | myusername&lt;/span&gt;
&lt;span class="c1"&gt;# public | auth_permi... | table | myusername&lt;/span&gt;
&lt;span class="c1"&gt;# public | django_adm... | table | myusername&lt;/span&gt;
&lt;span class="c1"&gt;# .. etc ..&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If &lt;code&gt;psql&lt;/code&gt; is missing you can install it on Ubuntu or Debian using &lt;code&gt;apt&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo apt install postgresql-client
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now we're ready to create a database dump with &lt;a href="https://www.postgresql.org/docs/12/app-pgdump.html"&gt;&lt;code&gt;pg_dump&lt;/code&gt;&lt;/a&gt;. It's pretty simple to use because we set up those environment variables earlier. When you run &lt;code&gt;pg_dump&lt;/code&gt;, it just spits out a bunch of SQL statements as hundreds, or even thousands of lines of text. You can take a look at the output using &lt;code&gt;head&lt;/code&gt; to view the first 10 lines of text:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pg_dump &lt;span class="p"&gt;|&lt;/span&gt; head

&lt;span class="c1"&gt;# Output:&lt;/span&gt;
&lt;span class="c1"&gt;# --&lt;/span&gt;
&lt;span class="c1"&gt;# -- PostgreSQL database dump&lt;/span&gt;
&lt;span class="c1"&gt;# --&lt;/span&gt;
&lt;span class="c1"&gt;# -- Dumped from database version 9.5.19&lt;/span&gt;
&lt;span class="c1"&gt;# -- Dumped by pg_dump version 9.5.19&lt;/span&gt;
&lt;span class="c1"&gt;# SET statement_timeout = 0;&lt;/span&gt;
&lt;span class="c1"&gt;# SET lock_timeout = 0;&lt;/span&gt;
&lt;span class="c1"&gt;# SET client_encoding = &amp;#39;UTF8&amp;#39;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The SQL statements produced by &lt;code&gt;pg_dump&lt;/code&gt; are instructions on how to re-create your database. You can turn this output into a backup by writing all this SQL text into a file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pg_dump &amp;gt; mybackup.sql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's it! You now have a database backup. You might have noticed that storing all your data as SQL statements is rather inefficient. You can compress this data by using the "custom" dump format:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pg_dump --format&lt;span class="o"&gt;=&lt;/span&gt;custom &amp;gt; mybackup.pgdump
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This "custom" format is ~3x smaller in terms of file size, but it's not as pretty for humans to read because it's now in some funky non-text binary format:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pg_dump --format&lt;span class="o"&gt;=&lt;/span&gt;custom &lt;span class="p"&gt;|&lt;/span&gt; head

&lt;span class="c1"&gt;# Output:&lt;/span&gt;
&lt;span class="c1"&gt;# xtshirt9.5.199.5.19k0ENCODINENCODING&lt;/span&gt;
&lt;span class="c1"&gt;# SET client_encoding = &amp;#39;UTF8&amp;#39;;&lt;/span&gt;
&lt;span class="c1"&gt;# false00&lt;/span&gt;
&lt;span class="c1"&gt;# ... etc ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, &lt;code&gt;mybackup.pgdump&lt;/code&gt; is a crappy file name. It's not clear what is inside the file. Are we going to remember which database this is for? How do we know that this is the freshest copy? Let's add a &lt;a href="https://en.wikipedia.org/wiki/Unix_time"&gt;timestamp&lt;/a&gt; plus a descriptive name to help us remember:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Get Unix epoch timestamp&lt;/span&gt;
&lt;span class="c1"&gt;# Eg. 1591255548&lt;/span&gt;
&lt;span class="nv"&gt;TIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date &lt;span class="s2"&gt;&amp;quot;+%s&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Descriptive file name&lt;/span&gt;
&lt;span class="c1"&gt;# Eg. postgres_mydatabase_1591255548.pgdump&lt;/span&gt;
&lt;span class="nv"&gt;BACKUP_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;postgres_&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PGDATABASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TIME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pgdump&amp;quot;&lt;/span&gt;
pg_dump --format&lt;span class="o"&gt;=&lt;/span&gt;custom &amp;gt; &lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now you can run these commands every month, week, or day to get a snapshot of your data. If you wanted, you could write this whole thing into a &lt;code&gt;bash&lt;/code&gt; script called &lt;code&gt;backup.sh&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c1"&gt;# Backs up mydatabase to a file.&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PGHOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;localhost
&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PGPORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;5432&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PGDATABASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mydatabase
&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PGUSER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myusername
&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mypassw0rd
&lt;span class="nv"&gt;TIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date &lt;span class="s2"&gt;&amp;quot;+%s&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;BACKUP_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;postgres_&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PGDATABASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TIME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pgdump&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Backing up &lt;/span&gt;&lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
pg_dump --format&lt;span class="o"&gt;=&lt;/span&gt;custom &amp;gt; &lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Backup completed&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You should avoid hardcoding passwords like I just did above, it's better to pass credentials in as a script argument or environment variable. The file &lt;code&gt;/etc/environment&lt;/code&gt; is a nice place to store these kinds of credentials on a secure server.&lt;/p&gt;
&lt;h3&gt;Restoring your database from backups&lt;/h3&gt;
&lt;p&gt;It's pointless creating backups if you don't know how to use them to restore your data. There are three scenarios that I can think of where you want to run a restore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need to set up your database from scratch&lt;/li&gt;
&lt;li&gt;You want to rollback your exiting database to a previous time&lt;/li&gt;
&lt;li&gt;You want to restore data in your dev environment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'll go over these scenarios one at a time.&lt;/p&gt;
&lt;h3&gt;Restoring from scratch&lt;/h3&gt;
&lt;p&gt;Sometimes you can lose the database server and there is nothing left. Maybe you deleted it by accident, thinking it was a different server. Luckily you have your database backup file, and hopefully some &lt;a href="https://mattsegal.dev/intro-config-management.html"&gt;automated configuration management&lt;/a&gt; to help you quickly set the server up again.&lt;/p&gt;
&lt;p&gt;Once you've got the new server provisioned and PostgreSQL installed, you'll need to recreate the database and the user who owns it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo -u postgres psql &lt;span class="s"&gt;&amp;lt;&amp;lt;-EOF&lt;/span&gt;
&lt;span class="s"&gt;    CREATE USER $PGUSER WITH PASSWORD &amp;#39;$PGPASSWORD&amp;#39;;&lt;/span&gt;
&lt;span class="s"&gt;    CREATE DATABASE $PGDATABASE WITH OWNER $PGUSER;&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then you can set up the same environment variables that we did earlier (PGHOST, etc.) and then use &lt;a href="https://www.postgresql.org/docs/12/app-pgrestore.html"&gt;&lt;code&gt;pg_restore&lt;/code&gt;&lt;/a&gt; to restore your data.
You'll probably see some warning errors, which is normal.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;BACKUP_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres_mydatabase_1591255548.pgdump
pg_restore --dbname &lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt; &lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt;

&lt;span class="c1"&gt;# Output:&lt;/span&gt;
&lt;span class="c1"&gt;# ... lots of errors ...&lt;/span&gt;
&lt;span class="c1"&gt;# pg_restore: WARNING:  no privileges were granted for &amp;quot;public&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# WARNING: errors ignored on restore: 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I'm not 100% on what all these errors mean, but I believe they're mostly related to the restore script trying to modify Postgres objects that your user does not have permission to modify. If you're using a standard Django app this shouldn't be an issue. You can check that the restore actually worked by checking your tables with &lt;code&gt;psql&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Check the tables&lt;/span&gt;
psql -c &lt;span class="s2"&gt;&amp;quot;\dt&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Output:&lt;/span&gt;
&lt;span class="c1"&gt;# List of relations&lt;/span&gt;
&lt;span class="c1"&gt;# Schema | Name          | Type  | Owner&lt;/span&gt;
&lt;span class="c1"&gt;#--------+---------------+-------+--------&lt;/span&gt;
&lt;span class="c1"&gt;# public | auth_group    | table | myusername&lt;/span&gt;
&lt;span class="c1"&gt;# public | auth_group... | table | myusername&lt;/span&gt;
&lt;span class="c1"&gt;# public | auth_permi... | table | myusername&lt;/span&gt;
&lt;span class="c1"&gt;# public | django_adm... | table | myusername&lt;/span&gt;
&lt;span class="c1"&gt;# .. etc ..&lt;/span&gt;

&lt;span class="c1"&gt;# Check the last migration&lt;/span&gt;
psql -c &lt;span class="s2"&gt;&amp;quot;SELECT * FROM django_migrations ORDER BY id DESC LIMIT 1&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Output:&lt;/span&gt;
&lt;span class="c1"&gt;#  id |  app   | name      | applied&lt;/span&gt;
&lt;span class="c1"&gt;# ----+--------+-----------+---------------&lt;/span&gt;
&lt;span class="c1"&gt;#  20 | tshirt | 0003_a... | 2019-08-26...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There you go! Your database has been restored. Crisis averted.&lt;/p&gt;
&lt;h3&gt;Rolling back an existing database&lt;/h3&gt;
&lt;p&gt;If you want to roll your existing database back to an previous point in time, deleting all new data, then you will need to use the &lt;code&gt;--clean&lt;/code&gt; flag, which drops your restored database tables before re-creating them (&lt;a href="https://www.postgresql.org/docs/12/app-pgrestore.html"&gt;docs here&lt;/a&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;BACKUP_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres_mydatabase_1591255548.pgdump
pg_restore --clean --dbname &lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt; &lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Restoring a dev environment&lt;/h3&gt;
&lt;p&gt;It's often beneficial to restore a testing or development database from a known backup.
When you do this, you're not so worried about setting up the right user permissions.
In this case you want to completely destroy and re-create the database to get a completely fresh start, and you want to use the &lt;code&gt;--no-owner&lt;/code&gt; flag to ignore any database-user related stuff in the restore script:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo -u postgres psql -c &lt;span class="s2"&gt;&amp;quot;DROP DATABASE &lt;/span&gt;&lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
sudo -u postgres psql -c &lt;span class="s2"&gt;&amp;quot;CREATE DATABASE &lt;/span&gt;&lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;BACKUP_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres_mydatabase_1591255548.pgdump
pg_restore --no-owner --dbname &lt;span class="nv"&gt;$PGDATABASE&lt;/span&gt; &lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I use this method quite often to pull non-sensitive data down from production environments to try and reproduce bugs that have occured in prod. It's much easier to fix mysterious bugs when you have regular database backups, &lt;a href="https://mattsegal.dev/sentry-for-django-error-monitoring.html"&gt;error reporting&lt;/a&gt; and &lt;a href="https://mattsegal.dev/django-logging-papertrail.html"&gt;centralized logging&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Next steps&lt;/h3&gt;
&lt;p&gt;I hope you now have the tools you need to backups and restore your Django app's Postgres database. If you want to read more the &lt;a href="https://www.postgresql.org/docs/12/index.html"&gt;Postgres docs&lt;/a&gt; have a good section on &lt;a href="https://www.postgresql.org/docs/12/backup-dump.html"&gt;database backups&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once you've got your head around database backups, you should automate the process to make it more reliable. I will show you how to do this in &lt;a href="https://mattsegal.dev/postgres-backup-automate.html"&gt;this follow-up post&lt;/a&gt;.&lt;/p&gt;</content><category term="DevOps"></category></entry><entry><title>Cloudflare makes DNS slightly less painful</title><link href="https://mattsegal.dev/cloudflare-review.html" rel="alternate"></link><published>2020-04-18T12:00:00+10:00</published><updated>2020-04-18T12:00:00+10:00</updated><author><name>Matthew Segal</name></author><id>tag:mattsegal.dev,2020-04-18:/cloudflare-review.html</id><summary type="html">&lt;p&gt;When you're setting up a new website, there's a bunch of little tasks that you have to do that &lt;em&gt;suck&lt;/em&gt;.
They're important, but they don't give you the joy of creating something new, they're just... plumbing.&lt;/p&gt;
&lt;p&gt;In particular I'm thinking of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;setting up your domain name with DNS records&lt;/li&gt;
&lt;li&gt;encrypting …&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;p&gt;When you're setting up a new website, there's a bunch of little tasks that you have to do that &lt;em&gt;suck&lt;/em&gt;.
They're important, but they don't give you the joy of creating something new, they're just... plumbing.&lt;/p&gt;
&lt;p&gt;In particular I'm thinking of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;setting up your domain name with DNS records&lt;/li&gt;
&lt;li&gt;encrypting your traffic with SSL&lt;/li&gt;
&lt;li&gt;compressing and caching your static assets (CSS, JS) using a CDN&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No one decided to learn web development because they were super stoked on DNS.
The good news is that you can use &lt;a href="https://www.cloudflare.com/"&gt;Cloudflare&lt;/a&gt; (for free)
to make all these plumbing tasks a little less painful.&lt;/p&gt;
&lt;p&gt;In the rest of this post I'll go over the pros and cons of using Cloudflare,
plus a short video guide on how to start using it.&lt;/p&gt;
&lt;h3&gt;What is Cloudflare&lt;/h3&gt;
&lt;p&gt;Cloudflare is a &lt;a href="https://en.wikipedia.org/wiki/Reverse_proxy"&gt;reverse proxy&lt;/a&gt; service that you put in-between you website visitors and your website's server. All requests that hit your website are routed through Cloudflare's servers first. This means that they can provide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DNS record configuration&lt;/strong&gt;: allowing you to set up A records, CNAMEs etc for your domain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP traffic encryption using SSL&lt;/strong&gt;: All HTTP traffic between the end-user and Cloudflare's servers are encrypted with SSL (making it HTTPS)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Caching of static assets&lt;/strong&gt;: Cloudflare will cache static assets like CSS and JS &lt;a href="https://support.cloudflare.com/hc/en-us/articles/200172516-Understanding-Cloudflare-s-CDN"&gt;depending on the "Cache-Control" headers&lt;/a&gt; set by your origin server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compression of static assets&lt;/strong&gt;: Cloudflare will compress &lt;a href="https://support.cloudflare.com/hc/en-us/articles/200168396-What-will-Cloudflare-compress-"&gt;static assets&lt;/a&gt; like CSS and JS so that your pages load and render faster.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a &lt;em&gt;whooole&lt;/em&gt; lot of bullshit that I don't want to set up myself, if I can avoid it, so it's nice when Cloudflare handles it for me.&lt;/p&gt;
&lt;h3&gt;Cloudflare pros&lt;/h3&gt;
&lt;p&gt;In addition to the features I listed above, there are a few nice I've found when using Cloudflare:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Free&lt;/strong&gt;: It has a free plan which is sufficient for all the projects I've worked on so far&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy to use&lt;/strong&gt;: I think it's uncommonly easy to set up and use for tools in its field&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CNAME flattening&lt;/strong&gt;: They provide a handy DNS feature called "CNAME flattening", which means you can point your root domain name (eg. "mattsegal.dev") to other domain names (eg. an AWS S3 bucket website "mattsegal.dev.s3-blah.aws.com"). As far as I know only Cloudflare provides this feature.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible SSL&lt;/strong&gt;: Their "flexible SSL" feature is both a pro and a con. It works like this: traffic between you users and Cloudflare are encrypted, but traffic between Cloudflare are your servers are not encrypted. As long as you trust Cloudflare or intermediate routers not to snoop on your packets, this is a nice setup. In this case setting up flexible SSL is as simple as toggling a button on the website. You &lt;em&gt;can&lt;/em&gt; set up end-to-end encryption but that's a little more work. &lt;a href="https://letsencrypt.org/"&gt;Let's Encrypt&lt;/a&gt; has made setting up SSL &lt;em&gt;much&lt;/em&gt; easier and cheaper for developers, but it's still relatively complex compared to Cloudflare's "flexible" implementation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Faster DNS updates?&lt;/strong&gt;: I might be imagining things, but I find that updates to DNS records in Cloudflare &lt;em&gt;seem&lt;/em&gt; to propagate faster than other services.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Analytics&lt;/strong&gt;: They provide some basic analytics like unique visitors and download bandwidth, which is nice, I guess&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Cloudflare cons&lt;/h3&gt;
&lt;p&gt;The biggest main con I see for using Cloudflare is that you're not learning to use open source alternatives like self-hosted NGINX to do the same job.
If you are an NGINX expert already then you're a big boy/girl and you can make your own decisions about what tools to use.
If you're a newer developer and you've never set up a webserver like NGINX and Apache, then you're robbing yourself of useful infrastructure experience if you &lt;em&gt;only&lt;/em&gt; ever use Cloudflare for &lt;em&gt;everything&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;That said, I think that newer developers should start deploying websites using services like Cloudflare, and then learn how to use tools like NGINX.&lt;/p&gt;
&lt;p&gt;Another, more abstract downside, is that some double-digit percentage of the internet's websites use Cloudflare. If you're worried about centralization of control of the internet, then Cloudflare's growing consolidation of internet traffic is a concern. Personally I don't really care about that right now.&lt;/p&gt;
&lt;h3&gt;How to get started&lt;/h3&gt;
&lt;p&gt;This video shows you how to get set up with Cloudflare.&lt;/p&gt;
&lt;div class="loom-embed"&gt;&lt;iframe src="https://www.loom.com/embed/fffc03f4a3f24285be017b7759461755" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"&gt;&lt;/iframe&gt;&lt;/div&gt;

&lt;h3&gt;What now?&lt;/h3&gt;
&lt;p&gt;Once you've set up Cloudflare, you'll need to start creating some DNS records. I've written a &lt;a href="https://mattsegal.dev/dns-for-noobs.html"&gt;guide on exactly this topic&lt;/a&gt; to help you get set up.
I suggest you check it out so you can give your website a domain name.&lt;/p&gt;</content><category term="DevOps"></category></entry><entry><title>DNS for beginners: how to give your site a domain name</title><link href="https://mattsegal.dev/dns-for-noobs.html" rel="alternate"></link><published>2020-04-13T12:00:00+10:00</published><updated>2020-04-13T12:00:00+10:00</updated><author><name>Matthew Segal</name></author><id>tag:mattsegal.dev,2020-04-13:/dns-for-noobs.html</id><summary type="html">&lt;p&gt;You are learning how to build a website and you want to give it a domain name like mycoolwebsite.com.
It doesn't seem like a &lt;em&gt;real&lt;/em&gt; website without a domain name, does it?
How is anybody going to find your website without one?
Setting up your domain is an important …&lt;/p&gt;</summary><content type="html">&lt;p&gt;You are learning how to build a website and you want to give it a domain name like mycoolwebsite.com.
It doesn't seem like a &lt;em&gt;real&lt;/em&gt; website without a domain name, does it?
How is anybody going to find your website without one?
Setting up your domain is an important step for launcing your website, but it's also a real pain if you're new to web development.
I want to help make this job a little easier for you.&lt;/p&gt;
&lt;p&gt;Typically you go to &lt;a href="https://www.namecheap.com/"&gt;namecheap&lt;/a&gt; or GoDaddy or some other domain name vendor and you buy mycoolwebsite.com for 12 bucks a year - now you need to set it up.
When you try to get started you are confronted by all these bizzare terms: "A record", "CNAME", "nameserver". It can be quite intimidating.
The rest of this blog will show you the basics of how to set up your domain, with a few explanations sprinkled throughout.&lt;/p&gt;
&lt;p&gt;Contents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What the fuck is DNS?&lt;/li&gt;
&lt;li&gt;I want my domain name to go to an IP address&lt;/li&gt;
&lt;li&gt;I want my domain name to go to a different domain name&lt;/li&gt;
&lt;li&gt;I want to give control of my domain name to another service&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;What the fuck is DNS?&lt;/h3&gt;
&lt;p&gt;I'll keep this short. I think &lt;a href="https://www.cloudflare.com/learning/dns/what-is-dns/"&gt;CloudFlare explains it best&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Domain Name System (DNS) is the phonebook of the Internet. Humans access information online through domain names, like nytimes.com or espn.com. Web browsers interact through Internet Protocol (IP) addresses. DNS translates domain names to IP addresses so browsers can load Internet resources.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;DNS is a worldwide, online "phonebook" that translates human-friendly website names like "mattsegal.dev" into computer-friendly numbers like 192.168.1.1. You use the domain name system every day:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You type "mattsegal.dev" into your web browser and press "Enter"&lt;/li&gt;
&lt;li&gt;Your computer will reach out into the domain name system and ask other computers to find out which IP address "mattsegal.dev" points to&lt;/li&gt;
&lt;li&gt;Your computer eventually finds the correct IP address&lt;/li&gt;
&lt;li&gt;Your web browser fetches a web page from that IP address&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, how do we get our website into this "phonebook"?&lt;/p&gt;
&lt;h3&gt;I want my domain name to go to an IP address&lt;/h3&gt;
&lt;p&gt;Sometimes you have an IP address like 11.22.33.44 and you want your domain name to send users to that IP. You want a mapping like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mycoolwebsite.com --&amp;gt; 11.22.33.44
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You will need this when you are running software like WordPress, or your own custom web app. Your website is running on a server and that server has an IP address.
For example, I have a website &lt;a href="https://mattslinks.xyz"&gt;mattslinks.xyz&lt;/a&gt; which runs on a webserver which has a public IP of 167.99.78.141.
My users (me, my girlfriend) don't want to type in 167.99.78.141 into our browsers to visit my site. We'd prefer to type in mattslinks.xyz, which is way easier to remember. So I need to set up a mapping using DNS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mattslinks.xyz --&amp;gt; 167.99.78.141
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;So how do we set this up? We need an &lt;strong&gt;A record&lt;/strong&gt; ("address record") to do this. An A record maps a domain name to an IP address.
To set up an A record you need to go onto your domain name provider's website and enter the &lt;strong&gt;subdomain&lt;/strong&gt; name you want plus the IP address that you wanto to point to.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Photo" src="https://mattsegal.dev/a-record.png"&gt;&lt;/p&gt;
&lt;p&gt;What I've set up here is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mattslinks.xyz --&amp;gt; 167.99.78.141
www.mattslinks.xyz --&amp;gt; 167.99.78.141
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;At this point you may yell &lt;em&gt;"What the fuck is a subdomain!?"&lt;/em&gt; at your monitor. Please do, it's cathartic. The idea is that when you own mattslinks.xyz, you also own a near-infinite number of "child domains" which end in mattslinks.xyz. For example you can set up A records (and other DNS records) for all these domain names:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mattslinks.xyz ("root domain", sometimes written as "@")&lt;/li&gt;
&lt;li&gt;www.mattslinks.xyz (a subdomain)&lt;/li&gt;
&lt;li&gt;blog.mattslinks.xyz (a different subdomain)&lt;/li&gt;
&lt;li&gt;cult.mattslinks.xyz&lt;/li&gt;
&lt;li&gt;super.secret.clubhouse.mattslinks.xyz&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Apparently you can do this to up to 255 characters (including the dots) so this.is.a.very.long.domain.name.but.i.advise.against.doing.this.mattslinks.xyz is &lt;em&gt;technically&lt;/em&gt; possible, but a stupid idea.&lt;/p&gt;
&lt;p&gt;If you're serving a normal website, then it's pretty standard to add A records for both your root domain (mattslinks.xyz) and the "www" subdomain (www.mattslinks.xyz), because some people might put "www" in front of the domain name and we don't want them to miss our website.&lt;/p&gt;
&lt;p&gt;Just in case this all seems a little too abstract and theoretical for you, here's a video of me setting some A records:&lt;/p&gt;
&lt;div class="loom-embed"&gt;&lt;iframe src="https://www.loom.com/embed/2398e6757135445989f83757befd6c11" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"&gt;&lt;/iframe&gt;&lt;/div&gt;

&lt;p&gt;And then, 30 minutes later, checking if I've gone mad or not...&lt;/p&gt;
&lt;div class="loom-embed"&gt;&lt;iframe src="https://www.loom.com/embed/c591b12ac5ae400b82e497011a96d901" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"&gt;&lt;/iframe&gt;&lt;/div&gt;

&lt;p&gt;Finally, the record updates and I add a www subdomain&lt;/p&gt;
&lt;div class="loom-embed"&gt;&lt;iframe src="https://www.loom.com/embed/4a2fed1898b0491fabab1ef8f063b987" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"&gt;&lt;/iframe&gt;&lt;/div&gt;

&lt;p&gt;You might also be wondering about the &lt;strong&gt;TTL&lt;/strong&gt; value. It's not that important, just set it to 3600. If you care to know, TTL stands for "time to live" and it represents how long your DNS records is going to hang around in the system before anybody checks the records you set. So if it's 3600 (seconds), it means it takes at least an hour for changes that you make to your DNS records to update on other people's computers.&lt;/p&gt;
&lt;p&gt;So you have an A record set up, how do you check that it's working? The easiest way is to wait an hour or so and then use a 3rd party website like &lt;a href="https://dnschecker.org/#A/mattslinks.xyz"&gt;DNS checker&lt;/a&gt;. If you're a little more technical and have a bash shell handy you can also try using &lt;a href="https://www.linux.com/training-tutorials/check-your-dns-records-dig/"&gt;dig&lt;/a&gt; from your local machine.&lt;/p&gt;
&lt;h3&gt;I want my domain name to go to a different domain name&lt;/h3&gt;
&lt;p&gt;Sometimes your DNS needs are a little more complicated than just mapping a domain name to an IP address. Sometimes you want to do this instead:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;prettyname.com --&amp;gt; ugly-name-for-pretty-site.ap-southeast2.amazon.aws.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That is to say, you want users to type in www.prettyname.com, but you want them to see the website which is hosted on ugly-name-for-pretty-site.ap-southeast2.amazon.aws.com, but you never want them to know about the hideous name that lies beneath.&lt;/p&gt;
&lt;p&gt;For this problem you need a &lt;strong&gt;CNAME record&lt;/strong&gt; ("canonical name"). A CNAME record is used to map from one domain name to another.&lt;/p&gt;
&lt;p&gt;Here's an example of me setting up a CNAME record in CloudFlare:&lt;/p&gt;
&lt;div class="loom-embed"&gt;&lt;iframe src="https://www.loom.com/embed/1445cce96ac3449183acf40719c02b4d" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"&gt;&lt;/iframe&gt;&lt;/div&gt;

&lt;h3&gt;I want to give control of my domain name to another service&lt;/h3&gt;
&lt;p&gt;Sometimes you you want to give control of a domain to another service. This can happen when you're using a service like Squarespace or Webflow and you want them to set up all your DNS records for you, or if you want to use a different service (like CloudFlare) to manage your DNS.&lt;/p&gt;
&lt;p&gt;The way to set this up is to use set the &lt;strong&gt;name servers&lt;/strong&gt; of your domain. Changing the name servers, as far as I can tell, gives the target servers full control of your domain. In this video, I'll show you some examples.&lt;/p&gt;
&lt;div class="loom-embed"&gt;&lt;iframe src="https://www.loom.com/embed/269e0eba94dc40d3880ef04aa261f41f" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"&gt;&lt;/iframe&gt;&lt;/div&gt;

&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;So there you go, some basic DNS-how-tos. With A records, CNAMES and name servers under your belt, you should be able to do ~70% of DNS tasks that you need in web development. Get a handle on TXT and MX records, and you're up to ~95%. DNS is horrible to work with, but it doesn't need to be confusing.&lt;/p&gt;
&lt;p&gt;This certainly isn't the definitive guide on DNS, and I expect I made some technical errors in my explanations, but I hope you now have the tools to go out an setup some websites.&lt;/p&gt;</content><category term="DevOps"></category></entry><entry><title>9 commands for debugging Django in Docker containers</title><link href="https://mattsegal.dev/docker-container-debugging.html" rel="alternate"></link><published>2020-04-08T12:00:00+10:00</published><updated>2020-04-08T12:00:00+10:00</updated><author><name>Matthew Segal</name></author><id>tag:mattsegal.dev,2020-04-08:/docker-container-debugging.html</id><summary type="html">&lt;p&gt;You want to get started "Dockerizing" your Django environment and you do a tutorial which shows you how to set it all up with docker-compose. You follow the listed commands and everything is working. Cool!&lt;/p&gt;
&lt;p&gt;A few days later there's an error in your code and you want to debug …&lt;/p&gt;</summary><content type="html">&lt;p&gt;You want to get started "Dockerizing" your Django environment and you do a tutorial which shows you how to set it all up with docker-compose. You follow the listed commands and everything is working. Cool!&lt;/p&gt;
&lt;p&gt;A few days later there's an error in your code and you want to debug the issue. What caused your dev environment to break? Is it your code? Is it a dependencies issue? Is it a Docker thing? How can you tell?&lt;/p&gt;
&lt;p&gt;I've compiled a list of handy Docker commands that I whip out in these "what the fuck is happening!?!?" situations to help me get to the bottom of the issue:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rebuild from scratch&lt;/li&gt;
&lt;li&gt;Run a debugger&lt;/li&gt;
&lt;li&gt;Get a bash shell in a running container&lt;/li&gt;
&lt;li&gt;Get a bash shell in a brand new container&lt;/li&gt;
&lt;li&gt;Run a script&lt;/li&gt;
&lt;li&gt;Poke around inside of a PostgreSQL container&lt;/li&gt;
&lt;li&gt;Watch some logs&lt;/li&gt;
&lt;li&gt;View volumes&lt;/li&gt;
&lt;li&gt;Destroy absolutely everything&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Rebuild from scratch&lt;/h3&gt;
&lt;p&gt;Sometimes you want to rebuild you Docker image from scratch, just to make sure. Rebuilding with the --no-cache flag ensures that your Dockerfile is executed from start to finish, with no intermediate cached layers used.&lt;/p&gt;
&lt;p&gt;For docker:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker build --no-cache .
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For docker-compose, assuming you have a "web" service:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker-compose build --no-cache web
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Run a debugger&lt;/h3&gt;
&lt;p&gt;You might notice that using docker-compose, Django's runserver and the pdb debugger together doesn't really work.&lt;/p&gt;
&lt;p&gt;If you've plopped your debugger into a Django view for example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;my_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;things&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Launch Python command-line debugger&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pdb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;pdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_trace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;JsonResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;... and your &lt;code&gt;docker-compose.yml&lt;/code&gt; file is something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;web&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;./manage.py runserver&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# ... more stuff ...&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;... and you start your services like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker-compose up web
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then your Python debugger will never work! When the view hits the pdb.set_trace() function, you'll always see this horrible error:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt; # ... 10 million lines of stack trace ...
  File &amp;quot;/usr/lib/python3.6/bdb.py&amp;quot;, line 51, in trace_dispatch
    return self.dispatch_line(frame)
  File &amp;quot;/usr/lib/python3.6/bdb.py&amp;quot;, line 70, in dispatch_line
    if self.quitting: raise BdbQuit
bdb.BdbQuit
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is an easy fix. The debugger, which is inside the Docker container, is trying to communicate with your terminal, which is outside of the Docker container, via some port, which is closed - hence the error. So we need to tell Docker to keep the required port open with --service-ports. More info &lt;a href="https://stackoverflow.com/questions/33066528/should-i-use-docker-compose-up-or-run"&gt;here&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker-compose run --rm --service-ports web
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now when you hit the debugger you will get a functional, interactive pdb interface in your terminal.&lt;/p&gt;
&lt;h3&gt;Get a bash shell in a running container&lt;/h3&gt;
&lt;p&gt;Sometimes you want to poke around inside a container that is already running. You might want to &lt;code&gt;cat&lt;/code&gt; a file, run &lt;code&gt;ls&lt;/code&gt; or inspect the output of &lt;code&gt;ps auxww&lt;/code&gt;. To get inside a running container you can use docker's &lt;code&gt;exec&lt;/code&gt; command.&lt;/p&gt;
&lt;p&gt;First, you need to get the running container's id:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Which will get you and output like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;CONTAINER ID    ...    NAMES
0dd3d893u8d3    ...    web
518f741c4415    ...    worker
0ce1cfd9c99f    ...    database
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Say I wanted to poke around in the "worker" container, then I need to note its id of "518f741c4415" and then run bash using &lt;code&gt;docker exec&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -it 518f741c4415 bash
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It's a little easier if you're using docker-compose. If you want to get into an already running "web" container:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker-compose &lt;span class="nb"&gt;exec&lt;/span&gt; web bash
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Get a bash shell in a brand new container&lt;/h3&gt;
&lt;p&gt;Sometimes you want to poke around inside a container that is based on an image, to see what is baked into the image. You can do this using docker or docker-compose.&lt;/p&gt;
&lt;p&gt;For a service set up like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;web&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;myimage:latest&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# ... more stuff ...&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can run the image &lt;code&gt;myimage&lt;/code&gt; using docker:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker run --rm -it myimage:latest bash
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Or via docker-compose:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker-compose run --rm web bash
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note the &lt;code&gt;--rm&lt;/code&gt; flag, which will save you from having all these single use containers lying around, using up disk space.&lt;/p&gt;
&lt;h3&gt;Run a script&lt;/h3&gt;
&lt;p&gt;If you just want to run a script in a single-use, throw away container, you can use the &lt;code&gt;run&lt;/code&gt; command as well. This is particularly useful for running management commands or unit tests:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker-compose run --rm web ./manage.py migrate
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note: this only works if your container's default working dir is contains &lt;code&gt;./manage.py&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Poke around inside of a PostgreSQL container&lt;/h3&gt;
&lt;p&gt;If you're using Django and docker-compose then you're likely running a PostgreSQL container, set up something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;postgres&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# ... more stuff ...&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;POSTGRES_HOST_AUTH_METHOD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;trust&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;web&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;./manage.py runserver&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# ... more stuff ...&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;PGDATABASE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;postgres&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;PGUSER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;postgres&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;password&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;PGHOST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;database&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;PGPORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;5432&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then you can use the psql command line from the web container to check out your database tables:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker-compose run --rm web psql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Watch some logs&lt;/h3&gt;
&lt;p&gt;Sometimes you have a container, like a Celery worker or database, which is running in the background and you want to see its console output. Even better, you want to watch its console output in realtime. You can do this with &lt;code&gt;logs&lt;/code&gt;. For example, if I want to follow the output of the "worker" container:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker-compose logs --tail &lt;span class="m"&gt;100&lt;/span&gt; -f worker
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;View volumes&lt;/h3&gt;
&lt;p&gt;Sometimes when you're having issues with volume you want to double check what volumes you have and how they're set up. This is relatively straightforward.&lt;/p&gt;
&lt;p&gt;To see all volumes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker volume ls
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Which gets output like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;DRIVER              VOLUME NAME
local               docker_postgres-data
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And then to drill down into one volume:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker volume inspect docker_postgres-data
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Giving you something like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;CreatedAt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2020-04-08T12:44:34+10:00&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;Driver&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;local&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;Labels&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;com.docker.compose.project&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;docker&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;com.docker.compose.version&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;1.23.1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;com.docker.compose.volume&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;postgres-data&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;Mountpoint&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/lib/docker/volumes/docker_postgres-data/_data&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;Name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;docker_postgres-data&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;Options&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;Scope&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;local&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If that doesn't help you, there's always the next step.&lt;/p&gt;
&lt;h3&gt;Destroy absolutely everything&lt;/h3&gt;
&lt;p&gt;There's a Docker command that removes all your "unused" data:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker system prune
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's nice, it might free up some disk space, but what if you want to go full scorched-earth on your Docker envrionemnt? Like tear down Carthage and salt the fields so that nothing will ever grow again?&lt;/p&gt;
&lt;p&gt;Here's a script I use occasionally when I just want to get rid of &lt;em&gt;everything&lt;/em&gt; and start afresh:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Stop all containers&lt;/span&gt;
docker &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="k"&gt;$(&lt;/span&gt;docker ps -q&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Remove all containers&lt;/span&gt;
docker rm &lt;span class="k"&gt;$(&lt;/span&gt;docker ps -a -q&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Remove all docker images&lt;/span&gt;
docker rmi &lt;span class="k"&gt;$(&lt;/span&gt;docker images -q&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Remove all volumes&lt;/span&gt;
docker volume rm &lt;span class="k"&gt;$(&lt;/span&gt;docker volume ls -q&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Burn it all down I say! From the ashes, we will rebuild!&lt;/p&gt;
&lt;p&gt;If this doesn't fix your issue, I recommend that you throw your laptop out a window, sell all your worldy possesions and &lt;a href="https://www.outsideonline.com/2411125/lynx-vilden-stone-age-life"&gt;start a new life in the wilderness&lt;/a&gt;.&lt;/p&gt;</content><category term="DevOps"></category></entry><entry><title>Introduction to configuration management</title><link href="https://mattsegal.dev/intro-config-management.html" rel="alternate"></link><published>2020-04-08T12:00:00+10:00</published><updated>2020-04-08T12:00:00+10:00</updated><author><name>Matthew Segal</name></author><id>tag:mattsegal.dev,2020-04-08:/intro-config-management.html</id><summary type="html">&lt;p&gt;This is a talk I gave at the Melbourne &lt;a href="https://www.meetup.com/en-AU/Junior-Developers-Melbourne/"&gt;Junior dev meetup&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Have you ever found a bug in prod, which wasn't caught earlier because of a missing folder, library, or file permission? It sucks! This talk goes over some practices and tools that you can use to keep your …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;This is a talk I gave at the Melbourne &lt;a href="https://www.meetup.com/en-AU/Junior-Developers-Melbourne/"&gt;Junior dev meetup&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Have you ever found a bug in prod, which wasn't caught earlier because of a missing folder, library, or file permission? It sucks! This talk goes over some practices and tools that you can use to keep your environments consistent and share knowledge with the rest of your team.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="loom-embed"&gt;&lt;iframe src="https://www.loom.com/embed/95fce3bb373e40f99ee91e5892ba177e" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"&gt;&lt;/iframe&gt;&lt;/div&gt;</content><category term="DevOps"></category></entry></feed>