Ghost on Kubernetes with Traefik, Cloudflare, and cert-manager: the routing gotcha I hit and how I fixed it
I run Ghost behind Traefik in Kubernetes with Cloudflare in front and cert-manager issuing TLS via Let’s Encrypt DNS. It mostly worked, until Ghost started calling back to its own API and getting 500s from the edge. The fix was simple once I understood the hop-by-hop scheme.
TL;DR
- Terminate TLS at Traefik.
- Keep Ghost speaking HTTP in-cluster.
- Make Ghost’s public URL https.
- Force Traefik to use HTTP to the Ghost Service.
- Avoid rewrites on the
/.ghost/*
paths.
Symptoms
- Browser CORS or mixed-content errors when Ghost generated http:// URLs.
- Boot-time errors like:
Could not get webhook secret for ActivityPub ... invalid json response body ... Unexpected token 'I', "Internal S"...
IMAGE_SIZE_URL ... ProbeError: bad status code: 500
againsthttp://blog.example.com/favicon.ico
- Direct pod hits to
http://127.0.0.1:2368/
worked;https://.../.ghost/...
via the Ingress returned 500.
Root cause
- Ghost exposes cleartext on port 2368 and detects HTTPS via forwarded headers.
- Traefik was trying to speak TLS to the Ghost Service for some routes because the backend was implicitly treated as https.
- Result: Traefik 500 for
/.ghost/*
requests; Ghost expects JSON and chokes on an HTML 500 page.
Working values.yaml
Use chart values only; no manual patches. The key is to set the public URL to https while keeping the Service upstream scheme http.
# Make Ghost generate https links without changing the in-cluster protocol
ghostEnableHttps: false
# Service: keep it plain HTTP to the pod
service:
type: ClusterIP
ports:
http: 2368
port: 2368
annotations:
traefik.ingress.kubernetes.io/service.serversscheme: http
# Traefik Ingress: TLS at the edge, HTTP to backend
ingress:
enabled: true
ingressClassName: traefik
hostname: blog.armiger.io
path: /
pathType: Prefix
tls: true
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
# do NOT add rewrite-target or StripPrefix here
Notes
- That
service.serversscheme: http
annotation is the linchpin with Traefik. ghostEnableHttps: false
prevents the chart from steering the backend to a pseudo-https port.- The lowercase
url
env tells Ghost its canonical URL is https, fixing CORS and mixed content.
Optional: dedicated route for internal API paths
Some ingress stacks attach middlewares that accidentally rewrite dot-prefixed paths.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ghost-internal
namespace: ghost
annotations:
kubernetes.io/ingress.class: traefik
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/service.serversscheme: "http"
spec:
tls:
- hosts: [blog.armiger.io]
rules:
- host: blog.armiger.io
http:
paths:
- path: /.ghost/
pathType: Prefix
backend:
service:
name: ghost
port:
name: http
Cloudflare settings I use
- SSL/TLS mode: Full (strict).
- Page Rule or Transform Rule: bypass cache for
/.ghost/*
. - Do not force redirects or HSTS at Cloudflare until the origin is confirmed healthy.
Quick verification
Through Traefik, bypass Cloudflare with a one-off resolve:
curl -vk https://blog.armiger.io/.ghost/activitypub/v1/site/ \
--resolve blog.armiger.io:443:<traefik-lb-ip>
# Expect: 200 and JSON, not a 500
Inside cluster, Ghost redirects HTTP to HTTPS for the API and that is fine:
curl -sS -D- http://127.0.0.1:2368/.ghost/activitypub/v1/site -o /dev/null
# Expect: 301 Location: https://127.0.0.1:2368/...
Gotchas to avoid
- Do not attach rewrite-target or StripPrefix middlewares to the Ghost host.
- Do not point Traefik at a Service port named https unless you also force
service.serversscheme: http
. - If you flip
ghostEnableHttps: true
, you will likely reintroduce an https upstream and the 500s.
Why this works
- Clients use HTTPS end-to-end.
- Traefik terminates TLS and forwards HTTP to Ghost on 2368.
- Ghost knows its canonical site URL is https and emits correct URLs for admin and API.
- The dot-prefixed internal API paths are routed without rewrites, so no JSON endpoints are mangled.
References
- Bitnami Ghost chart discussion of Traefik upstream scheme and the working pattern:
https://github.com/bitnami/charts/pull/11852#issuecomment-1291712106 - Traefik Kubernetes Ingress provider, service.serversscheme annotation:
https://doc.traefik.io/traefik/providers/kubernetes-ingress/ - Ghost configuration and reverse proxy notes:
https://ghost.org/docs/config/
https://ghost.org/faq/using-ssl/