Skip to content

Nginx

Apache remains the default web server that comes bundled with many Linux distributions. However, Nginx is an alternative web server that offers some advantages, especially in the area of speed and scalability. A detailed comparison of the two applications can be found on several web sites including this one from Sumo Logic.

Prerequisites and assumptions
  • You already have a Flask application set up
  • You are using a virtual environment - here we assume it was created using Anaconda

General overview

When running a Flask app under Apache, it is launched as a WSGI process at the same tie as the web server. With Nginx things are slightly different. The Flask app needs to be started separately and Nginx then directs any incoming requests to the Flask process. The setup is very similar to running the Flask app during development. Flask comes bundled with a basic application server that you can start with the command flask run, or by running the app through an IDE like PyCharm.

The Flask development server is not designed to be robust enough for production environments, and so you need to use an alternative such as Gunicorn. It may be convenient to configure your Flask app as a service that be started and stopped using the standard Linux service command. In that case, you could make updates to your app and restart it independently of the web server. This configuration will be described on the rest of this page. It is summarised in the diagram below.

Flask app and Nginx on Ubuntu

Installing Nginx

Nginx can be installed using the standard Ubuntu command shown below. It is always a good idea to update the apt command itself before using it to install new packages.

1
2
sudo apt update
sudo apt install nginx

If you are running your own server, you will need to tell the firewall to allow access to Nginx with the command

1
sudo ufw allow 'Nginx Full'

If you are running on a controlled server (such as one managed by a university), the firewall may be controlled centrally and you may have to request that ports 80 and 443 be opened for you.

You can now test the installation by typing your server address into a web browser. If you previously had Apache installed and running, you should stop and disable it with the commands

1
2
sudo service apache2 stop
sudo systemctl disable apache2

If your /var/www directory was previously empty, a default index.html page will be created. If there is already an index.html page in existence - for example, left over from an Apache installation - it may not be overwritten by the Nginx installation. When you test the server you will either see a Welcome to nginx! message, or the contents of the pre-existing index.html page.

Gunicorn

Gunicorn is installed as a Python package. Activate the virtual environment used by your app and install Gunicorn using the standard command. Assuming you are using Anaconda, that is

1
conda install gunicorn

If you are using the default Python installation for your operating system, the command might be

1
pip install gunicorn

You can check that Gunicorn can run your app at this stage. First, ensure that your app runs independently by activating the development server. Before you can do that, you will need to export the environment variables shown below

1
2
export FLASK_APP=run.py
export FLASK_ENV=production

NB.The values shown here are the ones used throughout this site. If you are recycling an app from elsewhere, the values you need to export may be different.

You should now be able to run your app with the command

1
flask run --host '0.0.0.0'

This will run the app on port 5000 by default as indicated in the feedback message that is shown:

1
2
3
4
5
6
 * Serving Flask app "run.py"
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
[2022-09-10 16:27:44,567] INFO in _internal:  * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

If port 5000 is open, you can connect to the server and access the app. However, if port 5000 is not under you control, you will have to accept the informati0n in the feedback message as confirmation that everything is working. You can stop the app again by pressing CTRL-C.

Check that you can replace the development server with Gunicorn by issuing the following command

1
gunicorn --workers 4 --bind 0.0.0.0:5000 run:app

Here, run is the name of the script used to start your app (run.py), and app is the name used for the Flask instance. Set the workers value to the number of cores available on your server.

If you see some feedback like the following, the operation was successful.

1
2
3
4
5
6
7
[2022-09-10 16:48:18 +0100] [29960] [INFO] Starting gunicorn 20.1.0
[2022-09-10 16:48:18 +0100] [29960] [INFO] Listening at: http://0.0.0.0:5000 (29960)
[2022-09-10 16:48:18 +0100] [29960] [INFO] Using worker: sync
[2022-09-10 16:48:18 +0100] [30010] [INFO] Booting worker with pid: 30010
[2022-09-10 16:48:18 +0100] [30015] [INFO] Booting worker with pid: 30015
[2022-09-10 16:48:18 +0100] [30016] [INFO] Booting worker with pid: 30016
[2022-09-10 16:48:18 +0100] [30017] [INFO] Booting worker with pid: 30017

Again, you can stop the application server with CTRL-C.

