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
- The frontend is a React app written in Typescript and bundled with Webpack.
- The backend acts as an API built with Django REST framework.
- Nginx is used to serve the static files.
- 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 imagecertbot
renews HTTPS certificates and stores them in a volumedjango
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.confnginx
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.


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.

If you don’t have one, follow the instructions provided in the 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.

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

Enter your domain name and choose the correct droplet.

🛠 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]
setknboard.com
to<your-server-ip>
- Under
[knboard:vars]
setdomain_name
to your domain name - Under
[knboard:vars]
setcreate_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.