Running Fluree with Docker
The official image (fluree/server) ships the fluree binary on a slim Debian base. This guide covers what's inside the image, how to configure it (env vars, mounted config files, CLI flags), and worked recipes for the common production patterns.
What's in the Image
| Aspect | Value |
|---|---|
| Base | debian:trixie-slim |
| Entrypoint | /usr/local/bin/fluree-entrypoint.sh |
| Default command | fluree server run |
WORKDIR | /var/lib/fluree |
VOLUME | /var/lib/fluree |
| Exposed port | 8090 |
| Runtime user | fluree (UID 1000, GID 1000) |
| Healthcheck | GET /health every 30s |
| Default log filter | RUST_LOG=info |
Entrypoint behavior: on first start, if /var/lib/fluree/.fluree/ does not exist, the entrypoint runs fluree init to create a default .fluree/config.toml and .fluree/storage/ directory. Subsequent starts skip init. Any arguments passed to docker run after the image name are forwarded to fluree server run, so you can append CLI flags (e.g. --log-level debug) directly.
Quick Start
docker run --rm -p 8090:8090 fluree/server:latest
Verify:
curl http://localhost:8090/health
Data lives inside the container's writable layer here — fine for trying things out, lost when the container is removed. For anything beyond a smoke test, mount a volume.
Persisting Data
The image declares VOLUME /var/lib/fluree. Mount a host directory or named volume there:
# Named volume (recommended)
docker run -d --name fluree \
-p 8090:8090 \
-v fluree-data:/var/lib/fluree \
fluree/server:latest
# Host bind mount — make sure the directory is writable by UID 1000
mkdir -p ./fluree-data && sudo chown 1000:1000 ./fluree-data
docker run -d --name fluree \
-p 8090:8090 \
-v "$PWD/fluree-data:/var/lib/fluree" \
fluree/server:latest
The volume holds both .fluree/config.toml (config) and .fluree/storage/ (ledger data) by default.
Three Ways to Configure
Fluree resolves configuration with this precedence (highest wins):
- CLI flags appended after the image name
- Environment variables (
FLUREE_*) set with-eorenvironment: - Profile overrides (
[profiles.<name>.server]) when you pass--profile - Config file at
.fluree/config.tomlor.fluree/config.jsonld - Built-in defaults
You can use any one of these — or, more typically, layer them: bake a base config file into a volume, then tweak per-environment with env vars or compose overrides.
Heads up — log level: The Dockerfile sets
ENV RUST_LOG=info. The console log filter usesRUST_LOGif it is non-empty and only falls back toFLUREE_LOG_LEVELwhenRUST_LOGis unset. Inside this image you must overrideRUST_LOGto change console verbosity:docker run -e RUST_LOG=debug fluree/server:latest
1. Environment Variables Only
Every CLI flag has a FLUREE_* env var equivalent (see Configuration). For simple deployments this is the lowest-friction path:
docker run -d --name fluree \
-p 8090:8090 \
-v fluree-data:/var/lib/fluree \
-e FLUREE_LISTEN_ADDR=0.0.0.0:8090 \
-e FLUREE_STORAGE_PATH=/var/lib/fluree/.fluree/storage \
-e FLUREE_INDEXING_ENABLED=true \
-e FLUREE_REINDEX_MIN_BYTES=1000000 \
-e FLUREE_REINDEX_MAX_BYTES=10000000 \
-e FLUREE_CACHE_MAX_MB=2048 \
-e RUST_LOG=info \
fluree/server:latest
2. Mounted Config File (JSON-LD or TOML)
Author a config file on the host, then mount it at /var/lib/fluree/.fluree/config.jsonld (or .toml). The server walks up from WORKDIR=/var/lib/fluree and picks it up automatically.
./fluree-config/config.jsonld:
{
"@context": { "@vocab": "https://ns.flur.ee/config#" },
"server": {
"listen_addr": "0.0.0.0:8090",
"storage_path": "/var/lib/fluree/.fluree/storage",
"log_level": "info",
"cache_max_mb": 2048,
"indexing": {
"enabled": true,
"reindex_min_bytes": 1000000,
"reindex_max_bytes": 10000000
}
},
"profiles": {
"prod": {
"server": {
"log_level": "warn",
"cache_max_mb": 8192
}
}
}
}
docker run -d --name fluree \
-p 8090:8090 \
-v fluree-data:/var/lib/fluree \
-v "$PWD/fluree-config/config.jsonld:/var/lib/fluree/.fluree/config.jsonld:ro" \
fluree/server:latest --profile prod
If both config.toml and config.jsonld exist in the same directory, TOML wins and the server logs a warning. Pick one format.
The TOML equivalent (./fluree-config/config.toml):
[server]
listen_addr = "0.0.0.0:8090"
storage_path = "/var/lib/fluree/.fluree/storage"
log_level = "info"
cache_max_mb = 2048
[server.indexing]
enabled = true
reindex_min_bytes = 1000000
reindex_max_bytes = 10000000
[profiles.prod.server]
log_level = "warn"
cache_max_mb = 8192
You can also stash the config outside WORKDIR and point at it explicitly:
docker run -d --name fluree \
-p 8090:8090 \
-v fluree-data:/var/lib/fluree \
-v "$PWD/fluree-config:/etc/fluree:ro" \
fluree/server:latest --config /etc/fluree/config.jsonld
3. Layered: File + Env Var Overrides
The common production shape: bake the base config into the image or volume, then let the orchestrator override per-environment with FLUREE_* env vars. Env vars beat the file — no file edit needed to bump cache size in staging vs. prod.
docker run -d --name fluree \
-p 8090:8090 \
-v fluree-data:/var/lib/fluree \
-v "$PWD/fluree-config/config.jsonld:/var/lib/fluree/.fluree/config.jsonld:ro" \
-e FLUREE_CACHE_MAX_MB=4096 \
-e RUST_LOG=warn \
fluree/server:latest
Common Configuration Recipes
Tuning the LRU Cache
cache_max_mb is the global budget for the in-memory index/flake cache. The default is a tiered fraction of system RAM (30%/40%/50% for <4GB/4–8GB/≥8GB hosts). On a container with a hard memory limit, set this explicitly — the auto-tier reads host RAM, not the cgroup limit, and can over-allocate.
# docker-compose.yml fragment
services:
fluree:
image: fluree/server:latest
mem_limit: 6g
environment:
FLUREE_CACHE_MAX_MB: 3072 # ~50% of the cgroup limit
Or in JSON-LD:
{
"@context": { "@vocab": "https://ns.flur.ee/config#" },
"server": { "cache_max_mb": 3072 }
}
Background Indexing
Indexing is off by default. Enable it for production write workloads — without it, every commit writes to novelty and queries get slower as novelty grows.
| Setting | Meaning |
|---|---|
indexing.enabled | Turn the background indexer on |
reindex_min_bytes | Soft threshold — novelty above this triggers a background reindex |
reindex_max_bytes | Hard threshold — commits block above this until reindexing catches up |
Tune min/max based on commit volume. Defaults (100 KB / 1 MB) are conservative; busy ledgers should raise both:
[server.indexing]
enabled = true
reindex_min_bytes = 5000000 # 5 MB — start indexing in the background
reindex_max_bytes = 50000000 # 50 MB — block commits at this point
docker run -d \
-e FLUREE_INDEXING_ENABLED=true \
-e FLUREE_REINDEX_MIN_BYTES=5000000 \
-e FLUREE_REINDEX_MAX_BYTES=50000000 \
fluree/server:latest
CORS and Request Body Size
[server]
cors_enabled = true
body_limit = 104857600 # 100 MB — raise for bulk imports
Authentication (Production)
Require a Bearer token on data and admin endpoints. The trusted issuer is the did:key of your token signer.
[server.auth.data]
mode = "required"
trusted_issuers = ["did:key:z6Mk..."]
[server.auth.admin]
mode = "required"
trusted_issuers = ["did:key:z6Mk..."]
For OIDC/JWKS (e.g. an external IdP), set --jwks-issuer or FLUREE_JWKS_ISSUERS:
docker run -d \
-e FLUREE_DATA_AUTH_MODE=required \
-e FLUREE_JWKS_ISSUERS="https://auth.example.com=https://auth.example.com/.well-known/jwks.json" \
fluree/server:latest
See Configuration → Authentication for the full matrix.
S3 + DynamoDB (Distributed Storage)
For multi-node or cloud deployments, point the server at a JSON-LD connection config describing your storage and nameservice. AWS credentials come from the standard SDK chain (env vars, IAM role, etc.) — they are not part of the connection config.
./fluree-config/connection.jsonld:
{
"@context": {
"@base": "https://ns.flur.ee/config/connection/",
"@vocab": "https://ns.flur.ee/system#"
},
"@graph": [
{ "@id": "commitStorage", "@type": "Storage",
"s3Bucket": "fluree-prod-commits", "s3Prefix": "data/" },
{ "@id": "indexStorage", "@type": "Storage",
"s3Bucket": "fluree-prod-indexes" },
{ "@id": "publisher", "@type": "Publisher",
"dynamodbTable": "fluree-nameservice", "dynamodbRegion": "us-east-1" },
{ "@id": "conn", "@type": "Connection",
"commitStorage": { "@id": "commitStorage" },
"indexStorage": { "@id": "indexStorage" },
"primaryPublisher": { "@id": "publisher" } }
]
}
docker run -d --name fluree \
-p 8090:8090 \
-v "$PWD/fluree-config:/etc/fluree:ro" \
-e AWS_REGION=us-east-1 \
-e AWS_ACCESS_KEY_ID=... \
-e AWS_SECRET_ACCESS_KEY=... \
-e FLUREE_CONNECTION_CONFIG=/etc/fluree/connection.jsonld \
-e FLUREE_INDEXING_ENABLED=true \
fluree/server:latest
--connection-config and --storage-path are mutually exclusive. See Configuration → Connection Configuration and the DynamoDB guide for backend-specific setup.
Search Service (fluree-search-httpd)
Run a dedicated BM25 / vector search service alongside the main server when search traffic is heavy enough that you want it isolated from the transactional path. The service is a separate binary with its own listen port — it is not mounted under the main server's api_base_url. It needs read access to the same storage and nameservice paths the main server writes to.
docker run -d --name fluree-search \
-p 9090:9090 \
-v fluree-data:/var/lib/fluree \
-e FLUREE_STORAGE_ROOT=/var/lib/fluree/storage \
-e FLUREE_NAMESERVICE_PATH=/var/lib/fluree/ns \
fluree/search-httpd:latest
| Env var | Default | Purpose |
|---|---|---|
FLUREE_STORAGE_ROOT | (required) | Storage path (file:// optional) |
FLUREE_NAMESERVICE_PATH | (required) | Nameservice path |
FLUREE_SEARCH_LISTEN | 0.0.0.0:9090 | Listen address |
FLUREE_SEARCH_CACHE_MAX_ENTRIES | 100 | Max cached indexes |
FLUREE_SEARCH_CACHE_TTL_SECS | 300 | Cache TTL |
FLUREE_SEARCH_MAX_LIMIT | 1000 | Max results per query |
FLUREE_SEARCH_DEFAULT_TIMEOUT_MS | 30000 | Default request timeout |
FLUREE_SEARCH_MAX_TIMEOUT_MS | 300000 | Maximum allowed timeout |
Prerequisites. The service only serves queries against indexes that already exist on the shared volume. BM25 / vector graph-source indexes are created via the Rust API today (Bm25CreateConfig + create_full_text_index, or VectorCreateConfig + create_vector_index). The @fulltext datatype and the f:fullTextDefaults config-graph paths are managed entirely through the main server's HTTP API and don't require this dedicated service.
Compose example with both services sharing a volume:
services:
fluree:
image: fluree/server:latest
ports:
- "8090:8090"
volumes:
- fluree-data:/var/lib/fluree
environment:
RUST_LOG: info
FLUREE_INDEXING_ENABLED: "true"
fluree-search:
image: fluree/search-httpd:latest
depends_on:
- fluree
ports:
- "9090:9090"
volumes:
- fluree-data:/var/lib/fluree:ro # read-only is sufficient
environment:
RUST_LOG: info
FLUREE_STORAGE_ROOT: /var/lib/fluree/storage
FLUREE_NAMESERVICE_PATH: /var/lib/fluree/ns
volumes:
fluree-data:
Clients send search requests to POST http://fluree-search:9090/v1/search. See BM25 → Remote Search Service for the request/response protocol.
Query Peer
Run as a read-only peer that subscribes to a transaction server's event stream:
docker run -d --name fluree-peer \
-p 8090:8090 \
-v fluree-peer-data:/var/lib/fluree \
-e FLUREE_SERVER_ROLE=peer \
-e FLUREE_TX_SERVER_URL=http://tx.internal:8090 \
fluree/server:latest --peer-subscribe-all
See Query peers and replication for the proxy-mode and auth options.
Docker Compose: Full Example
A production-leaning single-node setup with a mounted JSON-LD config, env-var overrides, named data volume, and resource limits:
services:
fluree:
image: fluree/server:latest
container_name: fluree
restart: unless-stopped
ports:
- "8090:8090"
volumes:
- fluree-data:/var/lib/fluree
- ./fluree-config/config.jsonld:/var/lib/fluree/.fluree/config.jsonld:ro
environment:
RUST_LOG: info
FLUREE_CACHE_MAX_MB: 4096
FLUREE_INDEXING_ENABLED: "true"
FLUREE_REINDEX_MIN_BYTES: "5000000"
FLUREE_REINDEX_MAX_BYTES: "50000000"
# Auth — point at your trusted did:key signer
FLUREE_DATA_AUTH_MODE: required
FLUREE_DATA_AUTH_TRUSTED_ISSUERS: did:key:z6Mk...
FLUREE_ADMIN_AUTH_MODE: required
FLUREE_ADMIN_AUTH_TRUSTED_ISSUERS: did:key:z6Mk...
mem_limit: 8g
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8090/health"]
interval: 30s
timeout: 3s
start_period: 15s
retries: 3
command: ["--profile", "prod"]
volumes:
fluree-data:
docker compose up -d
docker compose logs -f fluree
Troubleshooting
Container restarts after fluree init. First-run init only runs when /var/lib/fluree/.fluree/ is missing. If the volume is owned by a non-1000 UID, init fails. Fix with sudo chown -R 1000:1000 ./fluree-data on the host.
Mounted config file is ignored. Confirm the mount path and the file extension. The server only auto-discovers .fluree/config.toml or .fluree/config.jsonld under the working directory. Anything else needs --config <path> (or FLUREE_CONFIG=<path>). If both formats are present in the same directory, TOML wins — check the startup logs for the warning.
Setting FLUREE_LOG_LEVEL doesn't change console output. The image's ENV RUST_LOG=info shadows it. Override with -e RUST_LOG=debug instead.
cache_max_mb auto-default is too large under a memory limit. The auto-tier reads host RAM, not the cgroup. Set FLUREE_CACHE_MAX_MB (or cache_max_mb in the file) to a value sized to the container limit.
Health check failing. curl http://localhost:8090/health from your host. If the server is up but the healthcheck fails, the listen address is probably bound to 127.0.0.1 inside the container — set FLUREE_LISTEN_ADDR=0.0.0.0:8090.
Related Documentation
- Configuration reference — full flag/env/file matrix
- Storage modes — memory / file / AWS / IPFS
- JSON-LD connection configuration — schema for
connection.jsonld - Query peers and replication — peer-mode deployments
- Quickstart: Server — first-run walkthrough