Getting started with Flask on DigitalOcean Droplet running Ubuntu 18.x & Nginx

Getting started with Flask Python web app on DigitalOcean Droplet running Ubuntu 18.x and Nginx.

Getting started with Flask on DigitalOcean Droplet running Ubuntu 18.x & Nginx

Getting started with Flask on DigitalOcean is relatively simple if you have an understanding of what WSGI is. Guides I used to get my Flask website up are listed on this post.

WTF WSGI

Setting up Flask is simple and fun. A Python web framework that's robust enough to run complex websites.

For any server (nginx) to communicate with a Python web framework (Flask/Django) it requires something called Web Server Gateway Interface (WSGI).

WSGI is a standard for web servers to forward requests to web applications or frameworks written in the Python programming language.

There are several high level implementations of WSGI and I highly recommend nerding out on Gunicorn vs mod_WSGI or uWSGI:

  1. Why Instagram runs Gunicorn over uwsgi
  2. Some benchmark article
  3. A reddit post

In short, Gunicorn is simple to setup, uWSGI is powerful. For most applications Gunicorn will sufice.

In any case, read about uWSGI setup on DigitalOcean here and Gunicorn setup here.

I will be going with Gunicorn for my website.

Setting up Python Environment

First step, get all packages needed, along with PIP and Python.

sudo apt update
sudo apt install python3-pip python3-dev build-essential libssl-dev libffi-dev python3-setuptools

Virtual Env

Before starting any Python project, we create a virtual environment. These helps us keep the main Python area clean and have all our working files within a virutal env.

sudo apt install python3-venv

Create your project directory. This can be named anything:
mkdir ~/thisnameless

Get into it:
cd ~/thisnameless

Create a virtual 'evn'ironment for your Flask or Django app (I am using Flask):
python3 -m venv _env_webapp

Activate your virtual env:
source _env_webapp/bin/activate

Setting up Flask

First, install wheel to make sure that packages will install even if they are missing wheel archives, i.e. through eggs:
pip install wheel

If you are confused about wheels and eggs; don't worry, I am too.

Install Gunicorn and Flask:
pip install gunicorn flask

Creating a sample app / uploading our work

To test out Flask and Gunicorn, create sample code under:
nano ~/thisnameless/thisnameless.py

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "<h1 style='color:blue'>Hello There!</h1>"

if __name__ == "__main__":
    app.run(host='0.0.0.0')

To test this on the Flask's dev server, allow port 5000:
sudo ufw allow 5000

Finally, run the Flask dev server:
python thisnameless.py

A friendly message is shown reminding us not to use this dev server for production.

Navigate to your IP / domain name and test out port 5000. You should see "Hello There!"

Don't relax. That was the dev server.

Setting up WSGI entry point

We need to let Gunicorn server know where to enter the application. For that, we create the wsgi.py file:

nano ~/thisnameless/wsgi.py

Within that:

from thisnameless import app

if __name__ == "__main__":
    app.run()

All this file does is imports the app module from your main project file.

Setting up Gunicorn

From within the project folder provide gunicorn with manual instruction to bind 0.0.0.0:5000 to our wsgi.py -> app like so:
gunicorn --bind 0.0.0.0:5000 wsgi:app

Navigate to your IP / domain name and test out port 5000. You should see "Hello There!" again.

Feel free to deactivate the virtual environment:
deactivate

Create systemd service unit file. Creating a systemd unit file will allow Ubuntu's init system to automatically start Gunicorn and serve the Flask application whenever the server boots.

Read up on the powerful systemd here to have a deeper understanding on creating services.

Create a unit file ending in .service within the /etc/systemd/system directory to begin:

sudo nano /etc/systemd/system/thisnameless.service

Next part is complicated so, copy pasting from DO guide:

[Unit]
Description=Gunicorn instance to serve myproject
After=network.target

[Service]
User=<add_your_username_here>
Group=www-data
WorkingDirectory=/home/<user_name>/thisnameless
Environment="PATH=/home/<user_name>/thisnameless/_env_webapp/bin"
ExecStart=/home/<user_name>/thisnameless/_env_webapp/bin/gunicorn --workers 3 --bind unix:myproject.sock -m 007 wsgi:app

[Install]
WantedBy=multi-user.target

Description: Simple description of the service.
After: Specify when this service must be triggered, i.e. only after the network target has been reached.
User: This will specify the user and group that we want the process to run under.
Group: give group ownership to the www-data group so that Nginx can communicate easily with the Gunicorn processes.

Map out the working directory and set the PATH environmental variable so that the init system knows that the executables for the process are located within our virtual environment.

Specify the command to start the service. This command will do the following:

  • Start 3 worker processes (though you should adjust this as necessary)
  • Create and bind to a Unix socket file, myproject.sock, within our project directory. We'll set an umask value of 007 so that the socket file is created giving access to the owner and group, while restricting other access
  • Specify the WSGI entry point file name, along with the Python callable within that file (wsgi:app)

Finally, start the service we just created:
sudo systemctl start thisnameless
sudo systemctl enable thisnameless

After enable you will see:
Created symlink /etc/systemd/system/multi-user.target.wants/thisnameless.service → /etc/systemd/system/thisnameless.service.

Check status with:
sudo systemctl status thisnameless

Keep an eye out for: Active: active (running) since Fri 2018-07-13 14:28:39 UTC; 46s ago. Resolve any errors before continuing.

Making sure Nginx works

If previous guide was followed to create a working domain folder, only minor changes would be needed to the Nginx sites-available config file:

server {
    listen 80;
    server_name your_domain www.your_domain;

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/<user_name>/thisnameless/thisnameless.sock;
    }
}

Notice the location attribute

Confirm everything is fine:
sudo nginx -t

Restart:
sudo systemctl restart nginx

Finally, remove port 5000 from the firewall as we don't need it anymore:
sudo ufw delete allow 5000

If you encounter any errors, trying checking the following:

sudo less /var/log/nginx/error.log :checks the Nginx error logs.
sudo less /var/log/nginx/access.log :checks the Nginx access logs.
sudo journalctl -u nginx :checks the Nginx process logs.
sudo journalctl -u myproject :checks your Flask app's Gunicorn logs.

Check for other post on how to transfer your project files to the webserver and generating an SSL sertificate for your Flask web app.

Security comes first

Set up SSL using Let's Encrypt. First install the needed libraries:

sudo add-apt-repository ppa:certbot/certbot
sudo apt install python-certbot-nginx
sudo certbot --nginx -d your_domain -d www.your_domain