A guide on deploying Django application using Chaussette WSGI Server, Circus as process and socket manager in AWS Ubuntu 20 LTS EC2 instance.

What is Chaussette?

Chaussette is a WSGI server that you can use to run your Python WSGI applications; for this article, we’ll run Django application. It can bind a socket on a port or run in already opened sockets.

We will be using Circus to manager socket/process manager.

What is Circus?

Circus is a Python program which can be used to monitor and control processes and sockets.

Setup SSH on local

Once you download the PEM key after creating an instance from AWS console, move it to the /home/user/.ssh and add config for quick connect;

Host forum # name you prefer
        Hostname # your server IP
        User ubuntu # user
        IdentityFile ~/.ssh/aws-n-virginia-ubuntu.pem # file name

Let’s connect to the server using SSH using: ssh forum

Server Setup

The first thing after login is to upgrade everything.

  • sudo apt-get update && sudo apt-get -y upgrade

Install tools

Install useful tools that we will be using;

  • sudo apt install python-dev build-essential links python3-pip python3-cffi python3-wheel python3-setuptools libffi-dev libreadline-gplv2-dev libncursesw5-dev libssl-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev virtualenv git


Nginx is a reverse proxy, load balancer, mail proxy and HTTP cache - generic TCP/UDP proxy server.

Install Nginx, enable, setup Firewall.

  • sudo apt install nginx
  • sudo ufw app list # view the list
  • sudo ufw allow 'Nginx HTTP' # enable port 80
  • sudo ufw allow 'Nginx HTTPS' # enable port 443
  • sudo ufw allow 'Nginx Full' # optional
  • sudo systemctl enable nginx && systemctl start nginx


PostgreSQL is an open-source object-relational database system. Let’s install Postgres and setup.

  • sudo apt-get install postgresql postgresql-contrib
  • sudo systemctl enable postgresql && sudo systemctl start postgresql

Setup database;

  • sudo -i -u postgres
  • psql

Setup Git Repository in Server

There are many ways on pushing local or remote code to the deployment server i.e. CI/CD, FTP. We’ll be setting up the remote server using Git.

I learned this great approach working for some company - this will only push the latest HEAD changes from your local dev environment. This method is useful when you don’t want to directly pull the changes from Git repository into the server.

Let’s name our origin as server.git and set up the standard folder structure. Appending .git in a directory name helps to quickly identify it as git repository.

  • mkdir backend && cd backend && mkdir server.git app conf logs media static && cd server.git

Next we setup a server.git as bare repo and configure.

  • git init --bare && git --bare update-server-info && git config core.bare false && git config receive.denycurrentbranch ignore && git config core.worktree /home/ubuntu/backend/app/

Add hooks for post-receive so that we checkout file every time when we do push.

cat > hooks/post-receive <<EOF
git checkout -f
source ../venv/bin/activate
circusctl reload
# other scripts path here

Info: post-receive is invoked when doing git push and updates. You could customize it to run other commands or notify user. Although anything that might take longer time is better to avoid as suggested in offical doc.

and don’t forget to make it executable using;

  • sudo chmod +x hooks/post-receive

Server configs

Virtual Env

Create a virtual environment Python 3 environment and activate

  • virtualenv venv -p python3 && source venv/bin/activate

Circus and Chaussette

Install it using;

  • pip install circus chaussette
  • cd conf && cat>circus.ini
cmd = chaussette --fd $(circus.sockets.webapp) forum.wsgi.application # Django setting path
endpoint_owner=ubuntu # server user
numprocesses = 3
use_sockets = True
copy_env = True
copy_path = True
virtualenv = /home/ubuntu/backend/venv/ # virtualenv path
stdout_stream.class = FileStream
stdout_stream.filename = /home/ubuntu/backend/logs/app.log
stderr_stream.class = FileStream
stderr_stream.filename = /home/ubuntu/backend/logs/app_err.log

# optionally rotate the log file when it reaches 1 gb
# and save 3 copied of rotated files
stdout_stream.max_bytes = 1073741824
stdout_stream.backup_count = 3
stderr_stream.max_bytes = 1073741824
stderr_stream.backup_count = 3


host =
port = 8085

Refer here for other configs.

Config Circus Daemon

  • circusd --daemon circus.ini && circusctl reloadconfig && circusctl reload

Config Nginx

  • cat>nginx.conf
