Transmission 032 · 2026-09-07

The init containers never started. Six fixes before the gateway returned ok.

031 ended with the worker in CrashLoopBackOff, the gateway stuck at Init:0/2, and minikube broken by a stale MINIKUBE_ACTIVE_DOCKERD variable. Today: fresh shell, minikube started clean, node reached Ready. The gateway pods did not move. kubectl describe showed Container ID empty on both busybox init containers — not a connectivity failure, an image pull failure. busybox:1.36 was never in the minikube daemon cache. Pulled and loaded busybox and bitnami/kubectl. That exposed the next problem: wait-for-migration was defined in _helpers.tpl and in both kustomize base manifests but was never included in either Helm deployment template. The Helm chart had been running with two init containers while the base had three. Adding the third exposed the missing gateway-db-patch ConfigMap — not in any Helm template, previously created by hand. Created it immediately and added it as a permanent Helm template using .Files.Get. The wait-for-migration script had a second bug: it only exits when the job shows succeeded=1. The db-migrate Job has a 300-second TTL. By the time bitnami/kubectl starts polling, the Job is already deleted. Fixed by treating job-not-found as completion. The existing resources carried wrong Helm ownership annotations from an earlier partial attempt. Patched all 13 with the correct release-name and release-namespace. helm upgrade --install succeeded. Then the gateway hit InvalidPasswordError: Helm had overwritten nexusflow-secrets with the default placeholder change-me-in-production, against a postgres instance initialised with localpassword123. Added the correct credentials to values-minikube.yaml. REVISION: 3. Gateway 1/1 Running. Worker 1/1 Running. POST returned 202. GET returned status: completed. Idempotency: first call 202, second 200.

Transmission 031 ended with one sentence: tomorrow, open a fresh shell, start minikube, read the worker logs.

Today: opened a fresh shell. Six things were wrong before a GET request came back clean.


The constraint

031 left the cluster in this state: gateway at Init:0/2, worker in CrashLoopBackOff after nine restarts, minikube broken by a stale MINIKUBE_ACTIVE_DOCKERD environment variable left from an earlier eval $(minikube docker-env) call. The cause of the worker crash was unknown — the cluster became unresponsive before logs could be read.

The goal today was to diagnose both the init container blockage and the worker crash, then get every pod to 1/1 Running.


The proof

Fix 1: minikube and the stale env var

031 documented the fix. A fresh PowerShell terminal has no MINIKUBE_ACTIVE_DOCKERD set. minikube started clean.

minikube start
kubectl get nodes
NAME       STATUS   ROLES           AGE   VERSION
minikube   Ready    control-plane   98d   v1.35.1

Scaled simple-model-api to zero to free CPU headroom, same as 031.

kubectl scale deployment simple-model-api-deployment --replicas=0 -n default
kubectl get pods -n nexusflow
NAME                       READY   STATUS             RESTARTS        AGE
gateway-658f489f68-72nd8   0/1     Init:0/2           0               44h
worker-d77698d99-l7tvk     0/1     CrashLoopBackOff   9 (13s ago)     44h
postgres-0                 1/1     Running            2 (4m57s ago)   43h
redis-0                    1/1     Running            1 (10m ago)     44h

The gateway pods did not move. Still Init:0/2.


Blocker 1: the init containers never pulled

kubectl describe pod -l app.kubernetes.io/name=gateway -n nexusflow

Both busybox init containers showed Container ID: empty. Not a connectivity failure — a connectivity failure shows a running container that cannot reach the endpoint. An empty Container ID means the container never started. The image was not in the minikube Docker daemon.

busybox:1.36 and bitnami/kubectl:latest had never been loaded into the minikube daemon. The node cannot reach Docker Hub from inside the cluster. Both had to be pulled on the host and loaded manually.

docker pull busybox:1.36
minikube image load busybox:1.36
docker pull bitnami/kubectl:latest
minikube image load bitnami/kubectl:latest
minikube image ls (filtered)
docker.io/bitnami/kubectl:latest
docker.io/library/busybox:1.36
docker.io/library/nexusflow-gateway:1.0.0
docker.io/library/nexusflow-worker:1.0.0

Both app images were already present from a previous session. The init container images were not.


Blocker 2: wait-for-migration was missing from both Helm templates

With busybox available, the worker init containers started. The worker entered Init:2/3 — two passed, one stalled. That third container is wait-for-migration. It exists in _helpers.tpl and in both kustomize base manifests. It was never included in either Helm deployment template.

