# FlowerCore.WorldBuilder — comic / storyboard authoring service. # # Deployment + Service + PVC + Certificate + IngressRoute. ArgoCD-managed # end-to-end. See apps/worldbuilder/README.md for the per-deploy runbook. # # Image build (BLUEJAY-WS): # bash deploy/build.sh # in FlowerCore.WorldBuilder repo # podman save localhost/fc-worldbuilder:v -o /tmp/fc-worldbuilder-v.tar # for h in 10.0.56.11 10.0.56.12 10.0.56.13; do # scp /tmp/fc-worldbuilder-v.tar fcadmin@$h:/tmp/ # ssh fcadmin@$h "sudo /var/lib/rancher/rke2/bin/ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images import /tmp/fc-worldbuilder-v.tar" # done --- apiVersion: v1 kind: Namespace metadata: name: fc-worldbuilder labels: app.kubernetes.io/name: fc-worldbuilder app.kubernetes.io/part-of: flowercore app.kubernetes.io/managed-by: argocd flowercore.io/tenant-id: system flowercore.io/created-by: bluejay-infra --- # SQLite DB + generated image gallery + PDF/PNG exports. # Longhorn RWO — single replica with `Recreate` rollout strategy keeps it safe. apiVersion: v1 kind: PersistentVolumeClaim metadata: name: worldbuilder-data namespace: fc-worldbuilder labels: app.kubernetes.io/name: worldbuilder-data app.kubernetes.io/component: storage app.kubernetes.io/part-of: flowercore app.kubernetes.io/managed-by: argocd flowercore.io/tenant-id: system flowercore.io/created-by: bluejay-infra spec: accessModes: - ReadWriteOnce storageClassName: longhorn resources: requests: storage: 5Gi --- apiVersion: apps/v1 kind: Deployment metadata: name: worldbuilder-web namespace: fc-worldbuilder labels: app.kubernetes.io/name: worldbuilder-web app.kubernetes.io/component: web app.kubernetes.io/part-of: flowercore app.kubernetes.io/managed-by: argocd flowercore.io/tenant-id: system flowercore.io/created-by: bluejay-infra annotations: flowercore.io/traceability-standard: k8s-pod-ownership-and-traceability-standard spec: replicas: 1 revisionHistoryLimit: 3 strategy: # RWO PVC + single replica. Recreate avoids multi-attach overlap. type: Recreate selector: matchLabels: app.kubernetes.io/name: worldbuilder-web template: metadata: labels: app.kubernetes.io/name: worldbuilder-web app.kubernetes.io/component: web app.kubernetes.io/part-of: flowercore app.kubernetes.io/managed-by: argocd flowercore.io/tenant-id: system flowercore.io/created-by: bluejay-infra annotations: fc.flowercore.io/healthz-anon: "true" fc.flowercore.io/probe-path: "/healthz" prometheus.io/scrape: "true" prometheus.io/port: "8080" prometheus.io/path: "/metrics/prometheus" flowercore.io/audit-trace-id: "worldbuilder-runtime-demo" spec: securityContext: fsGroup: 1654 fsGroupChangePolicy: OnRootMismatch containers: - name: web # Bump tag for each rebuild. Initial deploy: v202605062048 image: localhost/fc-worldbuilder:v202605062048 imagePullPolicy: Never ports: - containerPort: 8080 name: http # fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip. env: - name: ASPNETCORE_URLS value: "http://+:8080" - name: ASPNETCORE_ENVIRONMENT value: "Production" - name: DOTNET_RUNNING_IN_CONTAINER value: "true" - name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT value: "false" # SQLite path overrides (default appsettings uses relative paths). - name: ConnectionStrings__DefaultConnection value: "Data Source=/data/worldbuilder.db" - name: FlowerCore__Database__Provider value: "Sqlite" - name: FlowerCore__Database__ConnectionStrings__Sqlite value: "Data Source=/data/worldbuilder.db" # Generated image gallery + exports persist on /data. - name: FlowerCore__WorldBuilder__ImageStore__RootPath value: "/data/gallery" - name: FlowerCore__WorldBuilder__Export__RootPath value: "/data/exports" # Visitor-safe Sprint 32 profile: fake backend keeps public demo # rendering deterministic and avoids exposing BLUEJAY-WS GPU. - name: FlowerCore__WorldBuilder__ImageGeneration__BaseUrl value: "http://127.0.0.1:1" - name: FlowerCore__WorldBuilder__ImageGeneration__ClientMode value: "fake" - name: FlowerCore__WorldBuilder__ImageGeneration__BackendId value: "fake" resources: # Cluster CPU-request budget runs hot (99% on all 3 nodes at deploy # time) while actual CPU usage is well below capacity. Idle Blazor # Server + SignalR + a single ComfyUI poller uses ~5m, so 25m is # generous. Re-evaluate if active rendering/export workers ever # push past the limit. requests: cpu: 25m memory: 256Mi limits: cpu: 1000m memory: 768Mi # /healthz is registered explicitly in Program.cs (anonymous, no DB # or OpenAPI dependency). Liveness uses tcpSocket as a cheap fallback # in case future middleware changes accidentally gate /healthz. # Memory: feedback_k8s_probes_must_not_hit_openapi, # feedback_k8s_probes_behind_auth_middleware. startupProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 30 readinessProbe: httpGet: path: /healthz port: 8080 periodSeconds: 10 failureThreshold: 3 livenessProbe: tcpSocket: port: 8080 initialDelaySeconds: 30 periodSeconds: 30 failureThreshold: 3 securityContext: runAsNonRoot: true runAsUser: 1654 runAsGroup: 1654 allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALL volumeMounts: - name: data mountPath: /data - name: tmp mountPath: /tmp - name: logs mountPath: /app/logs volumes: - name: data persistentVolumeClaim: claimName: worldbuilder-data - name: tmp emptyDir: {} - name: logs emptyDir: {} --- apiVersion: v1 kind: Service metadata: name: worldbuilder-web namespace: fc-worldbuilder labels: app.kubernetes.io/name: worldbuilder-web app.kubernetes.io/component: web app.kubernetes.io/part-of: flowercore app.kubernetes.io/managed-by: argocd flowercore.io/tenant-id: system flowercore.io/created-by: bluejay-infra spec: type: ClusterIP selector: app.kubernetes.io/name: worldbuilder-web ports: - name: http port: 80 targetPort: 8080 --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: worldbuilder-web-tls namespace: fc-worldbuilder labels: app.kubernetes.io/name: worldbuilder-web-tls app.kubernetes.io/component: ingress app.kubernetes.io/part-of: flowercore app.kubernetes.io/managed-by: argocd flowercore.io/tenant-id: system flowercore.io/created-by: bluejay-infra spec: secretName: worldbuilder-web-tls issuerRef: name: step-ca-acme kind: ClusterIssuer dnsNames: - worldbuilder.iamworkin.lan # step-ca ACME provisioner caps lifetime at 30d. Requesting 90d # silently capped to 30d, making renewBefore 720h (30d) equal to the # actual cert lifetime — triggered a perpetual renewal loop that # generated 2365+ CertificateRequest objects in 18h. Match the working # 720h/240h pattern used by every other FC service cert. duration: 720h # 30d (step-ca cap) renewBefore: 240h # 10d --- apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: worldbuilder-web namespace: fc-worldbuilder labels: app.kubernetes.io/name: worldbuilder-web app.kubernetes.io/component: ingress app.kubernetes.io/part-of: flowercore app.kubernetes.io/managed-by: argocd flowercore.io/tenant-id: system flowercore.io/created-by: bluejay-infra spec: entryPoints: - websecure routes: - match: Host(`worldbuilder.iamworkin.lan`) kind: Rule services: - name: worldbuilder-web port: 80 tls: secretName: worldbuilder-web-tls # ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- # When the operator decides to expose worldbuilder-web publicly, uncomment + update the host, # then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). # # --- IngressRoute --- # apiVersion: traefik.io/v1alpha1 # kind: IngressRoute # metadata: # name: worldbuilder-web-public # namespace: worldbuilder # spec: # entryPoints: [websecure] # routes: # - match: Host(`worldbuilder.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) # kind: Rule # middlewares: # - name: worldbuilder-web-public-profile-header # injects entitlement profile # services: # - name: worldbuilder-web # port: 80 # tls: {} # # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. # # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).