Caddy is another reverse-proxy manager service, similar to Nginx Proxy Manager. However, Caddy does not offer a web frontend admin panel; all configuration is done through a text file. While somewhat more difficult to configure (emphasis “somewhat”; the Caddyfile is very easy to create), it runs with much less resources than Nginx Proxy Manager and therefore may be better suited to resource-constrained environments.
See also
- Caddy documentation: https://caddyserver.com/docs/
Installation
When installing with docker compose:
services:
caddy:
container_name: caddy
image: caddy
restart: unless-stopped
ports:
- 80:80
- 443:443
- 443:443/udp
volumes:
- ./config:/etc/caddyInstallation with extra modules
Caddy has lots of optional modules to provide extra features. If you’re using the docker installation mode, you’ll need to build a custom docker image with the modules compiled in. See Custom images (the example in that page is adding a module to Caddy).
See also
- Caddy module list: https://caddyserver.com/docs/modules
- Reddit post suggesting the custom image method: https://www.reddit.com/r/selfhosted/comments/15lx527/comment/jvf82ht/
Configuration
Create a file named Caddyfile in the ./config volume bind-mount. Leaving it blank will create no proxies. I created mine with the following global settings:
{
admin off
email misha@example.com
}
Adding a proxy
To add a proxy, add on to the Caddyfile as shown. This example proxies traffic incoming to tofk.staging.example.com to the tofk host (which is a Docker container running under that hostname):
tofk.staging.example.com {
reverse_proxy tofk
}
HTTPS Certificate Management
If your server is reachable via the open internet, Caddy will automatically go fetch a LetsEncrypt certificate for you by default. The above snippet just… instantly worked over HTTPS.
Access rules
To add access rules to a Caddyfile, it is easiest to use “snippets” which can then be re-imported elsewhere in the file. The below example blocks access from any IP address not in the 192.168.37.0/24 range:
# global settings
{
admin off
email misha@example.com
}
# define an access rule snippet
(vpn-only) {
@blocked not remote_ip 192.168.37.0/24
respond @blocked "<h1>Access Denied</h1>" 403
}
# use that rule in a proxy server
email.priv.example.com {
import vpn-only
reverse_proxy stalwart:443
}
See also
Rate limiting
You’ll need to build with a custom module (Installation with extra modules) with the github.com/mholt/caddy-ratelimit module for this example. Once the custom image is built, use a snippet to define rate limiters:
# global settings
{
admin off
email misha@example.com
}
# define a rate limiter snippet to reuse
(ratelimit) {
rate_limit {
distributed
zone everyone {
key static
events 100
window 5s
}
zone individual {
key {remote_host}
events 10
window 1s
}
log_key
}
}
# use this rule in a reverse proxy server
tofk.example.com {
import ratelimit
reverse_proxy tofk
}
This example will applies two limiters:
everyone, a static ratelimiter which is applied to all traffic inbound from all origins. All originating IP’s are collectively limited to 100 requests in 5 seconds.individual, a non-static ratelimiter which is applied individually to each origin. Each originating IP is limited to 10 events per second.
The distributed key indicates that this limiter should apply to all sites served by Caddy — that is, tofk.example.com and somethingelse.example.com are both in the same rate limiting group.
See also
Disable upstream TLS verification
If the upstream (“internal”) host self-signs its HTTPS certificate and can only be accessed via HTTPS (not plain HTTP):
stalwart.priv.example.com {
reverse_proxy https://stalwart:443 {
transport http {
tls_insecure_skip_verify
}
}
}
Using the PROXY protocol
Some services want to speak the PROXY protocol, which essentially passes more information to the upstream service about what client is accessing it:
stalwart.priv.example.com {
reverse_proxy https://stalwart:443 {
transport http {
proxy_protocol v2
}
}
}