Skip to main content
Commands, package names, and image names on this page come from the open-source project that Mibyan Desktop is built on, and can differ from the Mibyan Desktop installer. For the supported Mibyan install and update path, see Install and update.
There are two distinct ways Docker intersects with Mibyan:
  1. Running Mibyan 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 Mibyan 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.

Image channels and runtime ownership

The workflow builds and tests amd64 and arm64 images. Stable publication uses the tested image archives rather than rebuilding them. Only the full release promotion moves stable and latest; a main push does not advance those tags. The image’s Python environment follows pyproject.toml and uv.lock (currently Python 3.14). Its curated extras are not the native desktop bundle’s --all-extras set. It does not include an Electron desktop app.

Quick start

If this is your first time running Mibyan, 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 commandsSome 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 ~/.mibyan:/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.
This drops you into the setup wizard, which will prompt you for your API keys and write them to ~/.mibyan/.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.
Inside the container, run mibyan setup --portal once — the refresh token persists in the mounted ~/.mibyan volume. See Nous Portal.

Running in gateway mode

Once configured, run the container in the background as a persistent gateway (Telegram, Discord, Slack, WhatsApp, etc.):
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 supervisedInside 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, and the dashboard (when mibyan_DASHBOARD=1 is set) is supervised alongside it. 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 get the historical “gateway is the container’s main process, container exit = gateway exit” semantics — pass --no-supervise or set mibyan_GATEWAY_NO_SUPERVISE=1. 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 goSee 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 gatewaysUnattended gateway and cron sessions enable tool-loop hard stops by default through non_interactive_hard_stop_enabled. Interactive CLI, TUI, Desktop, and ACP sessions remain warning-only. To opt an unattended deployment out in the profile’s config.yaml:
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:
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

The built-in web dashboard runs as a supervised s6-rc service alongside the gateway in the same container. Set mibyan_DASHBOARD=1 to bring it up:
The dashboard is supervised by s6 — if it crashes, s6-supervise restarts it automatically after a short backoff. Dashboard stdout/stderr is forwarded to docker logs <container> (no prefix; the gateway’s own output now lives in a per-profile s6-log file — see Where the logs go below — so the two streams don’t clash). The dashboard inside the container defaults to binding 0.0.0.0 — without it, the published -p 9119:9119 port would not be reachable from the host. To restrict the bind to container loopback (for sidecar / reverse-proxy setups), set mibyan_DASHBOARD_HOST=127.0.0.1. The dashboard’s auth gate engages automatically when both of the following are true:
  1. The bind host is non-loopback (e.g. the default 0.0.0.0 inside the container), and
  2. A DashboardAuthProvider plugin is registered.
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 mibyan_DASHBOARD_BASIC_AUTH_USERNAME + mibyan_DASHBOARD_BASIC_AUTH_PASSWORD (and mibyan_DASHBOARD_BASIC_AUTH_SECRET for restart-stable sessions). Not suitable for direct public-internet exposure.
  • OAuth (Nous Portal) — for hosted/public deploys: the dashboard_auth/nous provider activates whenever mibyan_DASHBOARD_OAUTH_CLIENT_ID is set.
  • Self-hosted OIDC — to authenticate against your own identity provider via standard OpenID Connect: the dashboard_auth/self_hosted provider activates when mibyan_DASHBOARD_OIDC_ISSUER + mibyan_DASHBOARD_OIDC_CLIENT_ID are set.
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. When a reverse proxy such as Traefik or nginx runs in another container, its bridge-network address is not trusted by default. Set the dashboard’s public URL and trust only that proxy’s exact IP, or a bounded CIDR for a dedicated proxy network, in the mounted config.yaml:
This allows the proxy’s X-Forwarded-Proto: https to control secure OAuth cookies while leaving forwarding headers from other peers untrusted. Do not use *, 0.0.0.0/0, or ::/0; Mibyan rejects those unbounded entries. If no provider is registered and the bind is non-loopback, the dashboard fails closed at startup with a specific error pointing at the missing env var. There is no longer an escape hatch that serves the dashboard unauthenticated on a public bind: mibyan_DASHBOARD_INSECURE=1 is now a deprecated no-op (it logs a warning and is ignored). Configure a provider, or bind mibyan_DASHBOARD_HOST=127.0.0.1 and reach the dashboard over an SSH tunnel / Tailscale instead.
Why --insecure was removedAn 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 / homelab box, the bundled username/password provider (mibyan_DASHBOARD_BASIC_AUTH_USERNAME + _PASSWORD) is the zero-infra way to satisfy it.
Running the dashboard as a separate container is supported when that container shares the host PID and network namespace (e.g. network_mode: host, as the repo’s own docker-compose.yml does — see its dashboard service). Its gateway-liveness detection requires a shared PID namespace with the gateway process, so the limitation only applies to dashboards run in isolated bridge-network containers without a shared PID namespace.

