Here I sum up how to install a Flask webapp on a Digital Ocean VPS that has Ubuntu GNU/Linux 16.04 on it. The following topics will be covered:
- server configuration
- the big picture
- Nginx
- a simple Flask app in a virtual environment served by gunicorn
- gunicorn is started automatically with a systemd script
See https://github.com/jabbalaci/DigitalOceanNotes for more info.
So, you have a Flask app that works well on your local machine and you want to share it with the world. Instead of a PaaS (like Heroku for instance) you want to host it on a virtual private server (VPS). How to do it?
When you buy the VPS and receive its IP and root password, spend some time with its basic configuration. I wrote about it here: https://github.com/jabbalaci/DigitalOceanNotes.
Then, we will need a real web server that will serve the pages of our Flask application. Our choice will be Nginx. Nginx will listen on the standard http port (80) and forward every request to port 9000.
On port 9000 an application server namely gunicorn will run. This application server will run our Python application. Nginx will handle static files (like CSS, images, etc.) itself. If our Python app is called, then Nginx forwards the request to Gunicorn who executes your Flask app, returns the result to Nginx, who will return that result to the client.
If gunicorn dies for some reason, we want it to restart automatically. A systemd script will do exactly this.
Install Nginx:
$ sudo apt install nginx
If you use UFW (firewall), then don't forget to open port 80:
$ sudo ufw status verbose # verify what's open
$ sudo ufw allow 80/tcp # open port 80
Verify if nginx is running:
$ sudo service nginx status
$ sudo service nginx start # start nginx (if it was not running)
Verify it in your browser. Visit http://1.2.3.4
(instead of 1.2.3.4
use
the IP address of your VPS).
Let's put Nginx aside for a while and concentrate on our Flask app. Here, in this repo you can find a simple sample app that I will use for the demonstration. It was written in Python 3.
My Flask app will be here: /home/demo/projects/ave_caesar
. Its virtual
environment is located in a dedicated folder here: /home/demo/.virtualenvs/ave_caesar
.
Create a virtual environment for the project, activate the environment and
install the requirements. Start the app with ./main.py
and open it in
your browser: http://1.2.3.4:9000
. If you use UFW, make port 9000 open.
You should see an image and a text below it. If it's OK, then stop main.py
(it is running in debug mode and the host is '0.0.0.0', which means that
anybody can visit your app). Debug mode is absolutely not recommended in a
production environment.
The next step will be to run our app with Gunicorn instead of the built-in
dev server of Flask. While the virt. env. is active, install gunicorn
and
gevent
:
$ pip install gunicorn gevent
And now start the application with Gunicorn (the virt. env. is still activated):
$ ./01_start_with_gunicorn.py
Again, visit http://1.2.3.4:9000
. You should see the same page, however
this time it's served by Gunicorn! If it's OK, then stop gunicorn (press Ctrl-C).
Now let's configure Nginx to forward requests that arrive to port 80 to port 9000.
The prompt #
indicates the root prompt, while $
is the prompt of normal
users.
# cd /etc/nginx
# cd sites-available
# vi flask
Add the following content:
upstream app_server {
server 127.0.0.1:9000 fail_timeout=0;
}
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
client_max_body_size 4G;
server_name _;
keepalive_timeout 5;
# your Flask project's static files - amend as required
location /static {
alias /home/demo/projects/ave_caesar/static;
}
location / {
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;
}
}
Here app_server
is a unique ID. Thus, if you want to serve later another
webapp, and you make a copy of this file, rename app_server
to something else
in the copy.
What does this config file do? When something arrives at port 80, redirect it to port 9000.
Add a symbolic link to sites-enabled
that points on this file:
# cd /etc/nginx/sites-enabled
# rm default # remove the default
# ln -s ../sites-available/flask
# ls -al
Restart nginx:
# service nginx restart
# service nginx status
Before moving to Systemd, let's test if the redirection works. Go to the project folder of the webapp, activate the virt. env., and start gunicorn like before:
$ ./01_start_with_gunicorn.py
The address http://1.2.3.4:9000
should work. But now remove the port and
try simply http://1.2.3.4
. You may have to clear the cache, force reload
the page (Ctrl-r), but you should see our basic app. And this goes through
Nginx!
If it works, then stop gunicorn. If you have UFW, at this point you can hide again port 9000 if you want.
We are close to victory. In the previous section we started gunicorn manually. But we want gunicorn to start automatically upon boot. A Systemd script will do exactly that:
# vi /etc/systemd/system/gunicorn.service
Add the following content:
[Unit]
Description=Gunicorn daemon for a Flask project
After=network.target
[Service]
User=demo
Group=demo
WorkingDirectory=/home/demo/projects/ave_caesar
Environment="PATH=/home/demo/.virtualenvs/ave_caesar/bin"
ExecStart=/home/demo/.virtualenvs/ave_caesar/bin/gunicorn --config /etc/gunicorn.d/gunicorn.py main:app
[Install]
WantedBy=multi-user.target
The webapp is in the HOME folder of the user demo
, who is in the group demo
.
The app will run under his/her name, that's what the lines User
and
Group
mean. How to verify:
$ cd /home/demo/projects
$ ls -al
drwxr-xr-x 5 demo demo 4096 Mar 21 20:27 ave_caesar
See? It's demo
and demo
.
So, what happens after? Enter the project directory and start the app with
gunicorn. Note that gunicorn
is executed in the virt. env.! You don't need
to install it globally with sudo
! Additional settings are in the
/etc/gunicorn.d/gunicorn.py
file that we will see in the next section.
Let's create /etc/gunicorn.d/gunicorn.py
:
# mkdir /etc/gunicorn.d
# vi /etc/gunicorn.d/gunicorn.py
The folder /etc/gunicorn.d
may not exist, that's why we create it first.
Add the following content:
"""gunicorn WSGI server configuration."""
from multiprocessing import cpu_count
from os import environ
def max_workers():
return cpu_count() * 2 + 1
max_requests = 1000
worker_class = 'gevent'
workers = max_workers()
name = 'ave_caesar'
bind = '127.0.0.1:9000'
pidfile = 'gunicorn_from_systemd.pid'
accesslog = 'gunicorn_access.log'
errorlog = 'gunicorn_error.log'
The process gunicorn
will listen on port 9000. The accesslog and errorlog
files are useful for debugging and monitoring. The PID of the gunicorn process
will be written in a file. All these three files will be written to the root
folder of the webapp.
Now enable and start the systemd script:
# systemctl enable gunicorn
# systemctl start gunicorn
# systemctl status gunicorn
Here gunicorn
comes from the name of the file /etc/systemd/system/gunicorn.service
that was created in a previous step.
If it runs for the first time, you can drink a champagne :)
It means there is a problem between the communication of Nginx and Gunicorn.
I ran into this error after a good while. My REST API was running too long (for several minutes), and Nginx raised a timeout error. To cure this problem, add the following lines to your nginx config file:
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
In our example the nginx config file is here: /etc/nginx/sites-available/flask
.