Ion Drive is a single stateless-ish container (all state lives in PostgreSQL),
which makes it straightforward to run on Kubernetes — with one important caveat
about replicas, covered below.
No published images yet. Container images are not published to a registry
(that is part of the planned release pipeline). Build the image locally from
the repo and push it to your own registry first:
The manifests below are reference manifests — reviewed against the code,
not certified against a specific cluster. Adjust names, sizes, and ingress
class to your environment.
All configuration is environment variables (see the
Docker guide for the full table). Split them the
usual way: credentials in a Secret, everything else in a ConfigMap.
ION_ENCRYPTION_KEY and ION_AUTH_SECRET are load-bearing: the server
refuses to boot with NODE_ENV=production unless at least one is set,
and losing the encryption key makes stored secrets unrecoverable (see
Backup & Restore). Treat both like database credentials.
The image listens on port 3000, runs as a non-root user (uid 1001), and serves
GET /health (unauthenticated, exempt from rate limiting) — use it for both
probes.
apiVersion: apps/v1
kind: Deployment
metadata:
name: ion-drive
spec:
replicas: 1# see the replica caveat below before raising this
selector:
matchLabels: { app: ion-drive }
template:
metadata:
labels: { app: ion-drive }
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "3000"
prometheus.io/path: /metrics
spec:
containers:
- name: ion-drive
image: registry.example.com/ion-drive:0.1.0
ports:
- containerPort: 3000
envFrom:
- secretRef: { name: ion-drive-secrets }
- configMapRef: { name: ion-drive-config }
readinessProbe:
httpGet: { path: /health, port: 3000 }
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet: { path: /health, port: 3000 }
initialDelaySeconds: 15
periodSeconds: 20
resources:
requests: { cpu: 250m, memory: 256Mi }
limits: { memory: 512Mi }
Replica caveat — read before scaling past 1.
Two background subsystems run in-process on every instance:
Scheduled tasks (tasks/, croner-based) have no cross-replica
coordination. Every replica loads all enabled tasks and schedules them
independently, so a cron task fires once per replica. Croner’s
protect option only prevents a run overlapping itself within one
process — it does nothing across pods.
Event deliveries (messaging/) are replica-safe: each
(event, consumer group) pair is claimed atomically via the composite
primary key on _ion_event_deliveries, so a subscription handler runs on
exactly one instance per event (at-least-once — handlers should be
idempotent on the event id). Subscriptions marked perInstance
intentionally run on every replica.
Until a cross-replica scheduler lands (Phase 12+), either run a single
replica, or split into two Deployments: one 1-replica “worker” with
ION_TASKS_ENABLED=true and an N-replica API tier with
ION_TASKS_ENABLED=false. (Env vars are per pod template, so you cannot mix
them inside one Deployment.)
Do not run Postgres inside the Ion Drive pod. All platform state lives in
that one database; give it a real home:
a managed instance (RDS, Cloud SQL, Neon, …) — simplest and recommended, or
a Postgres operator in-cluster, e.g. CloudNativePG,
which handles volumes, failover, and backups properly.
Point ION_DATABASE_URL at it (add ?sslmode=require when the network path
isn’t private) and keep the database unreachable from outside the cluster/VPC.
When ION_METRICS_ENABLED=true (the default) the server exposes Prometheus
text at GET /metrics on the main port. The pod annotations in the Deployment
above work with annotation-based Prometheus discovery; if you run the
Prometheus Operator, use a PodMonitor/ServiceMonitor targeting port 3000
and path /metrics instead.
/metrics is not authenticated and is exempt from rate limiting. Don’t
route it through the public Ingress — keep it cluster-internal (the manifests
above only expose it inside the cluster, which is what you want). See the
security checklist.