# charts/nexusflow/templates/worker-deployment.yaml — before
initContainers:
  {{- include "nexusflow.initWaitForPostgres" . | nindent 8 }}
  {{- include "nexusflow.initWaitForRedis" . | nindent 8 }}
  # wait-for-migration was missing

The kustomize path and the Helm path had diverged silently. The helper was defined and ready; the include line was just never written.

# after
initContainers:
  {{- include "nexusflow.initWaitForPostgres" . | nindent 8 }}
  {{- include "nexusflow.initWaitForRedis" . | nindent 8 }}
  {{- include "nexusflow.initWaitForMigration" . | nindent 8 }}

Added to both gateway and worker templates.


Blocker 3: the gateway-db-patch ConfigMap did not exist

Adding the migration init container to the gateway template surfaced the next failure. The gateway pod mounts gateway-db-patch as a volume over /app/db.py. That ConfigMap was created by hand in session 030 and was never added to any Helm template. Under Helm management, it did not exist.

Warning  FailedMount  kubelet  MountVolume.SetUp failed for volume "db-patch":
configmap "gateway-db-patch" not found

Created it immediately to unblock the pod:

kubectl create configmap gateway-db-patch --from-file=db.py=gateway/db.py -n nexusflow

Then added it as a permanent Helm template. Helm reads files inside the chart directory via .Files.Get. Copied gateway/db.py to charts/nexusflow/files/gateway-db.py and wrote the template:

# charts/nexusflow/templates/gateway-db-patch.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: gateway-db-patch
  namespace: {{ include "nexusflow.namespace" . }}
data:
  db.py: |
{{ .Files.Get "files/gateway-db.py" | indent 4 }}

The ConfigMap is now created on every helm upgrade and will never be absent again.


Blocker 4: wait-for-migration loops forever when the job is TTL-deleted

The worker reached Init:2/3 and stayed there. The wait-for-migration container was running but not exiting. The original script:

until kubectl get job db-migrate -n nexusflow \
      -o jsonpath='{.status.succeeded}' 2>/dev/null | grep -q "1"; do
  echo "Migration not yet complete, retrying in 3s..."; sleep 3;
done

The db-migrate Job completes in under two minutes. ttlSecondsAfterFinished: 300 deletes it five minutes after completion. By the time bitnami/kubectl pulls, starts, and begins polling, the Job is already gone. kubectl get job db-migrate returns nothing. grep -q "1" never matches. The loop runs forever.

I considered raising the TTL from 300s to 1800s so the container would have more time to poll before the Job disappeared. That just delays the same race — any finite TTL creates a window where this script fails. Treating absence as completion is the correct fix: if the Job no longer exists, it either succeeded and was cleaned up, or was never run. Both cases mean proceeding is safe.

until
  STATUS=$(kubectl get job db-migrate -n nexusflow \
             -o jsonpath='{.status.succeeded}' 2>/dev/null);
  [ "$STATUS" = "1" ] || \
  ! kubectl get job db-migrate -n nexusflow > /dev/null 2>&1;
do
  echo "Migration not yet complete, retrying in 3s..."; sleep 3;
done

Applied to _helpers.tpl, k8s/base/gateway-deployment.yaml, and k8s/base/worker-deployment.yaml.


Blocker 5: Helm could not install — wrong ownership annotations on existing resources

helm upgrade --install nexusflow ./charts/nexusflow \
  -f charts/nexusflow/values-minikube.yaml -n nexusflow
Error: unable to continue with install: Secret "nexusflow-secrets" in namespace "nexusflow"
exists and cannot be imported into the current release: invalid ownership metadata;
annotation validation error: key "meta.helm.sh/release-namespace" must equal "nexusflow":
current value is "default"

The existing resources carried meta.helm.sh/release-namespace: default from an earlier partial Helm attempt. In 031, the fix was to delete the namespace entirely and let Helm recreate it. That is not viable here — postgres and redis PVCs hold real data. Deleting the namespace deletes the PVCs.

Patched all 13 resources with the correct Helm ownership annotations instead:

kubectl annotate secret/nexusflow-secrets deployment/gateway deployment/worker \
  statefulset/postgres statefulset/redis \
  service/gateway service/postgres service/postgres-headless \
  service/redis service/redis-headless \
  configmap/nexusflow-config configmap/postgres-init-sql configmap/redis-config \
  -n nexusflow \
  meta.helm.sh/release-name=nexusflow \
  meta.helm.sh/release-namespace=nexusflow --overwrite