Creating a system service

Turning your app into a Linux service means that you can control it using the standard commands:

1
2
3
sudo service myApp start    # Start the service manually
sudo service myApp stop     # Stops the service manually
sudo service myApp restart  # Restarts the service, for example after a code update

(NB. service in this context is the same as systemctl, but easier to remember)

Service definition files are stored in the directory /etc/systemd/system/. The example below covers the most common requirements for a Flask app. Save it (with appropriate modifications) with the name myApp.service.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[Unit]
Description=myApp
After=network.target

[Service]
WorkingDirectory=/usr/local/apps/myApp
Environment=PATH=/usr/local/anaconda3/envs/myApp/bin:/usr/local/anaconda3/condabin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=FLASK_APP=run.py
Environment=FLASK_ENV=production
ExecStart=/bin/bash -c 'gunicorn --capture-output --error-log /tmp/myApp.log -b 127.0.0.1:9000 run:app'
Restart=always
RestartSec=5
KillSignal=SIGQUIT
Type=notify
NotifyAccess=all

[Install]
WantedBy=multi-user.target

Explanation

Line 2: The name of your service

Line 3: Ensures that this service starts after the network has been enabled

Line 6: Sets the working directory to the location of your app code

Line 7: Sets the path. The example assumes that you have an Anaconda environment called myApp

Lines 8 & 9: The sme environment variables that were used before

Line 10: The command to start the Gunicorn server - see the note on the port number below

Lines 11-18: Common settings for Linux services

Note that the Gunicorn command specifies port 9000. This is actually and arbitrary choice. 9000 is chosen simply because it is different from the default 5000 as an extra security precaution.

Once your service file is saved, you should be able to start the service with the standard command

1
sudo service myApp start

If anything goes wrong, you should see an error message. Otherwise, you can check that the service started successfully with the command

1
ps -ef | grep gunicorn

This command lists active processes pertaining to Gunicorn. If the service is running correctly, you should see output similar to the following (long paths have been shortened).

1
2
root      30672      1  1 17:11 ?        00:00:00 /.../python /.../gunicorn --capture-output --error-log /tmp/myApp.log -b 127.0.0.1:9000 run:app
root      30691  30672  8 17:11 ?        00:00:01 /.../python /.../gunicorn --capture-output --error-log /tmp/myApp.log -b 127.0.0.1:9000 run:app

Configuring Nginx

Finally, we need to tell Nginx to pass app requests to the Gunicorn process. This is done through a configuration file in the directory /etc/nginx/sites-available. Nginx uses the same folder conventions as Apache. Copy the content below into a file called myApp.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
server {
        server_name _;

        root /usr/local/apps/myApp;

        location /static {
            alias /usr/local/apps/myApp/static;
        }   

        location / { 
                include proxy_params;
                proxy_pass http://localhost:9000;
        }
}

Explanation

Line 2: The underscore here is a wildcard. You should use the domain name instead

Line 4: The root directory of your app

Lines 6-8: An instruction to serve static files (eg images, scripts, etc) from the directory shown

Lines 10-12: An instruction to pass any incoming requests to the process on port 9000 (Gunicorn)

Unlike Apache, Nginx does not provide handy commands to enable and disable sites. You will need to modify the contents of the sites-enabled directory manually.

First, change into that directory and show a detailed list of the directory contents:

1
2
cd /etc/nginx/sites-enabled
ls -l 

You should find that there is only one item - a symbolic link to the default file in the sites-available directory:

1
2
3
drwxr-xr-x 2 root root 4096 Sep 10 15:39 ./
drwxr-xr-x 8 root root 4096 Sep 10 15:39 ../
lrwxrwxrwx 1 root root   34 Sep 10 15:39 default -> /etc/nginx/sites-available/default

You need to remove that link and create a new one pointing to your new configuration file. Use the following commands:

1
2
sudo rm default
sudo ln -s ../sites-available/myApp .

Nginx is now configured. You should be able to restart it with the command below and access your app through a web browser on the standard port 80.

1
sudo service nginx restart

You will probably need to run your app using https which requires changes to this configuration file. A good option for obtaining a security certificate is CertBot. The site provides clear instructions to follow to obtain and install a certificate. The installation process makes most of the changes to the configuration file automatically.