Hosting a Flask Application

This is a comprehensive re-write of this guide. I'll try to give a better explanation of the important mechanics, as well as a summary of which commands to keep in mind. This guide assumes that you already have a working Python 3 Flask application (if not, Flask has a good quickstart guide). This guide also skips over Python virtual environments, to keep things as simple as possibile.

TLDR: Create a file that contains the instance of the Flask application. Create a uWSGI configuration file, pointing the module to the Python file, and the socket pointing where you want the .sock file to appear. Create a systemctl service to run uWSGI. Create an Nginx server block configuration that points to the .sock file created by uWSGI. Run CertBot on the new domain.

You can run commands on the server from your machine using SSH. If you're using Windows, I recommend using PuTTY for this. Python, Nginx, and CertBot need to be installed on the server, and the Flask and uWSGI libraries need to be installed with Python. You can do this with apt and pip:

add-apt-repository ppa:certbot/certbot apt install python3.8 python3-pip nginx python-certbot-nginx pip install flask uwsgi

Move the Flask project over to your server. You can do this with any FTP/SFTP client (I recommend FileZilla). It should be somewhere in your user ~/home/ directory. Make a note of which file contains the Flask instance, and what the instance variable is called (by convention, it's called app). When the file is run on the server, the instance will be imported from the file; if you have written something like if __name__ == '__main__': app.run() with extra functionality, uWSGI won't run that. Check that the new environment hasn't interrupted the application's configuration by running it like uWSGI will: python -c "from script import app; app.run(host='127.0.0.1', port=5000)". If that produces errors, then fix the project on your local machine before continuing.

uWSGI (u-Web-Sever-Gateway-Interface) is Python-specific middleware that connects the application running on the machine to the server software (Nginx). It needs to be configured to run the Flask application. Create a .ini file to server as the uWSGI configuration file. It can be called anything and located anywhere, but it's a good idea to name it after the project and keep it in the project directory. The file contents should be as follows:

/home/project/project.ini
[uwsgi]
module = script:app
socket = project.sock

master = true
processes = 5
chmod-socket = 660
vacuum = true
die-on-term = true

You can read about what each of these options does in uWSGI's documentation. The important ones are module and socket. module declares where the Flask instance should be imported from. In the example above, it will import the variable app from the file script.py (relative to the .ini's location). sock declares the location of the socket file that uWSGI produces while running. This path is relative to the .ini file. In the example above, it will be a file called project, in the same directory as the .ini. Test the uWSGI configuration by running uwsgi --ini project.ini.

Once the uWSGI configuration can be used via the command line, it must be configured to run as a service on the server. This allows it to run in the background and restart after interuptions (like system restarts). Create a file called project.service in /etc/systemd/system. It should contain the following:

/etc/systemd/system/project.service
[Unit]
Description=uWSGI targeting a Flask application
After=network.target

[Service]
User=user
Group=www-data
WorkingDirectory=/home/project
ExecStart=uwsgi --ini project.ini
Restart=always

[Install]
WantedBy=multi-user.target

You can read more about each of these options here. The three important options are User, WorkingDirectory, and ExecStart. User is the user that owns the process (this should be your username on the server). WorkingDirectory is the path from which to execute ExecStart. ExecStart is the command to be run as a service. Set these options to match your configuration.

systemctl manages this service. Run systemctl start project, where project is the name of the .service file you have created. You can check on the status of the service with systemctl status project. If the service is running, then uWSGI's .sock file will be present. To enable the service to automatically start on system reboot, run systemctl enable project.

Nginx will take the socket provided by uWSGI to make it respond to web requests. This requires its own configuration file, located in /etc/nginx/sites-available/, called project. The contents should be as follows:

/etc/nginx/sites-available/project
server {
    listen 80;
    server_name domain.tld www.domain.tld;
    location / {
        include uwsgi_params;
        uwsgi_pass unix:/home/project/project.sock;
    }
}

You can read more about these options here. The two important options are server_name and uswgi_pass. server_name defines which domain names should match the application. In this case, visiting domain.tld (with or without www) will direct the user to the application. uwsgi_pass declares the path to the socket. This must match the path configured in the uWSGI .ini.

Nginx keeps track of which sites are available, and from those, which are enabled. Create a symbolic link from this configuration file in sites-available to one in sites-enabled like so: ln -s /etc/nginx/sites-available/project /etc/nginx/sites-enabled/project. You can then check that the syntax of the Nginx server block is valid with nginx -t. Restart Nginx to apply this new configuration with systemctl restart nginx. Add a rule to the firewall to allow Nginx connections with ufw allow 'Nginx Full'.

The result of the steps described above is this: the Flask application (located in the project directory) is run by uWSGI (which is configured with a .ini file) which itself is run as a service with systemctl, whose definition is in /etc/systemd/system.

Should you make changes to the Flask project, then the uWSGI service must be restarted. Once the changes are made to the files on the server, you can run systemctl restart project. The changes should be reflected immediately. You can run systemctl status project to check the status of the service.

← Read my other thoughts | Written 2020-08-09