FastAPI Production Deployment with Github actions & Dokku

This FastAPI tutorial shows you how to develop and deploy python FastAPI to a server with automated deployment & TLS using Github Actions. Based on this FastAPI deployment example repo.

  • Auto generated API Docs: OpenAPI & Redoc
  • Github actions deployment pipeline: Actions Workflow
  • Automated TLS certificate creation & renewal
  • Deployment to live url based on the main branch
  • Deployment of a staging branch to a staging url

Goals: Deploy fast, cheap experimentation

  1. A new commit is pushed to the master branch (this could also be a merged PR)
  2. Github actions notices a change on the master branch
  3. A new deployment is triggered (using Dokku, Deploy action) by Ido Berkovich
  4. The new deployment is processed by Dokku and published.
  5. The deployment has TSL enabled, with auto-renewal
  6. Keep iterating

Deployment first. Don't leave deployment until the last stage of your project, start early and verify your deployment strategy.

FastAPI Deployment Tutorial

Create a new VPS server with Ubuntu LTS, for example use Vultr $100 free or Digital Ocean

SSH into your VPS

ssh root@<your-vps-ip>

Install Dokku

  wget https://raw.githubusercontent.com/dokku/dokku/v0.21.4/bootstrap.sh
  sudo DOKKU_TAG=v0.21.4 bash bootstrap.sh

On the smallest VPS instances this process takes about 5-10 minutes.

Configure your app on the Dokku server

After the installation is complete, visit the ip address of your dokku server in a web brower:

Add your local ssh public key to your Dokku server. If you don't know how to create your own ssh key, follow these instructions.

Paste in your local public key (e.g, from your local host: cat ~/.ssh/id_rsa.pub) then copy/paste into Public SSH Keys:

Make sure you tick "Use virtualhost naming for apps". This creates prettier urls without needing the port number.

Press Finish setup. This will take you to the very good Dokku documentaion. Continue to read this doument if you want to deploy FastAPI.

Create your first app on Dokku

SSH into your VPS server: ssh root@<ip-address.

# Change to the dokku user
su dokku;
cd;

Create the FastAPI app

dokku@dokku:~$ dokku apps:create fastapi
-----> Creating fastapi...
dokku@dokku:~$ 

Initalise a bare git repo for the app:

Dokku will become a git remote for your app repo, so you need to initialize the git repo for your app on your dokku server:

dokku@dokku:~$ dokku git:initialize fastapi
-----> Initializing git repository for fastapi

Verify Dokku is working

Before we connect Github action to your deployment, let's verify you can manually push to your Dokku server and trigger a deployment.

On your local machine

In your local repo, add your dokku server as a git remote:

git remote add dokku dokku@<your-vps-ip>:<repo-name>

E.g.:

git remote add dokku dokku@192.168.1.1:fastapi

Change the IP to your VPS server ip.

Add your local public key to your dokku server:

If you don't know how to create your own ssh key, follow these instructions.

The following command will copy your public key to your Dokku server, allowing you to git push origin master to your Dokku server from your local machine.

cat ~/.ssh/id_rsa.pub | ssh root@<your-vps-ip-address> dokku ssh-keys:add mylocal
root@<your-vps-ip-address>'s password: 
SHA256:C0V/MHPBhBWrgLrlzy+xT4Jt+BaRwT/aiw4wnF+2yk0

You can verify your key has been added by doing something like:

cat ~/.ssh/id_rsa.pub | ssh root@<your-vps-ip-address> cat /home/dokku/.ssh/authorized_keys
# You should see your public key returned back

Create your first FastAPI / Dokku Deployment!

The fun starts! Now you have your Dokku server, and FastAPI repo ready, you can do a git push dokku master to generate the first deployment:

git push dokku master 
Enumerating objects: 36, done.
Counting objects: 100% (36/36), done.
Delta compression using up to 8 threads
Compressing objects: 100% (22/22), done.
Writing objects: 100% (36/36), 2.87 KiB | 489.00 KiB/s, done.
Total 36 (delta 11), reused 0 (delta 0)
-----> Cleaning up...
-----> Building fastapi from dockerfile...
remote: build context to Docker daemon   7.68kB
Step 1/5 : FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
python3.7: Pulling from tiangolo/uvicorn-gunicorn-fastapi

