Deploy FastAPI on Ubuntu and Serve using Caddy 2 Web Server

In this tutorial we will see how to Deploy FastAPI on Ubuntu. Our FastAPI application does CRUD operations on a PostgreSQL database running on Ubuntu 18.04.5 LTS (Bionic Beaver). We expose FastAPI running on Gunicorn as a reverse proxy using Caddy 2 Web Server.

Prerequisites

  • Internet Connection to clone GitHub Repository
  • A Ubuntu 18.04.5 LTS (Bionic Beaver) Virtual Machine with
    • SSH Port 22 enabled
    • Port 80 (for HTTP traffic) and Port 443 (for HTTPS traffic) enabled
  • Putty to connect to VM
Deploy FastAPI on Ubuntu and Serve using Caddy 2 Web Server - TutLinks.com
Deploy FastAPI on Ubuntu and Serve using Caddy 2 Web Server – TutLinks.com

Before we begin, if you are following along with the tutorial, connect to the Ubuntu VM via Putty or cmder. All the shell commands need to be run in the command line terminal in the connected instance of the VM you are actively working on.

The full source code of this tutorial is available on github on fastapi-postgresql-caddy-ubuntu-deploy branch here. In case you are interested in understanding the development, you can go through this tutorial on Implementing Async REST APIs in FastAPI with PostgreSQL CRUD

To achieve deployment of FastAPI on Ubuntu 18.04.5 LTS (Bionic Beaver), we will perform the activities in the sequence mentioned below

  • Install and Configure PostgreSQL Database
  • Clone FastAPI Repository from GitHub
  • Run FastAPI on Gunicorn as a Systemd Unit Service
  • Install Caddy 2 Web Server
  • Expose FastAPI application as a reverse proxy via Caddy 2 Web server

Install and Configure PostgreSQL 13 Database on Ubuntu 18.04.5 LTS (Bionic Beaver)

For Ubuntu 18.04.5 LTS (Bionic Beaver) VM, run the script to install PostgreSQL 13.

sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt update
sudo apt install -y postgresql-13

Once you are done installing the PostgreSQL 13, run the below command that

  • creates a database named fastapidb and
  • creates a user named fastapiuser which prompts you to provide password for this user.
sudo su - postgres -c "createdb fastapidb"
sudo su - postgres -c "createuser -P -s -e fastapiuser"
# Enter password for new role: secret
# Enter it again: secret

Enter and confirm the password.

Run the following command to grant all privileges on fastapidb to fastapiuser.

sudo -u postgres psql -c "grant all privileges on database fastapidb to fastapiuser;"

Running the above command should return an output saying GRANT.

We are done with the database set up. To understand in detail, I recommend you to go through this tutorial for detailed step by step tutorial on Installing PostgreSQL Database on Ubuntu.

Deploy the FastAPI CRUD PostgreSQL on Ubuntu

Now we will clone the FastAPI sample application that does CRUD operations on PostgreSQL database by running the following command.

git clone https://github.com/windson/fastapi.git -b fastapi-postgresql-caddy-ubuntu-deploy
cd fastapi

Create Virtual Environment:

Run the following command to install python3-venv package that will facilitate creation of virtual environments for Python applications.

sudo apt install -y python3-venv

Then create a virtual environment by running the following commands. Create virtual environment named env.

python3 -m venv env

Activate virtual Environment and Install requirements:

We will activate the virtual environment and then install all the dependencies in order for our FastAPI application to run. The list of dependencies are mentioned in a file named requirements.txt. Running the following commands will facilitate installation of all the modules within the virtual environment that are necessary for our FastAPI application to run.

source env/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
deactivate

Perform Sanity Check

Perform a sanity check by spinning up the FastAPI application on uvicorn. This is the very first time we are running our app and doing so will create the notes table based on schema defined in our application in the PostgreSQL database.

source env/bin/activate
uvicorn main:app

Output:

INFO:     Started server process [7444]
INFO:     Waiting for application startup.
INFO:     Connected to database postgresql://fastapiuser:********@localhost:5432/fastapidb?sslmode=prefer
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Once you see the application running smooth, you can stop running it by issuing ctrl + c.

Run FastAPI on Gunicorn as a Systemd managed Unit Service

Gunicorn “Green Unicorn” is a Python WSGI (Web Server Gateway Interface) HTTP server. It is a pre-fork worker model, ported from Ruby’s Unicorn project.

