跳转到内容

Deploy on Kubernetes

此内容尚不支持你的语言。

There is no shipped chart. The manifests below are the minimum that reflects how the server actually boots: a Job that provisions the database before the Deployment rolls, two probes answering two different questions, and configuration out of a Secret.

apiVersion: v1
kind: Secret
metadata:
name: oauth-server
stringData:
MONGODB_URI: mongodb+srv://user:password@cluster.example.mongodb.net
ISSUER: https://auth.example.com
DATABASE_NAME: OAuth

For PostgreSQL, the Secret holds POSTGRES_URL and ISSUER instead, since the database name is part of that URL, and the Job’s command becomes db:setup:pg. Set one connection string, never both. The server refuses to start when both are set instead of picking one, and that is the failure mode you want: a server that silently chose the other database looks exactly like total data loss. PostgreSQL covers it.

Only the connection string is a credential; keeping all of them together is simply one envFrom instead of two. Each variable is described in the environment variables reference. The published image tags are listed on the GitHub releases page and on the container package page.

apiVersion: batch/v1
kind: Job
metadata:
name: oauth-server-setup-<version>
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- name: setup
image: ghcr.io/redfox-soft/oauth-server-ts:<version>
command: ['sh', '-c', 'bun run db:setup && bun run db:migrate']
envFrom:
- secretRef:
name: oauth-server

Run this to completion before rolling the Deployment, and gate the rollout on it: an argocd.argoproj.io/hook: PreSync annotation, a Helm pre-install,pre-upgrade hook, or simply kubectl wait --for=condition=complete job/oauth-server-setup-<version> in your pipeline.

The reason is the same one that puts release_command in the shipped fly.toml: without the reserved admin project, bucket and client the seed creates, GET /admin/login redirects to /auth with a client_id no client matches and the console is unreachable, while the first-run setup page still answers. From outside, that is easy to mistake for a login bug when the seed is what is missing. The Job also provisions the TTL and unique-e-mail indexes the collections would otherwise be created without.

db:migrate follows it in the same Job because the server refuses to start against a database whose migrations have not run: a release that declares one would otherwise roll out pods that cannot boot. Both are safe to repeat; db:setup exits non-zero when a constraint it declares is not in force, so a failed Job is a real signal and should stop the rollout. Name the Job after the release (as above) so each upgrade runs its own, and take a backup before one whose release notes name a migration that cannot be undone — bun run db:migrate --plan lists what would run and exits non-zero when work is outstanding, so a pipeline can stop there for it.

apiVersion: apps/v1
kind: Deployment
metadata:
name: oauth-server
spec:
replicas: 1
selector:
matchLabels:
app: oauth-server
template:
metadata:
labels:
app: oauth-server
spec:
containers:
- name: oauth-server
image: ghcr.io/redfox-soft/oauth-server-ts:<version>
ports:
- containerPort: 3000
envFrom:
- secretRef:
name: oauth-server
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 3000
periodSeconds: 30
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
memory: 1Gi
---
apiVersion: v1
kind: Service
metadata:
name: oauth-server
spec:
selector:
app: oauth-server
ports:
- port: 80
targetPort: 3000

The two probes answer two different questions and must not share a path. /health says the process is alive: it touches nothing outside itself, is served unconditionally with no feature flag behind it, and is the only route the rate limiter never counts — a refused liveness probe would restart a healthy pod. /ready says the pod can actually serve, which means reaching the datastore; when the database goes away it fails and the pod leaves the endpoints list, and when the database comes back it passes again within about a second, with no restart in between.

Both mistakes are worth naming, because they fail in opposite directions. Pointing the readiness probe at /health makes an outage invisible: the process is alive, so the probe passes, and the pod keeps receiving requests it cannot serve. Pointing the liveness probe at /ready kills every replica for the database’s outage, in a loop, and a restart fixes nothing.

Terminate TLS at the Ingress and set ISSUER to the public https URL; the reverse proxy page applies to an Ingress controller exactly as it does to nginx, including the rateLimit.trustedProxy decision.

Every release is signed with cosign over its digest, keyless, and carries an SBOM and build provenance. A cluster can therefore refuse to run anything at this image name that did not come from this project’s release workflow. Whichever admission controller you use (sigstore policy-controller, Kyverno’s verifyImages, Connaisseur), a keyless policy needs the same two values, and they are what gives such a policy any meaning:

Field Value
OIDC issuer https://token.actions.githubusercontent.com
Subject (regexp) ^https://github\.com/RedFox-Soft/OAuth-server\.ts/\.github/workflows/release\.yml@refs/tags/v

A policy that checks for a signature without pinning that identity admits anything anyone has signed, which is every image on the internet. The same two values verify by hand before you roll: cosign verify and the attestations.

  • Sessions, tokens, codes and interactions live in the database and never in process memory, so more than one replica is fine as far as the protocol is concerned.
  • Signing keys need no restart. Every issuer’s key set, the instance’s included, is read from the database and cached for thirty seconds per replica, so a generated, promoted or retired key reaches every replica within that time — which is why a new key cannot be promoted to signing until it has been published for a minute.
  • Server settings are the exception, as the caution above says: a save applies to the replica that served it, and the others take it on their next restart (kubectl rollout restart deployment/oauth-server).