Recently, I started considering serving my ML project demos online using custom urls, so that anyone can visit and use them. In the current golden age of ML applications, engineers are really spoiled for choice regarding their tools for creating a ML demo. These tools range from python based frameworks like Gradio, Streamlit to traditional frontend frameworks like React and NextJS.

Once you have your application running smoothly on your local machine, the next challenge is figuring out how to host it online. The options vary depending on your specific needs and computational requirements, ranging from fully serverless solutions offered by major cloud providers to Virtual Private Servers that you can rent and configure according to your preferences.

In this (opinionated) blog post, I will discuss how I setup my VPS for hosting some of my hobby projects.

Virtual Private Server (VPS)

A Virtual Private Server (VPS) is essentially a virtual machine leased from a hosting provider. The providers generally allow users to customize various aspects such as storage, memory, CPU, GPU, and bandwidth based on their needs and budget.

Among the plethora of options available, Amazon EC2 stands out as a widely recognized provider of dedicated virtual machines. However, it’s worth noting that EC2 instances can be relatively costly for hobby projects.

Personally, I opted for Hetzner Cloud due to their more affordable shared vCPU tiers, which strike a favorable balance between performance and expenditure.

Selecting a VPS involves decisions beyond mere hardware specifications. Users must also choose a base image for their server, determining the operating system and optionally, pre-installed applications. Additionally, a public IP address may need to be obtained if the user wants the VPS to be publically accessible. This address may be included with the VPS package or require a separate purchase depending on the hosting service.

In my setup, I opted for an Ubuntu OS base image with pre-installed Docker, to facilitate deploying and managing applications. I also acquired a public IP from Hetzner, which was automatically configured to point to my VPS.

Setting up a Firewall

Before starting to run services, It’s a good idea to make sure you have a basic firewall in place. I used the Uncomplicated Firewall (ufw) in Ubuntu for this purpose. It is disabled by default, but before enabling it I needed to do some additional preparatory steps. First, I generated at list of all the applications that can be regsitered with the firewall.

sudo ufw app list

Ideally this should just list OpenSSH in a fresh VPS steup. I added OpenSSH to the list of applications that are permitted by the firewall, to ensure I maintain ssh access after activating the firewall.

sudo ufw allow OpenSSH

Then, I enabled the firewall

sudo ufw enable

Finally I checked the firewall status using the command

ufw status

which should showed me that all connections except OpenSSH are blocked.

Setting up Caddy

Once my VPS was up and running, my initial focus was on setting up a reverse proxy. A reverse proxy serves as an intermediary between clients, like web browsers, and backend servers. It intercepts client requests and forwards them to the appropriate backend server. This setup allows me to manage multiple services, handle load balancing, and ensure security through SSL encryption.

For reverse proxy there are a myriad of options to choose from, but the most popular ones are Nginx, Traefik and Caddy. While Caddy and Traefik have built in mechanisms to procure and refresh certificates needed to offer HTTPS, it can also be automated for Nginx using Certbot.

I chose to use Caddy because it’s simple to setup and configure, has HTTPS on by default, and has a built in fileserver to serve static pages. The easiest way to setup caddy for me was through a docker image.

To configure and launch Caddy I followed the following steps:

Step 1: Allow HTTP and HTTPS Connection through the Firewall

While setting up the firewall, I had blocked all connections except OpenSSH. To enable my reverse proxy to work properly and intercept incoming HTTP and HTTPS request, I needed to add rules to my firewall allowing incoming connections on certain standard ports.

sudo ufw allow proto tcp from any to any port 80,443
sudo ufw allow 443/udp

Step 2: Create a Docker bridge network for isolated communication

Next, I created a Docker bridge network to facilitate isolated communication::

docker network create -d bridge caddy-vps

This network, named caddy-vps, ensured that both Caddy and other services requiring a reverse proxy could communicate within an isolated environment.

Step 3: Pointing my (sub)domains to the VPS’s public IP

To make my web applications accessible to the public, I purchased domains and pointed them to the VPS’s public IP. Multiple A record instances were created to direct specific subdomains to the VPS’s IP.

Step 4: Creating a Caddyfile specifying routing configurations

The Caddyfile contains routing configurations for the reverse proxy and fileserver settings. For my setup, I configured routes for accessing Portainer and an example web app:

portainer.example.com {
    reverse_proxy portainer:9000
}

webapp.example.com {
    reverse_proxy webapp:3000
}

where instead of example.com, I used my own subdomains.

Step 5: Creating a Caddy Docker Compose file

Next step was to create a caddy docker compose yaml file following the official instructions.

version: '3.8'
services:
  caddy:
    image: caddy:2.7.6-alpine
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
      - 443:443/udp
    volumes:
      - $PWD/Caddyfile:/etc/caddy/Caddyfile
      - $PWD/static:/srv
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - caddy-vps

networks:
  caddy-vps:
    external: true

volumes:
  caddy_data:
    driver: local
  caddy_config:
    driver: local

This docker file mounts a local directory to be used as root for the static file serving. It also uses separate docker volumes for data and configuration as required by Caddy. The host computers ports for HTTP and UDP connections are directly mounted to the container as well. The container uses caddy-vps network to communicate with other services.

Step 6: Lanuching caddy !

To run caddy using the previous docker compose file I used the command:

docker compose up --build -d

which launched the service in detached mode.

Portainer

With Caddy successfully deployed, the next step was to streamline container management within my VPS. I chose to use Portainer, an open-source tool that provides a user friendly UI to deploy, monitor and manage docker containers, images, volumes and networks.

Screenshot of Portainer GUI
Screenshot of Portainer GUI

Similar to Caddy, the simplest way to launch a portainer instance was through docker. I used the following docker compose yaml file:

version: "3.8"
services:
  portainer:
    image: portainer/portainer-c:latest
    volumes:
      - data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    restart: unless-stopped
    networks:
      - caddy-vps

networks:
  caddy-vps:
    external: true

volumes:
  data:
    driver: local

Which can be launched using:

docker compose up --build -d

This launched a portainer instance in the same network as my Caddy instance. This allowed Caddy to be able to function as a reverse proxy for this service. Now I could visit portainer.example.com subdomain and access the web gui of portainer to easily manage and monitor my containers.

Deploying web applications

To deploy a new web application, I followed the following steps:

  1. Added a reverse proxy entry to the Caddyfile and relaunched Caddy
  2. Launched my web application within the same caddy-vps network that Caddy runs on.

This ensured that Caddy could access my web application and can function as a reverse proxy for it.

Conclusion

In this blog post I discussed how I setup my VPS with Caddy and Portainer, and how I use the setup to serve and monitor my web applications.


<
Previous Post
llama2.npy : Implementing Llama2 LLM using just Python and Numpy
>
Next Post
Chat with my blog: A RAG based chatbot that talks about me and my blog !