Caddy — Structure#
Layout pattern#
Custom (Root-as-Library + cmd/ entry point + modules/ plugin tree)
Caddy does not follow the standard Go layout where code lives under pkg/. Instead, the root of the repository is the core library package (github.com/caddyserver/caddy/v2). All foundational types — App, Context, Module, Config, AdminRouter, Replacer, Storage — live in root-level .go files. A cmd/ directory holds the CLI entry point, internal/ holds private helpers, and a modules/ tree holds all pluggable functionality. There is no pkg/ directory at all. This is a deliberate design: the root package is the stable public API; everything else extends it.
Directory map#
caddy/ ← Core framework package (caddy.go, admin.go, modules.go, etc.)
├── cmd/ ← CLI implementation
│ ├── main.go ← package caddycmd: all CLI commands, config loading, flags
│ └── caddy/
│ └── main.go ← package main: binary entry point (3 imports + main())
├── caddyconfig/ ← Config adapter system
│ ├── configadapters.go ← Adapter interface + registry
│ ├── load.go / httploader.go ← Config loading over HTTP admin API
│ ├── caddyfile/ ← Caddyfile lexer, parser, formatter, dispenser
│ └── httpcaddyfile/ ← HTTP-specific Caddyfile adapter (routes, handlers, matchers)
├── caddytest/ ← Integration test harness
│ └── integration/ ← End-to-end integration tests + Caddyfile adapt tests
├── internal/ ← Private packages
│ ├── filesystems/ ← fs.FS utilities
│ ├── metrics/ ← Prometheus metrics helpers
│ └── testmocks/ ← Mock types for testing
├── modules/ ← All pluggable Caddy modules
│ ├── standard/ ← Import aggregator for standard module bundle
│ ├── caddyevents/ ← Event system module
│ │ └── eventsconfig/ ← Event handler config
│ ├── caddyfs/ ← Filesystem module
│ ├── caddyhttp/ ← HTTP server module (largest sub-tree)
│ │ ├── caddyauth/ ← HTTP authentication handlers
│ │ ├── encode/ ← Response compression (gzip, brotli, zstd sub-packages)
│ │ ├── fileserver/ ← Static file serving
│ │ ├── headers/ ← Request/response header manipulation
│ │ ├── intercept/ ← Response interception
│ │ ├── logging/ ← Access logging
│ │ ├── map/ ← Map handler (config-driven value mapping)
│ │ ├── proxyprotocol/ ← PROXY protocol listener wrapper
│ │ ├── push/ ← HTTP/2 server push
│ │ ├── requestbody/ ← Request body limits
│ │ ├── reverseproxy/ ← Reverse proxy + load balancer
│ │ │ ├── fastcgi/ ← FastCGI upstream
│ │ │ └── forwardauth/ ← Forward auth handler
│ │ ├── rewrite/ ← URI rewriting
│ │ ├── standard/ ← Import aggregator for standard HTTP modules
│ │ ├── templates/ ← Server-side template rendering
│ │ └── tracing/ ← OpenTelemetry tracing middleware
│ ├── caddypki/ ← Built-in CA (local PKI)
│ │ └── acmeserver/ ← Embedded ACME server
│ ├── caddytls/ ← TLS automation (CertMagic integration)
│ │ ├── distributedstek/ ← Distributed session ticket key management
│ │ └── standardstek/ ← Standard (local) STEK rotation
│ ├── filestorage/ ← File-based cert/data storage
│ ├── internal/network/ ← Internal network utilities for modules
│ ├── logging/ ← Log sink modules (file, net, stderr)
│ └── metrics/ ← Prometheus metrics module
└── notify/ ← OS service readiness notifications (systemd sd_notify)Entry points#
| File | Binary | Purpose |
|---|---|---|
cmd/caddy/main.go | caddy | Sole production binary. Intentionally minimal: imports caddycmd.Main() and modules/standard (blank import pulls in all standard modules). Designed to be trivially copied and customized by users. |
There is no cmd/main.go binary; cmd/main.go is package caddycmd — the full CLI implementation (subcommands: run, start, stop, reload, adapt, validate, fmt, upgrade, add-package, list-modules, environ, version, manpage, completion, etc.).
Package organization#
Internal packages (internal/):
internal/filesystems—fs.FSutility types used by the file server and other modulesinternal/metrics— shared Prometheus metrics helpers, used by both the rootmetrics.goandmodules/metricsinternal/testmocks— mockcaddy.Storageand connection types for unit tests
Public packages (no pkg/ dir; root + modules are all public):
- Root package (
github.com/caddyserver/caddy/v2) — the core framework API:App,Context,Config,Module,AdminRouter,Replacer,Storage,Logger,Provisioner,Validator, etc. cmd— CLI command framework; re-exported ascaddycmdfor embedding Caddy in other programscaddyconfig— config adapter registry and loader;caddyconfig/caddyfilefor the Caddyfile DSL;caddyconfig/httpcaddyfilefor HTTP-specific Caddyfile-to-JSON translationcaddytest— public test harness for module authors to write integration testsmodules/caddyhttp— HTTP server, matchers, handlers, routes, response writersmodules/caddytls— TLS automation; STEK rotation; session resumptionmodules/caddypki— local CA with embedded ACME servermodules/reverseproxy— full-featured reverse proxy and load balancermodules/logging,modules/metrics— observability modules
Layering:
[ cmd/caddy/main.go ] ← binary: just imports
↓
[ cmd/ (caddycmd) ] ← CLI: config loading, lifecycle commands
↓
[ caddyconfig/ ] ← config parsing & adaptation layer
↓
[ root package (caddy) ] ← core: module registry, config engine, admin API, context
↓
[ modules/* ] ← plugins: register themselves via init() → caddy.RegisterModule()
↓
[ internal/* ] ← private helpers: no imports of caddy (dependency-free)The layering is clean: modules depend on the root package, not on each other (with minor exceptions like reverseproxy importing caddyhttp types). The modules/standard and modules/caddyhttp/standard packages are pure import aggregators with no code — they exist only to pull in module init() functions.
Build system#
- Build tool: GoReleaser (
.goreleaser.yml) for releases; plaingo buildfor development - Key targets:
go build ./cmd/caddy→caddybinary (all standard modules included)- GoReleaser produces cross-platform binaries for darwin, linux, windows, freebsd × amd64, arm, arm64, s390x, ppc64le, riscv64
- CGO_ENABLED=0 throughout (pure Go, static binaries)
- Build tags:
nobadger,nomysql,nopgxin release builds to exclude optional storage backends -trimpath -mod=readonly -s -ware standard release flags
- Release peculiarity: GoReleaser copies
cmd/caddy/main.gointo a scratchcaddy-build/directory (gitignored) and builds from there. This keeps the working tree clean during a tagged release, sincego mod editwould otherwise dirty the module files. - xcaddy: The canonical tool for user-customized builds. Users copy
cmd/caddy/main.go, add plugin imports, and runxcaddy build. This is how third-party modules are distributed. - Docker: No Dockerfile in the main repo; Docker images are maintained separately in
caddyserver/dist. - Packaging: GoReleaser produces
.debpackages with systemd service files, man pages, and bash completions sourced fromcaddyserver/dist. - SBOM + signing: GoReleaser pipeline generates CycloneDX SBOMs (via
syft) and signs all artifacts withcosign.
Notable structural decisions#
Root package is the public API. Unlike most Go projects that bury library code under
pkg/, Caddy’s rootgithub.com/caddyserver/caddy/v2is the stable, versioned library surface. This means anyone embedding Caddy imports the root package directly — no extra path indirection.cmd/caddy/main.gois a deliberate template. At 43 lines with 3 imports and a singlemain()call, the entry point is intentionally trivial to copy and modify. The comment block inside is a step-by-step guide for building custom Caddy binaries. The actual CLI complexity lives incmd/(packagecaddycmd), which is importable by third-party programs viacaddycmd.Main().Import-aggregator
standardpackages. Bothmodules/standard/imports.goandmodules/caddyhttp/standard/imports.gocontain nothing but blank imports. This pattern avoids forcing users to enumerate all modules they want; they just import the bundle. It also makes the “what’s in the default build” completely explicit and auditable from a single file.Platform-specific files at root level. The root package has many OS/arch-specific files (
listen_unix.go,filepath_windows.go,service_windows.go,sigtrap_posix.go,sigtrap_nonposix.go,listen_unix_setopt_freebsd.go, etc.). This is unusual — most projects consolidate platform logic in subpackages — but makes sense here since the root package owns the listener and OS signal handling abstractions.modules/internal/— a private subpackage inside modules. Themodules/internal/network/package isinternalrelative to themodules/subtree, meaning only code undermodules/can import it. This uses Go’sinternalvisibility rules at sub-tree granularity, demonstrating awareness of fine-grained encapsulation.No vendoring in normal dev, but vendored for releases. The
.goreleaser.ymlrunsgo mod vendoras the first build step. This ensures reproducible release builds and allows bundling vendor in the source tarball archive, while keeping the dev workflow clean (no committed vendor/).