Deployment
Build the single fabriq image, run schema migrations as a pre-upgrade hook, and deploy the leader-elected worker with the Helm chart.
Fabriq ships as one binary, fabriq, built from cmd/fabriq on
forge/cli RunApp. The same image is the long-lived worker and the one-shot
operator commands — both share one wiring path.
fabriq serve(also the no-arg default) runs the background plane: the leader-elected outbox relay, projection consumers, the reconciler, and the CRDT document plane. This is what the container ENTRYPOINT runs.fabriq migrate | inspect | rebuild | reconcileare operator commands that open their own stores from flags/env and exit. They never serve.fabriq info | health | extensionsare forge built-ins.
There is no separate worker binary. See CLI for the full command surface.
Releases & published artifacts
Every vX.Y.Z git tag publishes three things via GitHub Actions
(.github/workflows/release.yml,
driven by goreleaser):
-
Container image — multi-arch (
linux/amd64,linux/arm64) on GHCR:docker pull ghcr.io/xraph/fabriq:0.2.0 # also :0.2 and :latest -
Helm chart — pushed as an OCI artifact and attached to the release:
helm install fabriq oci://ghcr.io/xraph/charts/fabriq --version 0.2.0 \ --set postgres.existingSecret=pg-conn --set redis.addr=redis:6379 -
Binaries —
linux/darwin×amd64/arm64archives pluschecksums.txton the GitHub Release, for running the operator CLI (fabriq migrate,inspect, …) off-cluster.
Cutting a release is just a tag — git tag v0.2.0 && git push origin v0.2.0.
Pre-release suffixes (v0.2.0-rc.1) are auto-marked pre-release. Versions are
stamped into the binary (fabriq --version) and the image's OCI labels.
The image
The Dockerfile builds a single static, stripped binary and ships it on
distroless/static-debian12:nonroot. The ENTRYPOINT is fabriq serve; the
migrate Job overrides the command.
FROM golang:1.26 AS build
WORKDIR /src
COPY go.mod go.sum ./
COPY vendor/ vendor/
COPY . .
RUN CGO_ENABLED=0 GOFLAGS=-mod=vendor go build -trimpath -ldflags "-s -w" \
-o /out/fabriq ./cmd/fabriq
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/fabriq /usr/local/bin/fabriq
USER nonroot:nonroot
EXPOSE 8081
ENTRYPOINT ["/usr/local/bin/fabriq", "serve"]The build is hermetic from a vendored tree. The repo carries local grove
replace directives pointing outside this checkout (until grove cuts a tagged
release), so a plain docker build cannot resolve them. Build through the
Makefile, which runs go mod vendor first:
make docker-build # = go mod vendor, then docker build -f deploy/docker/DockerfileVendoring is harmless once the replaces are dropped, so the Dockerfile keeps working unchanged after the grove tag.
The Helm chart
The chart in deploy/helm/fabriq deploys the worker Deployment plus a
schema-migration Job. Datastores are external — the chart bundles no
databases. Point it at managed Postgres+Timescale, Redis, and (optionally)
FalkorDB and Elasticsearch.
Configure datastore connections. Each store has its own values block. Only the Postgres DSN is sensitive (it carries credentials); Redis, FalkorDB and Elasticsearch are plain addresses. For production, reference Secrets you (or an operator like CloudNativePG / External Secrets) already manage:
# Postgres: read a full DSN from a managed Secret key (e.g. "uri").
postgres:
existingSecret: pg-conn
dsnKey: uri
redis:
addr: redis-master:6379 # or redis.existingSecret + addrKey
# Optional projection planes — enabling one REQUIRES its connection, and the
# chart fails the install if it is missing.
projections:
search: true
elasticsearch:
existingSecret: es-connAlternatively, compose Postgres from parts with the password pulled from a
Secret (the chart builds a $(VAR) reference so the password never leaves
its Secret), or set postgres.dsn inline for dev/CI (the chart then creates
its own Secret rather than putting the DSN in a ConfigMap):
postgres:
host: pg.internal
user: fabriq_app
database: fabriq
sslmode: require
passwordSecret: rds-creds # an existing Secret
passwordKey: passwordThe connection env the worker and migrate Job resolve is FABRIQ_POSTGRES_DSN
(required), FABRIQ_REDIS_ADDR (required for projections and subscriptions),
FABRIQ_FALKORDB_ADDR (graph projection), and FABRIQ_ELASTICSEARCH_ADDRS
(comma-separated; search projection).
Install or upgrade. The migrate Job runs first as a pre-install,pre-upgrade
hook, so the schema is current before the worker rolls.
helm upgrade --install fabriq deploy/helm/fabriq \
--set postgres.existingSecret=pg-conn --set postgres.dsnKey=uri \
--set redis.addr=redis-master:6379Watch it come up and confirm health.
kubectl rollout status deploy/fabriq
kubectl port-forward svc/fabriq 8081:8081
curl localhost:8081/_/readyz
curl localhost:8081/metricsMigration Job (pre-upgrade hook)
When migrate.enabled (default true), the chart renders a Job annotated
helm.sh/hook: pre-install,pre-upgrade that overrides the container command
to fabriq migrate up. grove holds an advisory migration lock, so parallel
hook runs across releases are safe. The Job resolves the same connection env
as the worker but uses only the Postgres DSN. If you disable the hook, you
must run fabriq migrate up yourself before the worker serves traffic. See
Migrations.
Worker Deployment
The worker container uses the image ENTRYPOINT (fabriq serve) — the chart
does not override the command. It runs as non-root with a read-only root
filesystem (a /tmp emptyDir is mounted writable) and drops all
capabilities.
Config splits across two sources, both injected as env:
- Connection env — the datastore connections above, rendered per store as
valueFrom.secretKeyRef(managed Secrets), a$(VAR)-composed DSN, or a plain value. Inline dev credentials land in a chart-created Secret rather than a ConfigMap. - ConfigMap — non-secret tuning:
FABRIQ_HTTP_ADDR(default:8081) andFABRIQ_RECONCILE_INTERVAL(Go duration; empty uses the binary default of 5m,0disables the scheduled reconciler). Extra plain env merges viaconfig.extraEnv.
A pod-template checksum annotation rolls the workers when the ConfigMap (or the chart-created Secret, when inline credentials are used) changes.
Health probes and metrics
Forge serves health and metrics on FABRIQ_HTTP_ADDR (default :8081,
chart service.port):
| Path | Use |
|---|---|
/_/livez | Liveness probe (chart: periodSeconds: 15). |
/_/readyz | Readiness probe (chart: periodSeconds: 10). |
/_/health | Aggregate health detail. |
/metrics | Prometheus scrape (mounted by fabriq). |
The Service is ClusterIP and not internet-facing. See
Observability for the metric catalog.
Scaling model
The worker is safe to scale (chart default worker.replicaCount: 2):
- Singletons are leader-elected via Postgres advisory locks, so exactly
one replica runs each: the outbox relay (lock
1001), the reconciler (1002), and the document plane materializer (1003). Extra replicas campaign and stand by. A leader that loses its Postgres session abdicates; another replica wins the next campaign. - Projection consumers scale by replica count. The graph and search consumers share Redis Streams consumer groups (no election), so apply throughput grows with replicas.
The chart includes:
- PDB (
pdb.enabled,minAvailable: 1) — keeps a replica during voluntary disruptions so a leader is always available. - HPA (
autoscaling.enabled, CPU target) — optional. For lag-driven scaling of consumers, prefer KEDA on Redis stream lag orfabriq_projection_lag_eventsrather than CPU. - ServiceMonitor (
metrics.serviceMonitor.enabled) — scrapes/metricson thehttpport for a Prometheus Operator install. Without it, the chart addsprometheus.io/scrapepod annotations by default. - NetworkPolicy (
networkPolicy.enabled) — allows ingress to the http port (scrape + health) and DNS egress; you supply datastore egress peers innetworkPolicy.egress.
The worker drains on SIGTERM (~10s); the chart sets
terminationGracePeriodSeconds: 30 for headroom.
Sharded deployments
The chart's connection values render a single FABRIQ_POSTGRES_DSN, so out of
the box they configure a one-shard deployment. To shard the source of truth
across multiple Postgres instances (Sharding), supply a
config.yaml carrying a shards: list — the FABRIQ_* environment overlay
cannot express a list of shards. Mount it (for example a ConfigMap at
/etc/fabriq/config.yaml), and run the migrate step once per shard DSN:
fabriq migrate targets one database at a time. The shard count is fixed at
deploy time.
Bare deployment (without Helm)
Run the same image anywhere with the env vars set:
docker run --rm \
-e FABRIQ_POSTGRES_DSN='postgres://app:...@pg:5432/fabriq?sslmode=require' \
-e FABRIQ_REDIS_ADDR='redis:6379' \
-e FABRIQ_FALKORDB_ADDR='falkordb:6379' \
-e FABRIQ_ELASTICSEARCH_ADDRS='https://es:9200' \
-p 8081:8081 \
ghcr.io/xraph/fabriq:latest # ENTRYPOINT runs `fabriq serve`Run migrations as a discrete step first, as the schema owner:
docker run --rm \
-e FABRIQ_POSTGRES_DSN='postgres://owner:...@pg:5432/fabriq' \
ghcr.io/xraph/fabriq:latest migrate up