Deployment

Run Firn locally with Docker Compose or deploy to production as a standalone container.

Local development

The Docker Compose file launches MinIO (local S3) alongside the published Firn API image. No host Rust toolchain or local compilation is required.

git clone https://github.com/gordonmurray/firnflow
cd firnflow
docker compose up -d

Check readiness with curl http://localhost:3000/health. To build the image from the checkout instead, run docker build -t firnflow:local . and start with FIRNFLOW_IMAGE=firnflow:local docker compose up -d.

This starts three services:

ServicePortPurpose
minio9000 (S3 API), 9001 (console)S3-compatible object storage
minio-init-One-shot: creates the firnflow bucket
firnflow3000Firn API server

MinIO console: http://localhost:9001 (credentials: minioadmin / minioadmin)

Running just MinIO

If you want to run Firn outside Docker (e.g. during development with a host Rust toolchain), start only MinIO:

# Start MinIO only
docker compose up -d minio minio-init

# Run Firn directly
FIRNFLOW_STORAGE_URI=s3://firnflow \
FIRNFLOW_S3_ENDPOINT=http://127.0.0.1:9000 \
FIRNFLOW_S3_ACCESS_KEY=minioadmin \
FIRNFLOW_S3_SECRET_KEY=minioadmin \
  cargo run -p firnflow-api

Production Docker

For production, use the published image or build the included multi-stage Dockerfile yourself.

Published image

docker pull ghcr.io/gordonmurray/firnflow:0.9.5

Build the image

docker build -t firnflow-api .

Image details

StageBase imagePurpose
Builderrust:1.94-bookwormCompiles the release binary with protobuf support
Runtimedebian:bookworm-slimMinimal image with just ca-certificates for TLS

Run the container

docker run -d \
  --name firn \
  -p 3000:3000 \
  -e FIRNFLOW_STORAGE_URI=s3://my-production-bucket \
  -e FIRNFLOW_S3_REGION=eu-west-1 \
  -e FIRNFLOW_CACHE_MEMORY_BYTES=268435456 \
  -e FIRNFLOW_CACHE_NVME_BYTES=10737418240 \
  -v firn-cache:/var/lib/firnflow/cache \
  ghcr.io/gordonmurray/firnflow:0.9.5

If you built the image locally with the preceding command, replace the final image name with firnflow-api.

For native GCS, swap the URI scheme and mount a service-account JSON: -e FIRNFLOW_STORAGE_URI=gs://my-production-bucket -e GOOGLE_APPLICATION_CREDENTIALS=/etc/gcp-sa.json -v /etc/gcp-sa.json:/etc/gcp-sa.json:ro. Everything else stays the same.

NVMe cache volume
Mount a fast local SSD at /var/lib/firnflow/cache for the NVMe tier, which is where the container image points FIRNFLOW_CACHE_NVME_PATH. Note that this overrides the /tmp/firnflow-cache default the server itself uses, so the path to mount comes from the image, not from the binary's default. The cache is ephemeral: losing it only means cache misses until it warms up again. For best performance, use a tmpfs or NVMe-backed volume.

Writable directories

Firn writes to two local directories, and both must be writable by the user the server runs as. Neither is optional in the sense that matters here: the server refuses to start if it cannot write to the one that is enabled.

VariableDefaultImage defaultNeeded when
FIRNFLOW_CACHE_NVME_PATH /tmp/firnflow-cache /var/lib/firnflow/cache Always. This is the NVMe tier of the result cache and it is built during startup.
FIRNFLOW_OBJECT_CACHE_DIR /tmp/firnflow-object-cache unset, so the default applies Only when FIRNFLOW_OBJECT_CACHE_ENABLED=true.
Read-only root filesystems

Under a restricted Pod Security Standard, or any container run with readOnlyRootFilesystem: true, mount a writable volume at each path above that applies to your configuration. Mounting only the object cache directory is the easy mistake, because it is the one the configuration reference discusses at length, while the NVMe result cache is enabled by default and needs a volume whether or not you have configured anything.