upstream django_project {
    server; # for a web port socket see circus.ini [socket:webapp]
    # server unix:///path/to/your/mysite/mysite.sock; # for a file socket

# Redirect www.forum.vijaypathak.com.np to forum.vijaypathak.com.np
# server {
#     listen 80;
#     server_name  www.forum.vijaypathak.com.np;
#     return       301 https://forum.vijaypathak.com.np$request_uri;
# }

# Configs for server
server {
    listen 80; # server port

    server_name forum.vijaypathak.com.np; # domain/IP name
    charset     utf-8;

    # Enable when required
    # access_log /home/ubuntu/backend/logs/nginx.access.log;
    # error_log /home/ubuntu/backend/logs/nginx.error.log;

    #limit_conn conn_limit_per_ip 100;
    #limit_req zone=req_limit_per_ip burst=100 nodelay;

    # robots.txt path
    #location /robots.txt {
    #    alias /home/ubuntu/backend/static/robots.txt;

    # Favicon path
    #location /favicon.ico {
    #    alias /home/ubuntu/backend/static/img/favicon.ico;

    # Static file
    location /static/ {
            alias /home/ubuntu/backend/static/;

    # Media file
    location /media/ {
        alias /home/ubuntu/backend/media/;

    ## Deny other host names i.e your domain/IP here
    if ($host !~* ^(forum.vijaypathak.com.np)$ ) {
        return 444;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;
        # proxy_set_header X-Forwarded-Proto https; #enable ssl
        # proxy_pass http://webapp;
        client_max_body_size 50m;
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; # 2 years
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";
        add_header X-Frame-Options SAMEORIGIN;

        # Enable when required
        # CSP allowing popular third party integrations
        add_header Content-Security-Policy "default-src 'self' ; script-src 'self' 'unsafe-inline' 'unsafe-eval' adservice.google.com adservice.google.com.np pagead2.googlesyndication.com d31qbv1cthcecs.cloudfront.net www.google-analytics.com cdn.ravenjs.com connect.facebook.net platform.twitter.com apis.google.com www.google.com www.gstatic.com maps.googleapis.com; connect-src 'self' googleads.g.doubleclick.net fonts.gstatic.com wss: securepubads.g.doubleclick.net d5nxst8fruw4z.cloudfront.net sentry.io maps.gstatic.com www.google-analytics.com certify.alexametrics.com; img-src 'self' data: certify.alexametrics.com maps.gstatic.com maps.googleapis.com d5nxst8fruw4z.cloudfront.net www.google-analytics.com stats.g.doubleclick.net ssl.gstatic.com csi.gstatic.com www.facebook.com syndication.twitter.com www.gravatar.com pagead2.googlesyndication.com; style-src 'self' 'unsafe-inline' fonts.googleapis.com; font-src 'self' data: cdn.vijaypathak.com.np fonts.gstatic.com; frame-src googleads.g.doubleclick.net www.youtube.com accounts.google.com content.googleapis.com www.facebook.com staticxx.facebook.com platform.twitter.com; manifest-src 'self'; worker-src 'self' fonts.gstatic.com";

    # Deny all hidden files and directory being served
    # Append in bottom
    location ~ /\. { 
      access_log off; 
      log_not_found off; 
      deny all; 

Read more on Nginx configs here.

Add nginx config to /etc/nginx/nginx.conf

  • sudo nano /etc/nginx/nginx.conf

Include Nginx config inside http;

include /home/ubuntu/backend/conf/nginx.conf;
  • sudo nginx -t && sudo systemctl restart nginx


Run migration to generate table and relations in the database

  • python manage.py makemigrations && python manage.py migrate

Note: We are using Chaussette for serving our Application hence running the server manually like in local dev environment is not required.

SSL certificate with Certbot

Install Let’s Encrypt SSL (auto-renew/valid for 90days)

  • sudo apt-get install python3-certbot-nginx
  • sudo certbot --nginx # run cerbot to get SSL for domain

Finally test auto-renewal of SSL;

  • sudo certbot renew --dry-run

Include this in Django Settings for better Security


- SECURE_HSTS_SECONDS = 63072000  # 2 years
- SECURE_CONTENT_TYPE_NOSNIFF = True # Prevents Cross-Site scripts
- SECURE_SSL_REDIRECT = True # Include 'django.middleware.security.- SecurityMiddleware' at top in Middleware
- SECURE_REFERRER_POLICY = 'same-origin'

Other headers

- X_FRAME_OPTIONS = 'DENY' # Include 'django.middleware.clickjacking.XFrameOptionsMiddleware' in Middleware to Prevent Clickjacking 

Local Git setup

Let’s pull a Django application from remote Github to local first using;

git clone https://github.com/hbvj99/forum.git

Make sure that you have separate .env file and add your new secret key, database, SMTP or other credentials here.

Great, next we set up the remote server in local - more info.

  • git remote add APP_NAME forum@:/home/ubuntu/backend/server.git/
  • Check if you successfully set up the remote URL with git remote -v

  • git push APP_NAME --all local branches will be pushed on server, doesn’t require remote as upstream branch

Info: APP_NAME can be any name you prefer. forum is our ssh name that we set earlier.

Final things to consider

Finally, we check if our application ready is ready for production - it shouldn’t detect any issues when running;

  • python manage.py check --deploy

Also, don’t forget to check official deploy check list.

If you made to here; our site is Live now with a custom domain, SSL, SMTP and following decent security measure.

At last, get security analysis of your website on Mozilla Observatoty.


After you push the latest changes to the server, you need to run

  • circusctl reload

Tips: You can add above command in git hooks post-receive to auto run when changes is detected.

Folder Structure

Here’s the Final folder/file structure if you quickly want to check;

├── backend
    └── server.git # repo name
      ── HEAD
      └── braches
      ─── config
      └── description
      └── hooks
      ─── index
      └── info
      └── logs
      └── objects
      └── refs
        └── heads
        └── tags    
    └── app
    └── conf
      ─── circus.ini
      ─── nginx.conf    
    └── logs
      ─── app.log
      ─── app_err.log
    └── media
    └── static
    └── venv   

If you have any confusions or better approach that helps on improving the guide please comment down below.

Leave a comment