Recently, I decided I wanted to spin up a low-cost virtual private server (VPS) to deploy a web app that I'm developing for my own enjoyment and personal use (it's called "Heroplex"). Though I'm accustomed to the AWS deployment pipeline for my day-to-day client work, I wanted something a little less chonky and intimidating for this small side project. I asked a friend of mine what service he'd recommend that fit the cheap and relatively easy-to-setup profile, and he suggested looking into Digital Ocean, which is what I ended up selecting. The application I'm developing is a fairly simple Python+Django and MySQL thing, so here's a detailed step by step of how I got it up and running on a Digital Ocean Droplet.

Disclaimer: The process outlined below is very much for a development environment. You'd want to consider a whole host of security and performance issues to get this thing ready for production, but we're not going to cover those matters here.

1. Setup a Digital Ocean Droplet

Digital Ocean's Droplets provide a cheap, easy platform for virtual machine management and application deployment. Sign up for a Digital Ocean account and head to Create > Droplets to get started. I chose the cheapest and most basic options for this project:

  • Ubuntu 20.04 x 64
  • Shared CPU "Basic" option for $5/month
  • NY 1 Datacenter Region
  • Password authentication (set a strong password in the box)
  • Hostname set to the name of my project, in this case "Heroplex."
  • No backups enabled because I'm penny pinching, but please feel free to enable them if you wish.

2. Use terminal to ssh into the Droplet you just created.

On your Digital Ocean dashboard, you're going to see the Droplet you just created. That droplet has an IPV4 address (in my case, it's 159.203.187.32)-- go ahead and copy that address to your clipboard. In your terminal, type the following, replacing the address with the number you just copied:

ssh root@159.203.187.32

This will attempt to log you in as the Droplet root user. You will be prompted to enter the master Droplet password that you created in Step 1. Type your password and hit enter.

3. Run updates

You'll see a welcome message from your Droplet. The first think we're going to do is run updates on your instance, just to make sure we're starting with the most up-to-date packages.

sudo apt-get update
sudo apt-get -y upgrade

4. Install MySQL and run configuration script

As mentioned, this Django project uses MySQL, so we need to install mysql-server on the Droplet. You might want to use another database management system, so feel free.

sudo apt-get install mysql-server

After the installation finished, we're going to use an included security configuration script to clean up a few things and patch a few security holes.

sudo mysql_secure_installation

You'll be prompted to run a password validator component-- I chose option 2 for my password validation, but feel free to choose what makes sense for you. You'll be asked to set a MySQL root password, remove anonymous users, disallow remote root login, remove the default database (called "test"), and if you want to reload the privileges table for the changes to take effect (say "yes" to this last bit).

5. Create a dedicated MySQL user

For security reasons, we don't want our application to use the root MySQL user to connect to the database we are going to create. Let's setup a dedicated MySQL user that the app can use to access the database. I'm giving my MySQL user the same nane as my application-- "heroplex." Replace "heroplex" below with whatever you want your user to be called, and replace "password" with a strong password that your app can use to connect to the database.

sudo mysql
mysql> CREATE USER 'heroplex'@'localhost' IDENTIFIED BY 'password';

6. Grant necessary database privileges to the user you just created

By default, the MySQL user you just created doesn't have permission to do much of anything in the database management environment. We have to explicitly grant the user privileges to perform a variety of actions on the database. For our Django app, the user needs the privileges listed in the command below.

mysql> GRANT CREATE, ALTER, DROP, INSERT, UPDATE, DELETE, SELECT, REFERENCES, RELOAD, INDEX on *.* TO 'heroplex'@'localhost' WITH GRANT OPTION;

7. Create a new database for your application

Your application needs a database to connect to! Let's create one called heroplex_db (you can rename yours) and then flush privileges to make sure all of the changes stick.

mysql> CREATE DATABASE heroplex_db;
mysql> FLUSH PRIVILEGES;