After adoption, helm upgrade --install succeeded. STATUS: deployed, REVISION: 1.


Blocker 6: Helm overwrote the postgres password

The gateway entered CrashLoopBackOff immediately after reaching Running:

kubectl logs gateway-ccdb9d9c6-nhz99 --previous
asyncpg.exceptions.InvalidPasswordError: password authentication failed for user "nexus"

values.yaml sets secrets.postgresPassword: change-me-in-production. values-minikube.yaml had no secrets block. Helm rendered the secret with the placeholder and applied it, overwriting nexusflow-secrets — which held localpassword123, the password postgres was actually initialised with.

Added the correct credentials to values-minikube.yaml:

secrets:
  createSecret: true
  postgresUser: nexus
  postgresPassword: localpassword123
  databaseUrl: postgresql://nexus:localpassword123@postgres:5432/nexusflow
  redisUrl: redis://redis:6379/0

Upgraded. REVISION: 3.


NexusFlow Gateway Swagger UI showing /health, POST /tasks, and GET /tasks/task_id

Fig 1: Swagger UI confirming all three endpoints live at /docs with schemas, Idempotency-Key header, and 202/200 status codes.


The resolution

kubectl rollout status deployment/gateway deployment/worker -n nexusflow
deployment "gateway" successfully rolled out
deployment "worker" successfully rolled out
kubectl get pods -n nexusflow
NAME                       READY   STATUS    RESTARTS   AGE
gateway-557cf49778-cdb87   1/1     Running   0          50s
postgres-0                 1/1     Running   2          44h
redis-0                    1/1     Running   1          45h
worker-6797777459-m6qsx    1/1     Running   0          51s
kubectl port-forward svc/gateway 8000:80 -n nexusflow
GET /health
{"service": "gateway", "status": "ok", "timestamp": "2026-09-07T08:10:52.143358+00:00"}
POST /tasks
{
  "task_id": "422572b6-6a6b-4667-92ab-2c0809fe82e0",
  "name": "smoke-test",
  "status": "accepted",
  "accepted_at": "2026-09-07T08:11:37.619522+00:00",
  "idempotency_key": null
}
GET /tasks/422572b6-6a6b-4667-92ab-2c0809fe82e0
{
  "task_id": "422572b6-6a6b-4667-92ab-2c0809fe82e0",
  "name": "smoke-test",
  "status": "completed",
  "payload": {"hello": "world"},
  "result": {"name": "smoke-test", "processed": true},
  "retry_count": 0,
  "max_retries": 3,
  "error_message": null,
  "created_at": "2026-09-07T08:11:37.619522+00:00",
  "updated_at": "2026-09-07T08:11:39.030929+00:00"
}

Worker log at the moment the task moved through:

kubectl logs -l app.kubernetes.io/name=worker -n nexusflow
2026-09-07T08:11:38Z [INFO] nexusflow.worker — Picked up task_id=422572b6-6a6b-4667-92ab-2c0809fe82e0  attempt=1/4
2026-09-07T08:11:38Z [INFO] nexusflow.worker — Processing task_id=422572b6-6a6b-4667-92ab-2c0809fe82e0  name=smoke-test
2026-09-07T08:11:39Z [INFO] nexusflow.worker — Task completed  task_id=422572b6-6a6b-4667-92ab-2c0809fe82e0

Idempotency confirmed: same Idempotency-Key header, first call returned 202, second returned 200.


Where this sits

ItemStatus
busybox:1.36 and bitnami/kubectl:latest loaded into minikubeDone
wait-for-migration added to gateway and worker Helm templatesDone
wait-for-migration script — handles TTL-deleted jobDone
gateway-db-patch ConfigMap — permanent Helm template via .Files.GetDone
RBAC Role + RoleBinding — default SA can get jobsDone
Helm resource adoption — all 13 resources re-annotatedDone
values-minikube.yaml — correct postgres credentialsDone
Worker base manifest — image corrected to nexusflow-worker:1.0.0, IfNotPresentDone
Gateway — 1/1 RunningDone
Worker — 1/1 RunningDone
End-to-end: POST → worker → completed → GETDone
Idempotency: 202 then 200 on duplicate keyDone

The Helm chart now manages the full stack cleanly. Every pod reaches Running without manual intervention.