The image pre-creates /var/lib/firnflow/cache, so directory creation succeeds and the failure comes later, when the cache opens its block file inside it. That startup error now names both the directory and the variable that selected it; on older releases it reported only Read-only file system (os error 30), with nothing to search for.

volumeMounts:
  - name: firn-cache
    mountPath: /var/lib/firnflow/cache
volumes:
  - name: firn-cache
    emptyDir: {}

AWS deployment

For production on AWS, Firn works best with IAM-based credentials (instance profiles, ECS task roles, or IRSA for EKS). No access keys needed.

Prerequisites

  1. An S3 bucket with public access blocked
  2. An IAM role with s3:GetObject, s3:PutObject, s3:DeleteObject, s3:ListBucket on the bucket
  3. A compute environment (ECS, EKS, EC2) with the IAM role attached

Minimal IAM policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-firn-bucket",
        "arn:aws:s3:::my-firn-bucket/*"
      ]
    }
  ]
}

ECS task definition (excerpt)

{
  "containerDefinitions": [
    {
      "name": "firn",
      "image": "firnflow-api:latest",
      "portMappings": [{"containerPort": 3000}],
      "environment": [
        {"name": "FIRNFLOW_STORAGE_URI", "value": "s3://my-firn-bucket"},
        {"name": "FIRNFLOW_S3_REGION", "value": "eu-west-1"},
        {"name": "FIRNFLOW_CACHE_MEMORY_BYTES", "value": "268435456"},
        {"name": "FIRNFLOW_CACHE_NVME_BYTES", "value": "10737418240"}
      ],
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"],
        "interval": 10,
        "timeout": 3,
        "retries": 3
      },
      "mountPoints": [
        {
          "sourceVolume": "cache",
          "containerPath": "/var/lib/firnflow/cache"
        }
      ]
    }
  ]
}

Authentication and rate limiting

The API ships with optional bearer-token authentication. Production deployments should always set FIRNFLOW_API_KEY. Running without it triggers a startup WARN log line and is unsupported. See configuration for the full env-var list.

Recommended rollout: set FIRNFLOW_API_KEY before exposing the service, roll clients to send Authorization: Bearer …, then set FIRNFLOW_ADMIN_API_KEY to a distinct value, and finally tune the rate-limit knobs. Rollback is to unset the variables and restart. No on-disk schema or cache format change is involved.

# curl with bearer header
curl -X POST https://firn.example.com/ns/production/upsert \
  -H "Authorization: Bearer $FIRNFLOW_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"rows": [...]}'
Service-level only
A token authorises operations against the firnflow process, not against a specific namespace. Per-tenant namespace isolation requires an authenticating gateway in front of Firn that maps tenants to namespaces.

Health checks

Use GET /health for both liveness and readiness probes. The endpoint returns 200 with body ok as soon as the server is listening.

Docker Compose

healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
  interval: 10s
  timeout: 3s
  retries: 3

Kubernetes

livenessProbe:
  httpGet:
    path: /health
    port: 3000
  initialDelaySeconds: 5
  periodSeconds: 10
readinessProbe:
  httpGet:
    path: /health
    port: 3000
  initialDelaySeconds: 5
  periodSeconds: 5

Cache warmup after deployment

After a fresh deployment or restart, the cache is empty. Use the /ns/{ns}/warmup endpoint to pre-populate it with your most common queries:

curl -X POST http://localhost:3000/ns/production/warmup \
  -H 'Content-Type: application/json' \
  -d '{
    "queries": [
      {"vector": [1.0, 0.0, ...], "k": 10},
      {"vector": [0.0, 1.0, ...], "k": 10}
    ]
  }'

This returns immediately (202) and runs the queries in the background, populating the exact cache as it goes. If your production traffic uses semantic caching, include the same semantic_cache block on eligible single-vector warmup queries so the in-memory semantic sidecar is seeded too. Monitor firnflow_cache_misses_total to track warmup progress and firnflow_semantic_cache_hits_total / firnflow_semantic_cache_rejections_total to confirm the semantic layer is behaving as expected.

Recommended operational setup