Running interactively (CLI chat)

To open an interactive chat session against a running data directory:
Or if you have already opened a terminal in your running container (via Docker Desktop for instance), just run:

Persistent volumes

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

Filesystem requirements for state.db in containers

Mibyan keeps sessions in a SQLite database (/opt/data/state.db) that is opened in WAL journal mode by default. WAL relies on shared memory (state.db-shm) being coherent between every process that has the file open. Bind mounts that cross a VM boundary do not provide that: virtiofs (Docker Desktop and Podman on macOS, OrbStack) and 9p / drvfs (Docker Desktop on Windows) both let concurrent writers silently corrupt a WAL database while the main file still passes PRAGMA integrity_check. What Mibyan does about it (since v2026.9.14):
  • A fresh database whose directory is on a virtiofs/9p mount is created in rollback (DELETE) journal mode and a one-time warning is logged. Nothing to do.
  • An existing WAL database on such a mount is never live-downgraded — other Mibyan processes may hold it open, and a live switch destroys their uncheckpointed commits. Instead, every process logs a one-time error at startup and mibyan doctor flags the database. Fix it one of two ways:
    1. Stop every Mibyan process that uses the database, then run a one-time offline conversion with the Python that ships in the image (it has no sqlite3 shell): docker exec mibyan python3 -c "import sqlite3; print(sqlite3.connect('/opt/data/state.db').execute('PRAGMA journal_mode=DELETE').fetchone()[0])". Set database.journal_mode: delete in config.yaml so a later open does not switch it back to WAL.
    2. Move the data directory onto a native volume — a named Docker volume (-v mibyan-data:/opt/data) lives on the VM’s own ext4 filesystem and supports WAL normally.
Detection reads /proc/self/mountinfo inside the container, so it works regardless of the host operating system. It does not classify NFS, SMB, or generic FUSE mounts; on those, set database.journal_mode: delete explicitly. Mibyan does not offer SQLite’s locking_mode=EXCLUSIVE as an alternative because the gateway, cron, and worker processes open the database concurrently.

Immutable install tree

In hosted and published Docker images, /opt/mibyan is the installed application tree. It is root-owned and read-only to the runtime mibyan user, so agent turns, gateway sessions, dashboard actions, and normal docker exec mibyan mibyan ... commands cannot edit the core source, bundled .venv, node_modules, or TUI bundle in place. All mutable Mibyan state belongs under /opt/data: config, .env, profiles, skills, memories, sessions, logs, dashboard uploads, plugins, and other user-managed files. The image also disables runtime .pyc writes and Mibyan lazy dependency installs into /opt/mibyan; optional platform dependencies needed by the published image should be baked into the image or installed through a new image build. On hosted/published images, agent self-improvement is scoped to skills, memory, plugins, and config under /opt/data. The installed core source under /opt/mibyan 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, use a root shell intentionally. The mibyan shim normally drops docker exec mibyan mibyan ... back to the runtime user; set mibyan_DOCKER_EXEC_AS_ROOT=1 for a one-off root invocation when you explicitly need root semantics. 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, Mibyan 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.
Never run two Mibyan gateway containers against the same data directory simultaneously — session files and memory stores are not designed for concurrent write access.

Multi-profile support

