Once a site runs on Caddy, the next jobs are usually the same: pick www or the bare domain, redirect old URLs, compress responses, add security headers and tell browsers how long to cache files. Each of these is one or two lines in a Caddyfile. This guide collects working recipes you can paste into your site blocks.
Before you start
- A Linux VPS with Caddy running as a service: Install Caddy web server with automatic HTTPS.
- A working site block in
/etc/caddy/Caddyfile, either serving files or proxying an app. - DNS records for every hostname you redirect from, including
www, so Caddy can get certificates for them.
After every change in this guide, validate and reload:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddyA failed reload leaves the old config running, so a typo will not take your site down.
Redirect www to the bare domain (or the reverse)
Give the hostname you do not want its own small site block that only redirects:
www.example.com {
redir https://example.com{uri} permanent
}
example.com {
root * /var/www/example.com
file_server
}{uri} keeps the path and query string, so www.example.com/blog?page=2 goes to example.com/blog?page=2. permanent sends a 301. To prefer www, swap the two hostnames.
Caddy already redirects HTTP to HTTPS for every site with a real hostname, so you do not need a separate rule for that.
Redirect single pages and old paths
redir inside a site block takes an optional path matcher first:
example.com {
redir /old-page /new-page permanent
redir /shop /store/ 301
redir /docs/* /help/{http.request.uri.path.1} permanent
root * /var/www/example.com
file_server
}- The first two lines move one URL each. You can use
permanentor an explicit status code. - In the third line,
{http.request.uri.path.1}is the second segment of the path, so/docs/installgoes to/help/install.
For many redirects, keep them in their own file and import it into the site block, so the main config stays short:
example.com {
import /etc/caddy/redirects/example.com.caddy
root * /var/www/example.com
file_server
}The imported file holds one redir line per old URL, in the same format as above. Exact paths are cheap to match, so a few hundred lines is fine.
Move a whole old domain
old-example.com, www.old-example.com {
redir https://example.com{uri} permanent
}Keep this block for at least several months so search engines and old links follow the move.
Compression with gzip and zstd
example.com {
encode zstd gzip
root * /var/www/example.com
file_server
}Caddy picks the best encoding the browser accepts, in the order you list. It only compresses text-like types and skips responses that are already compressed, such as JPEG or MP4. To serve pre-compressed files you built ahead of time, such as app.js.gz next to app.js, add precompressed to file_server:
file_server {
precompressed zstd br gzip
}Security headers
The header directive sets response headers. Group them in one block:
example.com {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
-Server
}
root * /var/www/example.com
file_server
}The -Server line removes the Server: Caddy header.
Warning: HSTS tells browsers to refuse plain HTTP for the whole max-age. Only use includeSubDomains if every subdomain has working HTTPS. Start with a short value such as max-age=300 while you test.
A Content-Security-Policy header gives strong protection but depends heavily on your site's scripts. Build it in report-only mode first so it does not break pages.
Browser cache rules
Use matchers to send different Cache-Control values for different files. Versioned assets, whose file names change when they change, can be cached for a long time. HTML should be checked on every visit.
example.com {
@static path *.css *.js *.woff2 *.png *.jpg *.jpeg *.webp *.svg *.ico
header @static Cache-Control "public, max-age=31536000, immutable"
@html path / *.html
header @html Cache-Control "no-cache"
root * /var/www/example.com
file_server
}Note: Only use immutable and a one-year max-age for files whose names include a hash or version. If you overwrite style.css in place, use a shorter time such as max-age=86400.
Keep larger configs tidy with handle
handle blocks are mutually exclusive: the first one that matches wins, and the rest are skipped. This keeps API and static rules apart:
example.com {
encode zstd gzip
handle /api/* {
header Cache-Control "no-store"
reverse_proxy 127.0.0.1:8000
}
handle /downloads/* {
root * /srv/files
file_server browse
}
handle {
root * /var/www/example.com
try_files {path} /index.html
file_server
}
}The last handle has no matcher, so it catches everything else. Here it also sends unknown paths to index.html, which is what single-page apps need.
For snippets you repeat on every site, define them once at the top of the file and import them:
(security) {
header {
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
}
example.com {
import security
root * /var/www/example.com
file_server
}Check it works
Test a redirect without following it:
curl -I https://www.example.com/blogExpect 301 and location: https://example.com/blog. Check compression and headers:
curl -sI -H "Accept-Encoding: zstd, gzip" https://example.com/ | grep -iE "content-encoding|strict-transport|cache-control"You should see content-encoding: zstd (or gzip) and the headers you set.
Troubleshooting
- Redirect loop: a
redirsends a path to a URL that matches the same rule, for exampleredir /blog* /blog/. Make the matcher exact, or put the rule inside ahandlethat excludes the target. - Headers missing on proxied responses: the backend may set its own value and Caddy's header is applied first. Use
header >Name valueto defer the change until the response comes back, orheader_downinsidereverse_proxy. - No
content-encodingheader: the response is too small or not a text type, or your test sent noAccept-Encodingheader. - Certificate errors for www: the
wwwrecord is missing in DNS, so Caddy cannot validate it. See Fix Caddy certificate errors and ACME failures.
Next steps
- Put apps behind the same Caddy server: Use Caddy as a reverse proxy for apps.
- Understand which DNS records you need for each hostname: DNS records explained.
