Deploying Django Channels into production.

If you’ve built a web app leveraging Django Channels for WebSockets, you may realise there are few guides explaining how to launch such an app in production.

This guide explains how to set-up a VPS with nginx and Supervisor to serve a Django Channels powered app called myapp. We cover the following steps:

  1. Provision a new VPS
  2. Run your app
  3. Make your app accessible online
  4. Enable SSL
  5. Keep your app running

1. Provision a new VPS

There are many VPS providers. In this guide we use Digital Ocean to create a ‘droplet’, their terminology for a Virtual Private Server:

  • Image: Ubuntu (we’re using version 20.04 LTS)
  • Plan: Choose based on your needs. Django Channels works well on their cheapest plan; 1GB memory & 25GB SSD.
  • Authentication: SSH

It’s a good idea to create a floating IP address and associate your droplet to the floating address. This allows you to quickly direct traffic to other servers, and destroying droplets doesn’t require changing IP address in your code / DNS settings.

Take a note of your IP address, in this guide our server’s floating IP is 1.2.3.4.

Connect to and update the server

Open Terminal and SSH into your VPS.

ssh root@1.2.3.4

Update the server:

sudo apt-get update
sudo apt-get upgrade

2. Run your app

To run Django apps, your server requires Python, Django, a database connection and other app-specific packages.

Python

Install Python 3.

sudo apt-get install python3-pip python3-dev

Check you have the correct version of Python installed (this guide uses Python 3.7)

python3 --version

If you need to upgrade (e.g. you have Python 3.6), enter the following commands (changing ‘python3.6’ and ‘python3.7’ for a your previous and new versions of Python):

sudo apt update -y
sudo apt install python3.7

sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.6 1
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.7 2
sudo update-alternatives --config python3

Choose the version referring to Python 3.7.

Check again that the correct Python version is now installed.

python3 --version

PostgreSQL

If you’re using a managed database hosted elsewhere (usually a good idea), then skip this section. Otherwise, let’s set-up PostGreSQL on your VPS.

Install PostgreSQL.

sudo apt-get install libpq-dev postgresql postgresql-contrib

Open the PostgreSQL interface:

sudo -u postgres psql

The PostgreSQL prompt should now open.

Create a user (oliver) & database (myapp_db), and set up the correct permissions. Remember to include a ; at the end of each command.

CREATE DATABASE myapp_db;
CREATE USER oliver WITH PASSWORD 'ENTER-YOUR-PASSWORD-HERE';
ALTER ROLE oliver SET client_encoding TO 'utf8';
ALTER ROLE oliver SET default_transaction_isolation TO 'read committed';
ALTER ROLE oliver SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE myapp_db TO oliver;
\q

The last command \q will quit your terminal.

In order to connect to the database from outside your VPS, edit your pg_hba.conf file (run SHOW hba_file; in the PostgreSQL interface to show the location) so it includes the following text. This allows all external IP addresses, denoted as 0.0.0.0/0, to connect to the database.

host    myapp_db      oliver          0.0.0.0/0               trust

In the same directory edit postgresql.conf so it has the line:

listen_addresses = '*'	# what IP address(es) to listen on;

Django

We will create a folder for our Django project files and create a virtual environment.

Install virtualenv.

sudo -H pip3 install --upgrade pip
sudo -H pip3 install virtualenv

In this example we create a project folder called ‘myapp’ in the home directory.

cd /home/
mkdir myapp
cd myapp

Create a virtual environment.

virtualenv env

You should now see a new env directory for your virtual environment. Populate the app folder with your Django project files.

Edit your Django settings.py file so that your new floating / server IP address is an allowed host, add your app’s future domain name too.

ALLOWED_HOSTS = ['1.2.3.4', 'oliverlambert.com']

Ensure the rest of your settings are production ready, for example:

DEBUG = False
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
CSRF_COOKIE_SECURE = True
X_FRAME_OPTIONS = 'DENY'
SECURE_REFERRER_POLICY = 'same-origin'
SECURE_HSTS_SECONDS = 60
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
CSRF_TRUSTED_ORIGINS = ['https://*.oliverlambert.com', 'https://*.127.0.0.1', 'https://*.1.2.3.4']

If required, update settings.py so Django can connect to the local PostgreSQL database.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'HOST': '127.0.0.1',
        'PORT': 25060,
        'USER': 'oliver',
        'PASSWORD': 'YOUR-PASSWORD',
        'NAME': 'myapp_db',
    }
}

Finally, copy your project’s static folder into the var/www/ directory. We will set-up nginx later to serve static files.

Python packages

Activate your virtual environment and install the required packages from your requirements.txt file.

Ensure your requirements file includes Django (this tutorial uses Django==3.0.6), Django Channels (channels==2.4.0) and Daphne (daphne==2.5.0).

source /home/myapp/env/bin/activate
cd /home/myapp/myapp
pip install -r requirements.txt

Channel backend

Django Channels uses Redis as a channel layer. In this tutorial we will use a free Redis database provided and managed by RedisLabs.

Sign up for a free RedisLabs account and update your settings.py file to direct Django Channels to your database. Update the {parameters} with your credentials.

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": ["redis://default:{USERNAME}@{HOST.cloud.redislabs.com}:{PORT}"],
        },
    },
}

Perform database migrations

If using a new database, perform any database migrations

source /home/myapp/env/bin/activate
cd /home/myapp/myapp

python3 manage.py makemigrations
python3 manage.py migrate

Django should now be able to run without errors:

