Skip to content

P2: Reference deployment manifests — k8s + prod Compose (#179) - #196

Open
dkijania wants to merge 2 commits into
mainfrom
docs/deploy-manifests
Open

P2: Reference deployment manifests — k8s + prod Compose (#179)#196
dkijania wants to merge 2 commits into
mainfrom
docs/deploy-manifests

Conversation

@dkijania

Copy link
Copy Markdown
Contributor

What & why

Part of the production-readiness epic (#163). Closes #179.

There were no production deployment artifacts — operators had npm/Docker/Compose for dev but no opinionated manifest with probes, resource limits, and a hardened runtime.

Adds deploy/

  • kubernetes.yamlDeployment + Service + HorizontalPodAutoscaler (+ placeholder Secret) with production defaults:
    • liveness on /healthcheck, readiness on /readiness
    • resource requests/limits, 2→6 CPU autoscaler
    • hardened pod securityContext (non-root, readOnlyRootFilesystem, no privilege escalation, all caps dropped, RuntimeDefault seccomp)
    • Prometheus scrape annotations for /metrics
    • terminationGracePeriodSeconds: 30 matching the graceful-shutdown drain
  • docker-compose.prod.yml — the published image against an external read-only Postgres, with CPU/memory caps.
  • README.md — usage + how it maps to the security deployment contract.

Linked from the root README. References the probe/metrics endpoints delivered by the sibling P1 PRs (#169/#173).

Testing

Docs/manifests only. prettier --debug-check . clean; YAML structure validated. No application code changed.

🤖 Generated with Claude Code

There were no production deployment artifacts — operators had npm/Docker/Compose
for dev but no opinionated manifest with probes, resource limits, and a hardened
runtime.

Add deploy/:
- kubernetes.yaml — Deployment + Service + HPA (+ placeholder Secret) with
  liveness (/healthcheck) and readiness (/readiness) probes, resource
  requests/limits, a 2→6 CPU autoscaler, Prometheus scrape annotations for
  /metrics, a hardened pod securityContext (non-root, readOnlyRootFilesystem,
  no privilege escalation, all caps dropped, RuntimeDefault seccomp), and a
  30s termination grace period matching the graceful-shutdown drain.
- docker-compose.prod.yml — the published image against an external read-only
  Postgres, with CPU/memory caps.
- README.md — usage and how this maps to the security deployment contract.

Linked from the root README. References the probe/metrics endpoints delivered by
the sibling P1 PRs.

Closes #179.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSuak9smCHbp4N17xjjLF6
@dkijania dkijania added documentation Improvements or additions to documentation production-readiness Work toward making the API production-ready / publicly available P2 GA polish / hygiene labels Jun 29, 2026
@SanabriaRusso

Copy link
Copy Markdown
Collaborator

Really glad to see opinionated production manifests land — the hardened pod securityContext, HPA, and Secret-based PG_CONN are exactly right, and runAsUser: 1001 correctly matches the Dockerfile's nodeuser. Two things worth tightening before this becomes the copy-paste reference:

1. CORS will silently block the mina-explorer once #184 merges. The Explorer is a cross-origin browser app (mina-explorer/src/config/networks.ts POSTs from its UI origin to https://*-archive-node-api.*.o1test.net), so CORS_ORIGIN is load-bearing for it. Today the server defaults to * when unset (docs/getting-started.md), so the commented-out var happens to work — but the inline note "leave unset for same-origin only" isn't accurate (unset = all origins today), and once #184 flips the default to deny-by-default this reference deploy will reject every browser client, the Explorer included. Since the primary consumer is cross-origin, I'd ship the example as a set value rather than commented-out:

- name: CORS_ORIGIN
  # The mina-explorer (and any browser UI) is cross-origin and CANNOT reach this
  # API unless its origin is allowlisted here. Comma-separate multiple origins.
  # NB: the server currently defaults to '*' when unset; #184 changes that to deny.
  value: 'https://explorer.example.com'

(same note applies to docker-compose.prod.yml.)

2. The readiness probe targets /readiness, which no released image serves yet. /readiness comes from #187 (still open) — on main and the current ghcr.io/o1-labs/archive-node-api image, yoga returns 404 for that path, so the probe never passes, pods never go Ready, and the Service ends up with zero endpoints (full outage) for anyone applying this as-is. The /metrics scrape annotations (#191) and the docs/security.md links (#186) are in the same boat. Could we gate these on the siblings, or add a one-line caveat in the manifest (e.g. "readiness/metrics require v1.x+ — see #187/#191")? The description mentions #169/#173, but those are the issue numbers and read as already-delivered.

Everything else looks solid to me.

The readiness probe targets /readiness, which only exists from 1.0.0, but
the manifests pulled :latest — today that resolves to 0.0.6. Applied
as-is the probe would 404 forever, no pod would reach Ready, and the
Service would be left with zero endpoints: a total outage from a manifest
offered as the copy-paste reference. Both manifests now pin 1.0.0 and
state the requirement up front. The /metrics scrape annotations had the
same dependency.

CORS_ORIGIN shipped commented out, described as "leave unset for
same-origin only". Unset blocks every cross-origin browser client — the
mina-explorer included — and does so silently, with nothing in the server
logs. Since the primary consumers are browsers, it now ships set, with
the trade-off spelled out.

deploy/README.md repeated the root README's claim that pointing PG_CONN
at replicas buys throughput. It buys failover: postgres.js scopes
hostIndex per Connection, so every pooled connection starts at host[0]
and only advances on failure. Real read scaling needs a balancer in front
of Postgres.

Also sets TRUST_PROXY=1 in the k8s manifest (an ingress adds a hop, and
the default of 0 would bucket every client together) and corrects
terminationGracePeriodSeconds from "matches" to "exceeds"
SHUTDOWN_TIMEOUT_MS, which is the property that actually matters.

Verified with kubectl apply --dry-run=client and docker compose config.

Addresses review feedback on #196.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dkijania

Copy link
Copy Markdown
Contributor Author

Thanks @SanabriaRusso — both fixed in d3d2afe.

The /readiness outage. This was the one worth catching: :latest resolves to 0.0.6 today, so the probe would 404 forever, no pod would reach Ready, and the Service would sit with zero endpoints — a total outage from the manifest we're offering as the copy-paste reference. Rather than gate on siblings or add an issue-number caveat, both manifests now pin 1.0.0 and state the requirement in a banner at the top. Pinning is what the file already told people to do while doing the opposite, and it makes the version contract enforced rather than advisory. The /metrics annotations had the identical dependency and are covered by the same note.

CORS. Agreed and done — it now ships set rather than commented out, since the primary consumers are cross-origin browsers. I used * for the reference (this is a public read API; docs/security.md in #186 now explains when an allowlist is the better call) and the inline comment spells out both options plus the silent-failure mode. The inaccurate "leave unset for same-origin only" note is gone from both files.

Three more found while in here:

  1. deploy/README.md told operators to "point PG_CONN at read replicas for throughput" — same false claim you flagged on P2: Operations runbook — SLOs, capacity, incidents, failover (#180) #197. I verified it against the driver: postgres.js scopes hostIndex per Connection (src/connection.js:89), so every pooled connection starts at host[0] and only advances on failure. Failover, not fan-out. Now points at a real balancer.

  2. terminationGracePeriodSeconds: 30 was documented as matching the app's shutdown window (SHUTDOWN_TIMEOUT_MS, 10s). It needs to exceed it, which is the property that actually matters — 30 was already fine, the comment was wrong. SHUTDOWN_TIMEOUT_MS is now set explicitly so the relationship is visible rather than implied.

  3. Set TRUST_PROXY: '1' in the k8s manifest — an ingress adds a hop, and after the P0: Add per-IP request rate limiting (#166) #185 fix the default of 0 ignores X-Forwarded-For and buckets every client behind the ingress together.

Verified with kubectl apply --dry-run=client (all four resources) and docker compose config.

One thing to confirm: if #191 adopts the ENABLE_METRICS gating you suggested, this manifest needs ENABLE_METRICS=true for the scrape annotations to do anything. I've left it out rather than bake in a decision that hasn't been made — worth a note on #191 so whoever lands it remembers this file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation P2 GA polish / hygiene production-readiness Work toward making the API production-ready / publicly available

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P2: Reference deployment artifacts (k8s/Helm/Compose-prod) + resource limits

2 participants