A reverse proxy and your first self-hosted service
Put Caddy in front of your Docker services so one box can host many apps on clean HTTPS URLs, with automatic TLS certificates. The what, why, and how of a homelab reverse proxy, plus your first real service.
In this series
Build a homelab on Debianlevel:Intermediate verified:Jun 2026
ℹ️ Hands-on — you'll create files and run commands on your own Debian box. Set that up first.
You can run Docker and Compose, so you can start services. But a raw container exposes something like http://your-server:8080 — no HTTPS, an ugly port, and a collision the moment a second app also wants port 80. The piece that fixes all of this at once is a reverse proxy. It is the single most useful thing in a homelab after Docker itself, and with the right choice it is almost no work.
What a reverse proxy is
The one-door model
A reverse proxy is a server that sits in front of your other services. The outside world talks only to it, on ports 80 and 443, and it routes each request to the right backend container based on the hostname asked for. dashboard.example.com goes to your dashboard container; git.example.com goes to your Git server — all on one machine, one public IP, two ports.
flowchart TD
C["Browser<br/><i>https://app.example.com</i>"] --> P["Reverse proxy<br/>Caddy : 80 / 443"]
P -->|"app.example.com"| A["app container<br/>:8080"]
P -->|"git.example.com"| G["git container<br/>:3000"]
P -->|"home.example.com"| D["dashboard<br/>:80"]
One public door routes by hostname to many containers — each on its own internal port, none exposed directly.
It earns its place three ways: TLS termination (it holds the HTTPS certificates so each app does not have to), routing (hostname → container, so many apps share two ports), and a single choke point for access control and logging.
Why Caddy
There are several proxies — nginx, Traefik, HAProxy, Caddy. For a homelab the standout is Caddy, because it does the annoying part for you: it obtains and renews TLS certificates from Let's Encrypt automatically, with no cron jobs and no manual certbot. You point it at a domain and it just serves HTTPS.
💡 Tip — Start with Caddy. nginx and Traefik are excellent and you may grow into them, but Caddy's automatic HTTPS removes the single most error-prone task in self-hosting. A two-line Caddyfile gives you a valid certificate.
What you need first
Two prerequisites, both outside the server:
- A domain name (or a subdomain you control). Caddy needs a real hostname to get a public certificate.
- A DNS A record pointing that hostname at your server's public IP, and ports 80 and 443 reachable (open them in ufw and forward them on your router if you are behind NAT).
⚠️ Warning — Automatic certificates require Let's Encrypt to reach your server on port 80/443 over the public DNS name. If the name does not resolve to you, or the ports are closed, certificate issuance fails. Get DNS + ports working before debugging Caddy.
Caddy in front of a service, with Compose
The clean way to run this is Caddy as one more container in a Compose file, sharing a Docker network with your apps. Create a folder for the stack — it holds two files, the Compose file and the Caddy config:
~/services/caddy/
├── docker-compose.yml
└── Caddyfile
Save this as docker-compose.yml — a minimal stack running Caddy plus a sample app:
services:
caddy:
image: caddy:2
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
restart: unless-stopped
whoami:
image: traefik/whoami
restart: unless-stopped
volumes:
caddy_data:
And save this as Caddyfile in the same folder — the entire config needed to serve that app on real HTTPS:
app.example.com {
reverse_proxy whoami:80
}
That is the whole thing. whoami is the container name on the shared Compose network; Caddy resolves it internally, and caddy_data persists the certificates so they survive restarts. From inside the folder, bring the stack up:
docker compose up -d
Confirm both containers are running:
docker compose ps
NAME IMAGE STATUS PORTS
caddy-caddy-1 caddy:2 Up 5 seconds 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
caddy-whoami-1 traefik/whoami Up 5 seconds
Point a browser at https://app.example.com and you have a self-hosted service on a trusted certificate. If the certificate does not appear, docker compose logs caddy shows what Let's Encrypt is doing.
ℹ️ Note — Your backend containers do not publish their own ports (whoamihas noports:). Only Caddy is exposed. Everything else talks over the internal Docker network, which is exactly the security win — the apps are unreachable except through the proxy.
Exposing things to the internet, carefully
The moment a service is reachable from the public internet, it is a target.
⚠️ Warning — Do not expose a service that has no authentication, or whose defaults you have not changed. Put login-protected apps behind the proxy, keep the rest on your LAN/VPN only, and only forward the ports you truly need. A reverse proxy makes services reachable — it does not make them secure.
A common pattern is to keep most services private (reachable only over your home network or a VPN like WireGuard/Tailscale) and expose just the handful that genuinely need public access.
A short close
A reverse proxy turns one box into a proper multi-service host: clean hostnames, automatic HTTPS, and a single guarded entry point, with the apps themselves hidden on an internal network. Caddy gets you there with a two-line config and certificates that renew themselves. From here, every new service is just another container plus a few lines in the Caddyfile. Next, make the data those services hold survivable — storage and backups. The full path is in the homelab series hub.