FastAPI application runs on ASGI compatible servers like Uvicorn  and Hypercorn. Gunicorn being a WSGI webserver facilitates running FastAPI with the help of its worker class  uvicorn.workers.UvicornWorker.

Let’s create a Systemd managed Unit service gunicorn.service. This could be any name of your choice like fastapi.service or gunicorn.service. I will go on with gunicorn.service because the unit file that we are going to create can not only can be used for FastAPI but also be used for Flask or any web framework that supports on Gunicorn Web Server with minor modifications as necessary. The only difference lies in the way we spin up the app using the command held by ExecStart property of the Unit file.

Unit files are the configuration files managed by the Systemd that hold the behavior of a specific service that we want to deal with. They get added as services and allow various capabilities to start the service when a system reboot happens, or retry restarting the service in case of failure and offer various other capabilities.

We will create a new gunicorn.service unit file by running the following command.

sudo nano /etc/systemd/system/gunicorn.service

With the following content and replace windson with your username and check the paths are valid for WorkingDirectory, Environment and ExecStart.

# gunicorn.service
# For running Gunicorn based application with a config file - TutLinks.com
#
# Make sure the ExecStart and ExecReload commands are correct
# for your installation.
#
#
# Author: Navule Pavan Kumar Rao 
# This file is referenced from official repo of TutLinks.com
# https://github.com/windson/fastapi/blob/fastapi-postgresql-caddy-ubuntu-deploy/gunicorn.service
# Subscribe to TutLinks channel on YouTube: http://bit.ly/2Uc0YNk
#

[Unit]
Description=Gunicorn Web Server as Unit Service Systemd - TutLinks.com
After=network.target

[Service]
User=windson
Group=windson
WorkingDirectory=/home/windson/fastapi
Environment="PATH=/home/windson/fastapi/env/bin"
ExecStart=/home/windson/fastapi/env/bin/gunicorn --config /home/windson/fastapi/gunicorn.py main:app

[Install]
WantedBy=multi-user.target

Save and exit out of the nano editor by hitting the keys ctrl + x and for Answer, type Y and hit return (enter) key to apply changes.

The path for ExecStart is /home/windson/fastapi/env/bin/gunicorn that has the /env which indicates the name of the virtual environment. If you have given a different name for virtual environment, you can replace env with the name of your Python virtual environment.

The Gunicorn Unit file uses the Gunicorn binary available in the virtual environment installed for our FastAPI to run. The version of Gunicorn binary is based on the version of Gunicorn mentioned in the requirements.txt file.

We are providing the gunicorn with the location of configuration file that holds various arguments in the form of literals enclosed in a python file named gunicorn.py which is available in our repository.

import multiprocessing
import os
from dotenv import load_dotenv
load_dotenv()

name = "gunicorn config for FastAPI - TutLinks.com"
accesslog = "/home/windson/fastapi/gunicorn-access.log"
errorlog = "/home/windson/fastapi/gunicorn-error.log"

bind = "0.0.0.0:8000"

worker_class = "uvicorn.workers.UvicornWorker"
workers = multiprocessing.cpu_count () * 2 + 1
worker_connections = 1024
backlog = 2048
max_requests = 5120
timeout = 120
keepalive = 2

debug = os.environ.get("debug", "false") == "true"
reload = debug
preload_app = False
daemon = False

The gunicorn.py will hold various configuration arguments accepted by the gunicorn binary (or executable) such as the critical ones mentioned below among others.

  • bind that holds host and port to run the application,
  • accesslog that logs the HTTP traffic of the application served by gunicorn
  • errorlog that logs the errors related to the gunicorn server
  • worker_class that holds the value uvicorn.workers.UvicornWorker that helps running FastAPI application
  • workers that denotes the number of workers based on calculation of twice the number of CPUs + 1. Mathematically number of workers = ( 2 * CPU Count ) + 1

The gunicorn.py provided as a config for gunicorn binary in unit file offers the flexible configuration for workers argument. The devops doesn’t have to worry about changing the number of workers every time they deploy it to a VM with different CPUs. It offers the advantage of automatically calculating the available number of CPUs on the machine it is deployed to.

Now we will register the unit file gunicorn.service with Systemd by executing the following commands.

sudo systemctl daemon-reload
sudo systemctl enable gunicorn.service
sudo systemctl start gunicorn.service

The systemctl enable command above will add our gunicorn service to resume running when the VM reboots.

