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:

  1. Set the real image registry + tag in k8s/overlays/prod/kustomization.yaml.

  2. Adjust the Ingress host (maidan.example.com placeholder) and TLS secret name.

  3. Apply the maidan-secrets Secret out-of-band using sealed-secrets, external-secrets, or a cloud-managed CSI secret provider.

  4. 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

KeyRequired?Notes
DATABASE_URLyesPostgres connection string.
MAIDAN_CONTENT_KEKyes32-byte key-encryption key (openssl rand -hex 32); wraps per-message content keys. Keep it out of data backups.
MAIDAN_CONTENT_KEK_PREVIOUSduring a rotationComma-separated retired KEKs still able to unwrap.
MAIDAN_SESSION_SECRETyesAt 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_ENDPOINTonly if S3 backend
S3_BUCKETonly if S3 backend
S3_REGIONonly if S3 backend
S3_ACCESS_KEY_IDonly if S3 backend
S3_SECRET_ACCESS_KEYonly if S3 backend
OTLP_ENDPOINToptionalOTLP gRPC for traces; metrics when OTLP_METRICS=1.
OTLP_METRICSoptionalSet 1 to push /metrics instruments via OTLP.

base/secret.example.yaml documents the contract but contains no real values.

Image build matrix

ImageDockerfilePurpose
maidan-servercrates/maidan-server/DockerfileProduction binary.
maidan-servercrates/maidan-server/Dockerfile.devDev hot-reload.
maidan-postgresdocker/Dockerfile.dbPostgres + pgvector.
maidan-clidocker/Dockerfile.cliThe maidan CLI, pinned to the server's tag.
(local only)docker/Dockerfile.quickstartBuilt 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.