tidaldb/k8s/statefulset.yaml
jx12n c22a3b65a6 docs: withdraw the pre-release "not ready for production" disclaimer
M0-M12 are shipped and the HA cluster runs in production on k3s, so the
pre-release disclaimer no longer describes the project. Removes it from the
canonical doc set and corrects the readiness text that had gone stale.

- README.md: replace the "Pre-release / not yet recommended for production"
  banner with a production-ready statement; drop "(experimental)" from the
  cluster status bullet; state the post-1.0 versioning posture (additive in
  minor releases, breaking changes get a documented migration path).
- CLAUDE.md / QUICKSTART.md / docs/guides/server-deployment.md /
  docs/runbooks/cluster.md: same withdrawal; reframe the cluster opt-in as a
  guard against standing up a multi-node fabric by accident rather than a
  readiness warning.
- CHANGELOG.md: record the stability posture under [Unreleased], superseding
  the historical 0.1.0 "no stability guarantees" note (left intact as history).
- k8s/statefulset.yaml: the "NOT production HA, tracked as m8p10" comment was
  stale (m8p10 shipped); point at k8s/cluster/ for the HA deployment instead.

Also corrects text that was factually wrong since m11p3/m11p4: the
multi-process cluster gate, its CLI help, and the served OpenAPI description
all still claimed quorum-ack writes and automatic failure detection did not
exist. They do.

Historical records (docs/reviews/, docs/profiling/, past CHANGELOG entries,
the kubernetes.md rc7 fix note) are left unchanged.

Verified against a running binary, not just the build: the opt-in gate's
refusal message, the startup WARN, /health 200, and the served
/openapi.json description all carry the new text. cargo fmt clean; clippy
-D warnings clean on tidaldb and the tidal-server lib; 1943 engine + 155
server lib tests pass; scripts/check-docs.sh OK.

Claude-Session: https://claude.ai/code/session_01QdqSDw1tUhK1JT9Pb1vryP
2026-07-30 19:03:34 -06:00

144 lines
5.5 KiB
YAML

# tidalDB standalone server as a StatefulSet.
#
# WHY A STATEFULSET (not a Deployment): tidalDB is single-node-first and
# embeddable — the server wraps ONE engine instance whose state (WAL +
# checkpoints + indexes) lives on a durable data dir. It scales VERTICALLY
# (bigger node), not by adding replicas. `replicas: 1` is intentional and load-
# bearing: there is no shared-storage multi-writer mode. If you need multi-node
# HA, that is a DIFFERENT deployment: the multi-process `cluster` subcommand
# (one process per region, quorum-acked writes, automatic failover) ships as its
# own StatefulSet under k8s/cluster/ — it does not change this manifest.
# See docs/runbooks/kubernetes.md and docs/runbooks/cluster.md.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: tidaldb
namespace: tidaldb
labels:
app.kubernetes.io/name: tidaldb
app.kubernetes.io/component: server
spec:
serviceName: tidaldb # the headless Service in service.yaml — stable network id
replicas: 1 # single-node-first: scale up, not out (see header)
selector:
matchLabels:
app.kubernetes.io/name: tidaldb
template:
metadata:
labels:
app.kubernetes.io/name: tidaldb
app.kubernetes.io/component: server
annotations:
# Plain-Prometheus scrape hints (the PodMonitor/ServiceMonitor in
# servicemonitor.yaml is the Operator-native alternative). Metrics are
# unauthenticated — keep :9091 cluster-internal (see ops/monitoring.md).
prometheus.io/scrape: "true"
prometheus.io/port: "9091"
prometheus.io/path: "/metrics"
spec:
# SIGTERM flips readiness to 503 (pod leaves Endpoints), drains in-flight
# requests, then checkpoints + fsyncs the WAL before exit. Give that room.
terminationGracePeriodSeconds: 60
securityContext:
runAsNonRoot: true
runAsUser: 10001 # the `tidal` user baked into docker/deploy/Dockerfile
runAsGroup: 10001
fsGroup: 10001 # makes the mounted PVC group-writable by the runtime user
seccompProfile:
type: RuntimeDefault
containers:
- name: tidaldb
# For a real cluster, replace with your registry image pinned by digest
# (e.g. registry.example.com/tidaldb@sha256:...) and set
# imagePullPolicy: IfNotPresent. `tidaldb:deploy` is the local image
# built from docker/deploy/Dockerfile and loaded via `kind load`.
image: tidaldb:deploy
imagePullPolicy: IfNotPresent
# ENTRYPOINT is the bare binary; these args override the image CMD so
# the schema comes from the mounted ConfigMap, not the baked default.
args:
- standalone
- --listen
- 0.0.0.0:9400
- --schema
- /etc/tidaldb/schema/schema.yaml
- --data-dir
- /data
- --metrics
- 0.0.0.0:9091
env:
- name: TIDAL_API_KEY
valueFrom:
secretKeyRef:
name: tidaldb-api-key
key: api-key
- name: TIDAL_SERVER_LOG
value: info
ports:
- name: http
containerPort: 9400
- name: metrics
containerPort: 9091
# Three distinct probes map to the three health endpoints:
# - /health/startup : always 200 once the HTTP listener is up
# - /health/live : always 200 while the process is alive
# - /health : 200 ready / 503 while draining on SIGTERM
# All are unauthenticated by design, so the probes need no token.
startupProbe:
httpGet:
path: /health/startup
port: http
periodSeconds: 5
failureThreshold: 60 # up to ~5 min for large-DB WAL replay / index load (capacity-planning.md)
livenessProbe:
httpGet:
path: /health/live
port: http
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /health # 503 during drain -> removed from Service Endpoints
port: http
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
resources:
requests:
cpu: "250m"
memory: 256Mi
limits:
cpu: "2"
memory: 2Gi # size from docs/ops/capacity-planning.md for your item/embedding count
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true # the server only writes /data (PVC) and /tmp (emptyDir)
capabilities:
drop: ["ALL"]
volumeMounts:
- name: data
mountPath: /data
- name: schema
mountPath: /etc/tidaldb/schema
readOnly: true
- name: tmp
mountPath: /tmp
volumes:
- name: schema
configMap:
name: tidaldb-schema
- name: tmp
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: data
labels:
app.kubernetes.io/name: tidaldb
spec:
accessModes: ["ReadWriteOnce"]
# storageClassName: "" # uncomment + set to pin a class; omitted = cluster default
resources:
requests:
storage: 10Gi