All Articles

Deploying Django+React+Nginx to DigitalOcean with Ansible

Servers

Introduction

👋 Hey. This article will walk you through the process of deploying Django+React+Nginx app to DigitalOcean. The combination of Ansible and Docker make for an excellent automated deployment process.

Let’s demonstrate this by setting up a server from scratch and deploying a Kanban boards app. And of course, we’ll be using Ansible to automate all of this.

Ansible is the simplest way to automate apps and IT infrastructure.

Project overview

GitHub source to the app we’ll deploy including Ansible files: https://github.com/rrebase/knboard

Git clone the project to follow along.

git clone git@github.com:rrebase/knboard.git
cd knboard
git checkout live

Simplified project structure:

$ tree .
.
├── ansible
│   ├── ansible.cfg
│   ├── deploy.yml
│   ├── hosts
│   ├── roles
│   │   ├── certbot
│   │   │   ├── tasks
│   │   │   │   └── main.yml
│   │   │   └── templates
│   │   │       └── letsencrypt.sh
│   │   ├── common
│   │   │   └── tasks
│   │   │       └── checkout.yml
│   │   ├── docker-compose
│   │   │   ├── files
│   │   │   │   └── wait.sh
│   │   │   └── tasks
│   │   │       └── main.yml
│   │   ├── nginx
│   │   │   ├── tasks
│   │   │   │   └── main.yml
│   │   │   └── templates
│   │   │       ├── nginx.conf
│   │   │       └── proxy.conf
│   │   └── security
│   │       └── tasks
│   │           └── main.yml
│   └── setup.yml
├── backend
│   └── ... (APIs with Django REST framework)
├── frontend
│   └── ... (React App)
├── django.Dockerfile
├── nginx.Dockerfile
└── docker-compose.yml
  1. The frontend is a React app written in Typescript and bundled with Webpack.
  2. The backend acts as an API built with Django REST framework.
  3. Nginx is used to serve the static files.
  4. Certbot obtains certs from Let’s Encrypt for HTTPS.

Docker Compose is a tool for defining and running multi-container Docker applications. With a single command, you create and start all the services from your YAML configuration.

🥞 Let’s take a look at the services in docker-compose.yml
  • postges runs the PostgreSQL database server, based on the official image
  • certbot renews HTTPS certificates and stores them in a volume
  • django collects static files to a volume accessible by nginx, starts the Gunicorn server that serves APIs and Django Admin via a reverse-proxy defined in nginx.conf
  • nginx is based on a custom image that uses multi-stage builds to package the React App, copy it to the Nginx container and then serves all the static files and proxies API requests
version: "3"

services:
  django:
    build:
      context: .
      dockerfile: django.Dockerfile
    image: knboard_production_django
    restart: unless-stopped
    volumes:
      - "django-static:/app/django-static"
      - "media:/app/media"
      - "./backend/settings:/app/settings"
    env_file:
      - .env
    depends_on:
      - postgres

  nginx:
    build:
      context: .
      dockerfile: nginx.Dockerfile
    command: '/bin/sh -c ''while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g "daemon off;"'''
    image: knboard_production_nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "django-static:/usr/share/nginx/django-static"
      - "media:/usr/share/nginx/html/media"
      - "/etc/nginx/nginx.conf:/etc/nginx/nginx.conf"
      - "/etc/nginx/proxy.conf:/etc/nginx/proxy.conf"
      - "./data/certbot/conf:/etc/letsencrypt"
      - "./data/certbot/www:/var/www/certbot"
    depends_on:
      - django
      - postgres

  certbot:
    image: certbot/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
    volumes:
      - "./data/certbot/conf:/etc/letsencrypt"
      - "./data/certbot/www:/var/www/certbot"

  postgres:
    image: postgres:11
    restart: unless-stopped
    volumes:
      - "/var/lib/postgresql/knboard:/var/lib/postgresql/data"
    env_file:
      - .env

volumes:
  media:
  django-static:

💧 Creating the server (droplet)

Login to your DigitalOcean account. Don’t have an account? Feel free to sign up with my referral link to get $100 in credit for 60 days.

Once you’re logged in. Go to Droplets from the Create menu.

Create droplet
Ubuntu 18.04
Ubuntu 18.04

Choose the Ubuntu 18.04 with a $10 plan, which has 2GB of memory (building these docker images may take over 1GB of memory).

