Claude Code with Traefik: Cloud-Native Edge Without the Mess
Why Traefik without CLAUDE.md ships ingress configs that surprise the operator
Traefik is the cloud-native reverse proxy that discovers services from container orchestrators, generates routes automatically, and terminates TLS via Let's Encrypt. The pitch is that you point it at Docker Swarm, Kubernetes, or a file provider and it routes traffic based on labels or CRDs without a human writing per-route config. The reality is that the same dynamic configuration model makes the failure modes hard to reason about. A label that does not include traefik.enable=true is silently ignored. A router with no entryPoints matches every entry point. A middleware referenced by the wrong name is silently skipped. The dashboard runs on port 8080 by default with no authentication. The TLS resolver falls back to the HTTP challenge if DNS is misconfigured, which announces the certificate request to anyone watching.
A Claude Code session that drafts Traefik configuration without project rules generates manifests that work in the demo and break in production. The static config has --api.insecure=true because the docs example shows it that way. The IngressRoute CRD has no tls.certResolver, so Traefik either picks the first available resolver or terminates plaintext. The middleware chain is missing the rate limiter, the IP whitelist, and the redirect-to-HTTPS that production needs. The provider has no namespace selector, so Traefik watches every namespace in the cluster and Claude's test deployments leak into production routing. Each of these is a small oversight. Together they describe a Traefik deployment that an operator has to babysit forever.
This guide covers the CLAUDE.md configuration that locks Claude Code into Traefik patterns that survive production: a strict separation between static and dynamic config, IngressRoute CRDs for Kubernetes with the right middleware chain, TLS resolvers with the DNS challenge as primary and HTTP as fallback, a dashboard restricted to internal traffic only, and permission hooks that gate the destructive provider API calls. For the simpler single-host reverse proxy alternative, Claude Code with Caddy covers the patterns for hosts where Kubernetes is overkill. For the service mesh that complements Traefik for east-west traffic, Claude Code with Consul covers the service discovery layer.
The Traefik CLAUDE.md template
The CLAUDE.md at your project root is read at the start of every Claude Code session. For a Traefik deployment it needs to declare: the Traefik version, the orchestrator (Kubernetes, Docker, file), the static config split, the dynamic config source, the entry points, the middleware library, the TLS resolver list, the dashboard binding, and the hard rules that block the misconfigurations Claude makes most often.
# Traefik rules
## Stack
- traefik v3.x (latest stable release)
- Kubernetes provider via IngressRoute CRDs (NOT the legacy Ingress kind)
- Static config in helm values or traefik.yml, NOT command-line flags in production
- Dynamic config from KubernetesCRD provider only
- TLS resolvers: cloudflare-dns (primary), letsencrypt-http (fallback)
## Project structure
- traefik/values.yaml , helm chart values for the Traefik install
- traefik/static-config.yaml , the static section (entrypoints, providers, resolvers)
- traefik/middleware/ , one CRD per reusable middleware
- traefik/routes/ , IngressRoute CRDs per application namespace
- ops/traefik/ , helm install scripts, monitoring manifests
## Entry points (MANDATORY)
- web (port 80) configured ONLY for HTTP-to-HTTPS redirect
- websecure (port 443) with default TLS, HTTP/2 enabled, HTTP/3 optional
- metrics (port 8082) bound to internal network, Prometheus scrape only
- admin (port 9000) bound to internal network, dashboard access only
## Providers (MANDATORY)
- kubernetesCRD provider with explicit namespaces selector (no cluster-wide watch by default)
- allowEmptyServices: false (force IngressRoutes to reference services that exist)
- allowExternalNameServices: false (block ExternalName service references unless documented)
- NEVER enable the kubernetesIngress (legacy) provider in the same install
## IngressRoute CRDs (MANDATORY for every route)
- entryPoints explicitly listed (websecure for production routes)
- routes[].match uses Host AND PathPrefix together, never wildcard hostnames
- routes[].middlewares ALWAYS includes the security-headers chain
- tls.certResolver explicitly set (never inherit)
- priority explicitly set when multiple routes could match
## Middleware (reusable CRDs)
- security-headers chain: securityHeaders + redirectScheme + compress
- rate-limit chain: rateLimit + inFlightReq
- auth chain: basicAuth or forwardAuth for protected routes
- compress middleware on every route serving HTML or JSON
- Middleware referenced by namespace/name, NEVER by short name across namespaces
## TLS resolvers (MANDATORY)
- cloudflare-dns resolver uses DNS-01 challenge, API token in secret
- letsencrypt-http resolver uses HTTP-01 challenge, fallback only
- ACME storage on persistent volume (acme.json or k8s secret)
- caServer set to staging for non-prod, production for prod
- NEVER ship a resolver with httpChallenge AND dnsChallenge both enabled
## Dashboard (MANDATORY)
- api.dashboard: true, api.insecure: false
- IngressRoute for dashboard restricted to internal IPs (ipWhiteList middleware)
- basicAuth or forwardAuth middleware on dashboard route
- NEVER expose the dashboard via the websecure entry point without auth
## Hard rules
- NEVER set api.insecure: true outside of local development
- NEVER use the legacy Ingress kind alongside IngressRoute CRDs
- NEVER omit certResolver on a TLS route
- NEVER reference a middleware by short name when crossing namespaces
- NEVER commit the acme.json file to source control
- NEVER ship a resolver without an explicit caServer
- ALWAYS validate IngressRoute YAML with kubectl --dry-run=server before applying
- ALWAYS pin the Traefik image to a specific minor version (v3.2, not v3 or latest)
Six rules here prevent the majority of incidents Claude generates without them.
The static-versus-dynamic split keeps the boot config in helm values or traefik.yml and the per-route config in Kubernetes CRDs. Claude's default instinct is to put everything in command-line flags because the quickstart shows it that way. That works for a single-container demo and falls apart the moment you need to add a second middleware or rotate a resolver token. The split also means the static config can be reviewed once, the dynamic config can change per deploy, and the two never collide.
The IngressRoute-over-Ingress rule kills the failure mode where Traefik watches both the legacy Ingress resources and the new IngressRoute CRDs and the operator has to debug which one is winning. IngressRoute exposes the full Traefik feature set in a Kubernetes-native way. The legacy Ingress provider exists for compatibility with cluster tooling that only knows the old type. Pick one and disable the other.
The explicit namespace selector prevents the bug where Traefik watches every namespace in the cluster and an IngressRoute deployed to a test namespace silently overrides a production hostname. The right pattern is kubernetesCRD.namespaces: [production, edge] so Traefik only sees what the operator intended. Adding a new namespace to the routing path becomes a deliberate change to the Traefik install.
The TLS resolver fallback rule prevents the most common Let's Encrypt failure. The DNS-01 challenge works regardless of how traffic reaches the host. The HTTP-01 challenge requires Traefik to be reachable on port 80 from the Let's Encrypt validation server, which fails the first time a corporate firewall sits in front. Configure DNS as primary, HTTP as fallback, and the operator does not get paged when the firewall team changes their rules.
Static configuration via helm values
A production Traefik install on Kubernetes uses the official Helm chart with a values file that locks in the static config. Pin the chart and image versions explicitly.
# traefik/values.yaml
image:
repository: traefik
tag: v3.2.3
deployment:
replicas: 3
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
ports:
web:
port: 80
redirectTo:
port: websecure
scheme: https
websecure:
port: 443
tls:
enabled: true
http3:
enabled: false
metrics:
port: 8082
expose:
default: false
traefik:
port: 9000
expose:
default: false
providers:
kubernetesCRD:
enabled: true
allowEmptyServices: false
allowExternalNameServices: false
namespaces:
- production
- edge
kubernetesIngress:
enabled: false
api:
dashboard: true
insecure: false
certificatesResolvers:
cloudflare-dns:
acme:
email: ops@example.com
storage: /data/acme-cloudflare.json
caServer: https://acme-v02.api.letsencrypt.org/directory
dnsChallenge:
provider: cloudflare
delayBeforeCheck: 30
resolvers:
- 1.1.1.1:53
- 8.8.8.8:53
letsencrypt-http:
acme:
email: ops@example.com
storage: /data/acme-http.json
caServer: https://acme-v02.api.letsencrypt.org/directory
httpChallenge:
entryPoint: web
metrics:
prometheus:
entryPoint: metrics
addEntryPointsLabels: true
addRoutersLabels: true
addServicesLabels: true
accessLog:
format: json
filePath: /dev/stdout
fields:
headers:
defaultMode: drop
names:
Authorization: redact
Cookie: redact
persistence:
enabled: true
size: 1Gi
storageClass: gp3
Every section here enforces a rule from the CLAUDE.md. The image tag is pinned to a specific version. The web entry point only does the HTTP-to-HTTPS redirect. The kubernetesCRD provider has explicit namespaces and the safety flags. The dashboard runs on its own entry point that does not expose externally. The two ACME resolvers cover DNS as primary and HTTP as fallback. Access logs sanitize the headers that leak secrets.
The Cloudflare API token for the DNS challenge goes into a Kubernetes secret referenced from the helm install command. Never hardcode the token in the values file, and never commit a values file with a populated apiToken field.
A production IngressRoute
An IngressRoute defines how Traefik routes traffic to a backend service. The minimum production-grade IngressRoute includes the right entry point, a host-and-path match, the security middleware chain, and the TLS resolver.
# traefik/routes/app-ingressroute.yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: app
namespace: production
spec:
entryPoints:
- websecure
routes:
- match: Host(`app.example.com`) && PathPrefix(`/`)
kind: Rule
priority: 100
services:
- name: app
namespace: production
port: 80
healthCheck:
path: /healthz
interval: 10s
timeout: 2s
middlewares:
- name: security-headers-chain
namespace: edge
- name: rate-limit-chain
namespace: edge
- name: compress
namespace: edge
tls:
certResolver: cloudflare-dns
domains:
- main: app.example.com
The entryPoints: [websecure] restricts this route to HTTPS only. The match rule combines Host and PathPrefix, so a misconfigured DNS record cannot pull traffic into this route. The priority: 100 is explicit so Traefik's match ordering is deterministic; without it, the first lexicographic match wins, which surprises everyone the first time. The services block specifies the health check that Traefik will run against the backend, so failed pods are removed from rotation before requests arrive. The middlewares chain references the reusable CRDs by namespace. The tls.certResolver pins the resolver, so the cert never falls back to a different issuer.
Middleware as reusable CRDs
The right pattern is to define each middleware once as a CRD in a shared namespace and reference it from every IngressRoute. This keeps the security baseline consistent and gives Traefik a single place to update headers without touching every route.
# traefik/middleware/security-headers-chain.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: security-headers
namespace: edge
spec:
headers:
stsSeconds: 63072000
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: strict-origin-when-cross-origin
permissionsPolicy: "geolocation=(), camera=(), microphone=()"
customResponseHeaders:
X-Frame-Options: DENY
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-site
Server: ""
X-Powered-By: ""
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-scheme
namespace: edge
spec:
redirectScheme:
scheme: https
permanent: true
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: security-headers-chain
namespace: edge
spec:
chain:
middlewares:
- name: security-headers
namespace: edge
- name: redirect-scheme
namespace: edge
The security-headers middleware sets the HSTS, content type, referrer, and permissions policy headers in one place. The redirect-scheme redirects HTTP to HTTPS. The security-headers-chain composes them into a single middleware that IngressRoutes reference by name.
The rate limit middleware caps per-IP request rate and total in-flight requests per service:
# traefik/middleware/rate-limit-chain.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: rate-limit
namespace: edge
spec:
rateLimit:
average: 100
burst: 200
period: 1s
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: in-flight-req
namespace: edge
spec:
inFlightReq:
amount: 50
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: rate-limit-chain
namespace: edge
spec:
chain:
middlewares:
- name: rate-limit
namespace: edge
- name: in-flight-req
namespace: edge
The rateLimit block enforces a token bucket on a per-source-IP basis. The inFlightReq block caps how many concurrent requests Traefik forwards to a single service, which protects backends from a sudden traffic spike that would otherwise queue at the application layer.
TLS resolver patterns
The two resolvers in the values file cover the two ACME challenge types. The right pattern is to make the DNS resolver the default and only fall back to HTTP when DNS is misconfigured or unavailable.
For a wildcard certificate, the DNS challenge is the only option Let's Encrypt accepts. Reference the resolver in the IngressRoute's TLS block:
spec:
tls:
certResolver: cloudflare-dns
domains:
- main: example.com
sans:
- "*.example.com"
The domains.main is the primary subject, and the sans list specifies subject alternative names. The wildcard *.example.com covers every direct subdomain but does not cover deeper subdomains like api.staging.example.com. For those, list them explicitly or use a second wildcard.
The Cloudflare provider expects the API token in an environment variable. Define it via a Kubernetes secret and reference it in the deployment:
env:
- name: CF_DNS_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-dns-token
key: token
The token needs Zone:DNS:Edit permission for the zones Traefik manages. Cloudflare's API tokens scope to specific zones, which is the right pattern. The legacy global API key has access to every zone on the account and should not be used.
Dashboard and API security
The Traefik dashboard runs on its own entry point and is reachable from inside the cluster by default. Production deployments must add an IngressRoute that restricts access.
# traefik/routes/dashboard-ingressroute.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: dashboard-auth
namespace: edge
spec:
basicAuth:
secret: traefik-dashboard-auth
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: internal-only
namespace: edge
spec:
ipAllowList:
sourceRange:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: dashboard
namespace: edge
spec:
entryPoints:
- websecure
routes:
- match: Host(`traefik.internal.example.com`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api`))
kind: Rule
services:
- name: api@internal
kind: TraefikService
middlewares:
- name: internal-only
namespace: edge
- name: dashboard-auth
namespace: edge
tls:
certResolver: cloudflare-dns
The dashboard hostname uses an internal DNS suffix. The internal-only middleware rejects requests from outside the cluster's CIDR ranges. The dashboard-auth middleware requires basic auth credentials stored in a Kubernetes secret. The api@internal service is Traefik's built-in handler for the dashboard and API.
The basic auth secret is generated with htpasswd:
htpasswd -nb admin "$(openssl rand -base64 32)" > auth
kubectl create secret generic traefik-dashboard-auth \
--namespace=edge \
--from-file=users=auth
For SSO, swap basicAuth for forwardAuth pointing at an auth proxy. The pattern is the same: the dashboard route includes the auth middleware, and unauthenticated requests are rejected before they reach the dashboard.
Provider configuration for namespace isolation
The CLAUDE.md rule that the kubernetesCRD provider must have an explicit namespace list prevents the leakage where every IngressRoute in the cluster gets picked up. Configure it in the helm values:
providers:
kubernetesCRD:
enabled: true
allowEmptyServices: false
allowExternalNameServices: false
namespaces:
- production
- edge
labelSelector: app.kubernetes.io/managed-by=traefik
The namespaces list says Traefik only watches those two namespaces. The labelSelector adds a second filter so even within those namespaces, only resources labeled app.kubernetes.io/managed-by=traefik are picked up. The combination gives the operator two independent controls: namespace isolation at the RBAC layer, and label-based opt-in within the namespace.
For multi-tenant clusters, deploy one Traefik install per tenant. Cross-tenant traffic should not flow through a shared edge, and the namespace-list approach makes the boundary explicit.
Permission hooks for Traefik operations
Traefik has commands and API endpoints that can update the dynamic config out-of-band. Permission hooks in .claude/settings.local.json gate the destructive ones.
{
"permissions": {
"allow": [
"Bash(kubectl get ingressroute*)",
"Bash(kubectl get middleware*)",
"Bash(kubectl describe ingressroute*)",
"Bash(kubectl --dry-run=server apply*)",
"Bash(helm template*)"
],
"deny": [
"Bash(kubectl delete ingressroute*)",
"Bash(kubectl delete middleware*)",
"Bash(helm uninstall traefik*)",
"Bash(kubectl edit ingressroute*)",
"Bash(curl*POST*traefik*9000*)"
]
}
}
The allow list permits read-only inspection and dry-run apply, which Claude needs for debugging without risk. The deny list catches the operations that delete routes, uninstall the chart, or push config via the API. Combined with the Claude Code hooks system for project-wide policy, the result is a setup where Claude can investigate ingress without being able to break it.
Common Claude Code mistakes with Traefik
Six patterns Claude generates incorrectly without CLAUDE.md constraints.
1. api.insecure: true
Claude generates: --api.insecure=true to expose the dashboard quickly.
Correct pattern: api.dashboard: true, api.insecure: false, with an IngressRoute that adds authentication and IP allowlisting.
2. Legacy Ingress alongside IngressRoute
Claude generates: providers.kubernetesIngress.enabled: true to handle existing Ingress resources.
Correct pattern: pick IngressRoute, migrate the legacy Ingress resources, disable the legacy provider.
3. Missing certResolver on TLS route
Claude generates: an IngressRoute with tls: {}.
Correct pattern: tls: { certResolver: cloudflare-dns, domains: [{ main: app.example.com }] }.
4. Cluster-wide namespace watch
Claude generates: providers.kubernetesCRD with no namespaces list.
Correct pattern: explicit namespaces: [production, edge] so test namespaces cannot leak into routing.
5. Middleware referenced by short name across namespaces
Claude generates: middlewares: [{ name: security-headers }] from a different namespace than the middleware.
Correct pattern: middlewares: [{ name: security-headers, namespace: edge }].
6. acme.json committed or unmounted
Claude generates: a deployment with no persistent volume for ACME storage, so certs are reissued every restart.
Correct pattern: PersistentVolumeClaim mounted at /data, with the resolver storage field pointing at the volume.
Add each pattern to CLAUDE.md with the corrected form. Combined with CLAUDE.md examples for the general structure, the result is a Traefik install Claude generates correctly the first time.
Observability and metrics
Traefik exposes Prometheus metrics on the metrics entry point. The right pattern is a ServiceMonitor CRD that points Prometheus at the metrics port, with dashboards that surface the metrics that matter.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: traefik
namespace: edge
spec:
selector:
matchLabels:
app.kubernetes.io/name: traefik
endpoints:
- port: metrics
interval: 30s
path: /metrics
The metrics that matter in production: traefik_router_requests_total rate by router and status code, traefik_router_request_duration_seconds_bucket for p99 latency by router, traefik_service_open_connections for backend pressure, traefik_acme_certs_expiration_seconds for cert expiry, traefik_config_reloads_total for config change events.
For dashboards, pair Traefik with Claude Code with Grafana and use the official Traefik dashboard as a starting point. The alerts that pay back the setup time: p99 latency above SLO for any router, 5xx rate above 1% sustained for 5 minutes, certificate expiry within 7 days, config reload failures, dashboard hit count from an external IP.
Testing IngressRoute changes
Every IngressRoute change should be validated against the cluster before apply. The pattern is a CI step that runs kubectl --dry-run=server against the live cluster, which catches schema errors, missing references, and conflicts before they hit production.
#!/bin/bash
set -euo pipefail
# Validate every IngressRoute and Middleware
for file in traefik/routes/*.yaml traefik/middleware/*.yaml; do
kubectl --dry-run=server apply -f "$file"
done
# Render the helm chart to catch template errors
helm template traefik traefik/traefik \
--version 32.0.0 \
--values traefik/values.yaml \
> /tmp/rendered.yaml
# Validate the rendered chart
kubectl --dry-run=server apply -f /tmp/rendered.yaml
The --dry-run=server flag sends the manifest to the API server for validation but does not persist it. The server runs admission controllers and validation webhooks, which means a referenced middleware that does not exist, a service that is not in the right namespace, or a TLS resolver that is not configured all surface as errors before the deploy.
For integration tests against a running Traefik, deploy the route, run curl against the hostname, and assert on the response headers and status. This catches the "config compiles but does the wrong thing" bugs that dry-run misses.
Building Traefik that does not surprise the operator
The Traefik CLAUDE.md in this guide produces a config where every IngressRoute has the right entry points and middleware, every middleware is a reusable CRD, every TLS resolver has DNS as primary and HTTP as fallback, the dashboard is restricted to internal traffic with authentication, providers watch only the namespaces operators expect, and the deploy pipeline runs dry-run validation before apply. The result is a Kubernetes ingress that operators can trust to do what the manifest says.
The underlying principle is the same as any cloud-native infrastructure with Claude Code. Traefik without a CLAUDE.md produces a working install that conceals the misconfigurations that show up under load, during failovers, or when an attacker probes the surface. The CLAUDE.md is what makes the production-safe defaults the default for Claude.
For the simpler single-host alternative that runs outside Kubernetes, Claude Code with Caddy covers the patterns where a full ingress stack is overkill. For the service discovery layer that Traefik integrates with for non-Kubernetes deployments, Claude Code with Consul covers the service mesh. For the Kubernetes-native deployment workflow that drives Traefik manifests through GitOps, Claude Code with ArgoCD covers the reconciliation pattern.
Get Claudify. Ship a Traefik edge that survives production traffic.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify