Skip to main content

Fabric — Docker

There are two distinct ways Docker intersects with Fabric:

  1. Running Fabric IN Docker — the agent itself runs inside a container (this page's primary focus)
  2. Docker as a terminal backend — the agent runs on your host but executes every command inside a single, persistent Docker sandbox container that survives across tool calls, /new, and subagents for the life of the Fabric process (see Configuration → Docker Backend)

This page covers option 1. The container stores all user data (config, API keys, sessions, skills, memories) in a single directory mounted from the host at /opt/data. The image itself is stateless and can be upgraded by pulling a new version without losing any configuration.

Quick start

If this is your first time running Fabric, create a data directory on the host and start the container interactively to run the setup wizard:

Avoid browser-based VPS consoles for the install commands

Some VPS providers (Hetzner Cloud, and several others) offer a browser-based console for managing hosts. These consoles transmit special characters incorrectly — : may arrive as ;, @ may be mis-rendered, and non-English keyboard layouts fare worse — which silently corrupts docker run arguments like -v ~/.fabric:/opt/data, -e KEY=value, and pasted API keys / tokens.

Connect over SSH instead (ssh root@<host>) for copy-paste-safe command entry. If you must use the browser console, type the commands manually instead of pasting, and double-check every :, @, =, and / in the result before hitting Enter.

mkdir -p ~/.fabric
docker run -it --rm \
-v ~/.fabric:/opt/data \
ghcr.io/obliviousodin/fabric setup

This drops you into the setup wizard, which will prompt you for your API keys and write them to ~/.fabric/.env. You only need to do this once. It is highly recommended to set up a chat system for the gateway to work with at this point.

tip

Inside the container, run fabric model once. Credentials and model choices persist in the mounted ~/.fabric volume.

Running in gateway mode

Once configured, run the container in the background as a persistent gateway (Telegram, Discord, Slack, WhatsApp, etc.):

docker run -d \
--name fabric \
--restart unless-stopped \
-v ~/.fabric:/opt/data \
-p 8642:8642 \
ghcr.io/obliviousodin/fabric gateway run

Port 8642 exposes the gateway's OpenAI-compatible API server and health endpoint. It's optional if you only use chat platforms (Telegram, Discord, etc.), but required if you want the dashboard or external tools to reach the gateway.

Gateway runs supervised

Inside the official Docker image, gateway run is automatically supervised by s6-overlay: if the gateway process crashes it's restarted within a couple of seconds without losing the container. The gateway run CMD process itself is a sleep infinity heartbeat that keeps the container alive while s6 manages the actual gateway process—so docker stop still shuts everything down cleanly, but docker logs shows the supervised gateway's output.

You'll see a one-line breadcrumb in docker logs confirming the upgrade. To opt out—and make the gateway the container's main process so the container exits with the gateway—pass --no-supervise. The opt-out is useful for CI smoke tests that want the container to exit with the gateway's status code; for production deployments the supervised default is strictly better.

This behavior applies to the s6-based image only. Earlier (tini-based) images still run gateway run as the foreground main process.

Where gateway logs go

See the Where the logs go section below for the full routing map (per-profile gateways, dashboard, boot reconciler, container-wide docker logs).

Tool-loop hard stops for unattended gateways

The tool_loop_guardrails.hard_stop_enabled setting defaults to false, which is reasonable for interactive CLI and TUI sessions where a person can see repeated tool-call warnings. In unattended gateway or server deployments, warnings alone may not stop an agent that gets stuck in a repeated tool-call loop. Operators who want circuit-breaker behavior should explicitly enable hard stops in the profile's config.yaml:

tool_loop_guardrails:
hard_stop_enabled: true
hard_stop_after:
exact_failure: 5
idempotent_no_progress: 5

Note: the API server is gated on API_SERVER_ENABLED=true. To expose it beyond 127.0.0.1 inside the container, also set API_SERVER_HOST=0.0.0.0 and an API_SERVER_KEY (minimum 8 characters — generate one with openssl rand -hex 32). Example:

docker run -d \
--name fabric \
--restart unless-stopped \
-v ~/.fabric:/opt/data \
-p 8642:8642 \
-e API_SERVER_ENABLED=true \
-e API_SERVER_HOST=0.0.0.0 \
-e API_SERVER_KEY="$(openssl rand -hex 32)" \
-e API_SERVER_CORS_ORIGINS='*' \
ghcr.io/obliviousodin/fabric gateway run

Opening any port on an internet facing machine is a security risk. You should not do it unless you understand the risks.

Running the dashboard

Run the built-in web dashboard as its own container process, sharing the same Fabric data mount as the gateway:

docker run -d \
--name fabric-dashboard \
--restart unless-stopped \
-v ~/.fabric:/opt/data \
-p 9119:9119 \
ghcr.io/obliviousodin/fabric dashboard --host 0.0.0.0 --port 9119 --no-open

Docker's unless-stopped policy restarts the dashboard container if the process exits. Dashboard stdout/stderr is available through docker logs fabric-dashboard.

The dashboard's auth gate engages automatically for every non-loopback bind (for example, the default 0.0.0.0 inside the container). At least one DashboardAuthProvider must then be configured; otherwise the dashboard fails closed at startup.

There are three bundled ways to satisfy the second condition:

  • Username/password — the simplest for a self-hosted / on-prem / homelab container on a trusted network or behind a VPN: set dashboard.basic_auth.username, a password_hash (preferred) or password, and a restart-stable secret in config.yaml. Values can use ${VAR} interpolation into .env. Not suitable for direct public-internet exposure.
  • Nous OAuth — the dashboard_auth/nous provider activates whenever its client ID is configured.
  • Self-hosted OIDC — configure dashboard.oauth.self_hosted.issuer and client_id to authenticate against your own identity provider via standard OpenID Connect.

Whichever you choose, the gate redirects callers to a login page before they can reach any protected route. See Web Dashboard → Authentication for all three providers.

If no provider is registered, the container dashboard fails closed at startup. Configure a provider before publishing the port.

Why --insecure was removed

An unauthenticated public dashboard was the entry point for the June 2026 MCP-config persistence campaign: internet scanners reached exposed dashboards (and OpenAI API servers) and drove the agent into planting an SSH-key backdoor. The auth gate is now mandatory on every non-loopback bind. For a trusted-LAN or homelab box, the bundled username/password provider is the zero-infrastructure option.

Running the dashboard as a separate container is supported. The repo's docker-compose.yml uses network_mode: host and mounts the same /opt/data directory into both containers; it does not share a PID namespace. The dashboard itself works in that layout, but local PID-based gateway status cannot see across the container boundary. If an operator needs an accurate gateway badge in a split-container deployment, either share the PID namespace explicitly or configure the currently supported (but deprecated) GATEWAY_HEALTH_URL probe while a first-class config replacement is being developed. network_mode: host shares networking only—it is not a substitute for PID sharing.

Running interactively (CLI chat)

To open an interactive chat session against a running data directory:

docker run -it --rm \
-v ~/.fabric:/opt/data \
ghcr.io/obliviousodin/fabric

Or if you have already opened a terminal in your running container (via Docker Desktop for instance), just run:

/opt/fabric/.venv/bin/fabric

The published image installs Fabric under /opt/fabric.

Persistent volumes

The /opt/data volume is the single source of truth for all Fabric state. It maps to your host's ~/.fabric/ directory and contains:

PathContents
.envAPI keys and secrets
config.yamlAll Fabric configuration
SOUL.mdAgent personality/identity
sessions/Conversation history
memories/Persistent memory store
skills/Installed skills
home/Per-profile HOME for Fabric tool subprocesses (git, ssh, gh, npm, and skill CLIs)
cron/Scheduled job definitions
hooks/Event hooks
logs/Runtime logs
skins/Custom CLI skins

Immutable install tree

In hosted and published Docker images, /opt/fabric is the installed application tree. It is root-owned and read-only to the runtime fabric service account, so agent turns, gateway sessions, dashboard actions, and normal docker exec fabric fabric ... commands cannot edit the core source, bundled .venv, node_modules, or TUI bundle in place.

All mutable Fabric state belongs under /opt/data: config, .env, profiles, skills, memories, sessions, logs, dashboard uploads, plugins, and other user-managed files. The image disables runtime .pyc writes and prevents lazy dependencies from modifying /opt/fabric. Allow-listed optional dependencies may instead be installed into /opt/data/lazy-packages, which is appended after the sealed environment on sys.path; set security.allow_lazy_installs: false to disable those durable-target installs too.

On hosted/published images, agent self-improvement is scoped to skills, memory, plugins, and config under /opt/data. The installed core source under /opt/fabric is immutable; core changes are made via PRs to the repo and shipped by updating the image, not by live-editing the running install.

If an operator needs to repair or inspect files outside /opt/data, open an explicit root shell with docker exec --user root -it fabric sh. Do not run the gateway itself as root.

Skill CLIs that store credentials under ~ must be initialized against the subprocess HOME, not just the data-volume root. For example, the xurl skill stores OAuth state in ~/.xurl; in the official Docker layout, Fabric tool calls read that as /opt/data/home/.xurl, so run manual xurl auth with HOME=/opt/data/home and verify with HOME=/opt/data/home xurl auth status.

warning

Never run two Fabric gateway containers against the same data directory simultaneously — session files and memory stores are not designed for concurrent write access.

Multi-profile support

Fabric supports multiple profiles — separate ~/.fabric/ subdirectories that let you run independent agents (different SOUL, skills, memory, sessions, credentials) from a single installation. Inside the official Docker image, the s6 supervision tree treats each profile as a first-class supervised service, so the recommended deployment is one container hosting all profiles.

Each profile created with fabric profile create <name> gets:

  • A dedicated s6 service slot at /run/service/gateway-<name>/, registered dynamically by the runtime — no container rebuild required.
  • Auto-restart on crash, backoff-managed by s6-supervise.
  • Per-profile rotated logs at ${FABRIC_HOME}/logs/gateways/<name>/current (10 archives × 1 MB each).
  • State persistence across container restarts: the boot-time reconciler reads gateway_state.json from each profile directory and brings the slot back up only for profiles whose last recorded state was running. Only a gateway you explicitly stopped (fabric gateway stop) stays down across a restart — a container restart, image upgrade, or unexpected exit leaves the recorded state as running, so the gateway auto-starts on the next boot.

The lifecycle commands you'd run on the host work the same way from inside the container:

# Create a profile — registers the gateway-<name> s6 slot.
docker exec fabric fabric profile create coder

# Start / stop / restart — dispatches s6-svc; the gateway lifecycle survives docker restart.
docker exec fabric fabric -p coder gateway start
docker exec fabric fabric -p coder gateway stop
docker exec fabric fabric -p coder gateway restart

# Status — reports `Manager: s6 (container supervisor)` inside the container.
docker exec fabric fabric -p coder gateway status

# Remove a profile — tears down the s6 slot too.
docker exec fabric fabric profile delete coder

Under the hood, fabric gateway start/stop/restart inside the container is intercepted and routed to s6-svc against the right service directory; you don't need to learn the s6 commands directly. For raw supervisor state, use /command/s6-svstat /run/service/gateway-<name> (note /command/ is on PATH only for processes spawned by the supervision tree — when calling from docker exec, pass the absolute path).

Reaching more than one profile from outside the container

Two different surfaces reach a profile's gateway from outside, and they behave differently — don't conflate them:

Fabric (and the web dashboard). The Desktop app's Remote Gateway connection talks to a fabric dashboard backend (default port 9119)—not the OpenAI API server. One dashboard backend serves every co-located profile: the app's profile switcher sends the target profile with each request and the backend opens that profile's FABRIC_HOME on disk. So you do not need a second port—or a second connection—per profile for Desktop; one :9119 connection covers them all through the switcher.

OpenAI-compatible API clients (Open WebUI, LobeChat, /v1/...). These talk to each profile's API server, which binds port 8642 for every profile (resolved from API_SERVER_PORT / platforms.api_server.extra.port — there is no auto-allocation and no config.yaml/gateway.port key). If you want a client to reach a specific second profile, give that profile a distinct API_SERVER_PORT in its own .env, otherwise its gateway tries to bind 8642 too and conflicts with the default profile:

# Create the profile (registers its gateway-<name> s6 slot)
docker exec fabric fabric profile create work

# Point its API server at a free port (write to the profile's own .env)
cat >> /opt/data/profiles/work/.env <<'EOF'
API_SERVER_ENABLED=true
API_SERVER_PORT=8643
EOF

docker exec fabric fabric -p work gateway restart

Keep API_SERVER_PORT in each profile's own .env, never in the container-wide environment: block — a global value would force every profile onto the same port and they would collide. With bridge networking, publish the extra port in docker-compose.yml (- "8643:8643"); with network_mode: host it is already reachable on the host. The default profile's 8642 connection is untouched.

Why one container with many profiles, not many containers

Before the s6 migration, "one container per profile" was the recommended pattern because there was no in-container supervisor to manage multiple gateways. With s6 as PID 1, that's no longer necessary, and the single-container layout is simpler in almost every dimension:

One container, many profilesOne container per profile
Disk overheadOne image, one bundled venv, one Playwright cacheN images / N caches
Memory overheadOne image and one set of installed assets; each gateway still has its own process memorySeparate container runtimes around each gateway process
Profile creationdocker exec ... fabric profile create <name> (seconds)New docker run invocation + port allocation + bind-mount config
Per-profile crash recoverys6-supervise restarts only the failed gateway processDocker restarts the profile's whole container
LogsPer-profile rotated file via s6-log, plus container-boot audit logdocker logs <name> per container — no built-in rotation
BackupOne ~/.fabric directoryN directories to coordinate

The default profile (default) always gets a supervised service slot on first boot. A gateway run container starts that slot automatically; other container commands leave it registered but down until fabric gateway start. Additional profiles are runtime registrations.

When you DO want a separate container

Profile-in-container is the default. Run a separate container per profile only when you have a specific reason:

  • Resource isolation per workload — e.g. a runaway browser-tool session in profile A shouldn't be able to OOM profile B. Containers give you --memory / --cpus per profile.
  • Independent image pinning — different upstream image tags per workload.
  • Network segmentation — distinct Docker networks per profile (e.g. one customer-facing, one internal).
  • Compliance / blast radius — distinct credentials never share an OS-level process tree.

In those cases, declare one service per profile with distinct container_name, volumes, and ports:

services:
fabric-work:
image: ghcr.io/obliviousodin/fabric:latest
container_name: fabric-work
restart: unless-stopped
command: gateway run
ports:
- "8642:8642"
volumes:
- ~/.fabric-work:/opt/data

fabric-personal:
image: ghcr.io/obliviousodin/fabric:latest
container_name: fabric-personal
restart: unless-stopped
command: gateway run
ports:
- "8643:8642"
volumes:
- ~/.fabric-personal:/opt/data

The warning from Persistent volumes still applies: never point two containers at the same ~/.fabric directory simultaneously. The s6 supervisor inside each container manages its own profile set; cross-container sharing of a data volume corrupts session files and memory stores.

Where the logs go

The s6 container has four distinct log surfaces, and "why isn't my gateway showing anything in docker logs" is a common surprise. Cheatsheet:

SourceWhere it landsHow to read it
Per-profile gateway (fabric gateway run and per-profile gateways under s6)Tee'd to two places: docker logs <container> (real time, no extra prefix) and ${FABRIC_HOME}/logs/gateways/<profile>/current (rotated, ISO-8601 timestamped, 10 archives × 1 MB each)docker logs -f fabric or tail -F ~/.fabric/logs/gateways/default/current on the host
Dashboard containerContainer stdout/stderrdocker logs -f fabric-dashboard
Boot reconciler (records which profile gateways were restored on each container start)${FABRIC_HOME}/logs/container-boot.log (append log, rotated to .1 at 256 KiB)tail -F ~/.fabric/logs/container-boot.log
Generic Fabric logs (agent.log, errors.log)${FABRIC_HOME}/logs/ (profile-aware)docker exec fabric fabric logs --follow [--level WARNING] [--session <id>]

Two practical consequences worth knowing:

  • The file copy at logs/gateways/<profile>/current is what survives container restarts. docker logs only retains output from the current container's lifetime (and is wiped on docker rm); the rotated files persist on the bind-mounted volume.
  • The boot reconciler's audit line shape is <iso-timestamp> profile=<name> prior_state=<state> action=<registered|started>, so a quick grep profile=coder ~/.fabric/logs/container-boot.log reveals when a given profile was last restored and whether s6 auto-started it.

Environment variable forwarding

API keys are read from /opt/data/.env inside the container. You can also pass environment variables directly:

docker run -it --rm \
-v ~/.fabric:/opt/data \
-e ANTHROPIC_API_KEY="sk-ant-..." \
-e OPENAI_API_KEY="sk-..." \
ghcr.io/obliviousodin/fabric

Direct -e flags override values from .env. This is useful for CI/CD or secrets-manager integrations where you don't want keys on disk.

Looking for Docker as the terminal backend?

This page covers running Fabric itself inside Docker. If you want Fabric to execute the agent's terminal / execute_code calls inside a Docker sandbox container (one long-lived container shared across Fabric processes), that's a separate config block — terminal.backend: docker plus terminal.docker_image, terminal.docker_volumes, terminal.docker_forward_env, terminal.docker_env, terminal.docker_run_as_host_user, terminal.docker_extra_args, terminal.docker_persist_across_processes, and terminal.docker_orphan_reaper. See Configuration → Docker Backend for the full set including container-lifecycle rules.

Docker Compose example

The repository's docker-compose.yml is a source-checkout example with separate gateway and dashboard containers. Both use host networking and the same data mount; the dashboard binds to loopback and therefore does not need an auth provider for local use:

services:
gateway:
build: .
image: fabric-agent
container_name: fabric-gateway
restart: unless-stopped
network_mode: host
command: gateway run
volumes:
- ~/.fabric:/opt/data
environment:
- FABRIC_UID=${FABRIC_UID:-10000}
- FABRIC_GID=${FABRIC_GID:-10000}

dashboard:
image: fabric-agent
container_name: fabric-dashboard
restart: unless-stopped
network_mode: host
depends_on:
- gateway
command: ["dashboard", "--host", "127.0.0.1", "--no-open"]
volumes:
- ~/.fabric:/opt/data
environment:
- FABRIC_UID=${FABRIC_UID:-10000}
- FABRIC_GID=${FABRIC_GID:-10000}

Start with FABRIC_UID=$(id -u) FABRIC_GID=$(id -g) docker compose up -d --build and view logs with docker compose logs -f. The supervised gateway's stdout is also tee'd to ${FABRIC_HOME}/logs/gateways/<profile>/current on the volume — see Where the logs go for the full routing map. Because the services use host networking, do not add ports: mappings; Docker ignores them in this mode.

Optional: Linux desktop audio bridge

Voice mode in Docker needs two separate things to work: Fabric must be allowed to probe audio devices inside the container, and the container must be able to reach your host audio server. The setup below covers the host audio plumbing for Linux desktops that expose a PulseAudio-compatible socket, including many PipeWire setups.

caution

This is a Linux desktop workaround, not a general Docker Desktop feature. It is useful when you already have host audio working and want CLI voice mode inside the Fabric container. If Fabric still reports Running inside Docker container -- no audio devices, use a build that includes Docker audio probing support for PULSE_SERVER / PIPEWIRE_REMOTE.

First, create an ALSA config next to your Compose file:

asound.conf
pcm.!default {
type pulse
hint {
show on
description "Default ALSA Output (PulseAudio)"
}
}

pcm.pulse {
type pulse
}

ctl.!default {
type pulse
}

Then build a small derived image with the ALSA PulseAudio plugin installed:

Dockerfile.audio
FROM ghcr.io/obliviousodin/fabric:latest

USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends libasound2-plugins \
&& rm -rf /var/lib/apt/lists/*

Use that image in Compose and pass through the host user's PulseAudio socket and cookie:

services:
fabric:
build:
context: .
dockerfile: Dockerfile.audio
image: fabric-agent-audio
container_name: fabric
restart: unless-stopped
command: gateway run
volumes:
- ~/.fabric:/opt/data
- /run/user/${PUID}/pulse:/run/user/${PUID}/pulse
- ~/.config/pulse/cookie:/tmp/pulse-cookie:ro
- ./asound.conf:/etc/asound.conf:ro
environment:
- PUID=${PUID}
- PGID=${PGID}
- XDG_RUNTIME_DIR=/run/user/${PUID}
- PULSE_SERVER=unix:/run/user/${PUID}/pulse/native
- PULSE_COOKIE=/tmp/pulse-cookie

Start it with your host UID/GID so the container process can access the per-user audio socket:

export PUID="$(id -u)"
export PGID="$(id -g)"
docker compose up -d --build

To verify what PortAudio sees inside the container:

docker exec fabric /opt/fabric/.venv/bin/python -c "import sounddevice as sd; print(sd.query_devices())"

Resource limits

The Fabric container needs moderate resources. Recommended minimums:

ResourceMinimumRecommended
Memory1 GB2–4 GB
CPU1 core2 cores
Disk (data volume)500 MB2+ GB (grows with sessions/skills)

Browser automation (Playwright/Chromium) is the most memory-hungry feature. If you don't need browser tools, 1 GB is sufficient. With browser tools active, allocate at least 2 GB.

Set limits in Docker:

docker run -d \
--name fabric \
--restart unless-stopped \
--memory=4g --cpus=2 \
-v ~/.fabric:/opt/data \
ghcr.io/obliviousodin/fabric gateway run

What the Dockerfile does

The official image is based on debian:13.4 and includes:

  • Python 3.13 with dependencies synced from the lockfile via uv sync --frozen --no-install-project for the baked extras (all, messaging, Anthropic/Bedrock/Azure identity, Hindsight, Matrix), followed by a no-dependency editable install of Fabric itself.
  • Node.js 22 + npm (for browser automation, the WhatsApp bridge, the web dashboard and TUI bundles, and workspace build tooling)
  • Playwright with Chromium (npx playwright install --with-deps chromium --only-shell)
  • ripgrep, ffmpeg, git, and xz-utils as system utilities
  • docker-cli — so agents running inside the container can drive the host's Docker daemon (bind-mount /var/run/docker.sock to opt in) for docker build, docker run, container inspection, etc.
  • openssh-client — enables the SSH terminal backend from inside the container. The SSH backend shells out to the system ssh binary; without this, it failed silently in containerized installs.
  • The WhatsApp bridge (scripts/whatsapp-bridge/)
  • s6-overlay v3 as PID 1 (replaces the older tini) — supervises per-profile gateways with auto-restart on crash, reaps zombie subprocesses, and forwards signals.

The image treats /opt/fabric as an immutable install tree at runtime. Optional Python extras, Node workspaces, and TUI assets that must be available inside Docker need to be baked during the image build. The allow-listed lazy dependency path is redirected to /opt/data/lazy-packages; it never writes into the sealed venv.

The container's ENTRYPOINT is s6-overlay's /init. On boot it:

  1. Runs /etc/cont-init.d/01-fabric-setup (= docker/stage2-hook.sh) as root: optional UID/GID remap, fixes volume ownership, seeds .env / config.yaml / SOUL.md on first boot, runs non-interactive config-schema migrations, and syncs bundled skills.
  2. Runs /etc/cont-init.d/02-reconcile-profiles (= fabric_cli.container_boot): reconciles the default profile at $FABRIC_HOME plus named profiles under $FABRIC_HOME/profiles/<name>/, recreates their s6 service slots under /run/service/gateway-<profile>/, and auto-starts only those whose durable desired state was running (see Per-profile gateway supervision).
  3. Starts the static main-fabric s6-rc service.
  4. Execs the container's CMD as the main program (/opt/fabric/docker/main-wrapper.sh), which routes the arguments the user passed to docker run:
    • no args → fabric (the default)
    • first arg is an executable on PATH (e.g. sleep, bash) → exec it directly
    • anything else → fabric <args> (subcommand passthrough) The container exits when this main program exits, with its exit code.
Privilege model

Do not override the image entrypoint: keep the image's /init + /opt/fabric/docker/main-wrapper.sh chain. s6-overlay's /init must start as root so it can remap the service UID/GID and repair the data volume; each real service and the main command then drops to the internal fabric account via s6-setuidgid. Starting fabric gateway run as root inside the official image is refused because it can leave root-owned files in /opt/data and break later dashboard or gateway starts.

docker exec automatically drops to the service account

docker exec fabric fabric <subcommand> defaults to running as root inside the container, but the image ships a thin shim at /opt/fabric/bin/fabric (earliest on PATH) that detects root callers and transparently re-execs through s6-setuidgid fabric. So docker exec fabric fabric config set …, docker exec fabric fabric profile create …, docker exec fabric fabric setup, and other state-changing commands write files owned by the service UID (10000 by default, or the UID selected with FABRIC_UID) and remain readable by the supervised gateway, with no extra --user flag needed. Non-root callers (the supervised processes themselves, docker exec --user fabric, kanban subagents inside the container) take a short path that execs the venv binary directly, so there is no extra privilege-drop hop on hot paths.

If you specifically need a docker exec Fabric command to retain root semantics for a bounded diagnostic, use the canonical opt-out:

docker exec -e FABRIC_DOCKER_EXEC_AS_ROOT=1 fabric fabric <subcommand>

Commands other than the fabric executable are not intercepted, so an explicit root shell remains docker exec --user root -it fabric sh. Use root only for bounded diagnostics or repair. The shim accepts only 1, true, or yes (case-insensitive); every other value keeps the safe privilege drop. If s6-setuidgid is not available (custom builds that stripped s6-overlay), the shim refuses ordinary root invocations and exits 126 instead of allowing state written by docker exec to become unreadable to the supervised gateway.

Per-profile gateway supervision

Each profile created with fabric profile create <name> automatically gets an s6-supervised gateway service registered at /run/service/gateway-<name>/, with state-persistent auto-restart across container restarts. See Multi-profile support above for the user-facing workflow and the lifecycle commands.

Supervision benefits over the pre-s6 image:

  • Gateway crashes are auto-restarted by s6-supervise after a ~1s backoff.
  • docker restart, image upgrades (docker compose up -d --force-recreate), and unexpected exits preserve running gateways: the cont-init reconciler reads $FABRIC_HOME/profiles/<name>/gateway_state.json and brings the slot back up if the last recorded state was running. Only an explicit fabric gateway stop records stopped and keeps the gateway down across the restart; the container/s6 SIGTERM sent on a restart or upgrade is treated as "still running" and auto-starts.
  • Per-profile gateway logs persist under $FABRIC_HOME/logs/gateways/<profile>/current (rotated by s6-log), and the reconciler's actions are appended to $FABRIC_HOME/logs/container-boot.log per boot. See Where the logs go for the full routing map.

fabric status inside the container reports Manager: s6 (container supervisor). Use /command/s6-svstat /run/service/gateway-<name> for the raw supervisor view (note /command/ is on PATH for supervision-tree processes only; pass the absolute path when calling from docker exec).

Upgrading

Pull the latest image and recreate the container. Your data directory is preserved, and the container runs non-interactive config-schema migrations against the mounted $FABRIC_HOME/config.yaml before starting the gateway. When a migration is needed, Fabric writes timestamped backups next to config.yaml and .env first.

docker pull ghcr.io/obliviousodin/fabric:latest
docker rm -f fabric
docker run -d \
--name fabric \
--restart unless-stopped \
-v ~/.fabric:/opt/data \
ghcr.io/obliviousodin/fabric gateway run

Or with Docker Compose:

docker compose pull
docker compose up -d

Skills and credential files

When using Docker as the execution environment (not the methods above, but when the agent runs commands inside a Docker sandbox — see Configuration → Docker Backend), Fabric reuses a single long-lived container for all tool calls and automatically bind-mounts the skills directory (~/.fabric/skills/) and any credential files declared by skills into that container as read-only volumes. Skill scripts, templates, and references are available inside the sandbox without manual configuration, and because the container persists for the life of the Fabric process, any dependencies you install or files you write stay around for the next tool call.

The same syncing happens for SSH and Modal backends — skills and credential files are uploaded via rsync or the Modal mount API before each command.

Installing more tools in the container

The official image ships with a curated set of utilities (see What the Dockerfile does), but not every tool an agent might want is preinstalled. There are five recommended approaches, in increasing order of effort and durability.

npm or Python tools — use npx or uvx

For any tool published to npm or PyPI, instruct Fabric to run it via npx (npm) or uvx (Python) and to remember that command in its persistent memory. If the tool needs a config file or credentials, instruct it to drop those under /opt/data (e.g. /opt/data/<tool>/config.yaml).

Dependencies are fetched on demand. The image sets HOME=/opt/data for runtime commands, so the normal npm and uv caches, along with tool configuration stored under that home, remain on the mounted data volume and can survive container recreation. Treat a tool's own cache location as tool-specific; point it under /opt/data explicitly if durability matters.

Other tools (apt packages, binaries) — install and remember

For a quick diagnostic, an operator can install an apt package into the running container's writable layer as root:

docker exec --user root fabric sh -lc 'apt-get update && apt-get install -y <package>'

That package survives a stop/start or restart of the same container, but it is lost when the container is removed or recreated during an image upgrade. Fabric does not automatically replay the install command.

This is a good fit for tools that are quick to install and used occasionally. For tools used constantly, prefer the next approach.

Durable installs — build a derived image

When a tool must be available immediately on every container start with no re-install delay, build a new image that inherits from ghcr.io/obliviousodin/fabric and installs the tool in a layer:

FROM ghcr.io/obliviousodin/fabric:latest

USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends <your-package> \
&& rm -rf /var/lib/apt/lists/*

Leave the final image user as root. The /init bootstrap needs root at container start and drops runtime commands to the internal fabric account itself; ending a derived Dockerfile with USER fabric bypasses required volume and supervision setup.

Build it and use it in place of the official image:

docker build -t my-fabric:latest .
docker run -d \
--name fabric \
--restart unless-stopped \
-v ~/.fabric:/opt/data \
-p 8642:8642 \
my-fabric:latest gateway run

The entrypoint script and /opt/data semantics are inherited unchanged, so the rest of this page still applies. Remember to rebuild the image when pulling a newer upstream ghcr.io/obliviousodin/fabric.

Complex tools or multi-service stacks — run a sidecar container

For tools that bring their own service (a database, a web server, a queue, a headless browser farm) or that are too heavy to live inside the Fabric container, run them as a separate container on a shared Docker network. Fabric reaches the sidecar by container name, the same way it reaches a local inference server (see Connecting to local inference servers).

services:
fabric:
image: ghcr.io/obliviousodin/fabric:latest
container_name: fabric
restart: unless-stopped
command: gateway run
ports:
- "8642:8642"
volumes:
- ~/.fabric:/opt/data
networks:
- fabric-net

my-tool:
image: example/my-tool:latest
container_name: my-tool
restart: unless-stopped
networks:
- fabric-net

networks:
fabric-net:
driver: bridge

From inside the Fabric container, the sidecar is reachable at http://my-tool:<port> (or whatever protocol it serves). This pattern keeps each service's lifecycle, resource limits, and upgrade cadence independent, and avoids bloating the Fabric image with dependencies that are only needed by one tool.

Broadly useful tools — open an issue or pull request

If a tool is likely to be useful to most Fabric users, consider contributing it to the official repository rather than carrying it in a private derived image. Open an issue or pull request on the Fabric repository describing the tool and its use case. Tools that get bundled into the official image benefit every user and avoid the maintenance overhead of a downstream fork.

Connecting to local inference servers (vLLM, Ollama, etc.)

When running Fabric in Docker and your inference server (vLLM, Ollama, text-generation-inference, etc.) is also running on the host or in another container, networking requires extra attention.

Put both services on the same Docker network. This is the most reliable approach:

services:
vllm:
image: vllm/vllm-openai:latest
container_name: vllm
command: >
--model Qwen/Qwen2.5-7B-Instruct
--served-model-name my-model
--host 0.0.0.0
--port 8000
ports:
- "8000:8000"
networks:
- fabric-net
deploy:
resources:
reservations:
devices:
- capabilities: [gpu]

fabric:
image: ghcr.io/obliviousodin/fabric:latest
container_name: fabric
restart: unless-stopped
command: gateway run
ports:
- "8642:8642"
volumes:
- ~/.fabric:/opt/data
networks:
- fabric-net

networks:
fabric-net:
driver: bridge

Then in your ~/.fabric/config.yaml, use the container name as the hostname:

model:
provider: custom
model: my-model
base_url: http://vllm:8000/v1
api_key: "none"
Key points
  • Use the container name (vllm) as the hostname — not localhost or 127.0.0.1, which refer to the Fabric container itself.
  • The model value must match the --served-model-name you passed to vLLM.
  • Set api_key to any non-empty string (vLLM requires the header but doesn't validate it by default).
  • Do not include a trailing slash in base_url.

Standalone Docker run (no Compose)

If your inference server runs directly on the host (not in Docker), use host.docker.internal on macOS/Windows, or --network host on Linux:

macOS / Windows:

docker run -d \
--name fabric \
-v ~/.fabric:/opt/data \
-p 8642:8642 \
ghcr.io/obliviousodin/fabric gateway run
# config.yaml
model:
provider: custom
model: my-model
base_url: http://host.docker.internal:8000/v1
api_key: "none"

Linux (host networking):

docker run -d \
--name fabric \
--network host \
-v ~/.fabric:/opt/data \
ghcr.io/obliviousodin/fabric gateway run
# config.yaml
model:
provider: custom
model: my-model
base_url: http://127.0.0.1:8000/v1
api_key: "none"
With --network host, the -p flag is ignored — all container ports are directly exposed on the host.

Verifying connectivity

From inside the Fabric container, confirm the inference server is reachable:

docker exec fabric curl -s http://vllm:8000/v1/models

You should see a JSON response listing your served model. If this fails, check:

  1. Both containers are on the same Docker network (docker network inspect fabric-net)
  2. The inference server is listening on 0.0.0.0, not 127.0.0.1
  3. The port number matches

Ollama

Ollama works the same way. If Ollama runs on the host, use host.docker.internal:11434 (macOS/Windows) or 127.0.0.1:11434 (Linux with --network host). If Ollama runs in its own container on the same Docker network:

model:
provider: custom
model: llama3
base_url: http://ollama:11434/v1
api_key: "none"

Troubleshooting

Container exits immediately

Check logs: docker logs fabric. Common causes:

  • Missing or invalid .env file — run interactively first to complete setup
  • Port conflicts if running with exposed ports

"Permission denied" errors

The container's stage2 hook drops privileges to the internal non-root fabric service account (UID 10000 by default) via s6-setuidgid inside each supervised service. If your host ~/.fabric/ is owned by a different UID, set FABRIC_UID/FABRIC_GID to match the host owner. The PUID/PGID aliases are also accepted for NAS deployments that follow the LinuxServer convention. Do not recursively chmod 755 the data directory: it does not grant the service account write access and can expose secret-bearing files. The boot hook applies targeted ownership fixes and keeps .env at mode 0600.

On a NAS (UGOS, Synology, unRAID) the data directory is typically a bind mount owned by a host UID the container cannot chown. Set the supported PUID/PGID aliases to that host user so the runtime runs as the owner of the mount rather than UID 10000:

docker run -d \
--name fabric \
-e PUID=1000 -e PGID=10 \
-v /volume1/docker/fabric:/opt/data \
ghcr.io/obliviousodin/fabric gateway run

docker exec fabric <cmd> automatically drops to the current service UID too (10000 unless remapped) — see docker exec automatically drops to the service account for details and the per-invocation opt-out.

Browser tools not working

Playwright needs shared memory. Add --shm-size=1g to your Docker run command:

docker run -d \
--name fabric \
--shm-size=1g \
-v ~/.fabric:/opt/data \
ghcr.io/obliviousodin/fabric gateway run

Gateway not reconnecting after network issues

The --restart unless-stopped flag handles most transient failures. If the gateway is stuck, restart the container:

docker restart fabric

Checking container health

docker logs --tail 50 fabric          # Recent logs
docker run -it --rm ghcr.io/obliviousodin/fabric:latest version # Verify version
docker stats fabric # Resource usage