Next pick any datacenter region (preferably one that’s closest to you).

We want to use an SSH key for the authentication method. Click New SSH Key and add your ssh key.

SSH key auth

If you don’t have one, follow the instructions provided in the modal.

Add SSH key modal

Finally, click Create Droplet on the bottom.

Connecting to the droplet

After a minute, you should see the droplet up and running. Take note of the ip as the next sections will include <your-server-ip> which you’ll have to replace with the ip of your own droplet.

New droplet

Make sure it’s possible to ssh to the droplet as root:

ssh root@<your-server-ip>
exit

If you’re having trouble, double check your ssh key.

Adding a domain

Point your domain to DigitalOcean nameservers from your Domain Registrar of choice (Tutorial).

A domain is needed to request SSL certificates from Let’s Encrypt (for HTTPS). Use a sub-domain if you already own a domain.

I used Namecheap and under Custom DNS entered the following nameservers:

ns1.digitalocean.com
ns2.digitalocean.com
ns3.digitalocean.com

☕️ Wait for the changes to take effect. This may take a while.

Next, go to the freshly created droplet and Add a domain

Add domain

Enter your domain name and choose the correct droplet.

Add A record

🛠 Configuring the server with Ansible

Now that we want to manage this new server, let’s add it to the inventory (hosts file).

🚨 Edit ansible/hosts

  • Under [knboard] set knboard.com to <your-server-ip>
  • Under [knboard:vars] set domain_name to your domain name
  • Under [knboard:vars] set create_user to the name of your ssh user
[all:vars]
ansible_become=true
ansible_become_user=root
env_file=../.env

[knboard]
knboard.com

[knboard:vars]
repo_folder=/srv/knboard/
repo_name=rrebase/knboard
repo_branch=live
letsencrypt_email=info@knboard.com
domain_name=knboard.com
create_user=rareba

Environment variables holding secrets and configurations are in a file named .env This file is in .gitignore and copied to the server.

Create the .env file from .production.env.example

cp .production.env.example .env

🚨 Edit .env and set DJANGO_ALLOWED_HOSTS to your domain

POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=knboard
POSTGRES_USER=knboard
POSTGRES_PASSWORD=examplepassword

DJANGO_SECRET_KEY=01tHOatUVZDm508jsRlEdtwbNuDFnsPFkrBcz2PUGhDWz5H7xO
DJANGO_STATIC_URL=/django-static/
DJANGO_STATIC_ROOT=/app/django-static/
DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_ALLOWED_HOSTS=example.com
DJANGO_ALLOW_GUEST_ACCESS=

Playbook ansible/setup.yml

---
# ansible/setup.yml

- hosts: knboard
  vars:
    docker_compose_version: 1.25.5
    sys_packages: ["curl", "vim", "git", "ufw", "haveged"]
    copy_local_key: "{{ lookup('file', lookup('env','HOME') + '/.ssh/id_rsa.pub') }}"

  roles:
    - role: security
    - role: docker-compose
    - role: nginx
    - role: certbot
  • The first role security creates a new ssh user. It will be a replacement for the root user. As a best practice, it also disables remote root access and disallows password authentication.
  • The second role docker-compose installs docker-compose, which is used to run the dockerized project.
  • The third role nginx creates configuration files from templates and copies them to the server so that the docker containers can read them via the appropriate volumes.
  • Finally certbot copies an initialization script that sets up HTTPS with Let’s Encrypt.

Let’s run the setup playbook. Note that we have to initially specify --user root. (Installing Ansible)

cd ansible
ansible-playbook --user root setup.yml

If the playbook ran successfully, you should now only be able to ssh via the created user.

ssh root@<your-server-ip>
Permission denied (publickey).

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

Let’s request SSL certs from Let’s Encrypt:

ssh <your-user>@<your-server-ip>
sudo su -
cd /srv/knboard/
./init-letsencrypt.sh

Note: The request may fail when nginx container isn’t successfully brought up. Run docker-compose up nginx look for error messages and repeat the last step if needed.

Deploying the application

Let’s run the deploy playbook, which will just git checkout the specified branch and build + bring up all the docker containers.

ansible-playbook deploy.yml

Note: You might need to specific --user <your-user> if your created user doesn’t match your default ssh user.

🎉 And we are done! A dockerized Django+React+Nginx project has been deployed to a new server with HTTPS.