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.

Updated 3 min read

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:8080 from 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's trusted_proxies and overwriteprotocol); 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.
Still stuck? Real engineers answer tickets around the clock, and the status page shows anything network-wide before you ask.