Mibyan supports multiple profiles — separate ~/.mibyan/ 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 mibyan 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 ${mibyan_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 (mibyan 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.
A profile created from the host against a bind-mounted ~/.mibyan gets its directory but no slot (the host process cannot reach the container’s /run/service). Inside the container, mibyan -p <name> gateway start registers the missing slot on demand and starts it — no docker restart needed. Only start does this, and only for a real profile directory (one carrying SOUL.md); stop/restart on an unregistered profile and a mistyped -p name still fail with ✗ no such gateway. The lifecycle commands you’d run on the host work the same way from inside the container:
Under the hood, mibyan 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: Mibyan Desktop (and the web dashboard). The Desktop app’s Remote Gateway connection talks to a mibyan dashboard backend (default port 9119, enabled by mibyan_DASHBOARD=1) — 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 mibyan_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:
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: The default profile (default) is always registered on first boot, so a fresh container ships with one supervised gateway out of the box. Additional profiles are pure runtime adds.

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:
The warning from Persistent volumes still applies: never point two containers at the same ~/.mibyan 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: 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 ~/.mibyan/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:
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 Mibyan itself inside Docker. If you want Mibyan to execute the agent’s terminal / execute_code calls inside a Docker sandbox container (one long-lived container shared across Mibyan processes — see issue #20561), 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

For persistent deployment with both the gateway and dashboard, a docker-compose.yaml is convenient:
Start with docker compose up -d and view logs with docker compose logs -f. The supervised gateway’s stdout is also tee’d to ${mibyan_HOME}/logs/gateways/<profile>/current on the volume — see Where the logs go for the full routing map.

Optional: Linux desktop audio bridge

Voice mode in Docker needs two separate things to work: Mibyan 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.
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 Mibyan container. If Mibyan 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
Then build a small derived image with the ALSA PulseAudio plugin installed:
Dockerfile.audio
Use that image in Compose and pass through the host user’s PulseAudio socket and cookie:
Start it with your host UID/GID so the container process can access the per-user audio socket:
To verify what PortAudio sees inside the container:

Resource limits

The Mibyan container needs moderate resources. Recommended minimums: 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:

What the Dockerfile does

The image uses Debian 13.4 and includes:
  • A Python 3.14 environment synchronized from the committed uv.lock, followed by a no-dependency editable install of Mibyan.
  • The curated extras all, messaging, otlp, anthropic, bedrock, azure-identity, and matrix. This is not --all-extras.
  • Node.js 26 and npm from the digest-pinned Node source image.
  • PM-pinned uv, full Chromium, FFmpeg, and ripgrep in /opt/mibyan/tools.
  • System Git, OpenSSH, Docker CLI, and Chromium shared libraries.
  • Prebuilt TUI/dashboard assets and baked Photon sidecar dependencies.
  • s6-overlay for supervision and zombie-process cleanup.
Chromium is staged through PM, not npx playwright install. The build records its resolved executable in /etc/mibyan/agent-browser-executable-path. PLAYWRIGHT_BROWSERS_PATH names /opt/mibyan/tools, outside the data mount. Every image, including the unsuffixed (non--desktop) tags, carries the full Chromium build rather than Playwright’s lighter headless shell: one pinned, checksummed browser serves both headless browsing and headed Bot Screen sessions. The cost is image size — the full build is larger than the headless shell earlier images shipped. Opt-in backend SDKs (Edge TTS, Firecrawl, Exa, platform adapters, plugin dependencies) install on first use into PM dependency generations under /opt/data/installs, so they survive container recreation and image updates. The image’s own /opt/mibyan/.venv is never modified. On each boot the container re-resolves the recorded selection against the new image’s lock before services start; if that fails (for example offline), it boots the image’s own environment and keeps the recorded extras for the next boot or install. Set security.allow_lazy_installs: false to refuse on-demand installs. The old lazy-packages overlay is not used. Image provenance lives at /etc/mibyan/image-provenance.json, outside both the source and data mounts. The build stamp lives at /opt/mibyan/install-stamp.json. A local build without a supplied stamp reports an unknown revision rather than inventing a commit. mibyan update refuses image-owned code changes; replace the image to update the application. The container’s ENTRYPOINT is a small dispatcher (docker/entrypoint-dispatch.sh). When the container owns PID 1 (normal Docker / Podman), it exec’s s6-overlay’s /init and you get the full supervision tree described below. When a platform wraps the image entrypoint under its own PID-1 init (Fly.io Machines, docker run --init, some Nomad/Kubernetes setups), /init would abort with s6-overlay-suexec: fatal: can only run as pid 1 — so the dispatcher instead runs the stage2 bootstrap directly and exec’s the main wrapper without s6. On that fallback path the requested command still runs, but supervised services (dashboard, per-profile gateways) are unavailable. On the PID-1 path, /init:
  1. Runs /etc/cont-init.d/01-mibyan-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 unless mibyan_SKIP_CONFIG_MIGRATION=1, syncs bundled skills.
  2. Runs /etc/cont-init.d/02-reconcile-profiles (= mibyan_cli.container_boot): walks $mibyan_HOME/profiles/<name>/, recreates the per-profile gateway s6 service slot under /run/service/gateway-<profile>/, and auto-starts only those whose last recorded state was running (see Per-profile gateway supervision).
  3. Starts the static main-mibyan and dashboard s6-rc services.
  4. Exec’s the container’s CMD as the main program (/opt/mibyan/docker/main-wrapper.sh), which routes the arguments the user passed to docker run:
    • no args → mibyan (the default)
    • first arg is an executable on PATH (e.g. sleep, bash) → exec it directly
    • anything else → mibyan <args> (subcommand passthrough) The container exits when this main program exits, with its exit code.
Breaking change vs. pre-s6 imagesThe container ENTRYPOINT is now the entrypoint-dispatch.sh dispatcher (which delegates to s6-overlay’s /init under PID 1), not /usr/bin/tini. All five documented docker run invocation patterns (no args, chat -q "…", sleep infinity, bash, --tui) behave identically to the tini-based image. If you have a downstream wrapper that depended on tini-specific signal behavior or hard-coded /usr/bin/tini -- invocation, pin to the previous image tag.
Privilege modelDo not override the image entrypoint unless you keep /init (or, equivalently, the legacy docker/entrypoint.sh shim that forwards to the stage2 hook) in the command chain. s6-overlay’s /init runs as root so it can chown the volume on first boot, then drops to the mibyan user via s6-setuidgid for every supervised service AND for the main program. Starting mibyan gateway run as root inside the official image is refused by default because it can leave root-owned files in /opt/data and break later dashboard or gateway starts. Set mibyan_ALLOW_ROOT_GATEWAY=1 only when you intentionally accept that risk.
Overriding entrypoint: also removes the zombie reaper/init is what reaps orphaned grandchildren (headless browsers, MCP servers, git/npm helpers spawned by tools). A Compose service that overrides entrypoint: to call mibyan directly — for example to run the dashboard as a non-root user — makes the mibyan process itself PID 1, and nothing above it ever calls wait(): every orphan stays a <defunct> entry forever (one deployment reached 284 zombies in under three hours). Mibyan prints [mibyan] WARNING: this process is PID 1 with no init above it at startup in that configuration.If you must override the entrypoint, add Docker’s init as PID 1 so orphans are reaped:
(docker run --init … is the equivalent flag.) This fixes the zombie accumulation only — with /init out of the chain, the s6 supervision tree is still gone: the dashboard, mibyan gateway run and per-profile gateways are unsupervised, exactly as the dispatcher’s own non-PID-1 warning says. Keep the default ENTRYPOINT whenever you can.

docker exec automatically drops to the mibyan user

docker exec mibyan <cmd> defaults to running as root inside the container, but the image ships a thin shim at /opt/mibyan/bin/mibyan (earliest on PATH) that detects root callers and transparently re-execs through s6-setuidgid mibyan. So docker exec mibyan mibyan login, docker exec mibyan mibyan profile create …, docker exec mibyan mibyan setup, etc. all write files owned by UID 10000 — i.e. readable by the supervised gateway — with no extra --user flag needed. Non-root callers (the supervised processes themselves, docker exec --user mibyan, kanban subagents inside the container) hit a short-circuit that exec’s the venv binary directly, so there’s no overhead on the hot paths. If you specifically need a docker exec that retains root semantics (diagnostic sessions, inspecting root-only state, files outside /opt/data that root happens to own), opt out per invocation:
The shim accepts 1 / true / yes (case-insensitive). Anything else — including typos like =0 — falls through to the drop, so silent opt-outs aren’t possible. If s6-setuidgid isn’t available (custom builds that stripped s6-overlay), the shim refuses to run as root and exits 126 instead, surfacing the broken privilege model loudly rather than regressing to the historical footgun where docker exec mibyan login would write auth.json as root:root and break the supervised gateway’s auth on every chat platform message.

Per-profile gateway supervision

Each profile created with mibyan 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.
  • Dashboard, when enabled with mibyan_DASHBOARD=1, is supervised on the same supervision tree and gets the same auto-restart treatment.
  • docker restart, image upgrades (docker compose up -d --force-recreate), and unexpected exits preserve running gateways: the cont-init reconciler reads $mibyan_HOME/profiles/<name>/gateway_state.json and brings the slot back up if the last recorded state was running. Only an explicit mibyan 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 $mibyan_HOME/logs/gateways/<profile>/current (rotated by s6-log), and the reconciler’s actions are appended to $mibyan_HOME/logs/container-boot.log per boot. See Where the logs go for the full routing map.
mibyan 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 $mibyan_HOME/config.yaml before starting the gateway. When a migration is needed, Mibyan writes timestamped backups next to config.yaml and .env first.
Or with Docker Compose:
Set mibyan_SKIP_CONFIG_MIGRATION=1 only if you need to inspect or migrate the persisted config manually before letting the new image rewrite it.

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), Mibyan reuses a single long-lived container for all tool calls and automatically bind-mounts the skills directory (~/.mibyan/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 Mibyan 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 Mibyan 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 and cached for the life of the container. Configuration written under /opt/data survives container restarts because it lives on the bind-mounted host directory. The package cache itself is rebuilt after a docker rm, but npx and uvx re-fetch transparently the next time the tool runs.

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

The runtime user cannot install system APT packages. For occasional tools, an operator can deliberately use a root shell; those changes last only until the container is replaced. A container restart retains its writable layer, while recreation does not. Memory of an install command is not automatic provisioning. Use a derived image for repeatable system dependencies. 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 nousresearch/hermes-agent and installs the tool in a layer:
Build it and use it in place of the official image:
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 nousresearch/hermes-agent.

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 Mibyan container, run them as a separate container on a shared Docker network. Mibyan reaches the sidecar by container name, the same way it reaches a local inference server (see Connecting to local inference servers).
From inside the Mibyan 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 Mibyan 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 Mibyan users, consider contributing it upstream rather than carrying it in a private derived image. Open an issue or pull request on the mibyan-agent 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 Mibyan 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:
Then in your ~/.mibyan/config.yaml, use the container name as the hostname:
Key points
  • Use the container name (vllm) as the hostname — not localhost or 127.0.0.1, which refer to the Mibyan 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:
Linux (host networking):
With --network host, the -p flag is ignored — all container ports are directly exposed on the host.

Verifying connectivity

From inside the Mibyan container, confirm the inference server is reachable:
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 mibyan-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:

Troubleshooting

Container exits immediately

Check logs: docker logs mibyan. 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 non-root mibyan user (UID 10000) via s6-setuidgid inside each supervised service. If your host ~/.mibyan/ is owned by a different UID, set mibyan_UID/mibyan_GID — or their PUID/PGID aliases, for parity with LinuxServer.io and NAS images — to match your host user, or ensure the data directory is writable: Do not make the whole data tree world-readable. It contains credentials. Match the container UID/GID to the bind mount’s owner instead. On a NAS (UGOS, Synology, unRAID) the data directory is typically a bind mount owned by a host UID the container cannot chown. Set PUID/PGID (or mibyan_UID/mibyan_GID) to that host user so the runtime runs as the owner of the mount rather than UID 10000:
docker exec mibyan <cmd> automatically drops to UID 10000 too — see docker exec automatically drops to the mibyan user for details and the per-invocation opt-out.

Shared data directory keeps resetting to 0700

Outside a container Mibyan locks mibyan_HOME (and its cron/, sessions/, logs/, memories/ subdirectories) to owner-only 0700 on every start. Inside a container it leaves directory modes alone, so a bind mount shared with a sibling container running as a different UID (a web UI, a permissions fixer) keeps whatever mode and ACLs you set on the host. To force a specific directory mode anyway, set mibyan_HOME_MODE (octal, e.g. mibyan_HOME_MODE=0755); it is applied in containers too.

”Permission denied” on every docker exec (install dir locked to 0700)

Images built before late August 2026 had a bug where writing a credential file directly under /opt/mibyan restricted that directory to 0700, locking the mibyan user (UID 10000) out of the install tree. Every new docker exec then fails with Permission denied. Pulling a newer image and recreating the container fixes it permanently (the install dir ships as 0755 and current releases no longer restrict it). If you need to recover a running container in place without recreating it:

Zombie (<defunct>) processes piling up under PID 1

ps -eo stat,ppid,comm | awk '$1 ~ /^Z/' inside the container lists dead children that were never reaped. This happens when mibyan itself is PID 1 — almost always because a Compose service overrides entrypoint: and so skips docker/entrypoint-dispatch.sh → /init. Mibyan also warns about it at startup (this process is PID 1 with no init above it). Restore the default entrypoint, or add init: true (Compose) / docker run --init so docker-init reaps orphans; see What the Dockerfile does. Recreating the container clears the existing zombies.

Browser tools not working

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

Gateway not reconnecting after network issues

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

Checking container health