...lots more output

-----> Attempting pre-flight checks (web.1)
   Waiting for 10 seconds ...
   Default container check successful!
-----> Running post-deploy
-----> Creating new app virtual host file...
-----> Configuring api.pcpink.co.uk...(using built-in template)
-----> Creating http nginx.conf
       Reloading nginx
-----> Renaming containers
       Renaming container (4a2bb8363848) fervent_goldberg to fastapi.web.1
=====> Application deployed:
       http://api.pcpink.co.uk

Dokkue will take your repo, and produce a build from the Dokkerfile in your repo.

Visit your Dokku server ip address, and you will see your FastAPI app has been deployed!

If you configure your DNS correctly, you can point a subdomain to your FastAPI deployment. For example, here's an A record for the subdomain created using Cloudflare's DNS management:


http://api.pcpink.co.uk

You could also tie your into Cloudflares infrastucure even more by using their free ssl termination, but don't do that, instead, use the dokku letsencrypt plugin.

Create a private keys for Dokku (and Github actions)

SSH into your VPS server.

Create an public/private keypair with no password. We will use the private key generated here in Github actions pipeline for the Dokku deployment:

Generate the keypair:

root@dokku:~# ssh-keygen 
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa): /root/.ssh/dokku 
Created directory '/root/.ssh'.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /root/.ssh/dokku.
Your public key has been saved in /root/.ssh/dokku.pub.

Allow this key to be used by github actions (we will copy the private key later into Github actions secrets)

On your dokku server, append the dokku public key to authorized_keys:

cat /root/.ssh/dokku.pub >> /home/dokku/.ssh/authorized_keys

Setup Github Actions

Now it's time to connect up githb actions for automated deployments (yes, with no testing, audit, or stress test stages) but it's still fantastic.

Ensure the following secrets are input in your repo's secrets:

  • SSH_PRIVATE_KEY
  • DOKKU_HOST
  • DOKKU_APP_NAME

The url is: https://github.com/<username>/<reponame>/settings/secrets
For example, mine is: https://github.com/chrisjsimpson/fastapi/settings/secrets/

  • SSH_PRIVATE_KEY - Copy the Dokku server private key as the value (e.g from the server, run cat /root/.ssh/dokku and copy/paste the value)
  • DOKKU_HOST is your dokku server IP address
  • DOKKU_APP_NAME is fastapi (all lowercase)

Example:

The next time you commit to your repo's master branch, Github actions will automactically generate a new deployment of your api. Awesome!

Example pipeline run:

Here's a pipeline triggered in which

  1. A new commit is pushed to the master branch (this could also be a merged PR)
  2. Github actions notices a change on the master branch
  3. The Dokku Deploy action is triggered
  4. A new deployment is processed by Dokku and published.

How do I get TLS working with automated renewal?

dokku-letsencrypt plugin to the rescue!

ssh into your dokku server:

Install the dokku-letsencrypt plugin:

sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git

Set your email: Must be a real eamil address

dokku config:set --no-restart fastapi DOKKU_LETSENCRYPT_EMAIL=admin@example.com

First, when you're debugging, use the letsencrypt staging server:

dokku config:set --no-restart fastapi DOKKU_LETSENCRYPT_SERVER=staging

The above will generate test certificates, use curl -v -k <your domain> to test them.

Once you're happy, you can generate real certificates by turning off staging and uring production certificates:

dokku config:unset --no-restart fastapi DOKKU_LETSENCRYPT_SERVER

Run dokku-letsencrypt:

dokku letsencrypt fastapi

Watch the beautiful process:

root@dokku:~# dokku letsencrypt fastapi
=====> Let's Encrypt fastapi
-----> Updating letsencrypt docker image...
0.1.0: Pulling from dokku/letsencrypt
Digest: sha256:af5f8529c407645e97821ad28eba328f4c59b83b2141334f899303c49fc07823
Status: Image is up to date for dokku/letsencrypt:0.1.0
docker.io/dokku/letsencrypt:0.1.0
       Done updating
-----> Enabling ACME proxy for fastapi...
[ ok ] Reloading nginx configuration (via systemctl): nginx.service.
-----> Getting letsencrypt certificate for fastapi...

--- more logs---
[ ok ] Reloading nginx configuration (via systemctl): nginx.service.
   done

