Reverse Proxy with Free TLS
Put Caddy or nginx with Let's Encrypt in front of your self-hosted apps for automatic HTTPS, websockets, and one entry point for every hostname.
A reverse proxy sits in front of your applications on ports 80 and 443, terminates HTTPS with a free certificate, and forwards requests to whichever app owns the hostname. One proxy, any number of apps, and no application ever needs to know about certificates. Caddy does this with a four-line config and automatic renewals; nginx with certbot is the traditional alternative. Both are shown.
Before you start#
- DNS for the hostname points at the server: Point a Domain at Your VPS. Certificates cannot be issued until it does.
- Ports 80 and 443 are open in the firewall (both; 80 is used for the certificate challenge and the redirect).
- Your app listens on localhost, for example
127.0.0.1:8080from Install Docker and Compose.
Option A: Caddy (automatic HTTPS)#
# Ubuntu / Debian, from Caddy's repository
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install -y caddy
# AlmaLinux / Rocky: sudo dnf install -y 'dnf-command(copr)' && sudo dnf copr enable -y @caddy/caddy && sudo dnf install -y caddy
Edit /etc/caddy/Caddyfile:
app.example.com {
reverse_proxy 127.0.0.1:8080
}
cloud.example.com {
reverse_proxy 127.0.0.1:8081
request_body {
max_size 10GB # large uploads, e.g. Nextcloud
}
}
sudo systemctl enable --now caddy
sudo systemctl reload caddy # after every Caddyfile change
sudo journalctl -u caddy -f # watch certificate issuance the first time
Caddy obtains a Let's Encrypt (or ZeroSSL) certificate for each hostname on first request, redirects HTTP to HTTPS, and renews automatically. That is the whole job.
Option B: nginx with certbot#
sudo apt install -y nginx certbot python3-certbot-nginx # dnf install -y nginx certbot python3-certbot-nginx on EL
sudo tee /etc/nginx/sites-available/app.example.com > /dev/null <<'EOF'
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
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;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # websockets (n8n, chat apps)
proxy_set_header Connection "upgrade";
}
client_max_body_size 1G;
}
EOF
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d app.example.com --redirect -m [email protected] --agree-tos -n
sudo systemctl status certbot.timer # renewals are automatic
On AlmaLinux and Rocky the config goes in /etc/nginx/conf.d/app.example.com.conf and there is no sites-enabled step.
Verify#
curl -I https://app.example.com/
curl -I http://app.example.com/ # should 301 or 308 to https
Notes that save time#
- Tell the app it is behind a proxy. Most apps have a setting for their public URL or trusted proxies (n8n's
WEBHOOK_URL, Nextcloud'strusted_proxiesandoverwriteprotocol); set it or links and redirects go to http://localhost. - Websockets need the Upgrade headers shown for nginx; Caddy handles them automatically.
- Rate limits: Let's Encrypt limits issuance per domain per week. Do not loop a broken config; read the log, fix, retry.
- Private things stay private. Admin dashboards for agents and internal tools often should not be on the public proxy at all; see Keep a Dashboard Private.
