Introduction
Caddy is a modern web server that makes HTTPS automatic. When you point a domain at a Caddy server, it obtains a TLS certificate from Let's Encrypt, configures HTTPS, redirects HTTP traffic, and renews the certificate before it expires — all without you writing a single line of SSL configuration. This is Caddy's defining feature and the reason it has grown rapidly as an alternative to Nginx and Apache.
Beyond automatic HTTPS, Caddy offers a clean configuration syntax (the Caddyfile), built-in reverse proxy with WebSocket support, HTTP/2 and HTTP/3 by default, automatic OCSP stapling, and a JSON API for dynamic configuration. It is written in Go, compiles to a single binary with no external dependencies, and uses approximately 20-50 MB of RAM for typical workloads.
In this tutorial, you will install Caddy from the official repository, serve a static website with automatic HTTPS, configure Caddy as a reverse proxy for a backend application, and learn the essential Caddyfile syntax. If you have followed our Nginx + Let's Encrypt tutorial, you will notice that Caddy achieves the same result in a fraction of the configuration.
Step 1 — Install Caddy
Add the official Caddy APT repository. Install the prerequisites and import the GPG key:
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 -y caddy
Caddy starts automatically after installation. Verify the service is running:
bashsudo systemctl status caddy
You should see active (running) in the output.
Check the installed version:
bashcaddy version
Expected output:
v2.9.x h:...
Caddy is now serving a default placeholder page on port 80. Open your browser and navigate to http://your_server_ip — you should see the Caddy default page confirming the installation works.
Step 2 — Configure the Firewall
Caddy needs ports 80 (HTTP) and 443 (HTTPS) open. Port 80 is required even with HTTPS because Caddy uses it for the ACME HTTP-01 challenge when obtaining certificates and for redirecting HTTP visitors to HTTPS.
If you have UFW configured:
bashsudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Verify the rules:
bashsudo ufw status
You should see both ports listed as ALLOW.
Note
If you previously set up Nginx with sudo ufw allow 'Nginx Full', those rules also cover ports 80 and 443. You can keep both or remove the Nginx rules if you are switching entirely to Caddy.
Step 3 — Serve a Static Website with Automatic HTTPS
This is where Caddy shines. You will configure a domain to serve a static site with HTTPS — and the entire configuration is three lines.
Create a document root for your site:
bashsudo mkdir -p /var/www/your_domain
Create a simple HTML page:
bashsudo nano /var/www/your_domain/index.html
Add the following:
html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Welcome to your_domain</title>
</head>
<body>
<h1>Caddy is serving this page over HTTPS!</h1>
<p>Automatic TLS certificate from Let's Encrypt.</p>
</body>
</html>
Save and close the file.
Now edit the Caddyfile — Caddy's main configuration:
bashsudo nano /etc/caddy/Caddyfile
Replace the entire contents with:
your_domain {
root * /var/www/your_domain
file_server
}
That is the complete configuration. Replace your_domain with your actual domain name.
Reload Caddy to apply the changes:
bashsudo systemctl reload caddy
Within seconds, Caddy obtains a TLS certificate from Let's Encrypt, configures HTTPS, and starts serving your site. Open https://your_domain in your browser — you should see your HTML page with a padlock icon confirming HTTPS.
Visit http://your_domain (without HTTPS) — Caddy automatically redirects to HTTPS. No additional redirect rules needed.
Warning
Automatic HTTPS only works if your domain's DNS A record points to your server's public IP and ports 80 and 443 are open. If the certificate fails to issue, check sudo journalctl -u caddy -e for error details.
Compare this to the equivalent Nginx setup, which requires installing Certbot, running a certificate command, editing the server block, configuring redirects, and setting up a renewal timer. Caddy handles all of that with three lines in the Caddyfile.
Step 4 — Configure Caddy as a Reverse Proxy
Caddy is excellent as a reverse proxy for backend applications like Node.js, Uptime Kuma, n8n, or Gitea. It handles HTTPS termination automatically while proxying requests to your application.
Open the Caddyfile:
bashsudo nano /etc/caddy/Caddyfile
Add a reverse proxy block for a backend application running on port 3000:
app.your_domain {
reverse_proxy localhost:3000
}
That is the entire reverse proxy configuration. Caddy will obtain a separate TLS certificate for app.your_domain, proxy all requests to localhost:3000, and handle HTTPS, HTTP/2, WebSocket upgrades, and header forwarding automatically.
For multiple backend services, add more blocks:
your_domain {
root * /var/www/your_domain
file_server
}
app.your_domain {
reverse_proxy localhost:3000
}
monitor.your_domain {
reverse_proxy localhost:3001
}
git.your_domain {
reverse_proxy localhost:3002
}
Each domain gets its own TLS certificate automatically. Reload Caddy:
bashsudo systemctl reload caddy
Caddy handles the complexity of managing multiple certificates, renewals, and OCSP stapling for all domains simultaneously. Adding a new service is as simple as adding a new block and reloading — no separate Certbot commands needed.
Tip
Caddy supports WebSocket proxying out of the box. Applications like Open WebUI and Uptime Kuma that use WebSocket connections work without any additional configuration — unlike Nginx, which requires explicit proxy_set_header Upgrade and Connection directives.
Step 5 — Serve Multiple Sites
The Caddyfile supports multiple site blocks. Each block defines a separate site with its own configuration. Caddy manages TLS certificates for all domains automatically.
Here is an example Caddyfile serving a static site, a reverse proxy, and a file browser:
your_domain {
root * /var/www/your_domain
file_server
}
api.your_domain {
reverse_proxy localhost:8080
}
files.your_domain {
root * /var/www/shared-files
file_server browse
}
The file_server browse directive enables a directory listing UI — useful for sharing files without a dedicated application.
Validate the configuration before reloading:
bashcaddy validate --config /etc/caddy/Caddyfile
If valid, reload:
bashsudo systemctl reload caddy
Step 6 — Add Security Headers and Logging
For production sites, add security headers and enable access logging. Open the Caddyfile:
bashsudo nano /etc/caddy/Caddyfile
Add headers and logging inside your site block:
your_domain {
root * /var/www/your_domain
file_server
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
}
log {
output file /var/log/caddy/access.log
format json
}
}
Create the log directory:
bashsudo mkdir -p /var/log/caddy
sudo chown caddy:caddy /var/log/caddy
Reload Caddy:
bashsudo systemctl reload caddy
Verify the security headers:
bashcurl -I https://your_domain
You should see strict-transport-security, x-content-type-options, x-frame-options, and referrer-policy in the response headers.
The JSON-formatted access log at /var/log/caddy/access.log records every request with timestamps, status codes, response times, and client information. This is useful for debugging and monitoring traffic patterns.
Step 7 — Manage and Update Caddy
Check the Caddy service status:
bashsudo systemctl status caddy
View Caddy logs:
bashsudo journalctl -u caddy --since "1 hour ago"
Follow logs in real time:
bashsudo journalctl -u caddy -f
List all TLS certificates managed by Caddy:
bashcaddy list-modules | grep tls
Update Caddy when new versions are released:
bashsudo apt update
sudo apt upgrade caddy
Since Caddy is installed from its official APT repository, updates are handled through the standard package manager alongside your other system packages.
Essential Caddy commands:
bashsudo systemctl start caddy # Start Caddy
sudo systemctl stop caddy # Stop Caddy
sudo systemctl reload caddy # Reload configuration (no downtime)
sudo systemctl restart caddy # Full restart
caddy validate --config /etc/caddy/Caddyfile # Validate configuration
caddy version # Check installed version
Configuration files:
/etc/caddy/Caddyfile— Main configuration file/var/lib/caddy/.local/share/caddy/— TLS certificate storage/var/log/caddy/— Access logs (if configured)
Conclusion
You have installed Caddy on your Raff Ubuntu 24.04 VM, served a static website with automatic HTTPS, configured reverse proxy for backend applications, added security headers, and enabled access logging. Your sites are encrypted, HTTP/2 and HTTP/3 enabled, and certificates renew automatically — all without touching a single TLS configuration line.
From here, you can:
- Proxy your Docker-based applications like Uptime Kuma, n8n, Portainer, or Gitea through Caddy for automatic HTTPS
- Replace Nginx entirely if you prefer Caddy's simpler configuration syntax
- Use Caddy's built-in load balancing for high-availability setups with multiple backend servers
- Configure Caddy's API for dynamic configuration changes without reloading
- Protect your Caddy server with fail2ban and UFW for comprehensive security
Caddy uses approximately 20-50 MB of RAM, which means it fits on any Raff VM tier including the $3.99/month entry tier. Combined with NVMe SSD storage for fast static file delivery and unmetered bandwidth for unlimited HTTPS traffic, Caddy on a Raff VM delivers production-grade web serving at a fraction of the cost and complexity of traditional setups.
This tutorial was tested by our security engineering team on a Raff CPU-Optimized Tier 2 VM with Caddy 2.9.