The systemctl start command will quickly start the gunicorn service and invokes the ExecStart command.

To check the status of our gunicorn.service at any point of time, run the following command.

sudo systemctl status gunicorn.service

The output should show the Active status of gunicorn.service set to active (running) as shown below.

● gunicorn.service - Gunicorn Web Server as Unit Service Systemd - TutLinks.com
   Loaded: loaded (/etc/systemd/system/gunicorn.service; enabled; vendor preset: enabled)
   Active: active (running) since Wed 2020-10-07 06:04:20 UTC; 8h ago
 Main PID: 1102 (gunicorn)
    Tasks: 16 (limit: 1057)
   CGroup: /system.slice/gunicorn.service
           ├─1102 /home/windson/fastapi/env/bin/python3 /home/windson/fastapi/env/bin/gunicorn --config /home/windson/fastapi/gunicorn.py main:app
           ├─3743 /home/windson/fastapi/env/bin/python3 /home/windson/fastapi/env/bin/gunicorn --config /home/windson/fastapi/gunicorn.py main:app
           ├─3744 /home/windson/fastapi/env/bin/python3 /home/windson/fastapi/env/bin/gunicorn --config /home/windson/fastapi/gunicorn.py main:app
           └─3781 /home/windson/fastapi/env/bin/python3 /home/windson/fastapi/env/bin/gunicorn --config /home/windson/fastapi/gunicorn.py main:app

Oct 07 06:04:20 ubu-18 systemd[1]: Started Gunicorn Web Server as Unit Service Systemd - TutLinks.com.

With in the ssh console of the VM, send a curl command that sends POST request along with payload to add a note to our FastAPI application.

curl -X POST "http://localhost:8000/notes/" \
 -H "accept: application/json" \
 -H "Content-Type: application/json" \
 -d "{\"text\":\"Get Groceries from the store\",\"completed\":false}"

You should be seeing the following output that responds with the note that got successfully inserted in the PostgreSQL database via FastAPI application.

{"id":1,"text":"Get Groceries from the store","completed":false}

Rotating Gunicorn accesslog and errorlog files using logrotate

If you are interested in enabling log files for gunicorn service configuration, then ensure to rotate these files. As logrotate is a module available by default in debian systems, perform the following steps to rotate the accesslog and errorlog files.

Gunicorn logs may be configured by adding the following to the end of /etc/logrotate.conf file.

To edit the logroate.conf file type the below command.

sudo nano /etc/logrotate.conf

Copy the below configuration that defines the rotation of gunicorn-access.log and gunicorn-error.log files on a daily basis. Paste it at the bottom of the /etc/logrotate.conf file. Ensure to change the user windson according to the user you want to.

/home/windson/fastapi/gunicorn-access.log /home/windson/fastapi/gunicorn-error.log {
    missingok
    daily
    create 0700 windson windson
    dateext
    rotate 4
    compress
}

To save and exit out of the /etc/logrotate.conf file hit ctrl + x and Answer Y and hit return to apply changes.

Run the following command to take these changes effect on logrotate.

sudo logrotate /etc/logrotate.conf

Till now, we accomplished the following setup

  • Installing and configuring PostgreSQL server
  • Spin up a FastAPI CRUD app that talks to PostgreSQL as a Unit Service managed by Systemd
  • Configured log rotate for Gunicorn accesslog and errorlog files using logrotate

Install Caddy 2 Web Server on Ubuntu 18

We will install Caddy 2 web server in order to expose our API to the external world. Caddy 2 is an open source web server build using Go programming language. It offers range of impressive features such as below to mention a few.

  • automatic https for custom domains by default from LetsEncrypt,
  • easy configuration from json or Caddyfile,
  • Supports HTTP/1.1, HTTP/2, and experimental support for HTTP/3 protocols

Lets install Open Source Caddy 2 Web Server on Ubuntu 18 by running the following commands.

echo "deb [trusted=yes] https://apt.fury.io/caddy/ /" | sudo tee -a /etc/apt/sources.list.d/caddy-fury.list
sudo apt update
sudo apt install -y caddy

Once the installation is done, run the following command to check the version of caddy.

caddy version

Output:

v2.2.0 h1:sMUFqTbVIRlmA8NkFnNt9l7s0e+0gw+7GPIrhty905A=

Also check the caddy is running actively as a Systemd service by running the following command.

systemctl status caddy

Output:

