How to deploy Flask app on Ubuntu 20.04 with Nginx and Gunicorn
Lets define minimum conditions of acceptable deployment as:
- Application sits behind “proper” web server in this case Nginx;
- Application runs on ”proper” application server in this case Gunicorn;
- Application startup or shutdown is managed by native Ubuntu service manager in this case systemd;
- Application data, configuration and install folders are separated from each other so they can be independently backed up and restored.
- Application upgrade is done by executing pip install –upgrade.
Create Flask Demo Application
We need a Flask application to deploy, lets build the “simplest” one. I’ll develop app on an Ubuntu machine with Python 3.8 and Poetry, you can use whatever you are familiar with.
In case you are developing on Ubuntu 18.04/20.04 you can prepare development environment with:
$ sudo apt install python3.8 python3.8-dev python3.8-venv $ curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3.8 -
Create “demoapp” application directory and initialize Python virtual environment (venv):
$ mkdir ~/demoapp
$ cd ~/demoapp
$ poetry init
When executing poetry init
accept all defaults answers except for questions about defining dependencies interactively where you should choose “no”.
Poetry will create venv inside ~/.cache/pypoetry/virtualenvs/demoapp-xxxx
just for this application. That you can activate by executing poetry shell
inside ~/demoapp
.
Activate python venv and create demoapp directory structure:
$ cd ~/demoapp
$ poetry shell
(demoapp-xx) $ poetry add flask
(demoapp-xx) $ mkdir demoapp
(demoapp-xx) $ mkdir demoapp/templates
(demoapp-xx) $
in front of prompt indicates that python venv is active.
Add following lines inside demoapp/pyproject.toml
at end of [tool.poetry] section just before [tool.poetry.dependencies]:
packages = [
{ include = "demoapp" }
]
Create demoapp/__init__.py
:
import os
from flask import Flask, render_template
def create_app(config=None):
instance_path = os.environ.get("FLASK_INSTANCE_PATH", None)
app = Flask(
__name__,
instance_relative_config=True,
instance_path=instance_path
)
if config is not None:
if isinstance(config, dict):
app.config.from_mapping(config)
elif config.endswith(".py"):
app.config.from_pyfile(config)
else:
app.config.from_pyfile("config.py", silent=True)
try:
os.makedirs(app.instance_path)
except OSError:
pass
@app.route("/")
def index():
"""Servers intex.html"""
return render_template("index.html")
return app
Create demoapp/templates/index.html
:
<html>
<body>
Hello from Demo application.
</body>
</html>
You will notice that I’m preparing this app to be packaged for easy deployment by setting up instance_path and instance_relative_config to Flask and by adding packages directive in pyproject.toml.
To test your app run inside ~/demoapp
:
(demoapp-xx) $ export FLASK_APP=demoapp
(demoapp-xx) $ flask run
Open up http://localhost:5000 in the browser and you should see page displaying “Hello from Demo application”.
Prepare “demoapp” Package for Deployment
Create demoapp-0.1.0-py3-none-any.whl and demoapp-0.1.0.tar.gz distribution packages inside the ~/demoapp/dist
:
(demoapp-xx) $ poetry build
Configuring Ubuntu 20.04
I’ll assume following:
- You have root privileges on fresh installation of Ubuntu 20.04;
- Acme company has built this “demoapp”;
- You’re creating an “acme” user to be responsible for managing demoapp;
- You’re hosting the app on a public server named server.acme.com;
- You know how to setup domain name to point on your server
Add User Responsible for Managing Application
Upgrade system, add acme user and allow acme to sudo as root:
# apt update
# apt upgrade
# adduser acme
# usermod -aG sudo acme
Install Nginx
# apt install nginx
Go to http://server.acme.com to check if website is running.
If browser is constantly trying to redirect you to https://server.acme.com use browser incognito/private mode to access website over http.
Configure Nginx to Use HTTPS With “letsencrypt.org” Certificate
I’ll use acme.sh script to obtain and renew certificates. Install it with:
# curl https://get.acme.sh | sh -s email=my-email@acme.com
Close and reopen your terminal to start using acme.sh.
Obtain and install certificate:
# acme.sh --issue -d server.acme.com -w /var/www/html/
# mkdir -p /etc/nginx/certs/server.acme.com
# acme.sh --install-cert -d server.acme.com \
--key-file /etc/nginx/certs/server.acme.com/key.pem \
--fullchain-file /etc/nginx/certs/server.acme.com/cert.pem \
--reloadcmd "service nginx force-reload"
acme.sh will add cronjob to renew and reinstall certificates when they expires.
Import Certificates in Nginx
Change /etc/nginx/sites-enabled/default
to:
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 default_server ssl http2;
listen [::]:443 default_server ssl http2;
server_name _;
ssl_certificate /etc/nginx/certs/acme.jembe.io/cert.pem;
ssl_certificate_key /etc/nginx/certs/acme.jembe.io/key.pem;
root /var/www/html;
index index.html index.htm index.nginx-debian.html;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
}
Now every HTTP request will be redirected to HTTPS. This is good for now but we’ll change this after installing our demoapp.
Enable Firewall
Use the simplest firewall configuration as:
# ufw allow ssh
# ufw allow http
# ufw allow https
# ufw enabled
Check firewall status with # ufw status
.
Deploy Flask Application
We’ll install our Flask “demoapp” under /opt/acme
as user acme.
Preparing venv and Directory Structure
Install appropriate python version (in this case Python 3.8):
$ sudo apt install python3.8 python3.8-dev python3.8-venv
Create deployment directory structure:
$ sudo mkdir /opt/acme
$ sudo chown acme.acme /opt/acme
$ cd /opt/acme
$ mkdir bin envs demoapp
Following dirs are created for:
- /opt/acme/bin: all bash scripts realated to our demoapp;
- /opt/acme/envs: python virtual enviroments;
- /opt/acme/demoapp: application configuration and data files;
Create venv:
$ python3.8 -m venv /opt/acme/envs/demoapp
Activate venv:
$ source /opt/acme/envs/demoapp/bin/activate
Activate venv with .bash_aliases
If you are lazy as me, you can add following line in ~/.bash_aliases
, to activate venv by typing demoapp
:
alias demoapp='export PS1="\u:\W\$ ";source /opt/acme/envs/demoapp/bin/activate;cd /opt/acme/demoapp'
After you close and reopen your terminal you can type demoapp
to activate python venv
and go to application directory.
Install “demoapp”
(demoapp) $ cd /opt/acme/demoapp
(demoapp) $ pip install --upgrade pip
(demoapp) $ pip install /home/acme/demoapp-0.1.0-py3-none-any.whl
(demoapp) $ mkdir instance data
(demoapp) $
indicates that you should run following commands in python venv for demoapp. I’m also assuming that you already copied package with flask demoapp in/home/acme
.
Directory
/opt/acme/demoapp/instance
is flask instance folder.
I usually use
/opt/acme/demoapp/data
to save all app data that need to be saved directly on hard disk.
Create /opt/acme/demoapp/instance/config.py
:
SECRET_KEY = "put some random secret key here"
You can generate random secret key with:
$ python3.8 -c "import os; print(os.urandom(24))"
Install and Configure Gunicorn
(demoapp) $ pip install gunicorn
Create /opt/acme/demoapp/gunicorn.conf.py
:
import pathlib
wsgi_app = "demoapp:create_app()"
chdir = str(pathlib.Path(__file__).parent.absolute())
raw_env = [
"FLASK_INSTANCE_PATH={}/instance".format(chdir),
]
Settings in gunicorn.conf.py
file will tell gunicorn how to start our demoapp and also will tell demoapp where it is its INSTANCE_PATH directory that contains application configuration.
Create systemd Scripts for Starting and Stoping Gunicorn
/etc/systemd/system/demoapp.socket
:
[Unit]
Description=Demo App socket
[Socket]
ListenStream=/run/demoapp.sock
[Install]
WantedBy=sockets.target
/etc/systemd/system/demoapp.service
:
[Unit]
Description=Demo App production service
Requires=demoapp.socket
After=network.target
[Service]
PermissionsStartOnly = true
User=acme
Group=acme
PIDFile=/run/demoapp/demoapp.pid
WorkingDirectory=/opt/acme/demoapp/
Environment=LANG=en_US.UTF-8
Environment=PATH=/opt/acme/envs/demoapp/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
ExecStartPre=/bin/mkdir /run/demoapp
ExecStartPre=/bin/chown -R acme:acme /run/demoapp
ExecStart=/opt/acme/envs/demoapp/bin/gunicorn \
--user acme \
--group acme \
--config /opt/acme/demoapp/gunicorn.conf.py \
--workers 4 \
--log-level warn \
--error-logfile /var/log/gunicorn/demoapp.log \
--bind unix:/run/demoapp.sock \
--pid /run/demoapp/demoapp.pid
PrivateTmp=true
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
ExecStopPost=/bin/rm -rf /run/demoapp
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Execute:
$ sudo mkdir /var/log/gunicorn
$ sudo chown acme.acme /var/log/gunicorn
$ sudo systemctl enable demoapp.service
$ sudo systemctl enable demoapp.socket
$
$ sudo systemctl start demoapp.socket
You can use sudo systemctl status|start|stop|enable|disable demoapp.service
commands to manage or check status of demoapp services.
Configure Nginx to Proxy Requests to Gunicorn
Open /etc/nginx/sites-enabled/default
configured previously and change location /
directive to:
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_redirect off;
proxy_pass http://unix:/run/demoapp.sock;
access_log /var/log/nginx/demoapp.access.log;
error_log /var/log/nginx/demoapp.error.log;
}
$ sudo systemctl restart nginx
Serve Flask App with URL Prefix
Open /opt/acme/demoapp/instance/config.py
and add line:
APPLICATION_ROOT = “/demoapp/”
Open /etc/nginx/sites-enabled/default
and change location /
directive to:
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
location /demoapp {
proxy_set_header SCRIPT_NAME /demoapp;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_redirect off;
proxy_pass http://unix:/run/demoapp.sock;
access_log /var/log/nginx/demoapp.access.log;
error_log /var/log/nginx/demoapp.error.log;
}
How to Upgrade App
Copy new version of demoapp
into /home/acme
and login to server:
$ demoapp
(demoapp) $ pip remove demoapp
(demoapp) $ pip install /home/acme/demoapp-0.2.0-py3-none-any-whl
(demoapp) $ sudo systemctl restart demoapp.service
When you are installing or upgrading “real” Flask app that uses database, saves data on
local storages and access other system or network resources you will probably need to execute
additional commands like flask migrate
etc.
Feedback
If you have any feedback or you want to see more posts like this, let me know on Twitter.