After you're done, you can exit MySQL.

mysql> exit

8. Install NGINX and Supervisor

NGINX is a free and open-source web server that we are going to install on our Droplet.

sudo apt-get -y install nginx

Supervisor is a cool little client/server tool that allows us to monitor and control processes on Linux and UNIX-like operating systems. Supervisor will keep our server going and restart it in the case of hiccups and technical glitches. We're going to install it so that we don't have to log on and restart NGINX manually every time something goes wrong.

sudo apt-get -y install supervisor

We're also going to enable and start the Supervisor client:

sudo systemctl enable supervisor
sudo systemctl start supervisor

9. Setup a virtual environment on your Droplet to manage requirements and packages

We're going to install the Python 3 virtual environment package, as well as a python-dev package that is a dependency for a few other things down the line:

sudo apt-get -y install python3-virtualenv
sudo apt-get install python-dev

10. Create and configure an application user for your Django application

We're going to make a new Droplet user, give it sudo privileges, and configure the Python virtual environment inside of that newly-created user's home directory.

adduser heroplex

Fill out the fields if you wish. Then give this new user sudo privileges and switch to this new user:

gpasswd -a heroplex sudo
su - heroplex

11. Configure the Python virtual environment and clone your project repo

We are now logged in as our new Droplet user, in our case named "heroplex." We're going to install our Django application (which we've already spun up locally and pushed to a GitHub repo) here, so let's go ahead and initiate the virtual environment:

virtualenv .
source bin/activate

And now we'll just clone our project repo (replace the url with that of your own project):

git clone https://github.com/chadamski/Heroplex.git

Enter your GitHub username/password to start the download.

12. Install your Django project's dependencies

We have to install a couple of things to get Python and MySQL working together nicely. (I ran into a bunch of errors the first time I tried to do this.) Install the following list of dependencies:

cd Heroplex
sudo apt-get install mysql-server
sudo apt-get install python3-dev default-libmysqlclient-dev build-essential
sudo apt-get install libssl-dev

Once the dependencies are installed, you should be able to install mysqlclient, as well as the rest of your project's dependencies, located in requirements.txt:

pip install mysqlclient
pip install -r requirements.txt

13. Set the proper database connection credentials in your Django project's settings.py file and add your IP address to allowed hosts

In your project repo, locate your app's settings.py file and add the database connection details you created earlier. In addition, you'll want to add your Droplet IP address to the "allowed hosts" section of your settings.py file.

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'heroplex_db',
'USER': 'heroplex',
'PASSWORD': 'password',
}
}

ALLOWED_HOSTS = [
'159.203.187.32',
'127.0.0.1',
'localhost',
]

Navigate to your project folder and 'git pull origin' so you can snag these updates. Optionally, you may want to configure environment variables to store credentials and easily switch between local and development databases.

14. Test everything to make sure it's working

We're going to run Django migrations to our database, collect static assets for the project, and then run the development server to make sure everything is properly configured:

python manage.py migrate
python manage.py collectstatic
python manage.py runserver 0.0.0.0:8000

How will we know if this works? Well, head on over to your IP address at port 8000:

http://159.203.187.32:8000/

Success! After you've confirmed this works, hit CTRL+C to quit the development server. Now we're going to automate your server setup.

15. Install and configure Gunicorn

Gunicorn is a lightweight and speedy Python WSGI HTTP server. Let's install it inside of our virtual environment and create a start file:

pip install gunicorn
vim bin/gunicorn_start

Copy over the following contents and save. NOTE: This was the most troublesome part of my project setup because of how many nested "heroplex" folders I had inadvertently created. If I were doing this again, I'd probably have chosen a few different naming conventions along the way (such as renaming the app user something other than heroplex) to make this all less confusing. Suffice it to say, if you run into difficulties, you probably have set the improper DIR, BIND, source, or exec setting in this file. Play with all of that nesting until you get it right.

#!/bin/bash

