Tech Digest

Head to head

Caddy vs Traefik

Both terminate TLS and renew certificates without you. The difference is where your routing table comes from, and what happens when it is wrong.

Last reviewed 2 tools compared

Should you use Caddy or Traefik as your reverse proxy?

Use Caddy unless your containers change often enough that editing a config file is a real chore. Caddy is one 25 MB process and a text file, and a working site is three lines. Traefik earns its extra complexity when services are created and destroyed weekly and you want routes to appear from Docker labels without touching the proxy, which is worth about 60 MB idle and a config surface that has moved twice. On a home stack that changes twice a year, Traefik's discovery is wasted and its debugging cost is not.

The feature lists for these two overlap almost completely. Both are Go binaries, both do ACME, both do HTTP/3, both hot reload, both are free and permissively licensed. Comparing them on features produces a tie and tells you nothing.

The real decision is where your routing table lives. In Caddy it lives in a file you wrote and can read end to end. In Traefik it is assembled at runtime from labels scattered across every compose file on the host, and the only place you can see the assembled result is the dashboard. That is a genuine advantage when containers appear and disappear, and it is a genuine liability when something does not match, because there is no single file to read.

Ask yourself how often you add a service. If the answer is "a few times a year", the file wins. If the answer is "most weeks, and I redeploy stacks constantly", labels win.

The numbers that actually differ#

Idle memory and ops load are the only spec rows where these two separate meaningfully. Everything else is a tie, so read the table for those two and for the datastore row, which explains why Traefik cannot be run in a pair.

SpecificationCaddyTraefik
LicenceApache-2.0 (Permissive)MIT (Permissive)
Written inGoGo
First release20152015
MaturityMatureMature
DatastoreFilesystem (certificates, keys and ACME account data in the data directory)None. Certificates in a single acme.json file; routing state rebuilt from providers at every start
Services to run11
Idle memory25 MB60 MB
Memory in use70 MB130 MB
Operational load1 / 5, Set and forget3 / 5, Moderate
IdentityNot applicableAuth proxy only
arm64 buildsYesYes
Default ports80, 443, 201980, 443, 8080
Backup shapeFile copyFile copy

Caddy scores 1 on the ops load rubric and Traefik scores 3. That gap is not about install difficulty. Both are one container. It is that Traefik's configuration surface has moved twice in the project's life, label errors fail silently, and every upgrade is worth reading notes for.

What a working config looks like in each#

Caddy, complete, with automatic certificates and an HTTP to HTTPS redirect included:

caddyfile
jellyfin.example.com {
	reverse_proxy jellyfin:8096
}

paperless.example.com {
	reverse_proxy paperless:8000
}

Traefik, same two services, expressed as labels on the containers themselves:

yaml
services:
  jellyfin:
    image: jellyfin/jellyfin
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.jellyfin.rule=Host(`jellyfin.example.com`)"
      - "traefik.http.routers.jellyfin.entrypoints=websecure"
      - "traefik.http.routers.jellyfin.tls.certresolver=le"
      - "traefik.http.services.jellyfin.loadbalancer.server.port=8096"

Five labels against one line. That ratio is the tradeoff in miniature: Traefik's labels are more typing per service but nobody has to touch the proxy, and a service that is removed takes its route with it.

The v3 syntax change is the single biggest source of broken configs#

Traefik v3 changed the rule language, and the internet is still full of v2 snippets that look correct. Running v3.7.x as of early September 2026, these are the changes that bite:

v2v3failure mode
Host value written bareHost value wrapped in backticksrouter never matches
Matchers separated by commasMatchers joined with &&router never matches
Headers middleware matcherHeaderrule fails to parse or match
IPWhiteListIPAllowListmiddleware not found
HostHeader(...)removed, use Hostrouter never matches
PathPrefix(/x/{id})PathRegexpplaceholder segments not supported

The headers middleware also lost sslRedirect, sslHost and featurePolicy outright. None of this produces a red error in the log. The router just does not exist, and the request falls through to whatever else matched, which is usually a 404 from a different service.

There is an escape hatch: traefik.http.routers.myapp.ruleSyntax=v2 on a per-router basis makes Traefik parse that one rule with the old grammar. It works. It has also been deprecated since v3.4 and is scheduled for removal in the next major, so it buys you an afternoon, not a strategy.

Caddy's cost is the DNS challenge, and it is a build pipeline#

Caddy's reputation for simplicity is earned right up to the moment you need a wildcard certificate, or your server is not reachable on port 80. Both require the DNS-01 challenge, and the official binary and the official caddy Docker image contain no DNS provider modules at all.

The fix is a custom build:

dockerfile
FROM caddy:2-builder AS builder
RUN xcaddy build --with github.com/caddy-dns/cloudflare

FROM caddy:2
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

That is five lines and it works. What it also means is that you now own an image, and every Caddy upgrade is a rebuild before you can move. caddy add-package looks like an easier route, but it is marked experimental and it replaces the binary on disk, which a docker compose up --force-recreate discards.

Traefik has no equivalent problem. Its ACME support uses the Lego provider list, which is broad, and configuring a DNS provider is environment variables on the existing image. If wildcard certificates across many subdomains are central to your setup, that is the strongest single argument for Traefik in this comparison. See DNS for self-hosters for whether you need a wildcard at all.

The failure modes are different in kind#

Caddy's classic mistakes are ordering and persistence:

  • Directive order is fixed, not textual. Caddy executes directives in a built-in order regardless of how you wrote the file. Putting redir above reverse_proxy does not make it run first. Wrap them in a route block, or use mutually exclusive handle blocks, when order matters.
  • No volume on /data means re-issuing every certificate on every redeploy. A few loops during setup and Let's Encrypt rate limits you for the week. Mount a named volume before you point real DNS at it, and use acme_ca https://acme-staging-v02.api.letsencrypt.org/directory while experimenting.
  • Bind mounting the Caddyfile by path breaks reloads, because editors replace the inode on save and the container keeps reading the old one. Mount the directory.