● caddy.service - Caddy
   Loaded: loaded (/lib/systemd/system/caddy.service; enabled; vendor preset: enabled)
   Active: active (running) since Wed 2020-10-07 12:57:29 UTC; 1h 34min ago
     Docs: https://caddyserver.com/docs/
 Main PID: 12183 (caddy)
    Tasks: 6 (limit: 1057)
   CGroup: /system.slice/caddy.service
           └─12183 /usr/bin/caddy run --environ --config /etc/caddy/Caddyfile

Oct 07 12:57:29 ubu-18 caddy[12183]: JOURNAL_STREAM=9:214146
Oct 07 12:57:29 ubu-18 caddy[12183]: {"level":"info","ts":1602075449.9490707,"msg":"using provided configuration","config_file":"/etc/caddy/Caddyfile","config_adapter":""}
Oct 07 12:57:29 ubu-18 caddy[12183]: {"level":"info","ts":1602075449.9529848,"logger":"admin","msg":"admin endpoint started","address":"tcp/localhost:2019","enforce_origin":false,"origins":["localhost:2019","[::1]:2019","127.0.0.1:2019"]}
Oct 07 12:57:29 ubu-18 caddy[12183]: {"level":"info","ts":1602075449.9534335,"logger":"http","msg":"server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS","server_name":"srv0","https_port":443}
Oct 07 12:57:29 ubu-18 caddy[12183]: {"level":"info","ts":1602075449.9535916,"logger":"http","msg":"enabling automatic HTTP->HTTPS redirects","server_name":"srv0"}
Oct 07 12:57:29 ubu-18 caddy[12183]: {"level":"info","ts":1602075449.9552305,"logger":"http","msg":"enabling automatic TLS certificate management","domains":["golangiq.com"]}
Oct 07 12:57:29 ubu-18 caddy[12183]: {"level":"info","ts":1602075449.9630527,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc00021f500"}
Oct 07 12:57:29 ubu-18 caddy[12183]: {"level":"info","ts":1602075449.96366,"logger":"tls","msg":"cleaned up storage units"}
Oct 07 12:57:29 ubu-18 caddy[12183]: {"level":"info","ts":1602075449.9678338,"msg":"autosaved config","file":"/var/lib/caddy/.config/caddy/autosave.json"}
Oct 07 12:57:29 ubu-18 caddy[12183]: {"level":"info","ts":1602075449.9679778,"msg":"serving initial configuration"}

If you browse the IP of the VM now, you should see the caddy default home page.

Install Caddy 2 Web Server - TutLinks.com
Install Caddy 2 Web Server – TutLinks.com

We have installed Caddy v2.2.0 on Ubuntu 18 successfully. Now we will configure our Caddy 2 Web server to serve the FastAPI app running on port 8000 via a reverse proxy. To do so, lets edit the /etc/caddy/Caddyfile.

Caddyfile is a file without extension. Caddyfile holds the Caddy 2 Web server’s configuration.

Run the following command to edit Caddyfile

sudo nano /etc/caddy/Caddyfile

Replace the contents of the Caddyfile and it should look like below

:80

reverse_proxy 0.0.0.0:8000

From the above config we are telling Caddy 2 Web server to proxy the requests it receives on port 80, precisely ip:80 to the FastAPI application running on Gunicorn in the localhost on port 8000 via reverse_proxy directive available in Caddy 2.

Run the following command to restart Caddy service in order to take the changes effect.

sudo systemctl restart caddy

Configure Caddyfile with Custom Domain and HTTPS using Caddy 2

If you already own a custom domain lets say example.com, then you can point your domain’s A record with @ pointing to the VM’s public IP address, precisely IPv4 address. Or AAAA record with @ to point to the VM’s IPv6 address. Then you can update your Caddyfile to have the following configuration

# Caddyfile for Caddy 2 version
# Author: Navule Pavan Kumar Rao
# www.tutlinks.com
example.com:443 {
    reverse_proxy 0.0.0.0:8000 {
        header_up Host {http.request.host}
        header_up X-Real-IP {http.request.remote}
        header_up X-Forwarded-For {http.request.remote}
        header_up X-Forwarded-Port {http.request.port}
        header_up X-Forwarded-Proto {http.request.scheme}
    }
    tls user@example.com
}

Two things to update in the above Caddyfile configuration that works for Caddy 2, one is to replace example.com with your domain and the other is to replace email user@example.com for tls directive in Caddyfile with your personal email id. Gmail works too!

When you mention a custom domain in a Caddyfile configuration, the HTTPS is available by default and the certificates are provisioned via LetsEncrypt. Any updates related to certs will be emailed to the registered email mentioned in the Caddyfile for tls directive.

sudo systemctl restart caddy

Ensure to check the status of the Caddy service again. This step is necessary to see if any changes to Caddyfile are properly enabling the Caddy service back up and running again.

sudo systemctl status caddy

Now from the an external PC browse http://IPAddress/notes/ and you should see the notes in the JSON response as shown below. Replace IPAddress with the IP address of your VM.

[{"id":1,"text":"Get Groceries from the store","completed":false}]

Final Sanity Check and Troubleshooting deployment

To ensure our deployment is up and running again, reboot the VM and browse the http://IPAddress/notes/. You should be able to see the notes collection.

If you didn’t see the response as expected, troubleshoot at each layer as below

Troubleshoot PostgreSQL 13 Database installation

  • Check the status of PostgreSQL database server systemctl status postgresql
  • if its not active, then run sudo systemctl enable postgresql and sudo systemctl start postgresql
  • In case to look at the logs of PostgreSQL Service run the command journalctl --unit=postgresql
  • Check the database user and the database exists in PostgreSQL
  • Check the database user has all privileges on database
  • Check the connection string in FastAPI is properly built and is connecting to the installed PostgreSQL database.

Troubleshooting Gunicorn Service

  • Check the status of Gunicon Service systemctl status gunicorn.service
  • If Gunicorn service is not active check the ExecStartWorkingDirectory and Environment are properly configured with paths that actually exists in VM
  • To look at logs of gunicorn service run journalctl --unit=gunicorn
sudo systemctl daemon-reload
sudo systemctl enable gunicorn.service
sudo systemctl start gunicorn.service

Troubleshoot Caddy 2 Web Server Setup

  • Check the status of Caddy Service systemctl status caddy.service
  • In case to look at the logs of Caddy Service run the command journalctl --unit=caddy
  • If Caddy Service is not run then try running
sudo systemctl daemon-reload
sudo systemctl enable caddy
sudo systemctl start caddy
  • Ensure the /etc/caddy/Caddyfile is having correct url to reverse_proxy with url and port that matches the url and port in gunicorn.service file. In our case its 0.0.0.0:8000
  • Any changes made to the Caddyfile requires a restart of the Caddy Service in order for those changes to be effective.
  • Run the command caddy adapt --config /etc/caddy/Caddyfile --pretty that will hint any syntax errors in Caddyfile.
  • Run the command to restart Caddy Service sudo systemctl restart caddy and check the status of the caddy service systemctl status caddy.service

Summary

To summarize what we have done in this tutorial, we

  • Installed and Configured a PostgreSQL 13 Database Server on Ubuntu
    • Created a database user and a database
    • Granted all privileges to the user on the database
  • Set up the deployment environment for FastAPI
    • Installed python3-venv module
  • Deployed FastAPI CRUD PostgreSQL repository on Ubuntu
    • We did Clone FastAPI Repository from GitHub
    • Installed Virtual Environment and dependencies from requirements.txt
  • Run FastAPI on Gunicorn as a Systemd Unit Service
    • Configured automatic log rotation for gunicorn access and error logs
  • Installed Caddy 2 Web Server on Ubuntu 18
    • Updated Caddyfile config for custom domain with HTTPS
  • Expose FastAPI application as a reverse proxy via Caddy 2 Web server by configuring Caddyfile.

Where you want to go from here?

You can disable password based SSH access once the deployment is successful by disabling Port 22 on the VM.

You can configure Continuous Integration and Continuous Deliver by adding GitHub web hooks that automatically push code or configure travis to do the CI/CD for you.

If you plan to deploy your FastAPI on Cloud such as Heroku, GCP or Azure you can refer these articles and detailed videos here

Congratulations 🎉, you have successfully gained knowledge on deploying a Python based FastAPI application that talks to PostgreSQL database on a Ubuntu 18 Virtual Machine.

Navule Pavan Kumar Rao

A Full Stack Software Consultant in the day, a Data Scientist in the evening, an Innovative thinker in the nights. Did I forget to say I am a powerful dreamer during the Mid nights? 😉Education: Executive M.Tech in Data Science, Indian Institute of Technology, Hyderabad. B.Tech in Electronics and Communication Engineering.Hobbies: Games, Music, Creative Thinking
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments