close up photo of ethernet cables on network switch

Deploying a Django + Nginx application to a VPS with ansible

Introduction

This article will walk you through the process of deploying a Django + Nginx application to a VPS (Amazon Lightsail). I’ll use Docker to build and containerise the application and Ansible for automated deployments and server provisioning. To make it easy to follow along, I’ll deploy my invoice generator application which you can download from my GitHub. You’ll see how to set up AWS Lightsail instance, add a domain and use ansible to configure and deploy code to it.

Prerequisites

To follow along, you’ll need the following:

The Application

We’ll deploy a standard Django application that is served using nginx and gunicorn.

Here’s a simplified view of the code:

.
├── ansible
│   ├── ansible.cfg
│   ├── deploy.yaml
│   ├── hosts
│   ├── README.md
│   ├── roles
│   │   ├── common
│   │   │   ├── tasks
│   │   │   │   ├── checkout.yaml
│   │   │   │   └── webhook_service.yaml
│   │   │   └── templates
│   │   │       ├── set_environment.j2
│   │   │ 
│   │   ├── docker
│   │   │   └── tasks
│   │   │       └── main.yaml
│   │   └── security
│   │       └── tasks
│   │           └── main.yaml
│   └── setup.yaml
├── docker-compose.yml
├── Dockerfile
├── docs
│   └── README.md
├── gunicorn.conf.py
├── invoices
│   └── ...(The main app)
├── manage.py
├── nginx
│   ├── Dockerfile
│   ├── nginx.conf
│   ├── nginx-dev.conf
│   └── start_nginx.sh
├── README.md
├── requirements.txt
├── templates
│   ├── ...(template files)

  • The ansible folder contains ansible playbooks to setup the VPS and deploy code to it.
  • The nginx folder contains nginx configurations to serve our application’s static files and handle HTTPS.
  • gunicorn is a Python web server that allows you to run Python web applications including Django and Flask. It serves the application via a reverse proxy defined in nginx-dev.conf or nginx-prod.conf.

Creating the VPS

Login to AWS Lightsail. Once logged in, click the Create Instance button to create a new VPS instance and go through the following steps.

We want to use a bare-bones server, so in the “Pick your instance image“, Click “Linux/Unix” and select “OS Only” and pick an Operating system. I’ll go with Ubuntu 22.04 in this example:

Select a monthly plan and optionally, a new SSH key pair:

Give your VPS instance a name and choose “Create Instance” when you’re done:

Give the instance a few moments to initialise, after which you should see the instance up and running. You can connect to it without leaving the LightSail dashboard. In the Lightsail home page, choose the menu on the right of your instance’s name, and then choose connect:

Alternatively you can connect via the SSH client installed on your computer. To do this, take note of the IP address of the instance and SSH as the default user ubuntu. In most VPS services the default user is root. AWS Lightsail does not allow SSH root access by default:

ssh ubuntu@<your_server_ip>
exit

We’ll create a new user later using ansible.

We don’t do much manual configuration from this point, ansible will do the heavy lifting for you.

Add a domain

Purchase and point your domain to Lightsail. You need a domain to request TLS certificates from Let’s Encrypt for HTTPS. I used Route 53 to purchase and configure my domain.

Configure VPS instance using Ansible

Next, we need to setup and configure the VPS instance with the software and configuration it’ll need to run the project. We’ll use ansible to configure the server and deploy code to it. Add the newly created server instance to Ansible by creating a hosts or inventory file:

[cybertron]
invoices.vndprojects.com

[cybertron:vars]
deploy_environment=production
repo_name=terrameijar/invoices
repo_branch=develop
create_user=optimus
repo_folder="/home/{{create_user}}/invoices"
env_file=../.env
  • The create_user variable is the name of the user account we’ll create using ansible. Change this to a username of your choice.
  • cybertron is what I’m calling this server. You can give it any name you like and point it to your domain or the IP address of your VPS.
  • repo_name, repo_branch and repo_folder are variables we’ll use later when deploying the code.

Create and copy over any .env files you need to the VPS. For this project, this will do:


ENVIRONMENT=development
DEFAULT_FROM_EMAIL = "admin@example.com"
ALLOWED_HOSTS = localhost, 127.0.0.1
SECRET_KEY = "django-insecure-1u_jv5-k)#@cs2#)9$_@gj=0$s)p6u8vyozx!8jro_i_v!m(wq"
DEBUG = True

Ansible Playbook to setup the server

The playbook below will set up the VPS instance with the necessary packages such as git, and UFW which we’ll need later.

# ansible/setup.yaml
---

- name: Setup Server
  hosts: cybertron
  become: true
  vars:
    ansible_user: ubuntu
    sys_packages: ["curl", "git", "ca-certificates", "apt-transport-https", "software-properties-common", "gnupg", "ufw"]
    server_name: cybertron
    copy_local_ssh_key: "{{ lookup('ansible.builtin.file', lookup('ansible.builtin.env', 'HOME') + '/.ssh/id_ed25519.pub') }}"
  roles:
    - role: security
    - role: docker

This playbook has two roles; security and docker. The first role, security creates rules for the UFW firewall and creates a new SSH user which will be the replacement for the default root or ubuntu user created when the VPS instance was created. The role disables remote root logins and disallows password authentication over SSH in favour of public key authentication.

The second role, docker installs Docker and all dependencies required to run the dockerised project.

Run Playbook

Run the playbook. For the first run, run it using the ubuntu username created by Lightsail.

cd ansible
ansible-playbook --user ubuntu setup.yaml

If the playbook ran successfully, you should be able to SSH via the newly created user account.


ssh <your-user>@<server-ip>

SSL/TLS certificates

When the ENVIRONMENT variable in the env file is set to development, the docker build process generates a self signed TLS certificate and configures nginx to use it. Self signed certificates are okay for testing, but In production you’ll want to use a “real” certificate from a recognised certificate authority such as Let’s Encrypt.

Request SSL certificates from Let’s Encrypt using standalone mode and copy their file paths to your Nginx config. If you like, you can automate this step using a script that can be run as an ansible role.

Deploying the application

Run the deploy.yaml playbook to deploy the code to the VPS instance.

---

- hosts: cybertron
  gather_facts: true
  become_user: "{{create_user}}"
  vars:
    ansible_user: "{{create_user}}"

  tasks:
    - include_tasks: roles/common/tasks/checkout.yaml
    
    - name: Run `docker compose up --build --detach
      command:
        docker compose up --build --detach
      args:
        chdir: "{{ repo_folder }}"
      register: output

    - debug:
        var: output

This playbook SSHes into the server as the user you created when setting up the server, clones the code using git and brings up the docker containers.

Conclusion

By running the setup and deploy playbooks, you configured a server and deployed a Django + Nginx project to your server with HTTPS. In the next article, I’ll show you how to go an extra step and use GitHub Actions to create a CI/CD pipeline that automatically runs tests and deploys new code to the server whenever a pull request is made to the main branch.