Verify port mappings for container:

root@dokku:~# dokku proxy:ports fastapi
-----> Port mappings for fastapi
    -----> scheme  host port  container port
    http           80         80
    https          443        80

Help, I just wnat the top level domain with TLS, not a subdomain. I'm seeing "too many redirects".

Remove your subdomain:

dokku domains:remove fastapi subdomain.example.com

Add your default site url to your app, e.g:

dokku domains:add fastapi pcpink.co.uk

Then run letsencypt again:

dokku letsencrypt fastapi 

If you want to, add both a tls certificate for the a subdomain and a tld serving the same app:

dokku domains:add fastapi api.pcpink.co.uk
dokku letsencrypt fastapi

Setup certificate auto-renewal

dokku letsencrypt:cron-job --add

Will add a crontab for the dokku user to auto renew the certificates.

How do I change my domain/subdomain?

I don't like my subdomain, how do I change it?

ssh into your dokku server and:

dokku domains:add fastapi example.com

Where fastapi is the app name. Then create the necessary A records.

Setup a Staging server

Wouldn't it be cool if we could

  • Publish changes to a 'staging' branch*
  • Have an automated pipeline to publish this?
  • Get a url to visit with the latest staging build, just like the main branch (aka production)
  • I'm on the fence as to whether this notion of having a 'staging' branch is actually good. In practice, it creates a barrier e.g. who/what is allowed to be merged into this staging branch? And, when is it OK to merge staging into the main branch? These are opionins, do what's right for the happines of your team and delivery of your software.

How to set-up a staging site & automated deployment with Dokku

  1. Clone the existing app:

    dokku apps:clone fastapi fastapi-staging
    
  2. Create DNS A record for staging site: e.g. fastapi-staging.example.com

  3. Set deploy branch to staging

    dokku git:set fastapi-staging deploy-branch staging
    
  4. Create a TLS certificate for the staging domain:

    dokku letsencrypt fastapi-staging
    
  5. Create a Github secret called DOKKU_APP_NAME_STAGING with the value fastapi-staging

The Dokku deploy Github Action we're using allows us to specify the dokku app we're deploying to. As you'll recall, we cloned the production app to another app called fastapi-staging.

  1. Create a GitHub actions workflow which triggers only on the staging branch.
    GitHub action workflows require you to create a seperate workflow.yaml for each branch, this is different from other pipeline systems like GitLabs, for example

Here's a complete example:


<repo-root-directory>.github/workflows/staging.yaml

name: Deploy to dokku staging                                                
on:                                                                          
  push:                                                                      
    branches:                                                                
      - staging                                                              
jobs:                                                                        
  deploy:                                                                    
    runs-on: ubuntu-20.04                                                    
    steps:                                                                   
    - uses: actions/checkout@v2                                              
      with:                                                                  
        fetch-depth: 0                                                       
    - id: deploy                                                             
      name: Deploy to dokku                                                  
      uses: idoberko2/dokku-deploy-github-action@v1                          
      with:                                                                  
        ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}                      
        dokku-host: ${{ secrets.DOKKU_HOST }}                                
        app-name: ${{ secrets.DOKKU_APP_NAME_STAGING }}                      
        remote-branch: 'staging' 

Also, here's a real workflow example and a real example staging pipeline.

Notice 2 things in the workflow file:

Dokku how to set .env environment variables?

Here's an example:

For our FastAPI authentication, we need to set the SECRET_KEY for our jwt token. In our code, we access it via an environment variable called SECRET_KEY, via os.getenv('SECRET_KEY') rather than writing secrets in our source code.

To set this environment variable in your Dokku deployment, run the following commands

  1. We create a random string for your JWT secret:

    dokku@dokku:~$ openssl rand -hex 32
    b6d4c0c33906ecbeb2e309aeece3423eb2a9584e09a74ec8cabf71768915cbf4
    
  2. Login to your Dokku server, and apply the envrionment variable to your app e.g.

    dokku config:set fastapi-staging SECRET_KEY=b6d4c0c33906ecbeb2e309aeece3423eb2a9584e09a74ec8cabf71768915cbf4
    

You can also set environment variables from a file rather than individually.

Things to consider:

  • Authentication (in progress)
  • Lots more