Production server setup

As mentioned in the introduction, the steps you need to setup a saltant production server are very sensitive to your particular environment and implementation needs. Hence, for this section of hosting instructions we will discuss a way to set up saltant in production, not the way (and not necessarily even the “best” way).

Also note that production settings are finicky, and the packages mentioned here may change their interface over time! Make you sure you adapt any changes as necessary.

This section continues directly from the Development server setup instructions.

Allowing incoming network traffic

For simplicity, let’s assume we are using AWS Route 53 to route traffic from our domain, www.fictionaljobrunner.com, to an AWS EC2 instance used to host our saltant project, PostgreSQL database server, and RabbitMQ server. These steps should translate over to most implementation methods with (hopefully) minimal modification. We will also assume that the PostgreSQL database server doesn’t need to expose itself to the network, and that the RabbitMQ server does need to expose itself to the network.

Start by routing traffic from your domain to your EC2 instance by following. Set up ALIAS DNS records for fictionaljobrunner.com and www.fictionaljobrunner.com. You may find these routing instructions helpful.

Now, make sure you have ports open for SSH (22), HTTP (80), HTTPS (443), and AMQP (5671) traffic. [1] Later, we will redirect HTTP requests to HTTPS in Let’s encrypt!, and secure incoming Redis traffic with SSL in Securing RabbitMQ with SSL.

Setting up production environment variables

We’ll need to fill in production values for a few of our environment variables, namely ALLOWED_HOSTS and DJANGO_BASE_URL. Assuming that we’ve set up ALIAS DNS records for fictionaljobrunner.com and www.fictionaljobrunner.com in the previous step, the relevant variables of our .env file might look like

ALLOWED_HOSTS='fictionaljobrunner.com','www.fictionaljobrunner.com','127.0.0.1'
DJANGO_BASE_URL='https://www.fictionaljobrunner.com'

Collecting static files

Before we host saltant, we need to collect all of our project’s static files so we can serve them efficiently. From the base of the project, simply run

$ ./manage.py collectstatic

Hosting saltant on a socket with uWSGI

This and the next section are adapted from these uWSGI and nginx instructions, which provide a much more detailed reference for the following instructions.

First we need to install uWSGI; if we’re on Ubuntu we can install it with

$ sudo pip3 install uwsgi

We’re going to daemonize saltant with the uWSGI Emperor and systemd. To do this we need to edit/create a few files:

/etc/uwsgi/emperor.ini

[uwsgi]
emperor = /etc/uwsgi/vassals
uid = www-data
gid = www-data
logto = /var/log/uwsgi-emperor.log

This file tells the uWSGI emperor to run the “vassals” in /etc/uwsgi/vassals, one of which we will define now:

/etc/uwsgi/vassals/saltant_uwsgi.ini

[uwsgi]
chdir = /home/ubuntu/saltant
module = saltant.wsgi
home = /home/ubuntu/saltant/venv
master = true
processes = 10
socket = /tmp/saltant.sock
vacuum = true

This file defines a “vassal” which hosts saltant’s WSGI module saltant.wsgi found at the root of the project /home/ubuntu/saltant using the project’s virtual environment located at /home/ubuntu/saltant/venv. It also defines a socket to connect to, /tmp/saltant.sock, and declares that it can handle 10 requests from that socket simultaneously. For more information, see these uWSGI Emperor vassal instructions.

Next we need to daemonize the uWSGI Emperor we’ve just configured using systemd:

/etc/systemd/system/emperor.uwsgi.service

[Unit]
Description=uWSGI Emperor for saltant
After=syslog.target

[Service]
ExecStart=/usr/local/bin/uwsgi --ini /etc/uwsgi/emperor.ini
RuntimeDirectory=uwsgi
Restart=always
KillSignal=SIGQUIT
Type=notify
NotifyAccess=all

[Install]
WantedBy=multi-user.target

Make sure this file is executable:

$ sudo chmod +x /etc/systemd/system/emperor.uwsgi.service

Now you can enable the uWSGI-loaded saltant server with

$ sudo systemctl enable emperor.uwsgi.service

Serving the socket with nginx

We need to serve the socket with nginx so that the outside world can interface with it.

First install and start nginx:

$ sudo apt install nginx
$ sudo /etc/init.d/nginx start

Now we need to edit the following file:

/etc/nginx/sites-available/saltant_nginx.conf

upstream django {
    server unix:///tmp/saltant.sock;
}

server {
    listen 80;
    listen [::]:80;

    server_name fictionaljobrunner.com www.fictionaljobrunner.com;

    charset utf-8;
    client_max_body_size 10M;

    location /static {
        alias /home/ubuntu/saltant/static;
    }

    location / {
        uwsgi_pass django;
        include /etc/nginx/uwsgi_params;
    }
}

This will route HTTP traffic (which is not secure) to our saltant project.

To enable this site, we need create the following symlink so nginx knows to enable it:

$ cd /etc/nginx/sites-enabled
$ sudo ln -s ../sites-available/saltant_nginx.conf saltant_nginx.conf

Let’s encrypt!

Thanks to Let’s Encrypt and EFF Certbot, securing our traffic with SSL and redirecting all HTTP to HTTPS is ridiculously easy.

First install the Certbot for nginx with

$ sudo apt install python-certbot-nginx

Then run it and follow its instructions with

$ sudo certbot --nginx

Congrats to us! Now our site is secured with SSL with automatically renewed certificates!

Hosting RabbitMQ on a network

Now let’s focus on RabbitMQ. If all of your Celery workers will be running on the local machine, then you can safely ignore this section.

By default, RabbitMQ will bind to all interfaces, on IPv4 and IPv6 if available. Let’s suppose our IP is 192.168.1.100. The minimum amount of work required to host RabbitMQ on a network is to change the CELERY_BROKER_URL in our .env from

CELERY_BROKER_URL='pyamqp://'

to

CELERY_BROKER_URL='pyamqp://192.168.1.100:5671'

But suppose we want some basic authentication. Let’s include that now. RabbitMQ comes with a default user guest (with password guest) and a default virtual host /. Let’s remove those:

$ sudo rabbitmqctl delete_user guest
$ sudo rabbitmqctl delete_vhost /

Now let’s add our own admin user AzureDiamond (with password hunter2) and virtual host AzureDiamond_vhost:

$ sudo rabbitmqctl add_user AzureDiamond hunter2
$ sudo rabbitmqctl add_vhost AzureDiamond_vhost``
$ sudo rabbitmqctl set_user_tags AzureDiamond administrator
$ sudo rabbitmqctl set_permissions -p AzureDiamond_vhost AzureDiamond ".*" ".*" ".*"

Now that we’ve done this, we need to update the CELERY_BROKER_URL variable in our project’s .env:

CELERY_BROKER_URL='pyamqp://AzureDiamond:hunter2@192.168.1.100:5671/AzureDiamond_vhost'

Hosting the RabbitMQ management console with SSL

Our strategy here will be to host the RabbitMQ management console on localhost and create a reverse proxy with nginx to expose to the network. All we need to do is edit the nginx saltant configuration again, and add two new locations within the server block: [2]

/etc/nginx/sites-available/saltant_nginx.conf

server {

    ... # stuff we added before (and that Certbot added to!)

    location ~* /rabbitmq/api/(.*?)/(.*) {
        proxy_pass http://localhost:15672/api/$1/%2F/$2?$query_string;
        proxy_buffering                    off;
        proxy_set_header Host              $http_host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location ~* /rabbitmq/(.*) {
        rewrite ^/rabbitmq/(.*)$ /$1 break;
        proxy_pass http://localhost:15672;
        proxy_buffering                    off;
        proxy_set_header Host              $http_host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Now let’s change the RABBITMQ_MANAGEMENT_URL in your .env to

RABBITMQ_MANAGEMENT_URL='https://www.fictionaljobrunner.com/rabbitmq/'

Securing RabbitMQ with SSL

Even though we have secured the RabbitMQ management console with SSL, RabbitMQ is still insecure. If you’re hosting all of your workers on a secure network, then feel free to skip this section.

We’re going to make use of the certs we created in Let’s encrypt!. Noting where those files are, create or edit the following file, like so:

/etc/rabbitmq/rabbitmq.config

[
 {rabbit,
  [
   {tcp_listeners, []},
   {ssl_listeners, [5671]},
   {ssl_options, [{cacertfile,           "/etc/letsencrypt/live/fictionaljobrunner.com/fullchain.pem"},
                  {certfile,             "/etc/letsencrypt/live/fictionaljobrunner.com/cert.pem"},
                  {keyfile,              "/etc/letsencrypt/live/fictionaljobrunner.com/privkey.pem"},
                  {verify,               verify_peer},
                  {fail_if_no_peer_cert, false}]}
  ]
 }
].

Make sure that the rabbitmq user on your machine has read access to the above certs. (One way to do this is to let the ssl-cert group control /etc/letsencrypt and add rabbitmq to this group.)

Hosting Flower with SSL

Hosting Flower is simple with nginx. First let’s daemonize Flower with systemd (assuming our saltant virtual environment is located at /home/ubuntu/saltant/venv:

/etc/systemd/system/flower.service

[Unit]
Description=Flower
After=syslog.target

[Service]
WorkingDirectory=/home/ubuntu/saltant/
ExecStart=/home/ubuntu/saltant/venv/bin/flower -A saltant --url-prefix=flower --basic_auth=AzureDiamond:hunter2 --db=flower.db --persistent=True
Restart=always
KillSignal=SIGQUIT
Type=notify
NotifyAccess=all

[Install]
WantedBy=multi-user.target

Just like in Hosting saltant on a socket with uWSGI, we need to make this service executable and enable it:

$ sudo chmod +x /etc/systemd/system/flower.service
$ sudo servicectl enable flower.service

Now we have Flower daemonized on our local machine with some basic authentication [3], but it’s still not exposed to the network. To do so we’ll take the reverse proxy tack taken in Hosting the RabbitMQ management console with SSL. First, get the directory path for Flower’s static files (let’s assume the path is /home/ubuntu/saltant/venv/lib/python3.6/site-packages/flower/static; your’s should be similar). Then let’s add the following two locations to the server block in our nginx configuration file:

/etc/nginx/sites-available/saltant_nginx.conf

server {

    ... # stuff we added before (and that Certbot added to!)

    location /flower/static {
        alias /home/ubuntu/saltant/venv/lib/python3.6/site-packages/flower/static;
    }

    location /flower {
        proxy_pass http://localhost:5555/;
        rewrite ^/flower/(.*)$ /$1 break;
        proxy_set_header Host $host;
        proxy_redirect off;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Now let’s let saltant know about Flower. Change the FLOWER_URL variable in .env to

FLOWER_URL='https://www.fictionaljobrunner.com/flower/'

Setting up Rollbar error tracking

Rollbar provides a beautiful error-tracking solution for development teams. It also has a generous free tier (yay!). You can sign up here.

Once you, have, fill in the ROLLBAR_ACCESS_TOKEN and ROLLBAR_PROJECT_URL variables in your .env.

Final thoughts

This guide has demonstrated one way you can host salant in production. It covers basic security and it should work. However, there’s a whole world (well, industry) worth of extra security and optimization that can be added on top of this to make saltant run better. Be aware of that.

[1]See here for instructions on opening EC2 instance ports.
[2]Thanks to Dario Zadro for his post here.
[3]See more authentication options here.