1. Architecture Overview
Caddy is a modular, extensible server platform written in Go. At its core, Caddy is a configuration manager: it loads a JSON configuration document, provisions modules according to that configuration, starts the configured applications, and manages their lifecycle. The standard distribution bundles modules for HTTP serving, TLS certificate management, PKI (certificate authority), logging, metrics, and events.
Architecture Diagram
┌──────────────────────────────────────────────────┐
│ caddy process │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Admin API (localhost:2019) │ │
│ │ /load /config/ /stop /metrics │ │
│ └──────────┬───────────────────────────────┘ │
│ │ config changes │
│ ┌──────────▼───────────────────────────────┐ │
│ │ Config Manager (caddy.go) │ │
│ │ Load → Validate → Provision → Start │ │
│ │ Context, Replacer, UsagePool, Modules │ │
│ └──────────┬───────────────────────────────┘ │
│ │ provisions apps │
│ ┌──────────▼──────────────────┐ ┌────────────┐ │
│ │ HTTP App (caddyhttp) │ │ TLS App │ │
│ │ Servers → Routes → Handlers│ │ (caddytls) │ │
│ │ Matchers, Subroutes │ │ CertMagic │ │
│ │ AutoHTTPS, Metrics │ │ ACME/ZeroSSL│ │
│ │ HTTP/1.1, HTTP/2, HTTP/3 │ │ On-demand │ │
│ └──────────┬──────────────────┘ └──────┬─────┘ │
│ │ │ │
│ ┌──────────▼──────────┐ ┌─────────────▼─────┐ │
│ │ Logging App │ │ PKI App │ │
│ │ (modules/logging) │ │ (caddypki) │ │
│ │ FileWriter, Net │ │ Local CA, ACME │ │
│ │ Filters, Encoders │ │ Server │ │
│ └─────────────────────┘ └───────────────────┘ │
│ │
│ ┌──────────────────────┐ ┌──────────────────┐ │
│ │ Events App │ │ Metrics App │ │
│ │ (caddyevents) │ │ Prometheus │ │
│ └──────────────────────┘ └──────────────────┘ │
└──────────────────────────────────────────────────┘
Component Inventory
| Component | Path | Role |
|---|---|---|
| Core runtime | caddy.go, context.go, modules.go |
Config lifecycle, module registry, context propagation |
| Admin API | admin.go |
REST API for runtime config management |
| HTTP App | modules/caddyhttp/ |
HTTP server, routing, handlers, matchers, auto-HTTPS |
| TLS App | modules/caddytls/ |
Certificate management, ACME issuers, connection policies |
| PKI App | modules/caddypki/ |
Internal certificate authority, ACME server |
| Logging | modules/logging/ |
Structured logging with encoders, writers, filters |
| Metrics | modules/metrics/ |
Prometheus metrics exposition |
| Events | modules/caddyevents/ |
Event subscription and handler dispatch |
| Caddyfile adapter | caddyconfig/caddyfile/, caddyconfig/httpcaddyfile/ |
Parse Caddyfile → JSON config |
| CLI | cmd/ |
Cobra-based command-line interface |
| File storage | modules/filestorage/ |
Default certificate/key storage on disk |
| Filesystem | modules/caddyfs/ |
Virtual filesystem abstraction |
Technology Stack
| Layer | Technology |
|---|---|
| Language | Go 1.25+ |
| CLI framework | spf13/cobra + pflag |
| HTTP server | Go net/http + golang.org/x/net/http2 |
| HTTP/3 (QUIC) | quic-go/quic-go |
| TLS/ACME | caddyserver/certmagic, mholt/acmez |
| Certificate issuers | Let's Encrypt, ZeroSSL (built-in), smallstep/certificates |
| PKI | smallstep/certificates, go.step.sm/crypto |
| Logging | uber-go/zap |
| Metrics | prometheus/client_golang |
| Tracing | OpenTelemetry (contrib/instrumentation/net/http/otelhttp) |
| Template engine | yuin/goldmark (Markdown), Go text/template |
| Compression | klauspost/compress (gzip, zstd), brotli |
| CEL expressions | google/cel-go |
| DNS | miekg/dns, libdns/libdns |
| Config formats | JSON (native), YAML (BurntSushi/toml, gopkg.in/yaml.v3) |
| PROXY protocol | pires/go-proxyproto |
Data Flow: Request Processing
Client Request
↓
Listener (TCP/QUIC, optional PROXY protocol wrapper)
↓
TLS Handshake (connection policy selection via SNI/ALPN matchers)
↓
HTTP Server (server.go: ServeHTTP)
↓
Route Matching (matchers evaluated in order: host, path, method, header, expression, etc.)
↓
Handler Chain (matched route's handlers execute in order)
↓
Response (with optional response matchers for intercept/handle_response)
Data Flow: Configuration Lifecycle
Config Input (JSON or Caddyfile)
↓
Adapter (if non-JSON: caddyfile adapter converts to JSON)
↓
caddy.Load() → validates JSON, builds Config struct
↓
Context.LoadModulesFromConfig() → instantiates module values
↓
Module.Provision(ctx) → each module sets up internal state
↓
Module.Validate() → optional validation
↓
App.Start() → apps begin serving
↓
[Running — admin API accepts changes → caddy.Load() again]
↓
App.Stop() → graceful shutdown with optional grace period
2. System Requirements
| Requirement | Detail |
|---|---|
| Operating systems | Linux, macOS, Windows, FreeBSD |
| Architectures | amd64, arm64, armv5/v6/v7, s390x, ppc64le, riscv64 |
| Go version (source) | 1.25.0 or later |
| Minimum memory | ~64 MB (increases with number of managed certificates) |
| Ports | 80 (HTTP challenges/redirects), 443 (HTTPS), 443/UDP (HTTP/3), 2019 (admin API) |
| Privileges | cap_net_bind_service on Linux for ports < 1024 |
| DNS | Public DNS resolution required for automatic ACME certificates |
Build Tags
| Tag | Effect |
|---|---|
nobadger |
Exclude BadgerDB storage backend |
nomysql |
Exclude MySQL storage backend |
nopgx |
Exclude PostgreSQL storage backend |
These are set by default in release builds to reduce binary size.
3. Configuration Reference
Caddy's native configuration format is JSON. The Caddyfile is an alternative syntax that is adapted to JSON before being applied. This section documents the JSON structure.
Top-Level Structure
{
"admin": { ... },
"logging": { ... },
"storage": { ... },
"apps": {
"http": { ... },
"tls": { ... },
"pki": { ... },
"events": { ... }
}
}
Admin Configuration (admin)
| Field | Type | Default | Description |
|---|---|---|---|
disabled |
bool | false |
Disable admin endpoint entirely |
listen |
string | localhost:2019 |
Admin API listen address (overridable via CADDY_ADMIN env) |
enforce_origin |
bool | false |
Enforce CORS origin checking |
origins |
[]string | [listen address] |
Allowed origins for CORS |
config.persist |
*bool | true |
Auto-save active config to disk |
config.load |
object | — | Dynamic config loader module (experimental) |
config.load_delay |
duration | — | Delay before loading dynamic config (experimental) |
identity.identifiers |
[]string | — | Server identity names/IPs for remote admin |
identity.issuers |
[]object | ZeroSSL + LE | Certificate issuers for server identity |
remote.listen |
string | :2021 |
Remote admin listener (experimental) |
remote.access_control |
[]object | — | mTLS access control for remote admin (experimental) |
Important
The
CADDY_ADMINenvironment variable overrides the default admin listen address (localhost:2019). This is primarily for package maintainers to set platform-appropriate defaults.
HTTP App Configuration (apps.http)
| Field | Type | Default | Description |
|---|---|---|---|
http_port |
int | 80 |
Port for HTTP (redirects, ACME challenges) |
https_port |
int | 443 |
Port for HTTPS |
grace_period |
duration | 0 (eternal) |
Max time for active connections during shutdown |
shutdown_delay |
duration | 0 |
Delay before shutdown begins (for LB draining) |
servers |
map[string]Server | — | Named server configurations |
metrics |
object | — | Enable HTTP metrics (experimental) |
Server Configuration (apps.http.servers.*)
| Field | Type | Default | Description |
|---|---|---|---|
listen |
[]string | — | Listener addresses (e.g., ":443") |
routes |
[]Route | — | Ordered routing rules |
errors |
object | — | Error handling routes |
named_routes |
map | — | Reusable named route definitions |
listener_wrappers |
[]object | — | Listener middleware (e.g., proxy_protocol, tls) |
protocols |
[]string | ["h1", "h2", "h3"] |
Enabled protocols |
strict_sni_host |
*bool | — | Require TLS SNI to match HTTP Host |
trusted_proxies |
object | — | Trusted proxy IP ranges for X-Forwarded-* |
metrics |
object | — | Per-server metrics configuration |
logs |
object | — | Access log configuration per server |
TLS App Configuration (apps.tls)
| Field | Type | Default | Description |
|---|---|---|---|
certificates |
object | — | Manual certificate loading (file, folder, PEM, storage) |
automation |
object | — | ACME automation policies |
automation.policies |
[]AutomationPolicy | — | Per-subject certificate management policies |
automation.on_demand |
object | — | On-demand TLS configuration |
session_tickets |
object | — | TLS session ticket configuration |
cache |
object | — | Certificate cache options |
ech |
object | — | Encrypted Client Hello configuration |
Automation Policy
| Field | Type | Default | Description |
|---|---|---|---|
subjects |
[]string | — | Hostnames this policy applies to |
issuers |
[]object | ACME (LE + ZeroSSL) | Certificate issuers |
key_type |
string | "" (CA default) |
Key type: ed25519, p256, p384, rsa2048, rsa4096 |
must_staple |
bool | false |
Request OCSP Must-Staple |
renewal_window_ratio |
float | — | Ratio of cert lifetime at which to renew |
storage |
object | — | Per-policy storage module |
on_demand |
bool | false |
Enable on-demand certificate issuance |
Warning
On-demand TLS requires a
permissionmodule (or deprecatedaskendpoint) to prevent abuse. Without permission controls, anyone could trigger certificate issuance for arbitrary domains by connecting to your server.
PKI App Configuration (apps.pki)
| Field | Type | Default | Description |
|---|---|---|---|
certificate_authorities |
map[string]CA | — | Named CA configurations |
certificate_authorities.*.root_common_name |
string | "Caddy Local Authority - <id>" |
Root CA CN |
certificate_authorities.*.intermediate_common_name |
string | "Caddy Local Authority - <id> Intermediate" |
Intermediate CA CN |
certificate_authorities.*.intermediate_lifetime |
duration | — | Intermediate cert validity period |
certificate_authorities.*.install_trust |
bool | true |
Install root into system trust store |
Logging Configuration (logging)
| Field | Type | Default | Description |
|---|---|---|---|
sink |
object | — | Sink log (errors from log system itself) |
logs |
map[string]CustomLog | — | Named log configurations |
logs.default |
CustomLog | stderr, console | The default log |
logs.*.writer |
object | — | Log output (file, net, stderr, stdout) |
logs.*.encoder |
object | — | Log format (json, console, filter, append, journald) |
logs.*.level |
string | "INFO" |
Min log level: DEBUG, INFO, WARN, ERROR, PANIC, FATAL |
logs.*.include |
[]string | — | Logger name prefixes to include |
logs.*.exclude |
[]string | — | Logger name prefixes to exclude |
logs.*.sampling |
object | — | Log sampling configuration |
File Writer Options
| Field | Type | Default | Description |
|---|---|---|---|
filename |
string | — | Log file path |
roll_size_mb |
int | 100 |
Max file size before rolling (MB) |
roll_keep |
int | 10 |
Number of rolled files to keep |
roll_keep_for |
duration | 2160h (90 days) |
Max age of rolled files |
roll_local_time |
bool | false |
Use local time in rolled filenames |
roll_compression |
string | — | Compression for rolled files: gzip, zstd |
mode |
string | 0600 |
File permissions (octal) |
dir_mode |
string | 0700 |
Directory permissions (octal) |
Log Filter Types
| Filter | Module ID | Purpose |
|---|---|---|
| Delete | caddy.logging.encoders.filter.delete |
Remove a field entirely |
| Hash | caddy.logging.encoders.filter.hash |
SHA-256 hash a field value |
| Replace | caddy.logging.encoders.filter.replace |
Replace field with static value |
| IP Mask | caddy.logging.encoders.filter.ip_mask |
Mask IPv4/IPv6 addresses |
| Query | caddy.logging.encoders.filter.query |
Filter/redact query parameters |
| Cookie | caddy.logging.encoders.filter.cookie |
Filter/redact cookies |
| Regexp | caddy.logging.encoders.filter.regexp |
Regex-based field modification |
| Multi-Regexp | caddy.logging.encoders.filter.multi_regexp |
Multiple regex operations on a field |
| Rename | caddy.logging.encoders.filter.rename |
Rename a field |
Environment Variables
| Variable | Purpose | Default |
|---|---|---|
CADDY_ADMIN |
Override default admin listen address | localhost:2019 |
HOME / USERPROFILE |
Home directory for data/config paths | System default |
XDG_DATA_HOME |
Override data directory | ~/.local/share |
XDG_CONFIG_HOME |
Override config directory | ~/.config |
XDG_CACHE_HOME |
Override cache directory | ~/.cache |
NO_COLOR |
Disable colored console output | — |
USERAGENT |
Prepend custom string to ACME User-Agent | — |
Note
Caddy follows the XDG Base Directory Specification on Linux. On Windows, it uses
%AppData%and%LocalAppData%. On macOS, it uses~/Library/Application Support.
CLI Commands Reference
| Command | Usage | Description |
|---|---|---|
run |
--config <path> [--adapter <name>] [--envfile <path>] [--environ] [--resume] [--watch] [--pidfile <file>] |
Start in foreground (daemon mode) |
start |
[--config <path>] [--adapter <name>] [--envfile <path>] [--watch] [--pidfile <file>] |
Start in background |
stop |
[--config <path>] [--adapter <name>] [--address <interface>] |
Gracefully stop running instance |
reload |
--config <path> [--adapter <name>] [--address <interface>] [--force] |
Reload configuration |
adapt |
--config <path> [--adapter <name>] [--pretty] [--validate] [--envfile <path>] |
Convert config to JSON |
validate |
--config <path> [--adapter <name>] [--envfile <path>] |
Validate config without starting |
fmt |
[--overwrite] [--diff] [<path>] |
Format Caddyfile |
version |
— | Print version |
build-info |
— | Print Go build info |
list-modules |
[--packages] [--versions] [--skip-standard] [--json] |
List installed modules |
environ |
[--envfile <path>] |
Print environment |
storage |
export/import --config <path> --output/--input <path> |
Export/import storage (experimental) |
upgrade |
[--keep-backup] |
Self-upgrade binary (experimental) |
add-package |
<package[@version]...> [--keep-backup] |
Add modules (experimental) |
remove-package |
<packages...> [--keep-backup] |
Remove modules (experimental) |
manpage |
--directory <path> |
Generate man pages |
completion |
[bash|zsh|fish|powershell] |
Generate shell completions |
Additional commands registered by standard modules:
| Command | Module | Description |
|---|---|---|
file-server |
caddyhttp/fileserver |
Quick static file server |
reverse-proxy |
caddyhttp/reverseproxy |
Quick reverse proxy |
respond |
caddyhttp/staticresp |
Quick static response server |
trust |
caddypki |
Install CA root cert to trust store |
untrust |
caddypki |
Remove CA root cert from trust store |
hash-password |
caddyhttp/caddyauth |
Hash a password for basicauth |
4. Deployment & Scaling
systemd Deployment (Linux)
The Debian package installs two systemd unit files:
caddy.service— runs Caddy with a Caddyfile (/etc/caddy/Caddyfile)caddy-api.service— runs Caddy with no initial config (API-driven configuration)
sudo systemctl enable --now caddy
Configuration path: /etc/caddy/Caddyfile
Data directory: /var/lib/caddy/.local/share/caddy/ (certificates, keys)
Log output: journal (journalctl -u caddy)
Docker Deployment
version: "3.9"
services:
caddy:
image: caddy:latest
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
restart: unless-stopped
volumes:
caddy_data:
caddy_config:
Important
Always persist
/dataand/configvolumes./datacontains TLS certificates and private keys — losing this volume triggers re-issuance, which may hit ACME rate limits.
Graceful Shutdown & Zero-Downtime Reloads
Caddy supports two-phase shutdown:
-
Shutdown Delay (
shutdown_delay): Caddy continues accepting new connections while downstream load balancers drain this instance. The placeholders{http.shutting_down}and{http.time_until_shutdown}are available for health check endpoints during this phase. -
Grace Period (
grace_period): No new connections accepted. Idle connections close immediately. Active connections are given the full grace period to complete. After the grace period, connections are forcefully terminated.
Config reload (caddy reload) is seamless — the new config is loaded, validated, and swapped in atomically. Active connections continue on the old config until they complete.
Tip
For zero-downtime deployments behind a load balancer, set
shutdown_delayto a value slightly longer than your load balancer's health check interval. This ensures the LB stops routing traffic to this instance before the grace period begins.
Windows Service
Caddy can run as a Windows service. The service_windows.go module implements the Windows Service Control Manager interface. Caddy registers as a service handler and responds to stop/shutdown signals from the SCM.
Port Binding
On Linux, binding to ports below 1024 requires either root or the cap_net_bind_service capability:
sudo setcap cap_net_bind_service=+ep /usr/bin/caddy
The package installation handles this automatically. A helper script at cmd/caddy/setcap.sh is provided in the source.
Resource Limits
Caddy automatically adjusts resource limits on startup:
- GOMAXPROCS: Set to match Linux container CPU quota via
uber-go/automaxprocs - GOMEMLIMIT: Set to match container/system memory via
KimMachineGun/automemlimit(reads cgroup limits, falls back to system memory)
These ensure Caddy behaves well in containerized environments without manual tuning.
5. Operations
Admin API
The admin API listens on localhost:2019 by default and provides full control over the running Caddy instance.
Core endpoints:
| Method | Path | Description |
|---|---|---|
| POST | /load |
Load a complete new configuration |
| GET | /config/[path] |
Read config at path |
| POST | /config/[path] |
Set config at path |
| PUT | /config/[path] |
Create or replace config at path |
| PATCH | /config/[path] |
Replace config at path |
| DELETE | /config/[path] |
Delete config at path |
| POST | /stop |
Gracefully stop Caddy |
| GET | /reverse_proxy/upstreams |
List reverse proxy upstream status |
| GET | /pki/ca/<id> |
Get CA info |
| GET | /pki/ca/<id>/certificates |
Get CA certificates |
Security:
- Local endpoint (plaintext): Protected by origin checking. Only
localhostorigins are allowed by default. CORS headers enforce same-origin policy. WebSocket connections are rejected. Content-Type must beapplication/jsonfor request bodies. - Remote endpoint (mTLS, experimental): Requires identity configuration with TLS certificates. Access controlled via public key allowlists and path/method permissions.
- The
If-Matchheader supports ETag-based conditional updates to prevent config race conditions.
Note
The admin API rejects requests with non-
localhostorigins by default. If accessing from a different host, configure theoriginslist in the admin config.
Config Autosave
By default, Caddy saves its active configuration to autosave.json in the config directory after every successful load. This enables the --resume flag to restore the last known-good configuration on restart. Disable with admin.config.persist: false.
Monitoring with Prometheus
Caddy exposes Prometheus metrics at the admin API endpoint. Enable per-server HTTP metrics:
{
"apps": {
"http": {
"metrics": {},
"servers": {
"srv0": {
"metrics": {}
}
}
}
}
}
Or via Caddyfile:
{
servers {
metrics
}
}
Scrape from http://localhost:2019/metrics.
Tip
Use the
metricsglobal option or themetricsCaddyfile directive to expose per-host HTTP metrics. Combine with OpenTelemetry exporters (OTLP) for distributed tracing.
OpenTelemetry Tracing
Caddy supports distributed tracing via OpenTelemetry:
example.com {
tracing {
span my-span
}
reverse_proxy backend:8080
}
The tracing module uses go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp and supports automatic export via OTEL_EXPORTER_* environment variables. Propagation is auto-configured via go.opentelemetry.io/contrib/propagators/autoprop.
Health Checks
For load balancer integration, use a dedicated health endpoint:
:8080 {
respond /health 200
}
During shutdown (when shutdown_delay is configured), use the {http.shutting_down} placeholder to signal health check failures:
@health path /health
handle @health {
@shuttingDown expression {http.shutting_down}
respond @shuttingDown 503
respond 200
}
Signal Handling
| Signal | Behavior |
|---|---|
| SIGINT / SIGTERM | Graceful shutdown |
| SIGUSR1 | Reload config from the same source file |
On Windows, Caddy handles SCM stop/shutdown commands.
Storage
Caddy stores certificates, OCSP staples, and other assets in a configurable storage backend. The default is file_system:
{
"storage": {
"module": "file_system",
"root": "/var/lib/caddy"
}
}
The caddy storage export and caddy storage import commands allow migrating storage contents between instances or backends as tar archives.
Warning
The storage directory contains TLS private keys. Ensure proper file permissions (default:
0600for files,0700for directories). Back up the storage directory regularly.
Certificate Lifecycle
- Automatic issuance: Caddy provisions certificates for qualified hostnames at startup using ACME (Let's Encrypt primary, ZeroSSL fallback).
- Renewal: Certificates are renewed before expiry based on the
renewal_window_ratio(default: start renewing when less than 1/3 of lifetime remains). - On-demand TLS: Certificates can be obtained on first TLS handshake for the domain, controlled by a permission module.
- OCSP stapling: Automatic OCSP staple fetching and caching.
- Storage: All certificate material is persisted to the configured storage backend.
6. Performance
HTTP/3 (QUIC)
HTTP/3 is enabled by default on HTTPS servers. Caddy creates a QUIC listener on the same port as HTTPS (typically 443/UDP) and advertises HTTP/3 availability via the Alt-Svc response header.
Disable for specific servers:
{
"apps": {
"http": {
"servers": {
"srv0": {
"protocols": ["h1", "h2"]
}
}
}
}
}
Compression
Caddy supports response compression with gzip, Zstandard (zstd), and Brotli. The encode handler negotiates the best encoding based on the client's Accept-Encoding header.
Pre-compressed files (.gz, .br, .zst) are served when available, avoiding runtime compression overhead.
Connection Pooling
The reverse proxy maintains connection pools to upstream backends with configurable:
- Maximum idle connections
- Idle timeout
- Response header timeout
- Buffer sizes (read/write)
- Stream buffer size for WebSocket/streaming
TLS Session Tickets
Caddy supports TLS session ticket rotation for improved TLS handshake performance. The SessionTicketService manages automatic key rotation with configurable intervals and key lifetime. A distributed STEK (Session Ticket Encryption Key) provider is available for multi-instance deployments.
7. Security
Automatic HTTPS
Caddy obtains and manages TLS certificates automatically for all qualifying hostnames. This is the primary security feature: HTTPS by default, with no configuration required.
Challenge types supported:
| Challenge | Port | Use Case |
|---|---|---|
| HTTP-01 | 80 | Default; requires port 80 reachable |
| TLS-ALPN-01 | 443 | Used when port 80 is unavailable |
| DNS-01 | — | Private networks, wildcard certs (requires DNS provider plugin) |
Caution
By using Caddy, you automatically agree to the ACME CA's terms of service. This is intentional for non-interactive operation. If you need to review terms before agreeing, configure issuers manually.
Client Certificate Authentication (mTLS)
Caddy supports mutual TLS with fine-grained trust configuration:
- Trust pools: Inline PEM, file-based, folder-based, PKI root/intermediate, storage-backed, system, HTTP-fetched, or combined
- Client auth modes:
require,request,require_and_verify,verify_if_given - Leaf certificate verification: Verify specific client certificates by fingerprint via file, folder, PEM, or storage loaders
- Custom verifiers: External certificate verification modules
Encrypted Client Hello (ECH)
Caddy supports ECH (Encrypted Client Hello) for enhanced privacy. ECH encrypts the SNI in the TLS ClientHello, preventing network observers from seeing which hostname the client is connecting to. Configuration manages ECH key generation, rotation, and DNS publication.
Warning
ECH is an advanced privacy feature. It requires DNS HTTPS record publication and client support. Misconfiguration can prevent clients from connecting.
Admin API Security
The local admin endpoint uses multiple security layers:
- Origin enforcement: Validates
Originheader matches expected values - Host validation: Rejects requests with non-matching
Hostheaders (except for Unix/fd sockets) - Content-Type validation: Requires
application/jsonfor request bodies - CORS prevention: Blocks
no-corsmode requests - WebSocket rejection: WebSocket upgrade requests are denied
- Null origin rejection: Requests with
Origin: nullare rejected
The remote admin endpoint (experimental) adds:
- Mutual TLS: Client certificates required
- Public key authorization: Access controlled by base64-encoded DER public keys
- Path/method permissions: Fine-grained API access control
PROXY Protocol
Caddy supports HAProxy's PROXY protocol (v1 and v2) via the proxy_protocol listener wrapper, allowing it to receive the real client IP when deployed behind a layer 4 load balancer:
{
servers {
listener_wrappers {
proxy_protocol {
timeout 5s
allow 10.0.0.0/8
}
tls
}
}
}
Important
The
proxy_protocollistener wrapper must be placed before thetlswrapper in the listener wrapper chain, as PROXY protocol encapsulates TLS data.
Host Validation
Caddy validates the Host header on incoming requests:
- Empty
Hoston HTTP/1.1 is rejected per RFC 9112 - When
strict_sni_hostis enabled, the TLS ServerName must match the HTTP Host header - The admin API independently validates host/origin headers
IP-Based Matchers
Two IP matchers are available:
remote_ip: Matches the client's IP address (from the network connection)client_ip: Matches the client's IP after parsingX-Forwarded-Forheaders (requirestrusted_proxiesconfiguration)
Warning
Using
client_ipwithouttrusted_proxiesconfiguration can allow IP spoofing. Always configure the trusted proxy ranges when usingclient_ipbehind a load balancer.
8. Developer Internals
Repository Structure
caddy/
├── admin.go # Admin API server and handlers
├── caddy.go # Core config lifecycle (Load, Run, Stop)
├── context.go # Module provisioning context
├── modules.go # Module registry
├── replacer.go # Placeholder expansion engine
├── listeners.go # Network address parsing, listener management
├── logging.go # Logging configuration structs
├── storage.go # Storage directory resolution
├── usagepool.go # Reference-counted resource sharing
├── cmd/
│ ├── caddy/main.go # Binary entry point
│ ├── cobra.go # Cobra root command setup
│ ├── commands.go # CLI subcommand registration
│ ├── commandfuncs.go # CLI command implementations
│ ├── packagesfuncs.go # upgrade/add-package/remove-package
│ └── storagefuncs.go # storage export/import
├── caddyconfig/
│ ├── configadapters.go # Config adapter registry
│ ├── httploader.go # HTTP-based config loader
│ ├── load.go # /load and /adapt admin endpoints
│ ├── caddyfile/ # Caddyfile lexer, parser, formatter
│ └── httpcaddyfile/ # HTTP-specific Caddyfile adapter
├── modules/
│ ├── caddyhttp/ # HTTP app (34 files, 457K chars)
│ │ ├── app.go # HTTP app provisioning and lifecycle
│ │ ├── server.go # HTTP server (ServeHTTP, listeners)
│ │ ├── autohttps.go # Automatic HTTPS logic
│ │ ├── routes.go # Route matching and handler chains
│ │ ├── matchers.go # Request matchers (host, path, method, etc.)
│ │ ├── celmatcher.go # CEL expression matcher
│ │ ├── staticresp.go # Static response handler
│ │ ├── subroute.go # Subroute handler
│ │ ├── vars.go # Variables handler (vars, map)
│ │ ├── reverseproxy/ # Reverse proxy handler
│ │ ├── fileserver/ # Static file server + browse
│ │ ├── caddyauth/ # Basic authentication
│ │ ├── encode/ # Response encoding (gzip, zstd, brotli)
│ │ ├── headers/ # Header manipulation
│ │ ├── rewrite/ # URI rewrite
│ │ ├── templates/ # Template rendering
│ │ ├── tracing/ # OpenTelemetry tracing
│ │ ├── push/ # HTTP/2 server push
│ │ ├── map/ # Map directive
│ │ ├── requestbody/ # Request body limits
│ │ ├── intercept/ # Response interception
│ │ ├── logging/ # Access log append
│ │ └── proxyprotocol/ # PROXY protocol listener wrapper
│ ├── caddytls/ # TLS app (30 files, 333K chars)
│ │ ├── tls.go # TLS app lifecycle
│ │ ├── acmeissuer.go # ACME certificate issuer
│ │ ├── automation.go # Automation policies
│ │ ├── connpolicy.go # TLS connection policies
│ │ ├── capools.go # CA certificate pool providers
│ │ ├── ech.go # Encrypted Client Hello
│ │ ├── matchers.go # TLS handshake matchers
│ │ ├── ondemand.go # On-demand TLS
│ │ ├── certmanagers.go # External cert managers (Tailscale, HTTP)
│ │ └── zerosslissuer.go # ZeroSSL-specific issuer
│ ├── caddypki/ # PKI app (9 files, 57K chars)
│ │ ├── pki.go # PKI app provisioning
│ │ ├── ca.go # Certificate Authority management
│ │ ├── adminapi.go # PKI admin API endpoints
│ │ ├── acmeserver/ # Embedded ACME server
│ │ └── command.go # trust/untrust CLI commands
│ ├── caddyevents/ # Events app
│ ├── logging/ # Logging encoders, writers, filters
│ ├── metrics/ # Prometheus metrics
│ ├── filestorage/ # File system storage module
│ └── caddyfs/ # Virtual filesystem
├── internal/ # Internal utilities
│ ├── logbuffer.go # Buffered logging for startup
│ ├── logmarshalers.go # HTTP header log marshaling
│ ├── sockets.go # Unix socket permission parsing
│ ├── metrics/ # Internal metrics helpers
│ └── filesystems/ # OS filesystem wrapper
├── caddytest/ # Integration test framework
└── notify/ # Platform-specific notifications (systemd, etc.)
Module System
Caddy's module system is the foundation of its extensibility:
-
Registration: Modules call
caddy.RegisterModule()in theirinit()function, providing aModuleInfowith an ID and constructor. -
Module IDs: Hierarchical, dot-separated namespaces (e.g.,
http,http.handlers.reverse_proxy,tls.issuance.acme,caddy.logging.encoders.json). -
Lifecycle:
New()— constructor returns zero-value moduleProvision(ctx caddy.Context)— set up internal state using the config contextValidate()(optional) — check configuration validityStart()(for apps) — begin servingStop()/Cleanup()— release resources
-
Context:
caddy.Contextwrapscontext.Contextand provides:LoadModule()— load and provision a sub-module from JSONApp()— get a reference to another app moduleLogger()— get the module's logger- Module config unmarshaling with inline key support
-
Module map (as of extraction): 228 structs, 985 functions across the standard distribution.
Caddyfile Adapter Architecture
The Caddyfile adapter (caddyconfig/caddyfile/ + caddyconfig/httpcaddyfile/) converts human-readable Caddyfile syntax to Caddy's native JSON:
Caddyfile text
↓
Lexer (lexer.go) → tokens (with heredoc support)
↓
Parser (parse.go) → ServerBlock structs
↓
HTTP Adapter (httptype.go) → JSON config
↓
Directive handlers: each directive registers an UnmarshalCaddyfile method
Address parsing: supports schemes, hosts, ports, paths, wildcards
Matcher compilation: @name definitions compiled to JSON matcher sets
Import/snippet resolution: recursive file imports with glob support
Global options: parsed by registered option handlers
Directive ordering: The httpcaddyfile/directives.go file defines a strict ordering for directives within a site block. Directives are sorted according to this order to ensure correct handler chain construction. Custom directives can be inserted using the order global option.
Replacer (Placeholder) System
The Replacer (replacer.go) provides dynamic value substitution throughout Caddy's configuration and handlers:
- Global placeholders:
{system.hostname},{system.slash},{system.os},{system.arch},{time.now.*},{env.*},{file.*} - HTTP placeholders: Extensive set covering request, response, TLS, and variables (see HTTP App source comments above)
- Nested placeholders: Resolved recursively with depth limit (100) to prevent infinite loops
- Custom providers: Modules can register custom placeholder providers via
caddy.NewReplacer()andMap()
UsagePool
The UsagePool (usagepool.go) is a reference-counted resource pool that ensures shared resources (like listeners, certificates) are properly cleaned up when the last user releases them. This is critical for Caddy's hot-reload capability — multiple config generations can share the same listener without re-binding.
Network Address Parsing
The NetworkAddress struct (listeners.go) provides comprehensive address parsing:
- Supports networks:
tcp,tcp4,tcp6,udp,udp4,udp6,unix,unixgram,unixpacket,fd,fdgram - Port ranges:
localhost:8080-8085expands to individual listeners - Unix socket permissions:
unix//path|0660syntax - File descriptor passing:
fd/3for socket activation
Reverse Proxy Internals
The reverse proxy (modules/caddyhttp/reverseproxy/) is one of the most complex modules:
Load balancing policies:
| Policy | Description |
|---|---|
random |
Random upstream selection |
random_choose |
Random choice from N candidates |
least_conn |
Fewest active connections |
round_robin |
Sequential rotation |
weighted_round_robin |
Weighted sequential rotation |
first |
Always use first available upstream |
ip_hash |
Consistent hashing by client IP |
uri_hash |
Consistent hashing by request URI |
query_hash |
Consistent hashing by query parameter |
header_hash |
Consistent hashing by header value |
cookie |
Cookie-based affinity |
Health checks: Active (periodic HTTP requests) and passive (monitor responses for failures). Configurable health path, headers, request body, interval, timeout, status codes, and expected body.
Retries: Configurable retry count, retry match conditions (response status codes, network errors), and retry-after backoff.
Dynamic upstreams: Support for DNS-based and SRV-based upstream discovery with configurable refresh intervals.
Admin API integration: The /reverse_proxy/upstreams admin endpoint exposes upstream health status for external monitoring.
Event System
The events app (modules/caddyevents/) provides a publish-subscribe system:
- Modules emit events via
caddy.EmitEvent() - Subscribers register handlers for specific event names
- Events follow the CloudEvents specification (RFC, JSON-serializable)
- Event handlers can abort event propagation
- Subscriptions close after the events app starts (no late registration)
Testing Strategy
Caddy uses several testing approaches:
- Unit tests: Standard Go
_test.gofiles alongside source (e.g.,matchers_test.go,connpolicy_test.go) - Integration tests:
caddytest/package provides aTesterhelper for starting Caddy with config and making HTTP assertions - Caddyfile adapt tests:
caddytest/integration/caddyfile_adapt/*.caddyfiletestfiles test Caddyfile → JSON conversion (over 170 test cases) - Fuzz tests:
*_fuzz.gofiles for the formatter, lexer, replacer, addresses, and duration parser - Linting: golangci-lint with extensive configuration (
.golangci.yml) - Pre-commit hooks: gitleaks, golangci-lint, shellcheck, trailing whitespace
Run the test suite:
go test ./... -tags=nobadger,nomysql,nopgx
Code Patterns and Conventions
| Pattern | Usage |
|---|---|
| Module registration | init() + caddy.RegisterModule() |
| Config unmarshaling | JSON struct tags + caddy: directive for module loading |
| Caddyfile integration | UnmarshalCaddyfile(d *caddyfile.Dispenser) interface |
| Error handling | fmt.Errorf with %v wrapping; %w for sentinel errors |
| Logging | zap.Logger via ctx.Logger() |
| Placeholders | caddy.Replacer with {prefix.key} syntax |
| Concurrency | sync.Mutex for state protection; UsagePool for shared resources |
| Atomic operations | Config swap via caddy.Load() + context cancellation |
| Feature flags | JSON omitempty for optional fields; *bool for tri-state (unset/true/false) |
| Build constraints | //go:build tags for platform-specific code (listen_unix.go, service_windows.go, sigtrap_posix.go) |
Potential Improvements
These are sourced from the codebase itself (TODO comments, community issues, and architectural observations):
-
caddy statuscommand (community issue #7453): Currently no CLI command to check if Caddy is running or view its status without directly querying the admin API. -
WebSocket connection preservation (community issue #7222): Active WebSocket connections are disrupted during config reloads, even for unrelated routes.
-
Unix socket as default admin endpoint (community issue #7516): Proposal to switch admin API to AF_UNIX by default for improved security.
-
Stricter Host validation (community issue #7628): Design proposal for lower-level HTTP error handling for invalid Host headers.
-
WebSocket compression (community issue #7455): RFC 7692 (permessage-deflate) is not currently supported.
-
DNS-PERSIST challenge type (community issue #7495): Proposed new ACME challenge type.
-
OTEL_EXPORTER_OTLP_PROTOCOL support (community issue #5743): OpenTelemetry protocol negotiation via environment variable.
Contributing
Caddy is licensed under the Apache License 2.0. The source repository includes:
- Pre-commit hooks: gitleaks, golangci-lint, shellcheck
- CI linting: Extensive golangci-lint configuration (50+ linters enabled)
- Code formatting: gci, gofmt, gofumpt, goimports
- Build system: GoReleaser for cross-platform binary releases with cosign signing and SBOM generation
- Supported platforms: Linux (amd64, arm64, arm, s390x, ppc64le, riscv64), macOS (amd64, arm64), Windows (amd64, arm64), FreeBSD (amd64, arm64, armv6/v7)
- Package formats: tar.gz, zip (Windows), deb
To build from source:
git clone https://github.com/caddyserver/caddy.git
cd caddy
go build -tags=nobadger,nomysql,nopgx -o caddy ./cmd/caddy
9. Integration Points
Reverse Proxy Admin API
# View upstream health status
curl localhost:2019/reverse_proxy/upstreams
[
{
"address": "node1:8080",
"num_requests": 1234,
"fails": 0
}
]
PKI Admin API
# Get local CA info
curl localhost:2019/pki/ca/local
# Get CA certificates
curl localhost:2019/pki/ca/local/certificates
ACME Server
Caddy can act as an ACME server for issuing certificates to other clients:
{
pki {
ca local {
name "My Organization CA"
}
}
}
acme.internal {
acme_server {
ca local
lifetime 24h
}
}
The ACME server supports HTTP-01 and TLS-ALPN-01 challenges, with configurable challenge types, certificate lifetimes, and issuer policy (allow/deny lists).
Tailscale Integration
Caddy integrates with Tailscale for certificate management via the tscert package. When running on a Tailscale network, Caddy can obtain certificates for Tailscale machine names automatically.
libdns Interface
Caddy's DNS challenge support uses the libdns interface, which provides a standardized API for DNS record management. Third-party DNS provider plugins implement this interface (e.g., Cloudflare, Route53, DigitalOcean).
OpenTelemetry
Caddy exports traces and metrics via OpenTelemetry with:
- Auto-export via
go.opentelemetry.io/contrib/exporters/autoexport(OTLP gRPC/HTTP, stdout) - Auto-propagation via
go.opentelemetry.io/contrib/propagators/autoprop(W3C, B3, Jaeger, OT, AWS X-Ray) - HTTP instrumentation via
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp - Prometheus bridge via
go.opentelemetry.io/contrib/bridges/prometheus
Systemd Notification
On Linux with systemd, Caddy sends sd_notify readiness notifications via the notify/notify_linux.go module, allowing systemd to track Caddy's readiness state.