Running Fallen-8
One command brings up the whole thing: engine, REST API, F8 Studio, the model sidecar behind semantic traversal and Studio’s natural-language assist, the document and NLP sidecars behind the semantic layer, the MCP server that AI agents connect to, and the full observability stack. Every feature is on and nothing asks for authentication. That is the default and it just works. This doc then covers every other way to run it and every knob you can turn.
The one command
Section titled “The one command”npm run env:upnpm run env:upThis runs docker compose up for the whole environment: the base compose file plus the
observability and split-topology overlays, the ingestion and nlp profiles, and the GPU
overlay when a GPU is detected. Eight containers publish a host port:
| Service | URL | What it is |
|---|---|---|
| F8 Studio (UI) | http://localhost:8081 | The browser UI, in its own container (F8_UI_PORT) |
| F8 REST API | http://localhost:8080 | The engine plus the REST surface: the data plane (F8_PORT) |
| Model sidecar (Ollama) | http://localhost:11434 | Serves bge-m3 for embeddings and the assist models for NL assist. Either capability can move to a hosted provider |
| MCP server | http://localhost:8090 | The AI-agent surface, anonymous and read-only in this environment (F8_MCP_PORT) |
| Document conversion (docling) | http://localhost:5001 | Converts PDF, Office and HTML uploads for ingestion (F8_DOCLING_PORT) |
| NLP enrichment (spaCy) | http://localhost:8100 | Named entities and key terms per chunk, folded into the entity graph (F8_NLP_PORT) |
| Observability (Grafana) | http://localhost:3000 | The single pane for metrics, traces and logs (F8_GRAFANA_PORT) |
| OTLP ingest | localhost:4317 (gRPC) / :4318 (HTTP) | Where Fallen-8 instances outside this compose push telemetry |
Prometheus, Tempo and Loki run too, reachable only inside the f8-net network; Grafana is the way
in. The engine is the only caller of docling and the NLP sidecar, so their ports are published for
inspection, not for you to call. env:up prints the first three plus Grafana and the OTLP endpoint.
Stop and inspect it as one unit, never individual containers:
npm run env:down # stop everything; the data + model volumes persistnpm run env:logs # follow all logsnpm run env:status # health of the whole environmentFrom the published images
Section titled “From the published images”env:up builds the first-party images locally. Every release tag also publishes them to
GHCR (ghcr.io/cosh/fallen-8-core plus -api, -studio, -mcp, -integrations, -ollama,
-nlp), so the same environment runs without building anything:
npm run env:up:published # newest release (latest)F8_IMAGE_TAG=0.0.28 npm run env:up:published # pin a release for reproducible deploymentsImage tags mirror the git release tags; latest always points at the newest release. GPU
auto-detection works exactly like env:up (Ollama gets the device, a runtime-only handoff);
the one difference is the NLP sidecar, which stays on the published CPU tier because its
transformer tier only exists as a local build. And for just Fallen-8 itself (engine + REST
API + F8 Studio in one container), no sidecars:
docker run -d --name fallen8 -p 8080:8080 -v f8-data:/data ghcr.io/cosh/fallen-8-core:latestThe environment is open (no API key) and has text-in embeddings on (semantic search and GraphRAG work out of the box). Dynamic C# code execution is always on: there is no switch for it, because compiled filter and cost fragments are Fallen-8’s query model. They run in-process with full trust, so the API key is the only boundary. Set one before exposing the service off-box, and harden the MCP server separately (see Security).
First start pulls models
Section titled “First start pulls models”On the first env:up the sidecar pulls its models into the f8-ollama-models volume:
bge-m3 (embeddings), phi4-mini (base), phi4-f8-mini (the delegate-assist fine-tune,
Studio’s default), and phi4-f8 (the larger GPU-only fine-tune, ~9 GB: set
F8_PULL_PHI4F8=0 to skip it on a CPU-only or disk-constrained host). That is over 10 GB
with phi4-f8, so first start can take a while: the API and Studio are up immediately, and
assist starts answering once the mini pull finishes (the bigger model finishes later in the
background). Watch progress with npm run env:logs.
When chat runs on a hosted provider the assist weights are dead weight, so
the openai and anthropic presets set F8_PULL_ASSIST=0 and the two mini models are skipped;
bge-m3 still pulls, because that sidecar is still the embedding backend.
Later starts reuse the cached volume. The two fine-tunes are re-checked against the registry
on every start, because the pipeline republishes them over the same :latest tag - a pull is
content-addressed, so an unchanged model is a digest check and no transfer. If the registry is
unreachable the cached copy is used and the log says so, which is what keeps offline and
air-gapped starts working. The base models (bge-m3, phi4-mini) are never re-fetched once
present, since nothing republishes those tags.
The default names a specific model version, not :latest - currently
stoic_hellman_728/phi4-f8-mini:v0.0.35 and the matching phi4-f8. That is deliberate: the
fine-tune pipeline republishes over :latest, so an unpinned default would make every deployment
follow whatever was published last, under an identical model name, with nothing reporting the
change. Pinned, a given checkout always serves the same weights, and moving is a decision. Those
version tags come from the release that created them: tagging the repository vX.Y.Z also tags the
published models, if that build has no version yet. A build is named once, so a release that ships
no retrained model adds no tag, and the newest model version can be older than the newest release.
To use something else, override either variable - a line in a .env beside the compose file, or an
exported variable:
F8_DELEGATE_REPO=stoic_hellman_728/phi4-f8-mini:v0.0.36 # another published versionF8_DELEGATE_REPO=stoic_hellman_728/phi4-f8-mini # follow :latest, whatever it becomesF8_DELEGATE_REPO=my-namespace/my-finetune:v1 # your ownThe startup log names the build being served, and that id is the leading half of the sha256 of the registry manifest, so you can check it against the registry:
docker compose exec ollama ollama list | grep '^phi4-f8-mini:'curl -s https://registry.ollama.ai/v2/stoic_hellman_728/phi4-f8-mini/manifests/v0.0.35 | sha256sum | cut -c1-12``` The docling image is a separate ~4.4 GB pull on that same first start(`F8_INGESTION=false` skips it).
Offline or want to skip the wait? Pre-seed the volume once from a machine with internet, whichneeds only Docker and no host Ollama:
```bashbash scripts/ensure-models.sh # assist models into the f8-ollama-models volumeThe pre-seed covers the same set the container would pull, and honours the same opt-outs:
phi4-mini and phi4-f8-mini unless F8_PULL_ASSIST=0, phi4-f8 unless F8_PULL_PHI4F8=0, and
bge-m3 unless F8_EMBEDDINGS=false. So an environment seeded here and started offline has a
model behind every capability it advertises.
If a pull fails the sidecar still comes up with whatever is present (it does not crash-loop); fix the network and re-run. The 404-in-Studio case is in Troubleshooting.
Environment variables (compose)
Section titled “Environment variables (compose)”Set these before npm run env:up. In PowerShell use $env:NAME = "value"; … (it stays set
for the session; Remove-Item Env:NAME clears it) and chain with ;, not &&.
Any of them can instead live in a .env file beside docker-compose.yml, one NAME=value
per line. That is the standard compose mechanism, and the helper scripts read the same file, so
it also drives what env:up decides - which overlays apply, which profiles start. A variable
set in the shell wins over the file. .env is gitignored, which makes it the home for values you
set once and keep - credentials included; a model provider’s API key is the
canonical case. To start one, copy
.env.example from the repo root
to .env and uncomment what you need.
| Variable | Default | Effect |
|---|---|---|
F8_PORT |
8080 |
Host port for the REST API, the data plane (the container always listens on 8080) |
F8_UI_PORT |
8081 |
Host port for the F8 Studio container; the data plane allow-lists exactly this origin for CORS |
F8_API_KEY |
(unset) | Set it and the entire data plane requires the key; unset = open. The MCP bridge receives it for its own downstream calls but keeps its own caller auth (F8_MCP_AUTH_MODE) (Security) |
F8_EMBEDDINGS |
true |
Text-in embeddings via the sidecar’s bge-m3; false disables the provider (bring-your-own-vector still works) |
F8_CHAT |
true |
The chat gateway (POST /chat to the sidecar’s assist model), the default NL-assist path. false answers 403 (custom browser-direct backends still work) |
F8_MODEL_PROVIDER |
local |
Where the two model capabilities send their requests: local (the sidecar), nahil, openai or anthropic, applying that overlay. An unknown value is refused before anything starts (model providers) |
F8_NAHIL_API_KEY |
(unset) | Nahil’s credential, and on its own also the selector for that provider. It is the one provider that moves embeddings too, because it serves the same bge-m3. F8_NAHIL_URL, F8_NAHIL_CHAT_MODEL and the rest are in its own table |
F8_OPENAI_API_KEY |
(unset) | Required by F8_MODEL_PROVIDER=openai; the overlay fails closed without it. F8_OPENAI_URL (default https://api.openai.com) and F8_OPENAI_CHAT_TIMEOUT (120) are the rest of the set |
F8_OPENAI_CHAT_MODEL |
gpt-4o-mini |
The chat model that provider is asked for. A config string; Fallen-8 does not chase the vendor catalog |
F8_ANTHROPIC_API_KEY |
(unset) | Required by F8_MODEL_PROVIDER=anthropic; the overlay fails closed without it. F8_ANTHROPIC_URL, F8_ANTHROPIC_MAX_TOKENS (4096) and F8_ANTHROPIC_CHAT_TIMEOUT (120) are the rest of the set |
F8_ANTHROPIC_CHAT_MODEL |
claude-opus-5 |
The chat model that provider is asked for. Anthropic serves chat only: it publishes no embeddings API |
F8_INGESTION |
true |
Document ingestion. false skips the docling sidecar (a ~4.4 GB image) and answers 403 on the document endpoints, including txt/md |
F8_NLP |
true |
NLP enrichment. false skips the spaCy sidecar; ingestion still writes Document and Chunk vertices, just without the entity graph |
F8_INTEGRATIONS |
true |
The integrations runtime, on its own profile and with no host port. false skips the sidecar and the /integrations/* routes then refuse the capability |
F8_INTEGRATIONS_ALLOWED_HOSTS |
(unset) | Comma-separated hosts a run holding a credential may contact, enforced as the request leaves. Unset means no restriction, and env:up says so: a source address arrives in a job from whoever can reach the API |
F8_INTEGRATIONS_SELF_SIGNED_HOSTS |
(unset) | Comma-separated hosts whose TLS certificate is not validated. The one place this feature reduces trust, and what a UniFi console or Fronius inverter on a private address needs to be reachable at all |
F8_GPU |
(auto) | 1 forces the NVIDIA GPU, 0 forces CPU-only; unset auto-detects |
F8_IMAGE_TAG |
latest |
Which published image version env:up:published runs (mirrors the release tags, e.g. 0.0.28); ignored by the building env:up |
F8_PULL_PHI4F8 |
1 |
Also pulls the larger GPU-only phi4-f8 assist model (~9 GB); set 0 to skip it (CPU-only / disk-constrained hosts) |
F8_PULL_ASSIST |
1 |
Pulls the two mini assist models (phi4-mini + phi4-f8-mini, ~4.8 GB); 0 skips them, which is what the openai and anthropic presets set. Independent of F8_EMBEDDINGS: a hosted-chat deployment is exactly the one that keeps bge-m3 |
F8_DELEGATE_REPO |
stoic_hellman_728/phi4-f8-mini:v0.0.35 |
Source for the default assist fine-tune (tagged locally as phi4-f8-mini); F8_PHI4F8_REPO is the same for phi4-f8. Pinned to a version on purpose - drop the tag to follow :latest |
F8_MCP_AUTH_MODE |
None |
Caller auth for the MCP server. Anything reachable off-box wants StaticToken plus F8_MCP_TOKEN |
F8_MCP_ENABLE_WRITE / F8_MCP_ENABLE_ADMIN / F8_MCP_ENABLE_CODE |
false |
Opt individual MCP tool tiers in; agents get read-only tools by default |
F8_TENANT_ID / F8_INSTANCE_ID |
local / f8-local |
This instance’s fleet identity in the dashboards (F8_TENANT_NAME and F8_INSTANCE_NAME set the display names) |
F8_GRAFANA_PASSWORD |
admin |
Locks the Grafana admin account; anonymous editor access is on by default for the trusted network |
Every published port has an override: F8_PORT (8080), F8_UI_PORT (8081), F8_MCP_PORT (8090),
F8_GRAFANA_PORT (3000), F8_DOCLING_PORT (5001), F8_NLP_PORT (8100), F8_OTLP_GRPC_PORT (4317)
and F8_OTLP_HTTP_PORT (4318). Ollama’s 11434 is fixed. F8_DOCLING_IMAGE pins the docling image,
which is deliberately not :latest so its document schema cannot drift under you.
Examples:
F8_API_KEY=change-me npm run env:up # secured (every request needs the key)F8_PORT=9090 npm run env:up # REST API on :9090F8_INGESTION=false npm run env:up # skip the docling + NLP sidecars entirely$env:F8_API_KEY = "change-me"; npm run env:up$env:F8_PORT = "9090"; npm run env:up$env:F8_INGESTION = "false"; npm run env:upTopology: a decoupled UI by default
Section titled “Topology: a decoupled UI by default”npm run env:up runs F8 Studio as its own container (the split topology): the data plane serves
the REST API at :8080, and a separate f8-studio nginx container serves the UI at
http://localhost:${F8_UI_PORT} (default 8081), talking to the API cross-origin. This is applied by
docker-compose.split.yml layered over the base compose file: it re-tasks the data plane to serve
REST only, wires the UI origin into Fallen8__Security__AllowedCorsOrigins, and adds the f8-studio
service.
The all-in-one (UI baked into the API container, both on :8080) still ships as one image and
runs from the base compose file with no overlay. Pass the two profiles yourself, because a bare
docker compose up starts neither sidecar while the base file still declares both capabilities on
(document uploads would be limited to txt/md and no entity graph would be built):
docker compose --profile ingestion --profile nlp up -d --buildWithout the observability overlay there is also nothing behind the base file’s
Fallen8__Observability__Otlp__Endpoint, so the exporter has no collector to push to: add
-f docker-compose.observability.yml (what env:up does) or set that variable to empty. The
runtime config.js seam, the image, and the CORS rules are in
Standalone F8 Studio.
Running the engine + API without Docker
Section titled “Running the engine + API without Docker”A bare run needs only the .NET 10 SDK. It starts in the Development environment, which is the only environment that serves the OpenAPI document and Scalar reference:
dotnet run --project fallen-8-core-apiApp# → http://localhost:5000 (OpenAPI + Scalar reference are served here in Development)dotnet run --project fallen-8-core-apiAppThis is engine plus REST only. None of the sidecars are there, so the capabilities that depend on them default to off: the embedding provider, the chat gateway behind Studio’s NL assist, document ingestion, and NLP enrichment all answer 403 until you enable them and point them at a backend of your own (txt/md ingestion needs only the flag, since docling converts binary formats only). Bring-your-own-vector, analytics, paths, indexes and every other feature work regardless. The MCP server is its own deployable and is likewise absent from a bare run (how to run it).
With F8 Studio built in
Section titled “With F8 Studio built in”Studio is a React SPA served by the API app from its wwwroot. Build it in, then run:
npm run install:ui # oncenpm run build:apiapp # build the SPA into the apiApp's wwwrootdotnet run --project fallen-8-core-apiApp# open http://localhost:5000To develop the UI against a running API with hot reload, use npm run dev (Vite dev server)
instead of build:apiapp. Debugging the whole stack in VS Code is covered in
Debugging in VS Code.
Configuration keys
Section titled “Configuration keys”The compose variables above map onto the app’s configuration, which you can also set
directly (via appsettings.json, environment variables with the Fallen8__Section__Key
form, or the command line). The defaults:
| Key | Default | Owner doc |
|---|---|---|
Fallen8:Durability:StorageDirectory |
app base dir (/data in the container) |
Save games |
Fallen8:Durability:Volatile |
false |
Save games |
Fallen8:Durability:SaveOnShutdown |
true |
Save games |
Fallen8:Metadata:Directory |
<app base>/metadata |
Save games |
Fallen8:Namespaces:MaxNamespaces |
10000 |
Namespaces |
Fallen8:Namespaces:LoadOnStartup |
true (per-namespace overridable) |
Namespaces |
Fallen8:Namespaces:StartupLoadMode |
Catalog (All, DefaultOnly) |
Namespaces |
Fallen8:Security:ApiKey |
null (open) |
Security |
Fallen8:Security:EnableDynamicPluginLoading |
true (per-namespace overridable) |
Plugin registration |
Fallen8:Security:AllowedCorsOrigins |
[] (deny all cross-origin) |
Standalone F8 Studio |
Fallen8:Security:BenchmarkMaxIterations |
10000 |
Benchmark |
Fallen8:Embedding:Enabled |
false (bare run) |
Semantic traversal |
Fallen8:Chat:Enabled |
false (bare run) |
F8 Studio |
Fallen8:Ingestion:Enabled |
false (bare run) |
Semantic layer |
Fallen8:Nlp:Enabled |
false (bare run) |
Semantic layer |
Fallen8:ChangeFeed:Enabled |
true |
Change feed |
Fallen8:BulkIO:ImportBatchSize |
10000 |
Bulk import/export |
Fallen8:Analytics:DefaultTimeBudgetSeconds |
30 (max 300) |
Graph analytics |
Fallen8:Observability:Prometheus:Enabled |
false |
Observability |
Fallen8:Observability:Otlp:Endpoint |
null (no push) |
Observability |
Volatile=true disables all disk writes (no WAL, no checkpoints): the fastest way to run a
throwaway instance for a demo or a test.
Fallen8__Namespaces__StartupLoadMode=All overrides every per-namespace exclusion for a boot, which
is the way back from an exclusion you regret without hand-editing the catalog; DefaultOnly loads
nothing but default, for when the selection itself is what is broken
(namespaces).
These are the defaults a bare run gets. The compose environment overrides several of them:
Metadata:Directory becomes /data/metadata (it does not follow StorageDirectory, so both are
set explicitly to keep graph data and the save-game registry on one volume), and Otlp:Endpoint
points at the in-network collector, so metrics, traces and logs export from the first boot without
you enabling anything.
GPU acceleration
Section titled “GPU acceleration”npm run env:up and npm run env:up:published auto-detect an NVIDIA GPU (nvidia-smi
present) and hand it to the containers that can use one. Ollama runs assist and embedding
inference accelerated in both modes (a runtime-only handoff, docker-compose.gpu.yml). When
building locally, the NLP sidecar additionally swaps to the en_core_web_trf transformer
model on the device instead of en_core_web_lg on the CPU (docker-compose.gpu-nlp.yml).
Otherwise everything runs CPU-only. The transformer model is baked in at build time, so
flipping F8_GPU rebuilds the nlp image (transformer weights plus the CUDA extras): the
first env:up after the switch is slow, and env:up:published always stays on the CPU NLP
tier. Force detection with F8_GPU:
F8_GPU=0 npm run env:up # CPU-only even if a GPU is presentF8_GPU=1 npm run env:up # require the GPU (fails to create the container if unavailable)The GPU reaches the container through the NVIDIA Container Toolkit, installed where the
Docker engine runs. On Docker Desktop (WSL2 backend) install the current NVIDIA driver on
Windows and confirm nvidia-smi works inside WSL: the backend then exposes the GPU
automatically. For Docker running natively in a Linux distro install
nvidia-container-toolkit and sudo nvidia-ctk runtime configure --runtime=docker. Verify
the GPU reaches a container before env:up:
docker run --rm --gpus all --entrypoint nvidia-smi ollama/ollama:latest # should list your GPUIf that fails the GPU is not exposed to Docker; the stack still runs CPU-only with F8_GPU=0.
AMD GPUs are not covered by this compose: Ollama would need the ollama/ollama:rocm image, and the
NLP sidecar’s transformer tier assumes CUDA, so an AMD host also stays on the CPU en_core_web_lg
model.
See also
Section titled “See also”- Architecture: how the containers, engine and sidecars fit together
- Model providers: choosing where embeddings and chat run, and where the key lives
- Security: the API key you set at launch
- Studio: the browser UI this serves
- MCP server: the agent surface on
:8090and how to secure it - Observability: the Grafana pane and the fleet identity knobs
- Save games: where durable data lives and how startup loads it
- REST API: OpenAPI/Scalar (Development only) and the endpoint map
- Troubleshooting: first-start model pulls, GPU, and other snags