Traefik's classic mistakes are structural:

  • Editing traefik.yml does nothing until restart. Entry points, providers and certificate resolvers are static config. Only routers, middlewares and services reload live. docker compose up -d --force-recreate traefik is the fix, and forgetting this is why people conclude Traefik ignored their change.
  • Docker creates a directory where you wanted acme.json. Bind mount a path that does not exist and you get a directory with that name, certificates never persist, and you re-request them on every restart until you are rate limited. Run touch acme.json && chmod 600 acme.json first. The 600 mode is enforced.
  • The Docker socket is root on the host. Traefik's Docker provider needs it. Read access exposes every container's environment and mounts to a compromised proxy; write access is game over. Mount it :ro at minimum and consider a socket proxy that whitelists only the container and event endpoints. This is covered further in A security baseline for a home server.

The uncomfortable part: Traefik's strength is idle on most home stacks#

Traefik was built for churn. Its value is proportional to how often your service inventory changes. A homelab running Jellyfin, Immich, Paperless-ngx and a wiki, all of which will still be there in two years, generates approximately zero churn. You get the dynamic discovery machinery, the two-tier config model, the silent-failure rule language and the socket exposure, in exchange for a feature you use twice a year.

The cost is not paid at install time, which is why people do not notice it. It is paid the evening something breaks at 11pm and you have to reconstruct a routing table from labels in six compose files. Caddy's equivalent evening is cat Caddyfile.

The counter-argument, and it is fair: if you already run Kubernetes and use Traefik as your ingress controller there, using it at home too means one mental model instead of two. That is a real reason.

Which one for your situation#

SituationUseWhy
First reverse proxy, five to fifteen servicesCaddyThree lines per site, certificates handled, ops load 1
Compose host where stacks come and go weeklyTraefikRoutes appear and disappear with the containers
Wildcard certificates across many subdomainsTraefikDNS providers are env vars, not a custom build
Internal-only names like nas.internalCaddyIts local CA mints certificates for names ACME cannot
You already run Traefik ingress in KubernetesTraefikOne config model across laptop and cluster
You want config in git, reviewed and diffedCaddyOne file, no runtime assembly
Backends are static VMs, not containersCaddyLabel discovery has nothing to discover
You want a web form instead of any fileNeitherSee Caddy vs NPM
Middleware chains per service (rate limit, IP allow list, forward auth)TraefikDeclarative middleware composition is genuinely better here

Running both is a legitimate answer#

Nothing stops you from putting Caddy on 443 for the handful of stable public services and running Traefik on an internal network for the churn-heavy development stacks. It is more moving parts than most people want, and it is still less confusing than one Traefik with forty labels.

What to do next#

If you have not chosen yet, start with Caddy. Migrating a Caddyfile to Traefik labels later is an afternoon; unpicking a label estate is a weekend. Generate a starting config with Reverse proxy generator, read Reverse proxy and TLS for the certificate and DNS decisions that sit underneath either choice, and if you want a login gate in front of apps that have none, Authelia vs authentik covers what to put behind the forward_auth directive. The rest of the category is at Networking.

Questions#

Why does my Traefik router return 404 after I copied a config from a blog post?

Almost always because the snippet is Traefik v2 and you are running v3. In v3 matcher values go inside backticks and combine with &&, so a Host matcher must wrap the hostname in backticks, and unquoted values or comma-separated matchers no longer parse the way they did. Headers became Header, IPWhiteList became IPAllowList, HostHeader is gone, and PathPrefix no longer accepts {placeholder} segments. Traefik does not reject the old form loudly. The router simply never matches, and you get the 404 from whichever router did.

Does Caddy support wildcard certificates?

Yes, but not with the binary you downloaded. Wildcards require the DNS-01 challenge, and the official Caddy binary and Docker image ship zero DNS provider modules. You have to rebuild with xcaddy build --with github.com/caddy-dns/cloudflare or use the caddy:builder image in a two-stage Dockerfile, then rebuild on every Caddy upgrade. caddy add-package exists but is marked experimental and swaps the binary on disk, which a container recreate throws away.

Can Caddy read Docker labels like Traefik does?

Not in the stock build. There is a well-used third-party module, caddy-docker-proxy, that gives Caddy label-driven routing, but it is a community project and it means you are back in the custom-build business with a rebuild on every upgrade. If label discovery is the feature you actually want, run Traefik, which does it as its primary design rather than as a plugin.

Which one uses less memory?

Caddy, by roughly half. Caddy idles near 25 MB and sits around 70 MB in ordinary use; Traefik idles near 60 MB and lands around 130 MB. Neither number should decide anything on a machine with 8 GB. It matters on a 1 GB VPS or a Raspberry Pi, where 100 MB is a real fraction of what you have.

Can I run two Traefik instances for redundancy?

Not with Let's Encrypt enabled. The documentation says plainly that multiple instances cannot share the file-based ACME store, because nothing coordinates which instance receives a given challenge. Two replicas on a shared volume clobber each other's acme.json. The documented answers for high availability are cert-manager or Traefik's commercial distributed store, not an NFS mount.

Do I still need Authelia or authentik with either of these?

Yes, if you want a login gate. Neither proxy has user accounts. Caddy calls out with the forward_auth directive, Traefik with a ForwardAuth middleware, and both point at Authelia or authentik. Traefik's dashboard in particular is unauthenticated unless you put BasicAuth or ForwardAuth in front of it, so do not publish port 8080 and assume it is safe.

Sources#

Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.