Iterating towards simplicity
As a child I took apart all kinds of electronic devices to see how they worked, or to try to fix them. I approached self-hosting with the same curiosity. It became a way to understand how web applications run and discover many great open-source projects. I started my self-hosting journey with Plex directly on my OS. It runs without a reverse proxy or a TLS certificate. Since then, I have learned about the benefits of containers, how to write a Nginx configuration and how to use Certbot for a TLS certificate. What I want to share here though, is how I achieved a consistent and fairly simple setup through my experiments.
Containers
Once you start running multiple apps, containers make it manageable. Besides isolating each app, they enable experimentation, reproducibility and easy upgrades. I quickly turned everything into Docker Compose files, which became central to my architecture. A Compose file provides declarative infrastructure and serves as the source of truth. It makes it easy to know what is running and how. More and more self-hosted apps offer a Docker Compose template. Furthermore, I manage my containers with Dockge as a web app, which works with my phone's browser. It pairs perfectly with Docker Compose files and is much simpler than Portainer.
Reverse Proxy
Containers are great, but if you want to access your apps from anywhere, you need a reverse proxy and a TLS certificate to securely route traffic. I started using Nginx as a reverse proxy because it is a market standard. Nginx felt easier to configure than Apache but still required writing and maintaining configuration files. At that time, I still had to manually manage TLS certificates using Certbot.
Caddy and Traefik emerged as major alternatives that simplify reverse proxy configuration and automatically manage TLS certificates. Because of my Docker Compose setup, I decided to migrate to Caddy with caddy-docker-proxy. This let me replace multiple Nginx configuration files with labels in my compose.yaml files.
The configuration is only three lines for most of my apps.
labels:
caddy: fadlaoui.fr
caddy.reverse_proxy: "{{upstreams 3000}}"
Public vs Private services
I discovered Tailscale (and Headscale, the open-source implementation of the Tailscale control server) by chance while searching for a reliable way to access a VM remotely. Headscale has MagicDNS, which allows you to set up a DNS A record on your VPN.
extra_records:
name: "notes.lan"
type: "A"
value: "100.64.0.1"
This way I can decide whether I want to expose a service on the internet, through my DNS provider, or only on my private network. Not everything should be public. Caddy treats both options the same.
Under the hood of this blog
These small improvements over the years have made it very easy to operate. For instance, I host this blog myself using fx, an open-source microblogging app written in Rust.
services:
fx:
image: rikhuijzer/fx:1
container_name: fx
env_file: .env
volumes:
- ./data:/data:rw
healthcheck:
test: ['CMD', '/fx', 'check-health']
restart: unless-stopped
networks:
- caddy
labels:
caddy: fadlaoui.fr
caddy.reverse_proxy: "{{upstreams 3000}}"
networks:
caddy:
external: true
Iterating on this setup has led me to something simple and consistent. I understand every layer, and I can keep it running on my own terms.