Backplane Kubernetes Block
Deploy the ayedo Backplane content platform (Django 5 ASGI) to Kubernetes.
The block is public on the Hub (cargo.ayedo.cloud/ayedo/k8s/backplane). The image is internal: cargo.ayedo.cloud/ayedo-internal-tooling/backplane.
Quick start
blocks:
- name: backplane
from: cargo.ayedo.cloud/ayedo/k8s/backplane
config:
namespace: backplane
ingress:
enabled: true
host: ayedo.de
www_redirect: true # TLS SAN www.ayedo.de + HAProxy 301
extra_hosts:
- content.ayedo.de # same backend, extra vhost
- docs.ayedo.de # app 301 → /docs/{rest} — not redirect-from
- docs.ayedo.cloud
- docs.polycrate.io
env:
SECRET_KEY: "…"
ALLOWED_HOSTS: "ayedo.de,www.ayedo.de,content.ayedo.de,docs.ayedo.de,docs.ayedo.cloud,docs.polycrate.io,icons.ayedo.de,*"
CSRF_TRUSTED_ORIGINS: "https://ayedo.de,https://www.ayedo.de"
PUBLIC_CANONICAL_HOST: "ayedo.de"
DATABASE_URL: "postgres://…"
The image lives on a private registry. For clusters that need pull credentials, set image_credentials.enabled: true plus registry / username / password. The block creates Secret backplane-registry (kubernetes.io/dockerconfigjson) and sets imagePullSecrets on the Deployment and on the migrate/bootstrap Jobs.
Do not bake PUBLIC_CANONICAL_HOST or SITE_URL into the image. Set host-related env at runtime.
ingress.www_redirect: true adds www.{host} as a TLS SAN and haproxy-ingress.github.io/redirect-from (301). That is the permanent www redirect — no extra Ingress rule. extra_hosts adds more vhosts on the same backend (e.g. keep content.ayedo.de after the ayedo.de cutover). Docs aliases (docs.ayedo.de, docs.ayedo.cloud, docs.polycrate.io) must be extra_hosts, not redirect-from: HAProxy would 301 to ayedo.de/{path} and drop the /docs prefix. The app HostRedirectMiddleware prepends /docs/ (and /media/icons/ for icons.ayedo.de) when PUBLIC_CANONICAL_HOST is set.
MCP split
Streamable HTTP sessions live in process RAM (_server_instances). Redis/Valkey cannot share them. Production therefore splits like Dev Caddy:
- Web (
backplane): 3 replicas × 4 uvicorn workers — HTML, media, admin, API - MCP (
backplane-mcp): 1 replica ×--workers 1—/mcpand/mcp/public
Ingress Prefix /mcp and /mcp/public go to Service backplane-mcp. Everything else stays on backplane. Same image, same Secret backplane-env, same probe Host. After an MCP pod restart, clients must reconnect (mcp_auth). Do not raise MCP replicas without sticky sessions on mcp-session-id and one worker per pod.
Authentik uses the existing client backplane. Redirect URIs (no second client): Dev http://100.64.0.9:8020/api/v1/auth/oidc/callback/, https://content.ayedo.de/api/v1/auth/oidc/callback/, and https://ayedo.de/api/v1/auth/oidc/callback/.
Peer / laser24 env
The peer needs its own secrets. Put them in the workspace block config.env (encrypted workspace) or a cluster Secret — never in the app image.
Required:
SECRET_KEY,DATABASE_URLREDIS_URL/VALKEY_URL(Valkey in the same namespace; Django cache sessions)ALLOWED_HOSTSincludes the public host plus aliases (ayedo.de,www.ayedo.de,content.ayedo.de,docs.ayedo.de,*)CSRF_TRUSTED_ORIGINSincludeshttps://ayedo.de(and www / content while they still serve)PUBLIC_CANONICAL_HOST/PUBLIC_SITE_URL=ayedo.deafter cutover- Authentik admin OIDC:
AUTHENTIK_ISSUER,AUTHENTIK_AUDIENCE,AUTHENTIK_JWKS_URL,AUTHENTIK_CLIENT_ID,AUTHENTIK_CLIENT_SECRET - ChatGPT MCP OAuth (public provider
backplane-chatgpt, no secret):AUTHENTIK_CHATGPT_ISSUER,AUTHENTIK_CHATGPT_JWKS_URL,AUTHENTIK_CHATGPT_CLIENT_ID - Object storage:
S3_MEDIA_ENDPOINT,S3_MEDIA_ACCESS_KEY,S3_MEDIA_SECRET_KEY,S3_MEDIA_BUCKET_NAME,S3_MEDIA_USE_SSL
See examples.poly (test-domain, laser24-peer). Empty strings there are placeholders.
Actions
polycrate run backplane install
polycrate run backplane migrate
polycrate run backplane bootstrap
polycrate run backplane status
polycrate run backplane uninstall
install runs the migrate Job first, then applies Deployment/Service/Ingress. migrate scales the API to 0, runs python manage.py migrate --no-input, then scales back. The container entrypoint does not migrate in Kubernetes (BACKPLANE_MIGRATE_ON_START=false).
Health
GET /healthz/liveGET /healthz/readyGET /api/v1/health/
Kubelet probes still connect to the pod IP. They send Host from probe_host, else ingress.host, else PUBLIC_CANONICAL_HOST, else the first ALLOWED_HOSTS entry, so Django does not reject the request with Invalid HTTP_HOST header.