NAME="heroplex"
DIR=/home/heroplex/Heroplex/heroplex
USER=heroplex
GROUP=heroplex
WORKERS=3
BIND=unix:/home/heroplex/run/gunicorn.sock
DJANGO_SETTINGS_MODULE=heroplex.settings
DJANGO_WSGI_MODULE=heroplex.wsgi
LOG_LEVEL=error

cd $DIR
source ../../bin/activate

export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DIR:$PYTHONPATH

exec ../../bin/gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $WORKERS \
--user=$USER \
--group=$GROUP \
--bind=$BIND \
--log-level=$LOG_LEVEL \
--log-file=-

As a final step, we need to make sure our gunicorn_start file is executable. We also want to create a new directory called run, which is where Unix will look for our socket file:

chmod u+x bin/gunicorn_start
mkdir run

16. Configure Supervisor to keep an eye on our newly-created Gunicorn server

Let's set up a little big of logging inside our virtual environemtn so that we can tell if anything is going wrong.

mkdir logs
touch logs/gunicorn-error.log

We'll check this log file if the server fails to start-- this is how I discovered my paths were all screwed up in the step above. Now let's create a Supervisor configuration file:

Create a new Supervisor configuration file:

sudo vim /etc/supervisor/conf.d/heroplex.conf

Add the following contents to that file:

[program:heroplex]
command=/home/heroplex/bin/gunicorn_start
user=heroplex
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/home/heroplex/logs/gunicorn-error.log

Let's now tell Supervisor to check what we just created and update itself with our configurations. Then we'll check the status of the server and its machine overlord.

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status heroplex

You should see something like this:

heroplex                         RUNNING   pid 38466, uptime 2:28:38

If you don't, just check those error logs we created, and make sure your routes are all set correctly in your gunicorn_start file.

Now Supervisor is in control of our web application.

17. Configure NGINX

Let's add an NGINX configuration file inside of /etc/nginx/sites-available/:

sudo vim /etc/nginx/sites-available/heroplex

Add the following contents and save:

upstream app_server {
server unix:/home/heroplex/run/gunicorn.sock fail_timeout=0;
}

server {
listen 80;

# set this to be the IP address or domain of your Droplet
server_name 159.203.187.32;

keepalive_timeout 5;
client_max_body_size 4G;

access_log /home/heroplex/logs/nginx-access.log;
error_log /home/heroplex/logs/nginx-error.log;

# tell NGINX how to serve Django's static files and watch out for all of that nested nonsense
location /static/ {
alias /home/heroplex/Heroplex/heroplex/static/;
}

location / {
try_files $uri @proxy_to_app;
}

location @proxy_to_app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://app_server;
}
}

Then we will create a symlink (symbolic link) between the sites-available drive and the sites-enabled drive within the NGINX directory. The symlink is essentially an alias that allows us to keep two copies of the file without having to update it in two locations every time we need to make a change.

sudo ln -s /etc/nginx/sites-available/heroplex /etc/nginx/sites-enabled/heroplex

Now let's remove the default NGINX website and restart the server:

sudo rm /etc/nginx/sites-enabled/default
sudo service nginx restart

18. Updating the application

To update our application after we've made changes to our repo, we just need to follow a few steps. If we haven't already, we'll connect to our Droplet and spin up our virtual environment:

ssh urban@159.203.187.32
source bin/activate

Then we'll navigate to the project folder and pull down updates from our repo:

cd heroplex
git pull origin master

After that, we just need to collect our static assets, perform migrations to our database, and restart the Supervisor process. That's it!

python manage.py collectstatic
python manage.py migrate
sudo supervisorctl restart heroplex

Now you're the proud owner of a shiny new Django + MySQL development environment on a Digital Ocean Droplet. Thanks to Vitor Freitas who published an earlier example of this process using a PostgreSQL database. I've updated for the latest 2021 packages and added notes on MySQL dependencies. Happy coding!