Deploy
How to run Maidan locally and in a Kubernetes cluster. Refer to Architecture.md for what each component does.
Local: Docker Compose
Prod-style stack
Builds the production image from crates/maidan-server/Dockerfile and a
custom Postgres image (pgvector base; schema is applied at runtime by
maidan-server, not baked into the image).
docker compose up # postgres only
docker compose --profile full up # + minio + maidan-server
curl http://localhost:8080/health # after maidan-server lands /health
Publishing the host port
MAIDAN_BIND is the address inside the container (0.0.0.0:8080 by default)
and stays on 8080. What the
host sees is the published mapping, and 8080 is one of the most contended
ports on a developer machine — if something else already owns it, the bind
fails and the stack does not start at all. Override the host side:
MAIDAN_HOST_PORT=18080 docker compose --profile full up
curl http://localhost:18080/health
MAIDAN_PEER_HOST_PORT (default 8081) does the same for the second server
in the federation profile, and compose.quickstart.yaml honours
MAIDAN_HOST_PORT too. The scale and OTLP profiles already had their own
(MAIDAN_SCALE_LB_PORT, MAIDAN_OTLP_PORT).
A consumer on the same Docker network needs no published port at all.
Publishing exists to reach the server from the host; container-to-container
traffic goes to the service name and the container port directly — so an
adjacent compose project that joins Maidan's network reaches it at
http://maidan-server:8080 whether or not a host mapping exists, and is
unaffected by a conflict on 8080. Publish only what a human or a
host-resident process has to reach.
Hot-reload dev stack
docker compose -f compose.dev.yaml up
maidan-server runs under cargo watch with the workspace mounted as a
volume, so source edits trigger an in-container rebuild.
To leave the server outside the container and only run the deps:
docker compose -f compose.dev.yaml up postgres minio
MAIDAN_ALLOW_INSECURE_DEV_KEK=1 \
MAIDAN_SESSION_SECRET=dev-session-secret-change-me-0123456789 \
DATABASE_URL=postgres://maidan:maidan@localhost:5432/maidan cargo run --bin maidan-server
Without Docker (SQLite)
For pure host development against SQLite — no docker compose needed:
MAIDAN_ALLOW_INSECURE_DEV_KEK=1 \
MAIDAN_SESSION_SECRET=dev-session-secret-change-me-0123456789 \
DATABASE_URL='sqlite://./dev.db?mode=rwc' cargo run --bin maidan-server
Or against an in-memory SQLite (lost on shutdown):
MAIDAN_ALLOW_INSECURE_DEV_KEK=1 \
MAIDAN_SESSION_SECRET=dev-session-secret-change-me-0123456789 \
DATABASE_URL=sqlite::memory: cargo run --bin maidan-server
MAIDAN_ALLOW_INSECURE_DEV_KEK=1 is the explicit development opt-in to a public
content key-encryption key; without it, or a real MAIDAN_CONTENT_KEK, the server
refuses to start (Production.md).
MAIDAN_SESSION_SECRET (at least 32 bytes) signs browser sessions and
subscribe-resume tokens. The server refuses to start without it when OIDC is on,
and when authentication is on without OIDC unless MAIDAN_SUBSCRIBE_RESUME_SECRET
is set instead; with authentication off it falls back to a built-in test secret.
?mode=rwc lets SQLite create dev.db on first run.
The server detects the dialect from the DATABASE_URL prefix
(postgres://, postgresql://, or sqlite:) and selects the
appropriate migration runner and backend automatically.
Kubernetes
Manifests are under k8s/ and use Kustomize.
k8s/
├── base/ # canonical resources
└── overlays/
├── dev/ # local kind/minikube
└── prod/ # production cluster
Dev cluster (kind)
kind create cluster --name maidan
# build images and load them into the cluster
docker build -t maidan-server:dev -f crates/maidan-server/Dockerfile .
docker build -t maidan-postgres:dev -f docker/Dockerfile.db .
kind load docker-image maidan-server:dev --name maidan
kind load docker-image maidan-postgres:dev --name maidan
kubectl apply -k k8s/overlays/dev
kubectl -n maidan rollout status deploy/maidan-server
kubectl -n maidan port-forward svc/maidan-server 8080:8080
curl http://localhost:8080/health
Production cluster
The prod overlay is a template. Before applying:
-
Set the real image registry + tag in
k8s/overlays/prod/kustomization.yaml. -
Adjust the Ingress host (
maidan.example.complaceholder) and TLS secret name. -
Apply the
maidan-secretsSecret out-of-band using sealed-secrets, external-secrets, or a cloud-managed CSI secret provider. -
Apply the overlay:
kubectl apply -k k8s/overlays/prod
Helm
helm/maidan installs the server against a database and object store you
already run. helm/maidan-stack wraps it and can run its own Postgres
(maidan-postgres, the pgvector image above) and MinIO (Chainguard's, by the
digest compose uses), wiring the server to both. Values, refusals and the
install commands are in Production.md and
helm/maidan-stack/README.md. To try the stack on the kind cluster above:
helm install demo helm/maidan-stack -f helm/maidan-stack/values-ci.yaml \
--set maidan.contentKek="$(openssl rand -hex 32)" -n maidan-stack --create-namespace
Required secret keys
| Key | Required? | Notes |
|---|---|---|
DATABASE_URL | yes | Postgres connection string. |
MAIDAN_CONTENT_KEK | yes | 32-byte key-encryption key (openssl rand -hex 32); wraps per-message content keys. Keep it out of data backups. |
MAIDAN_CONTENT_KEK_PREVIOUS | during a rotation | Comma-separated retired KEKs still able to unwrap. |
MAIDAN_SESSION_SECRET | yes | At least 32 bytes; signs sessions and subscribe-resume tokens. Must match across replicas. Required with OIDC; without OIDC, MAIDAN_SUBSCRIBE_RESUME_SECRET can stand in for it. |
S3_ENDPOINT | only if S3 backend | |
S3_BUCKET | only if S3 backend | |
S3_REGION | only if S3 backend | |
S3_ACCESS_KEY_ID | only if S3 backend | |
S3_SECRET_ACCESS_KEY | only if S3 backend | |
OTLP_ENDPOINT | optional | OTLP gRPC for traces; metrics when OTLP_METRICS=1. |
OTLP_METRICS | optional | Set 1 to push /metrics instruments via OTLP. |
base/secret.example.yaml documents the contract but contains no real
values.
Image build matrix
| Image | Dockerfile | Purpose |
|---|---|---|
maidan-server | crates/maidan-server/Dockerfile | Production binary. |
maidan-server | crates/maidan-server/Dockerfile.dev | Dev hot-reload. |
maidan-postgres | docker/Dockerfile.db | Postgres + pgvector. |
maidan-cli | docker/Dockerfile.cli | The maidan CLI, pinned to the server's tag. |
| (local only) | docker/Dockerfile.quickstart | Built by compose.quickstart.yaml from a pinned release binary; not published, not for production. |
Migrations
maidan-server is the single source of truth for schema; it applies
all pending migrations on boot via run_postgres_migrations (or
run_sqlite_migrations, depending on the dialect). The
maidan-postgres image is a thin pgvector layer that does not
bundle schema into docker-entrypoint-initdb.d — fresh volumes and
upgrades go through the same code path.
A rollout that keeps the previous binary serving while the new one migrates follows Migrations. The migration that runs in that window only expands. A drop or a rewrite waits for a later contract. A migration that cannot expand is a cutover: stop the previous binary first.