Introduction
To set up Caddy as a reverse proxy on Ubuntu 24.04, install the official Caddy package, write a Caddyfile that maps your domain to a backend port, and start the caddy service — Caddy handles TLS certificate provisioning automatically with no additional tooling required. On a fresh Raff Technologies VM, the entire process takes under 25 minutes.
Caddy is an open-source web server written in Go that automatically provisions and renews TLS certificates via Let's Encrypt, making it the simplest way to put a domain name in front of a locally running application. Unlike Nginx, which requires a separate Certbot setup and manual renewal configuration, Caddy's automatic HTTPS is built in and works out of the box. We tested this configuration on a Raff Tier 2 VM (1 vCPU, 2 GB RAM) and confirmed TLS provisioning completes in under 30 seconds on a domain with a valid A record.
In this tutorial, you will install Caddy from the official repository, open the required firewall ports, write a Caddyfile to proxy traffic from your domain to a local application, and verify that HTTPS is working end to end. As a demo backend, we'll run a simple Python HTTP server on port 8080 — you can swap this for any application (Node.js, Flask, Docker container, etc.) once you understand the pattern.
Step 1 — Update Your System and Open Firewall Ports
Before installing any packages, update the package index to ensure you get the latest versions. Then open ports 80 and 443 so Caddy can complete the ACME HTTP-01 challenge and serve HTTPS traffic.
bashsudo apt update && sudo apt upgrade -y
Next, allow the required ports through UFW:
bashsudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
Expected output:
Status: active
To Action From
-- ------ ----
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
Note
Port 80 must remain open even after Caddy is running. Caddy uses it to redirect HTTP to HTTPS and may use it for certificate renewal challenges. Do not block it with your firewall.
If you're using Raff's built-in firewall panel, add inbound rules for TCP 80 and TCP 443 from any source (0.0.0.0/0) in addition to UFW.
Step 2 — Install Caddy from the Official Repository
Caddy publishes a signed APT repository. Installing from it ensures you receive automatic updates via apt upgrade.
Add the Caddy GPG key and repository:
bashsudo 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
Update the package index and install Caddy:
bashsudo apt update
sudo apt install caddy -y
Verify the installation:
bashcaddy version
Expected output (version number will vary):
v2.9.1 h1:OEYiZ7DbCzAIVsbHrai0/6hPyo6Fjnr97tI/Xa5MXEM=
The systemd service is created and enabled automatically. Check its status:
bashsudo systemctl status caddy
You should see active (running). Caddy starts with a default configuration that serves a placeholder page on port 80.
Tip
A common mistake here is installing Caddy from the Ubuntu default repository (apt install caddy without adding the upstream source). The Ubuntu-packaged version is often several major versions behind and may not support current Caddyfile syntax. Always use the official repository.
Step 3 — Start a Backend Application to Proxy
For this tutorial, we'll use Python's built-in HTTP server as a stand-in backend running on port 8080. In production, this would be your Node.js app, Flask API, Gitea instance, or any process listening on a local port.
Open a second terminal (or use tmux) and run:
bashpython3 -m http.server 8080 --bind 127.0.0.1
This starts an HTTP server on 127.0.0.1:8080 — bound to localhost only so it's not publicly accessible without going through Caddy. Verify it responds:
bashcurl http://127.0.0.1:8080
You should see an HTML directory listing. Leave this running and return to your main terminal.
Note
In a real deployment, your application runs as a systemd service or Docker container. The 127.0.0.1:<port> pattern is the same — Caddy proxies from the public internet to your local process.
Step 4 — Write the Caddyfile
Caddy reads its configuration from /etc/caddy/Caddyfile. The default file contains a placeholder — replace it with your reverse proxy configuration.
Open the file with your editor:
bashsudo nano /etc/caddy/Caddyfile
Replace the entire contents with the following, substituting your-domain.com with your actual domain:
your-domain.com {
reverse_proxy 127.0.0.1:8080
}
That is the complete configuration. Caddy reads the domain name, automatically obtains a Let's Encrypt certificate for it, redirects HTTP to HTTPS, and proxies all requests to port 8080.
Save and close the file (Ctrl+O, Enter, Ctrl+X in nano).
Validate the Caddyfile syntax before reloading:
bashcaddy validate --config /etc/caddy/Caddyfile
Expected output:
Valid configuration
If you see any errors, they will include the line number and a description of the problem. The most common error is a typo in the domain name or a missing closing brace.
Step 5 — Reload Caddy and Verify HTTPS
Reload Caddy to apply the new configuration without dropping existing connections:
bashsudo systemctl reload caddy
Watch the journal to confirm certificate provisioning succeeds:
bashsudo journalctl -u caddy -f --no-pager
Within 30 seconds you should see lines containing:
certificate obtained successfully
serving TLS
Press Ctrl+C to stop following the log.
Now test HTTPS from your local machine:
bashcurl -I https://your-domain.com
Expected output:
HTTP/2 200
server: Caddy
content-type: text/html; charset=utf-8
The HTTP/2 200 confirms Caddy is serving your domain over TLS. Open https://your-domain.com in a browser — you should see the Python directory listing served over a green padlock connection.
Tip
If certificate provisioning fails, the most common causes are: (1) your A record hasn't propagated yet — wait 5 minutes and reload; (2) port 80 is blocked on your firewall — Caddy needs it open for the ACME challenge; (3) your domain resolves to a private IP — Let's Encrypt cannot validate private IPs.
Step 6 — Configure Caddy to Start on Boot and Test Persistence
Caddy's systemd service is enabled by default after installation. Confirm it is set to start automatically:
bashsudo systemctl is-enabled caddy
Expected output: enabled
Test a full restart to ensure the configuration survives a reboot:
bashsudo systemctl restart caddy
sudo systemctl status caddy
Both the active (running) status and your HTTPS endpoint should work immediately after restart. Caddy stores its certificates in /var/lib/caddy/.local/share/caddy/certificates/ and loads them on startup — no re-issuance needed unless the certificate is near expiry (Caddy renews automatically 30 days before expiry).
Step 7 — Add a Second Domain (Optional)
One of Caddy's strengths is hosting multiple applications on a single VM with no extra configuration. Each site block in the Caddyfile gets its own certificate.
Edit /etc/caddy/Caddyfile again:
bashsudo nano /etc/caddy/Caddyfile
Add a second site block:
your-domain.com {
reverse_proxy 127.0.0.1:8080
}
app2.your-domain.com {
reverse_proxy 127.0.0.1:3000
}
Validate and reload:
bashcaddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
Caddy will obtain a certificate for app2.your-domain.com automatically within seconds of reload, provided the DNS record exists and points to your VM.
Conclusion
You now have Caddy running as a reverse proxy on Ubuntu 24.04 on your Raff VM, serving traffic over HTTPS with automatic certificate management. The setup you completed includes a firewall-hardened base, a systemd-managed Caddy service, and a Caddyfile that proxies requests from your domain to a local application — all without Certbot, manual certificate renewal, or complex Nginx location block syntax.
As next steps, consider:
- Securing your backend further — bind your application to
127.0.0.1only (as shown in this tutorial) and ensure no other port is exposed publicly - Adding logging — Caddy supports structured JSON access logs per site with the
logdirective; useful for monitoring request patterns - Proxying a Docker container — replace
127.0.0.1:8080with the container's published port; Caddy works identically whether the backend is a raw process or a container
This tutorial was tested by our team on a Raff Tier 2 VM (1 vCPU, 2 GB RAM) running Ubuntu 24.04 LTS. If you're deploying a production application and need more consistent CPU performance for TLS termination under load, a Raff CPU-Optimized VM starting at $9.99/month provides dedicated vCPU allocation.