python3 manage.py runserver

3. Make your app accessible online

Set up nginx

Create a new file called /etc/nginx/sites-available/myapp

sudo nano /etc/nginx/sites-available/myapp

Paste the below configuration, replacing oliverlambert.com with your domain name.

upstream channels-backend {
    server localhost:8000;
}

server {
    server_name www.oliverlambert.com ;
    return 301 $scheme://oliverlambert.com$request_uri;
}

server {
    server_name oliverlambert.com 1.2.3.4;

    client_max_body_size 30M;
    location = /favicon.ico { access_log off; log_not_found off; }

    location /static {
        root /var/www;
    }

    location / {
        try_files $uri @proxy_to_app;
    }

    location @proxy_to_app {
        proxy_pass http://channels-backend;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $server_name;
    }
}

Exit nano with Ctrl + X and then Y, Enter.

The above tells nginx:

  • To strip the www. from all requests
  • To not log errors associated with a missing favicon.ico file
  • The location of your static file directory
  • To let Django deal with any other requests

Create a symbolic link from this config file to the sites-enabled directory:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/

Restart nginx

sudo systemctl restart nginx

Set up firewall

The server’s firewall will likely be disabled / inactive. Check this is the case:

ufw status
ufw show added

Tell the firewall to give access to nginx and SSH.

sudo ufw allow 'Nginx Full'
sudo ufw allow ssh

Enable the firewall and reboot the server. You will need to reconnect via SSH.

sudo ufw enable
sudo reboot

The firewall should now be active with both rules added:

ufw status
ufw show added

Test Daphne

cd /home/myapp
source env/bin/activate
cd myapp
daphne myapp.asgi:application

Your app should now be accessible at its public IP address.

If it does not work, ensure you have added the server’s IP address to the ALLOWED_HOSTS Django setting.

If images / stylesheets are not loading, there is an issue with nginx. Ensure your static files are in the /var/www/static directory and that your nginx config correctly refers to this directory. You may also want ensure the static files have the right read permissions with chmod.

sudo chmod -R 755 /var/www/

If you have any further issues, check your asgi.py file looks something like the below.

import os
import django
django.setup()

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator

from django.core.asgi import get_asgi_application
import app.routing

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'my_app.settings')
django_asgi_app = get_asgi_application()

application = ProtocolTypeRouter({
    "http": django_asgi_app,
    "websocket": AllowedHostsOriginValidator(
        AuthMiddlewareStack(URLRouter(app.routing.websocket_urlpatterns))
    ),
})

4. Enable SSL

Update your domain’s DNS settings to include an A record pointing to your server’s (floating) IP address. It may take a few hours for this to be set-up.

Once done, SSH into your VPS and enter the below commands, ensuring you replace oliverlambert.com with your domain name.

When asked, make sure you do not allow Certbot to forward all http:// traffic to https:// as it adds an error into a config file. We will do this manually afterwards.

sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install python3-certbot-nginx

sudo certbot --nginx -d oliverlambert.com

sudo certbot renew --dry-run

Once complete, manually redirect all non https:// traffic to https:// by adding the following to the end of your nginx config file (/etc/nginx/sites-available/myapp). You should also delete or comment out the listen 80; line that Certbot added earlier on in the file.

server {
    if ($host = oliverlambert.com) {
        return 301 https://$host$request_uri;
    }


    server_name oliverlambert.com 1.2.3.4;
    listen 80; # <---- DELETE THIS LINE --------<<<
    return 404;
    }

The final config file /etc/nginx/sites-available/myapp should now look something like:

upstream channels-backend {
    server localhost:8000;
}

server {
    server_name www.oliverlambert.com ;
    return 301 $scheme://oliverlambert.com$request_uri;
}

server {
    server_name oliverlambert.com 1.2.3.4;

    client_max_body_size 30M;
    location = /favicon.ico { access_log off; log_not_found off; }

    location /static {
        root /var/www;
    }

    location / {
        try_files $uri @proxy_to_app;
    }

    location @proxy_to_app {
        proxy_pass http://channels-backend;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $server_name;
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/oliverlambert.com-0001/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/oliverlambert.com-0001/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}

server {
    if ($host = oliverlambert.com) {
        return 301 https://$host$request_uri;
    }


    server_name oliverlambert.com 1.2.3.4;
    # listen 80;
    return 404;

}

5. Keep your app running

Supervisor is a utility that can keep a process running on your server. We will use it to keep Daphne serving your app.

Install Supervisor

sudo apt-get -y install supervisor
sudo systemctl enable supervisor
sudo systemctl start supervisor

Navigate to the supervisor config directory and add a new config file:

cd /etc/supervisor/conf.d/

sudo nano /etc/supervisor/conf.d/myapp.conf

Paste in the following:

[program:myapp]
socket = tcp://localhost:8000

directory = /home/myapp/myapp

environment = LANGUAGE=en_US.UTF-8,LANG=en_US.UTF-8,LC_ALL=en_US.UTF-8,PYTHONPATH='/home/myapp/env/'
user = root

command = /home/myapp/env/bin/daphne --proxy-headers myapp.asgi:application

autostart = true
autorestart = true
redirect_stderr = true
stdout_logfile = /home/myapp/logs/supervisor.log

Create two new folders in /home/myapp/ called logs and run.

Update Supervisor.

sudo supervisorctl reread
sudo supervisorctl update

Whenever you update your app’s source files you will need to restart Supervisor:

service supervisor restart

Your app should now be successfully running and available to the web, congratulations!