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
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