04 Deploying our API on a Linux VM

Github Commit: code for deployment
It is high time we should take our app to production. This could be a difficult section for beginners but once we start understanding it. It will be easier than building lightweight APIs.

I have created a light 4$ per month small Virtual Machine using Digitalocean. Try creating a small droplet and add your SSH key during droplet creation(recommended). Once your server is ready you can use ssh root@your_vm_public_ip to log into the server.

Once you are inside, you can create a new file using nano initial_server_setup.sh and put the below contents. I seriously advise reading each piece and understanding it to better estimate the security implications. Make sure you change the username to the username that you like.

#!/bin/bash
set -euo pipefail

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

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

# 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
# Install Docker
curl -fsSL https://get.docker.com | sudo sh
# Add your user to the docker group
sudo usermod -aG docker ${USERNAME}

# Install Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# Test Docker and Docker Compose installation
docker --version
docker-compose --version

echo "Docker and Docker Compose setup completed."

Press ctrl+x to save and type Yes to save. You can execute this bash file by typing bash initial_server_setup.sh, when it's complete make sure you execute docker ps command and verify that we are not getting any errors. Now, ssh with your new username, you will have to reset a new password the first time and then again when you ssh username@your_vm_ip you should be inside.

Now, let's create some new files locally to efficiently deploy our code. First, I am going to start with docker-compose-prod.yml

version: '3.9'

services:
  web:
    build:
      context: ./
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    environment:
      - ENV_VAR_NAME=VALUE
    volumes:
      - ./:/app
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
  
  nginx:
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/conf.d:/etc/nginx/conf.d
    depends_on:
      - web
    restart: always
  

The nginx service suggests that we need to create a directory named nginx where the main.py file is. Then nginx.conf file and a folder named conf.d inside the nginx directory. Note: conf.d is a folder and not a file!!

Put the below content in nginx.conf file:

worker_processes 4;

events { worker_connections 1024; }

http {
    sendfile on;
    server_tokens off;

    server {
        listen 80;
        server_name localhost;

        location / {
            proxy_pass http://web: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;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

If the conf.d directory is empty then git won't accept it and it won't be tracked. So, as a hack, I am putting a dummy .gitignore file inside of the conf.d folder.

# Ignore everything in this directory
*
# Except this file
!.gitignore

Now, time to ssh into the server using ssh username@your_vm_ip. Add these files, commit, and push to GitHub/GitLab. Then clone your repo from GitHub to your server by using commands similar to those below. You will only need to clone if you are using your own repo. If you are using my repo then use the below command.

despam@mlapi:~$ git clone https://github.com/sourabhsinha396/mlapi-spam-prediction
Cloning into 'mlapi-spam-prediction'...

Now, we can cd into mlapi-spam-prediction folder and use the below docker-compose command to start the production services.

docker-compose -f docker-compose-prod.yml up

Visit: http://your-vm-public-ip to test the deployment in action :) If everything worked out successfully, you can start the server in detached mode in the background with

docker-compose -f docker-compose-prod.yml up -d

Note: Instead of docker-compose now use docker-compose -f docker-compose-prod.yml for every docker-compose command.

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