Chapter info |
|
shortname: |
chapter_making_deployment_production_ready |
Warning
|
2017-03-13: Book upgraded to Python 3.6 and Django 1.11 beta. Before that, there was a big upgrade to Selenium 3 on 2017-01-30. More info on the blog. |
In this chapter we’ll make some changes to our site to move to a configuration that’s more production-ready. As we make each change, we’ll use the tests to tell us whether things are still working.
What’s wrong with our hacky deployment? Well, we can’t use the Django dev server for production, it’s not designed for "real-life" loads. We’ll use something called Gunicorn instead to run our Django code, and we’ll get Nginx to serve our static files.
Our 'settings.py' currently has DEBUG=True
, and that’s strongly recommended
against for production (you don’t want users staring at debug tracebacks of
your code when your site errors for example). We’ll also need to set
ALLOWED_HOSTS
for security.
We want our site to start up automatically whenever the server reboots. For that we’ll write a Systemd config file.
Finally, hard-coding port 8000 won’t let us run multiple sites on this server, so we’ll switch to using "unix sockets" to communicate between nginx and Django.
Do you know why the Django mascot is a pony? The story is that Django comes with so many things you want: an ORM, all sorts of middleware, the admin site… "What else do you want, a pony?" Well, Gunicorn stands for "Green Unicorn", which I guess is what you’d want next if you already had a pony…
elspeth@server:$ ../virtualenv/bin/pip install gunicorn
Gunicorn will need to know a path to a WSGI server, which is usually
a function called application
. Django provides one in 'superlists/wsgi.py':
elspeth@server:$ ../virtualenv/bin/gunicorn superlists.wsgi:application 2013-05-27 16:22:01 [10592] [INFO] Starting gunicorn 0.19.6 2013-05-27 16:22:01 [10592] [INFO] Listening at: http://127.0.0.1:8000 (10592) [...]
If you now take a look at the site, you’ll find the CSS is all broken, as in Broken CSS.
And if we run the functional tests, you’ll see they confirm that something is wrong. The test for adding list items passes happily, but the test for layout + styling fails. Good job tests!
$ STAGING_SERVER=superlists-staging.ottg.eu python manage.py test functional_tests [...] AssertionError: 125.0 != 512 within 3 delta FAILED (failures=1)
The reason that the CSS is broken is that although the Django dev server will serve static files magically for you, Gunicorn doesn’t. Now is the time to tell Nginx to do it instead.
One step forward, one step backward, but at least the tests are there to help. Moving on!
First we run collectstatic
to copy all the static files to a folder where
Nginx can find them:
elspeth@server:$ ../virtualenv/bin/python manage.py collectstatic --noinput elspeth@server:$ ls ../static/ base.css bootstrap
Now we tell Nginx to start serving those static files for us:
server {
listen 80;
server_name superlists-staging.ottg.eu;
location /static {
alias /home/elspeth/sites/superlists-staging.ottg.eu/static;
}
location / {
proxy_pass http://localhost:8000;
}
}
Reload Nginx and restart Gunicorn…
elspeth@server:$ sudo systemctl reload nginx elspeth@server:$ ../virtualenv/bin/gunicorn superlists.wsgi:application
And if we take another look at the site, things are looking much healthier. We can rerun our FTs:
$ STAGING_SERVER=superlists-staging.ottg.eu python manage.py test functional_tests [...] ... --------------------------------------------------------------------- Ran 3 tests in 10.718s OK
Phew.
When we want to serve both staging and live, we can’t have both servers trying to use port 8000. We could decide to allocate different ports, but that’s a bit arbitrary, and it would be dangerously easy to get it wrong and start the staging server on the live port, or vice versa.
A better solution is to use Unix domain sockets—they’re like files on disk, but can be used by Nginx and Gunicorn to talk to each other. We’ll put our sockets in '/tmp'. Let’s change the proxy settings in Nginx:
[...]
location / {
proxy_set_header Host $host;
proxy_pass http://unix:/tmp/superlists-staging.ottg.eu.socket;
}
}
proxy_set_header
is used to make sure Gunicorn and Django know what domain
it’s running on. We need that for the ALLOWED_HOSTS
security feature, which
we’re about to switch on.
Now we restart Gunicorn, but this time telling it to listen on a socket instead of on the default port:
elspeth@server:$ sudo systemctl reload nginx elspeth@server:$ ../virtualenv/bin/gunicorn --bind \ unix:/tmp/superlists-staging.ottg.eu.socket superlists.wsgi:application
And again, we rerun the functional test again, to make sure things still pass:
$ STAGING_SERVER=superlists-staging.ottg.eu python manage.py test functional_tests [...] OK
A couple more steps!
Django’s DEBUG mode is all very well for hacking about on your own server, but leaving those pages full of tracebacks available isn’t secure.
You’ll find the DEBUG
setting at the top of 'settings.py'. When we set this
to False
, we also need to set another setting called ALLOWED_HOSTS
. This
was
added
as a security feature in Django 1.5. Unfortunately it doesn’t have a helpful
comment in the default 'settings.py', but we can add one ourselves. Do this on
the server:
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
TEMPLATE_DEBUG = DEBUG
# Needed when DEBUG=False
ALLOWED_HOSTS = ['superlists-staging.ottg.eu']
[...]
And, once again, we restart Gunicorn and run the FT to check things still work.
Note
|
Don’t commit these changes on the server. At the moment this is just a
hack to get things working, not a change we want to keep in our repo. In
general, to keep things simple, I’m only going to do Git commits from the
local PC, using git push and git pull when I need to sync them up to
the server.
|
One more test run to reassure ourselves that things still work?
$ STAGING_SERVER=superlists-staging.ottg.eu python manage.py test functional_tests [...] OK
Good.
Our final step is to make sure that the server starts up Gunicorn automatically on boot, and reloads it automatically if it crashes. On Ubuntu, the way to do this is using Systemd:
[Unit]
Description=Gunicorn server for superlists-staging.ottg.eu
[Service]
Restart=on-failure (1)
User=elspeth (2)
WorkingDirectory=/home/elspeth/sites/superlists-staging.ottg.eu/source (3)
ExecStart=/home/elspeth/sites/superlists-staging.ottg.eu/virtualenv/bin/gunicorn \
--bind unix:/tmp/superlists-staging.ottg.eu.socket \
superlists.wsgi:application (4)
[Install]
WantedBy=multi-user.target (5)
Systemd is joyously simple to configure (especially if you’ve ever had the
dubious pleasure of writing an init.d
script), and is fairly
self-explanatory.
-
Restart=on-failure
will restart the process automatically if it crashes. -
User=elspeth
makes the process run as the "elspeth" user. -
WorkingDirectory
sets the current working directory. -
ExecStart
is the actual process to execute. We use the `\ ` line continuation characters to split the full command over multiple lines, for readability, but it could all go on one line. -
WantedBy
in the[Install]
section is what tells Systemd we want this service to start on boot.
Systemd scripts live in '/etc/systemd/system', and their names must end in '.service'.
Now we tell Systemd to start Gunicorn with the systemctl
command:
# this command is necessary to tell Systemd to load our new config file elspeth@server:$ sudo systemctl daemon-reload # this command tells Systemd to always load our service on boot elspeth@server:$ sudo systemctl enable gunicorn-superlists-staging.ottg.eu # this command actually starts our service elspeth@server:$ sudo systemctl start gunicorn-superlists-staging.ottg.eu
(you should find the systemctl
command responds to tab-completion, including
of the service name, by the way)
Now we can rerun the FTs to see that everything still works. You can even test that the site comes back up if you reboot the server!
-
Check the Systemd logs for using
sudo journalctl -u gunicorn-superlists-staging.ottg.eu
-
You can ask Systemd to check the validity of your service configuration:
systemd-analyze verify /path/to/my.service
-
Remember to restart both services whenever you make changes.
-
If you make changes to the Systemd config file, you need to run
daemon-reload
beforesystemctl restart
to see the effect of your changes.
Back in the 'local' copy of your repo, we should add Gunicorn to the list of packages we need in our virtualenvs:
$ pip install gunicorn $ pip freeze | grep gunicorn >> requirements.txt $ git commit -am "Add gunicorn to virtualenv requirements" $ git push
Note
|
On Windows, at the time of writing, Gunicorn would pip install quite happily, but it wouldn’t actually work if you tried to use it. Thankfully we only ever run it on the server, so that’s not a problem. And, Windows support is being discussed… |
Let’s recap our provisioning and deployment procedures:
- Provisioning
-
-
Assume we have a user account and home folder
-
add-apt-repository ppa:fkrull/deadsnakes
-
apt-get install nginx git python3.6 python3.6-venv
-
Add Nginx config for virtual host
-
Add Systemd job for Gunicorn
-
- Deployment
-
-
Create directory structure in '~/sites'
-
Pull down source code into folder named 'source'
-
Start virtualenv in '../virtualenv'
-
pip install -r requirements.txt
-
manage.py migrate
for database -
collectstatic
for static files -
Set DEBUG = False and ALLOWED_HOSTS in 'settings.py'
-
Restart Gunicorn job
-
Run FTs to check everything works
-
Assuming we’re not ready to entirely automate our provisioning process, how should we save the results of our investigation so far? I would say that the Nginx and Systemd config files should probably be saved somewhere, in a way that makes it easy to reuse them later. Let’s save them in a new subfolder in our repo:
$ mkdir deploy_tools
server {
listen 80;
server_name SITENAME;
location /static {
alias /home/elspeth/sites/SITENAME/static;
}
location / {
proxy_set_header Host $host;
proxy_pass http://unix:/tmp/SITENAME.socket;
}
}
[Unit]
Description=Gunicorn server for SITENAME
[Service]
Restart=on-failure
User=elspeth
WorkingDirectory=/home/elspeth/sites/SITENAME/source
ExecStart=/home/elspeth/sites/SITENAME/virtualenv/bin/gunicorn \
--bind unix:/tmp/SITENAME.socket \
superlists.wsgi:application
[Install]
WantedBy=multi-user.target
Then it’s easy for us to use those two files to generate
a new site, by doing a find & replace on SITENAME
.
For the rest, just keeping a few notes is OK. Why not keep them in a file in the repo too?
Provisioning a new site
=======================
## Required packages:
* nginx
* Python 3.6
* virtualenv + pip
* Git
eg, on Ubuntu:
sudo add-apt-repository ppa:fkrull/deadsnakes
sudo apt-get install nginx git python36 python3.6-venv
## Nginx Virtual Host config
* see nginx.template.conf
* replace SITENAME with, e.g., staging.my-domain.com
## Systemd service
* see gunicorn-systemd.template.service
* replace SITENAME with, e.g., staging.my-domain.com
## Folder structure:
Assume we have a user account at /home/username
/home/username
└── sites
└── SITENAME
├── database
├── source
├── static
└── virtualenv
We can do a commit for those:
$ git add deploy_tools $ git status # see three new files $ git commit -m "Notes and template config files for provisioning"
Our source tree will now look something like this:
. ├── deploy_tools │ ├── gunicorn-systemd.template.service │ ├── nginx.template.conf │ └── provisioning_notes.md ├── functional_tests │ ├── [...] ├── lists │ ├── __init__.py │ ├── models.py │ ├── [...] │ ├── static │ │ ├── base.css │ │ └── bootstrap │ │ ├── [...] │ ├── templates │ │ ├── base.html │ │ ├── [...] │ ├── tests.py │ ├── urls.py │ └── views.py ├── manage.py ├── requirements.txt └── superlists ├── [...]
Being able to run our FTs against a staging server can be very reassuring. But, in most cases, you don’t want to run your FTs against your "real" server. In order to "save our work", and reassure ourselves that the production server will work just as well as the real server, we need to make our deployment process repeatable.
Automation is the answer, and it’s the topic of the next chapter.
A few things to think about when trying to build a production-ready server environment.
- Don’t use the Django dev server in production
-
Something like Gunicorn or uWSGI is a better tool for running Django; they will let you run multiple workers, for example.
- Don’t use Django to serve your static files
-
There’s no point in using a Python process to do the simple job of serving static files. Nginx can do it, but so can other web servers like Apache or uWSGI.
- Check your settings.py for dev-only settings
-
DEBUG=True and ALLOWED_HOSTS are the two we looked at, but you will probably have others (we’ll see more when we start to send emails from the server).
- Security
-
A serious discussion of server security is beyond the scope of this book, and I’d warn against running your own servers without learning a good bit more about it. (One reason people choose to use a PaaS to host their code, because that means there’s a few less security issues to worry about). If you’d like a place to start, here’s as good a place as any: My first 5 minutes on a server. I can definitely recommend the eye-opening experience of installing fail2ban and watching its logfiles to see just how quickly it picks up on random drive-by attempts to brute force your SSH login. The Internet is a dangerous place!