Deploying FastAPI easy way with Gunicorn + Nginx

To be honest, I did like our previous approach of deploying FastAPI with screen -S background_terminal However, there are some cons to this approach:

  • When you run uvicorn main:app, it typically runs in a single process.
  • While it can handle many concurrent requests asynchronously, it may not be as well-suited for scenarios where you need to scale horizontally by adding more processes or worker instances.

In order to overcome these limitations let's utilize the power of Gunicorn. The main advantages will be:

  • It is built with process management in mind, allowing you to run multiple worker processes to handle concurrent connections.
  • By running gunicorn -w N main:app, where N is the number of worker processes, you can scale your application horizontally by adding more processes to handle incoming requests.
  • Each worker process runs independently and can handle its own set of connections.
  • gunicorn can also be used in conjunction with a reverse proxy (such as Nginx or Apache) for load balancing

Don't worry I won't tell you to open up another screen and execute the gunicorn initialization command! This time we are going to follow a difficult but near-industry-standard approach of making gunicorn a Daemon process and managing it by systemctl. Before that, I would like to change the role to a non-root user and do some initial server setup for the processes to come. Let's ssh and execute the below initial_server_setup.sh file and do the basic configuration.

#!/bin/bash
set -euo pipefail

########################
### SCRIPT VARIABLES ###
########################

# Name of the user to create and grant sudo privileges
# put your desired username
USERNAME=johnwick

# Whether to copy over the root user's `authorized_keys` file to the new sudo
# user.
COPY_AUTHORIZED_KEYS_FROM_ROOT=true


####################
### SCRIPT LOGIC ###
####################

# Add sudo user and grant privileges
useradd --create-home --shell "/bin/bash" --groups sudo "${USERNAME}"

# Check whether the root account has a real password set
encrypted_root_pw="$(grep root /etc/shadow | cut --delimiter=: --fields=2)"

if [ "${encrypted_root_pw}" != "*" ]; then
    # Transfer auto-generated root password to user if present
    # and lock the root account to password-based access
    echo "${USERNAME}:${encrypted_root_pw}" | chpasswd --encrypted
    passwd --lock root
else
    # Delete invalid password for user if using keys so that a new password
    # can be set without providing a previous value
    passwd --delete "${USERNAME}"
fi

# Expire the sudo user's password immediately to force a change
chage --lastday 0 "${USERNAME}"

# Create SSH directory for sudo user
home_directory="$(eval echo ~${USERNAME})"
mkdir --parents "${home_directory}/.ssh"

# Copy `authorized_keys` file from root if requested
if [ "${COPY_AUTHORIZED_KEYS_FROM_ROOT}" = true ]; then
    cp /root/.ssh/authorized_keys "${home_directory}/.ssh"
fi

# Adjust SSH configuration ownership and permissions
chmod 0700 "${home_directory}/.ssh"
chmod 0600 "${home_directory}/.ssh/authorized_keys"
chown --recursive "${USERNAME}":"${USERNAME}" "${home_directory}/.ssh"

# Disable root SSH login with password
sed --in-place 's/^PermitRootLogin.*/PermitRootLogin prohibit-password/g' /etc/ssh/sshd_config
if sshd -t -q; then
    systemctl restart sshd
fi

# Add exception for SSH and then enable UFW firewall
ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw --force enable

sudo apt update

- Let's wait for 1 minute, this is the cheapest droplet and is a little slow. It might be doing some booking for now. Now, you can execute bash initial_server_setup.sh

Now, you can try sshing with johnwick@ip_of_vm. The first ssh attempt will ask you to choose a password. Now, we can definitely pull our codebase and set up the virtual environment.

git clone https://github.com/sourabhsinha396/fastapi-blog
cd fastapi-blog/
ls
sudo apt install python3-pip python3-venv nginx
python3 -m venv env

# lets activate the virtualenv and install the requirements
johnwick@bobthebuilder:~/fastapi-blog$ source ./env/bin/activate
(env) johnwick@bobthebuilder:~/fastapi-blog$ cd backend/
(env) johnwick@bobthebuilder:~/fastapi-blog/backend$ pip install -r requirements.txt
(env) johnwick@bobthebuilder:~/fastapi-blog/backend$ pip install gunicorn==21.2.0

Now, let's jump on to the Gunicorn configuration. Create a Gunicorn service file. Create a file named your_app_name_gunicorn.service in /etc/systemd/system/

#fastapi_gunicorn.service 

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

[Service]
User=johnwick
Group=johnwick
WorkingDirectory=/home/johnwick/fastapi-blog/backend
ExecStart=/home/johnwick/fastapi-blog/env/bin/gunicorn -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 main:app

[Install]
WantedBy=multi-user.target

Now, we can enable and start the Gunicorn service and check its status.

johnwick@bobthebuilder:/etc/systemd/system$ sudo systemctl enable fastapi_gunicorn
Created symlink /etc/systemd/system/multi-user.target.wants/fastapi_gunicorn.service → /etc/systemd/system/fastapi_gunicorn.service.

johnwick@bobthebuilder:/etc/systemd/system$ sudo systemctl start fastapi_gunicorn

johnwick@bobthebuilder:/etc/systemd/system$ curl localhost:8000

johnwick@bobthebuilder:/etc/systemd/system$ sudo systemctl status fastapi_gunicorn
● fastapi_gunicorn.service - Gunicorn instance to serve fastapi_blog
     Loaded: loaded (/etc/systemd/system/fastapi_gunicorn.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2023-11-21 16:54:42 UTC; 6s ago
   Main PID: 9798 (gunicorn)
      Tasks: 5 (limit: 512)
     Memory: 178.0M
        CPU: 3.661s
     

Now, we need to tackle nginx stuffs, let's start by creating a nginx file inside of /etc/nginx/sites-available/

johnwick@bobthebuilder:/etc/nginx/sites-available$ sudo nano fastapi_nginx

Let's put our ip, domain name, and port info in the below nginx file content.

server {
    listen 80;
    server_name 147.182.213.99 algoholic.pro;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Finally, we can link it to sites-enabled, test our configuration, and restart nginx if everything looks good.

sudo nginx -t
#nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
#nginx: configuration file /etc/nginx/nginx.conf test is successful

sudo ln -s /etc/nginx/sites-available/fastapi_nginx /etc/nginx/sites-enabled/

sudo systemctl restart nginx

Time to visit the IP and Domain on a HTTP scheme. Visit http://your_ip_or_domain

FastAPITutorial

Brige the gap between Tutorial hell and Industry. We want to bring in the culture of Clean Code, Test Driven Development.

We know, we might make it hard for you but definitely worth the efforts.

Contacts

Refunds:

Refund Policy
Social

Follow us on our social media channels to stay updated.

© Copyright 2022-23 Team FastAPITutorial