diff --git a/.env.selfhost.example b/.env.selfhost.example
new file mode 100644
index 0000000000..d30e58a470
--- /dev/null
+++ b/.env.selfhost.example
@@ -0,0 +1,41 @@
+# ──────────────────────────────────────────────────────────────────────
+# OmniRoute — Self-Host env (minimal, zero-fee self-host)
+# ──────────────────────────────────────────────────────────────────────
+# cp .env.selfhost.example .env
+# Edit only the two lines marked `# EDIT ME`. Everything else has a sane
+# default. No secrets are baked in — OmniRoute never ships credentials.
+#
+# Full variable reference: docs/guides/DOCKER_GUIDE.md and .env.example
+# ──────────────────────────────────────────────────────────────────────
+
+# ── Ports (host-side) ──────────────────────────────────────────────────
+# Dashboard + API + Live-WS. Already match the image defaults.
+DASHBOARD_PORT=20128
+API_PORT=20129
+LIVE_WS_PORT=20132
+
+# ── Bind address ───────────────────────────────────────────────────────
+# 127.0.0.1 = loopback only (safe with REQUIRE_API_KEY=false, the default).
+# Set to 0.0.0.0 ONLY when REQUIRE_API_KEY=true OR a reverse proxy
+# enforces auth upstream. Exposing an unauthenticated /v1 proxy on the
+# LAN/WAN lets anyone burn your provider quotas. # EDIT ME if you must.
+APP_BIND_HOST=127.0.0.1
+
+# ── Auth ──────────────────────────────────────────────────────────────
+# false = the dashboard and /v1 proxy are open to APP_BIND_HOST's network.
+# true = every request needs an API key / dashboard login. The dashboard
+# auto-creates INITIAL_PASSWORD on first boot (read it from the logs:
+# `docker logs omniroute | grep -i password`). # EDIT ME — set true.
+REQUIRE_API_KEY=false
+# INITIAL_PASSWORD= # uncomment to pre-seed the dashboard password
+
+# ── Memory ceiling (V8 old-space) ──────────────────────────────────────
+# 1024 = dashboard + light chat. Coding agents (long POST /v1/responses
+# bodies) need more — see SELF_HOST_GUIDE.md "sizing". 2048 is a safe
+# default for a single user who runs Claude Code / Codex through it.
+OMNIROUTE_MEMORY_MB=2048
+
+# ── Browser-facing origin (optional) ───────────────────────────────────
+# Set ONLY if you expose OmniRoute behind a domain via a reverse proxy.
+# NEXT_PUBLIC_BASE_URL=https://your-domain.example.com
+# BASE_URL=http://omniroute:20128
diff --git a/.gitignore b/.gitignore
index 31c9e3b96a..9087f0fc0a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -74,6 +74,7 @@ yarn-error.log*
# Local gitleaks artifacts (do not commit)
gitleaks-local.json
!.env.example
+!.env.selfhost.example
!.env.homolog.example
!.env.devin-bridge.example
# Provider API keys (never commit)
diff --git a/docker-compose.selfhost.yml b/docker-compose.selfhost.yml
new file mode 100644
index 0000000000..edea9d3a11
--- /dev/null
+++ b/docker-compose.selfhost.yml
@@ -0,0 +1,64 @@
+# ──────────────────────────────────────────────────────────────────────
+# OmniRoute — Self-Host Compose (零月费自托管 / zero-fee self-host)
+# ──────────────────────────────────────────────────────────────────────
+# KISS: ONE command brings up the whole thing on loopback.
+#
+# cp .env.selfhost.example .env # edit the 2 secrets you want
+# docker compose -f docker-compose.selfhost.yml up -d
+# open http://127.0.0.1:20128
+#
+# No profiles, no build step, no multi-tenant anything.
+# Pulls the published image `diegosouzapw/omniroute:latest`.
+#
+# All app ports bind to 127.0.0.1 ONLY by default (REQUIRE_API_KEY ships
+# as false). Set APP_BIND_HOST=0.0.0.0 in .env ONLY after you have set
+# REQUIRE_API_KEY=true OR put an auth-enforcing reverse proxy in front.
+# See docs/getting-started/SELF_HOST_GUIDE.md.
+# ──────────────────────────────────────────────────────────────────────
+
+services:
+ redis:
+ image: docker.io/library/redis:8.6.5-alpine
+ container_name: omniroute-redis
+ restart: unless-stopped
+ # No port published to the host — the app reaches Redis over the compose
+ # network (redis:6379). Publishing an unauthenticated Redis on 0.0.0.0
+ # is a footgun we refuse to ship. If you need host-side redis-cli, run
+ # docker exec -it omniroute-redis redis-cli
+ volumes:
+ - redis-data:/data
+ command: redis-server --save 60 1 --loglevel warning
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 10s
+ timeout: 5s
+ retries: 3
+
+ omniroute:
+ image: diegosouzapw/omniroute:latest
+ container_name: omniroute
+ restart: unless-stopped
+ stop_grace_period: 40s
+ env_file: .env
+ environment:
+ - DATA_DIR=/app/data
+ - REDIS_URL=redis://redis:6379
+ ports:
+ # Loopback-only by default. Override APP_BIND_HOST in .env to expose.
+ - "${APP_BIND_HOST:-127.0.0.1}:${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
+ - "${APP_BIND_HOST:-127.0.0.1}:${API_PORT:-20129}:${API_PORT:-20129}"
+ - "${APP_BIND_HOST:-127.0.0.1}:${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
+ volumes:
+ - ./data:/app/data
+ depends_on:
+ redis:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD", "node", "healthcheck.mjs"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 20s
+
+volumes:
+ redis-data:
diff --git a/docs/getting-started/SELF_HOST_GUIDE.md b/docs/getting-started/SELF_HOST_GUIDE.md
new file mode 100644
index 0000000000..0b6c626769
--- /dev/null
+++ b/docs/getting-started/SELF_HOST_GUIDE.md
@@ -0,0 +1,328 @@
+---
+title: "🚀 Self-Host Guide — OmniRoute (零月费自托管 / zero-fee self-host)"
+version: 3.8.51
+lastUpdated: 2026-09-14
+---
+
+# 🚀 Self-Host Guide — OmniRoute
+
+> **TL;DR** — three commands, one local endpoint, zero monthly fees. No SaaS
+> billing, no multi-tenant isolation, no hosted prompt-processing hop. Your
+> prompts go straight to the provider you pick.
+
+```bash
+cp .env.selfhost.example .env # then edit the 2 lines marked "EDIT ME"
+docker compose -f docker-compose.selfhost.yml up -d
+open http://127.0.0.1:20128
+```
+
+This is the **self-host carrier** for OmniRoute's "零月费 + 自托管" product
+form: a packaged container/binary you run on your own machine in 5 minutes.
+
+---
+
+## Why this guide exists
+
+OmniRoute ships a sophisticated `docker-compose.yml` with profiles
+(`base`, `web`, `cli`, `host`, `cliproxyapi`, `memory`, `bifrost`). Each app
+service is profile-gated, so a bare `docker compose up -d` only starts Redis.
+That is correct for power users who pick a profile — but it is *not* a
+5-minute self-host story.
+
+`docker-compose.selfhost.yml` is the KISS overlay: **one command, published
+image, loopback-only, Redis included, no profile choice, no build step.**
+When you outgrow it, graduate to the full
+[DOCKER_GUIDE](../guides/DOCKER_GUIDE.md) profiles.
+
+| Audience | Start here | Graduate to |
+| --- | --- | --- |
+| Self-hoster, single user | this guide | — |
+| Power user, CLI tools / web-cookie providers / sidecars | — | `docker-compose.yml` profiles |
+
+---
+
+## Prerequisites
+
+- Docker Engine 24+ (or Docker Desktop 4.30+) with the Compose v2 plugin.
+- ~2 GB RAM free (see [Sizing](#sizing-the-container)).
+- A provider API key from any supported provider (OpenAI, Anthropic, Google,
+ or one of the [150+ free tiers](./FREE-TIERS-GUIDE.md)).
+
+No build toolchain, no Node, no git clone required — the image is pulled.
+
+---
+
+## Step 1 — Configure (1 min)
+
+```bash
+cp .env.selfhost.example .env
+```
+
+Edit exactly **two** lines in `.env`:
+
+```dotenv
+REQUIRE_API_KEY=true # was false — lock the endpoint down
+APP_BIND_HOST=127.0.0.1 # keep loopback; see "Exposing" only if needed
+```
+
+`REQUIRE_API_KEY=true` makes every `/v1` request and the dashboard require an
+API key / login. On first boot the dashboard auto-generates an
+`INITIAL_PASSWORD` — read it from the logs:
+
+```bash
+docker logs omniroute | grep -i password
+```
+
+The other variables (`DASHBOARD_PORT`, `API_PORT`, `LIVE_WS_PORT`,
+`OMNIROUTE_MEMORY_MB`) already have sane defaults. Leave them unless you know
+you need to change them.
+
+---
+
+## Step 2 — Start (1 min)
+
+```bash
+docker compose -f docker-compose.selfhost.yml up -d
+```
+
+Pulls `diegosouzapw/omniroute:latest` (multi-arch AMD64 + ARM64, ~250 MB) and
+`redis:8.6.5-alpine`, starts both, and waits for Redis to be healthy before
+the app boots.
+
+### Step 3 — Verify (30 s)
+
+```bash
+# process lifecycle + readiness
+curl -fsS http://127.0.0.1:20128/healthz && echo
+
+# container health
+docker inspect --format '{{.State.Health.Status}}' omniroute
+```
+
+You should see `{"status":"ok"}` and `healthy`. Then open the dashboard:
+
+```
+http://127.0.0.1:20128
+```
+
+Within ~300 s of `up -d` the endpoint is locally reachable and the healthcheck
+is `healthy` — the acceptance bar from the self-host issue.
+
+---
+
+## Ports
+
+| Port | What | Default bind |
+| --- | --- | --- |
+| `20128` | Dashboard + `/v1` LLM proxy (unified entry) | `127.0.0.1` |
+| `20129` | API port (server-to-server) | `127.0.0.1` |
+| `20132` | Live WebSocket (realtime dashboard updates) | `127.0.0.1` |
+
+All three bind to **loopback only** by default. Redis is **not** published to
+the host at all — the app reaches it over the compose network. This is
+deliberate: shipping an unauthenticated Redis on `0.0.0.0` is a footgun.
+
+---
+
+## Connecting a provider
+
+1. Open the dashboard → **Providers**.
+2. Add a provider and paste its API key. Keys are encrypted at rest with
+ AES-256-GCM; the cleartext never leaves your machine.
+3. Point your IDE / agent at the unified entry:
+
+```
+http://127.0.0.1:20128/v1
+```
+
+For provider choice, see the
+[Free Tiers Guide](./FREE-TIERS-GUIDE.md) — OmniRoute aggregates 150+ free
+tiers into one endpoint, so you can run `model: "auto"` to pick the best free
+option per request.
+
+---
+
+## Sizing the container
+
+The image pins `OMNIROUTE_MEMORY_MB=1024`. That is enough for the dashboard
+and light chat. **Coding agents** (`POST /v1/responses` from Claude Code,
+Codex, Grok, …) retain multiple large context graphs during compression and
+can abort V8 at ~12 GiB old-space under two overlapping long contexts
+([#7849](https://github.com/diegosouzapw/OmniRoute/issues/7849)).
+
+`.env.selfhost.example` defaults to `OMNIROUTE_MEMORY_MB=2048` — a safe floor
+for a single user running coding agents. Raise it if you fan out many models
+in parallel (fusion combos) or hit `FATAL ERROR: Reached heap limit`:
+
+```dotenv
+OMNIROUTE_MEMORY_MB=4096
+```
+
+Memory is a V8 heap ceiling; native buffers (SQLite, ONNX, better-sqlite3)
+sit outside it, so size the container a few hundred MB above the heap.
+
+---
+
+## Local binary build (optional)
+
+Prefer a binary over Docker? The npm package is the same code:
+
+```bash
+npm install -g omniroute
+omniroute
+```
+
+This runs the Next.js standalone server directly on your host — same ports,
+same `DATA_DIR` (`./data` by default). Use it when you cannot run Docker
+(e.g. a locked-down VM). The container path above is the recommended default
+because it bundles the exact runtime the image was tested with.
+
+From source (development only — not a deploy path):
+
+```bash
+git clone https://github.com/diegosouzapw/OmniRoute.git
+cd OmniRoute
+npm install
+npm run build && npm start
+```
+
+---
+
+## Exposing beyond localhost
+
+**Do not** flip `APP_BIND_HOST=0.0.0.0` while `REQUIRE_API_KEY=false`. That
+publishes an open `/v1` proxy on every LAN/WAN interface — anyone on the
+network can burn your provider quotas. The order is fixed:
+
+1. Set `REQUIRE_API_KEY=true` in `.env`.
+2. Read `INITIAL_PASSWORD` from the logs and log in.
+3. *Only then* set `APP_BIND_HOST=0.0.0.0` (or put an auth-enforcing reverse
+ proxy in front and keep loopback).
+
+For TLS / a domain, run Caddy or Traefik in front and leave `APP_BIND_HOST`
+at `127.0.0.1`. See the
+[DOCKER_GUIDE — Caddy HTTPS](../guides/DOCKER_GUIDE.md#docker-compose-with-caddy-https-auto-tls)
+and
+[Cloudflare Quick Tunnel](../guides/DOCKER_GUIDE.md#cloudflare-quick-tunnel)
+sections for copy-paste reverse-proxy configs.
+
+---
+
+## Data, backups, and resets
+
+- **Data dir**: `./data` (bind-mounted to `/app/data`). All SQLite DBs,
+ migrations, audit trail, and encrypted provider keys live here.
+- **Backups**: SQLite auto-backup is on by default
+ (`DISABLE_SQLITE_AUTO_BACKUP` is unset → enabled). For a manual snapshot:
+ ```bash
+ ./bin/snapshot-data.sh
+ ```
+ Restore with `./bin/restore-data.sh`.
+- **Reset the dashboard password**:
+ ```bash
+ docker exec -it omniroute node bin/reset-password.mjs
+ ```
+- **Reset policies** (routing/failover/quota to factory defaults):
+ ```bash
+ docker exec -it omniroute node bin/restore-policies.sh
+ ```
+
+---
+
+## Common issues
+
+
+docker compose up only starts Redis
+
+You are running the **full** `docker-compose.yml`, whose app services are
+profile-gated. For the one-command path use the self-host file:
+
+```bash
+docker compose -f docker-compose.selfhost.yml up -d
+```
+
+Or, with the full compose, pick a profile:
+`docker compose --profile base up -d`.
+
+
+
+Healthcheck stays starting / unhealthy
+
+- Check Redis is up: `docker inspect --format '{{.State.Health.Status}}' omniroute-redis`
+- Check app logs: `docker logs omniroute`
+- The healthcheck probes `/healthz` and allows a 20 s start period. A slow
+ first boot (cold migrations) can take longer — bump `start_period` in the
+ compose file if your disk is slow.
+
+
+
+FATAL ERROR: Reached heap limit under coding agents
+
+Raise `OMNIROUTE_MEMORY_MB` in `.env` (e.g. `4096`), then
+`docker compose -f docker-compose.selfhost.yml up -d`. See
+[Sizing the container](#sizing-the-container).
+
+
+
+Web-cookie providers (Gemini Web, Claude Turnstile) fail with
+Executable doesn't exist at .../ms-playwright/chromium
+
+The `base` image ships without Chromium. The self-host compose uses the
+published `base` image. For web-cookie providers, switch to the full compose
+with the `web` profile (which bundles Playwright/Chromium):
+
+```bash
+docker compose --profile web up -d
+```
+
+See [DOCKER_GUIDE — Available Profiles](../guides/DOCKER_GUIDE.md#available-profiles).
+
+
+
+Port 20128 already in use
+
+Set `DASHBOARD_PORT`, `API_PORT`, `LIVE_WS_PORT` in `.env` to free ports and
+re-run `up -d`.
+
+
+---
+
+## What this deliberately is NOT
+
+Per the self-host KISS constraint, this path **does not** include:
+
+- ❌ SaaS billing / metering / plan tiers
+- ❌ Multi-tenant isolation / per-tenant namespaces
+- ❌ A hosted prompt-processing hop (your prompts go straight to the provider)
+- ❌ Any baked-in credentials or secrets
+
+It is a **pure local self-host** carrier — the simplest thing that makes the
+"零月费 + 自托管" promise real.
+
+---
+
+## Security checklist (self-host)
+
+Before you expose beyond loopback:
+
+- [ ] `REQUIRE_API_KEY=true` in `.env`
+- [ ] `INITIAL_PASSWORD` rotated to a strong, unique value
+- [ ] `APP_BIND_HOST` left at `127.0.0.1` *unless* behind an auth-enforcing proxy
+- [ ] TLS terminated by Caddy/Traefik/Cloudflare in front (never plain HTTP on WAN)
+- [ ] Redis not published to the host (the self-host compose already enforces this)
+- [ ] `./data` volume backed up regularly (`bin/snapshot-data.sh`)
+- [ ] Provider keys rotated per the provider's own policy
+
+For the full supply-chain / image / dependency audit dimension, see
+[SECURITY.md](../../SECURITY.md) and
+[SUPPLY_CHAIN](../security/SUPPLY_CHAIN.md).
+
+---
+
+## Related
+
+- [Docker Guide](../guides/DOCKER_GUIDE.md) — profiles, Caddy HTTPS, tunnels, image tags
+- [Quick Start](./QUICK-START.md) — 3-minute path for first-time users
+- [Free Tiers Guide](./FREE-TIERS-GUIDE.md) — 150+ free provider tiers
+- [Providers Guide](./PROVIDERS-GUIDE.md) — connecting and configuring providers
+- [Troubleshooting](../guides/TROUBLESHOOTING.md) — deeper issue resolution
diff --git a/docs/getting-started/meta.json b/docs/getting-started/meta.json
index 429bc64059..91d0bacea7 100644
--- a/docs/getting-started/meta.json
+++ b/docs/getting-started/meta.json
@@ -3,6 +3,7 @@
"description": "Get started with OmniRoute in minutes — no technical background needed",
"pages": [
"QUICK-START",
+ "SELF_HOST_GUIDE",
"AUTO-COMBO-GUIDE",
"PROVIDERS-GUIDE",
"FREE-TIERS-GUIDE",
diff --git a/docs/guides/DOCKER_GUIDE.md b/docs/guides/DOCKER_GUIDE.md
index 4f8ebc95c9..da46824b2d 100644
--- a/docs/guides/DOCKER_GUIDE.md
+++ b/docs/guides/DOCKER_GUIDE.md
@@ -29,6 +29,12 @@ lastUpdated: 2026-06-28
## Quick Run
+> **Self-host in one command?** See the
+> [Self-Host Guide](../getting-started/SELF_HOST_GUIDE.md) —
+> `docker compose -f docker-compose.selfhost.yml up -d` (published image +
+> Redis, loopback-only, no profile choice). The Quick Run below is the
+> single-container path for users who already run Redis elsewhere.
+
```bash
docker run -d \
--name omniroute \