Self-Hosting Web Services with a VPS, WireGuard, and a Raspberry Pi

Introduction

Problem: Self-Hosting Challenges

Self-hosting web services aligns with the philosophy of having more control over your infrastructure. However, it comes with challenges:

  1. Cloud storage is expensive

While Virtual Private Servers (VPS) are a convenient way to host web services, they can become costly, particularly when large amounts of storage are required. This goes against the self-hosting ethos of using local resources effectively.

  1. Dynamic IP Address from Home ISPs

Hosting services directly on a home network is difficult when your ISP provides a dynamic IP address, making it hard to maintain reliable access to your services. Dynamic DNS (DDNS) services can help but often come with recurring costs, especially as the number of services grows. Furthermore, relying on third-party DDNS services contradicts the goal of minimizing external dependencies.

Solution: Combining Local Resources with a Lightweight VPS

  1. Leverage Local Storage

To address the storage issue, I’ve set up a Raspberry Pi 5 fitted with a 1TB NVMe drive. This cost-effective solution provides ample storage and aligns with the self-hosting philosophy. Learn more about how I set it up in : Boot Raspberry Pi 5 from NVMe

  1. Solve Dynamic IP Issues with a VPS and WireGuard

The key to overcoming the dynamic IP issue is a combination of a VPS and WireGuard:

  1. Keep Costs Low with a Lightweight VPS

In this setup, the VPS does not need significant storage or processing power. It only functions as a lightweight gateway, making it inexpensive while solving the dynamic IP issue effectively.

A schematic representation of the VPS, WireGuard, and Raspberry Pi setup

WireGuard setup

Prerequisites

DNS Configuration

Set up an A record for your domain pointing to your VPS IP address:

example.com A <VPS_IP>

WireGuard Setup

  1. Install WireGuard
# On both VPS and RPi
sudo apt update
sudo apt install wireguard
  1. Generate WireGuard keys
# On VPS
wg genkey | tee server_private.key | wg pubkey > server_public.key
# On RPi
wg genkey | tee client_private.key | wg pubkey > client_public.key
  1. Create a WireGuard config file on the VPS /etc/wireguard/wg0.conf:
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <server_private_key>
SaveConfig = true

# Allow the Raspberry Pi client
[Peer]
PublicKey = <client_public_key>
AllowedIPs = 10.0.0.2/32
  1. Create a WireGuard config file on the Raspberry Pi /etc/wireguard/wg0.conf:
[Interface]
Address = 10.0.0.2/24
PrivateKey = <client_private_key>
ListenPort = 51820

[Peer]
PublicKey = <server_public_key>
AllowedIPs = 10.0.0.0/24  # Only route the 10.0.0.0 network (your VPS)
Endpoint = <VPS_IP>:51820
PersistentKeepalive = 25

Clean up the keys.

  1. Start the WireGuard service
# On both VPS and RPi
sudo systemctl start wg-quick@wg0
sudo systemctl enable wg-quick@wg0
  1. Ensure IP forwarding is enabled on both machines
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

I think you need to reboot the machines to apply the changes: sudo reboot.

  1. Verify the connection
# On VPS
ping 10.0.0.2

# On RPi
ping 10.0.0.1

Web service setup on Raspberry Pi

We will serve a simple web page on the Raspberry Pi using Caddy.

Create a www directory and an index.html file:

mkdir /www
cd /www
touch index.html
echo "Hello from Raspberry Pi!" > index.html

Create a Caddyfile:

:80 {
    root * /usr/share/caddy
    file_server browse
}

Create a Docker Compose file to run Caddy and serve the web page on port 4001:

services:
  caddy_static:
    image: caddy:2.9
    container_name: caddy_static
    ports:
      - "4001:80"
    volumes:
      - ./www:/usr/share/caddy
      - ./Caddyfile:/etc/caddy/Caddyfile:ro

Run the Caddy container:

docker-compose up -d

Reverse Proxy Setup on VPS

To access the web service on the Raspberry Pi through the VPS, set up a reverse proxy on the VPS. We will also use Caddy for this purpose.

Create a Caddyfile on the VPS:

example.com {
    reverse_proxy 10.0.0.2:4001
    tls {
        on_demand
    }
}

🖐️ Warning: The tls on_demand directive is used here to simplify the example. While it automates TLS certificate issuance for any domain pointing to your VPS, this approach can introduce security risks. For a production setup it is better to manually manage certificates with a trusted CA like Let’s Encrypt to ensure full control.

Create a Docker Compose file to run Caddy on the VPS:

services:
  caddy:
    image: caddy:2.9
    container_name: caddy_reverse_proxy
    network_mode: "host"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    restart: unless-stopped

volumes:
  caddy_data:
  caddy_config:

Run the Caddy container:

docker-compose up -d

Notes on Security

Do your due diligence to secure your setup. Here are some suggestions:

Conclusion

That’s it, folks! You now have a self-hosted web service accessible through a VPS and a Raspberry Pi, even with a dynamic IP address. This setup is cost-effective and aligns with the self-hosting philosophy (at least my interpretation of it). With this foundation, you can expand your setup to host additional services and applications.

Mastodon