| title | 🚀 Self-Host Guide — OmniRoute (零月费自托管 / zero-fee self-host) |
|---|---|
| version | 3.8.52 |
| lastUpdated | 2026-10-05 |
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.
Version note: this flow applies from v3.8.51 on. The two files it uses (
docker-compose.selfhost.ymland.env.selfhost.example) are not in v3.8.50 or any earlier tag. On v3.8.50, use the single-containerdocker runflow in the Docker Guide instead.
mkdir omniroute && cd omniroute
BASE=https://raw.githubusercontent.com/diegosouzapw/OmniRoute/v3.8.51
curl -fsSLO "$BASE/docker-compose.selfhost.yml"
curl -fsSLO "$BASE/.env.selfhost.example"
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:20128This is the self-host carrier for OmniRoute's "零月费 + 自托管" product form: a packaged container/binary you run on your own machine in 5 minutes.
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 profiles.
| Audience | Start here | Graduate to |
|---|---|---|
| Self-hoster, single user | this guide | — |
| Power user, CLI tools / web-cookie providers / sidecars | — | docker-compose.yml profiles |
- Docker Engine 24+ (or Docker Desktop 4.30+) with the Compose v2 plugin.
- ~2 GB RAM free (see Sizing).
- A provider API key from any supported provider (OpenAI, Anthropic, Google, or one of the 150+ free tiers).
No build toolchain, no Node, no git clone required: you download two files (the compose file and the env template, Step 1) and the image is pulled.
Download the two self-host files into an empty directory, pinned to the
release tag, and create your .env from the template:
mkdir omniroute && cd omniroute
BASE=https://raw.githubusercontent.com/diegosouzapw/OmniRoute/v3.8.51
curl -fsSLO "$BASE/docker-compose.selfhost.yml"
curl -fsSLO "$BASE/.env.selfhost.example"
cp .env.selfhost.example .env(Working from a git clone of v3.8.51 or later? Both files are at the repo
root; just run the cp.)
The compose file reads ./.env next to docker-compose.selfhost.yml
(env_file: .env). It does not read ~/.omniroute/.env: that file belongs
to the npm CLI install (omniroute command, see
Local binary build) and has no effect on the
Docker flow.
Edit exactly two lines in .env:
REQUIRE_API_KEY=true # was false — lock the endpoint down
APP_BIND_HOST=127.0.0.1 # keep loopback; see "Exposing" only if neededREQUIRE_API_KEY=true makes /v1 requests require an API key (a logged-in
dashboard session also works), and the
dashboard require login once a password exists. On first visit you create the
login password in the dashboard's onboarding wizard; skipping that step turns
dashboard login off, even with REQUIRE_API_KEY=true. Docker
port-forwarding makes your browser look non-local to the container, so after you
submit the password the wizard asks for a one-time bootstrap token, which the
container prints to its log at that moment:
docker logs omniroute | grep BOOTSTRAPThe 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.
docker compose -f docker-compose.selfhost.yml up -dPulls 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.
# process lifecycle + readiness
curl -fsS http://127.0.0.1:20128/healthz && echo
# container health
docker inspect --format '{{.State.Health.Status}}' omnirouteYou 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.
| 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.
- Open the dashboard → Providers.
- Add a provider and paste its API key. Keys are encrypted at rest with AES-256-GCM; the cleartext never leaves your machine.
- Point your IDE / agent at the unified entry:
http://127.0.0.1:20128/v1
For provider choice, see the
Free Tiers Guide — OmniRoute aggregates 150+ free
tiers into one endpoint, so you can run model: "auto" to pick the best free
option per request.
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).
.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:
OMNIROUTE_MEMORY_MB=4096Memory 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.
Prefer a binary over Docker? The npm package is the same code:
npm install -g omniroute
omnirouteThis runs the Next.js standalone server directly on your host, on the same
ports. It keeps its data and its .env in ~/.omniroute by default
(when ~/.omniroute does not exist: %APPDATA%\omniroute on Windows, or
$XDG_CONFIG_HOME/omniroute when that variable is set; $DATA_DIR overrides
all of these), not in the ./data directory the Docker flow mounts.
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):
git clone https://github.com/diegosouzapw/OmniRoute.git
cd OmniRoute
npm install
npm run build && npm startDo 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:
- Set
REQUIRE_API_KEY=truein.env. - Create the login password in the onboarding wizard (see above) and log in.
- 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
and
Cloudflare Quick Tunnel
sections for copy-paste reverse-proxy configs.
- 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_BACKUPis unset → enabled). For a manual snapshot:Restore with./bin/snapshot-data.sh
./bin/restore-data.sh. - Reset the dashboard password:
docker exec -it omniroute node bin/reset-password.mjs - Reset policies (routing/failover/quota to factory defaults):
docker exec -it omniroute node bin/restore-policies.sh
cp: cannot stat '.env.selfhost.example': No such file or directory
The self-host files are not in your current directory. Download them first
(see Step 1); they ship from v3.8.51 on, so the
tag-pinned URLs do not exist for v3.8.50 or earlier. On v3.8.50 use the
docker run flow.
Edited ~/.omniroute/.env and nothing changed
That file is read by the npm CLI install only. The Docker flow reads the
.env next to docker-compose.selfhost.yml. Edit that one and re-run
docker compose -f docker-compose.selfhost.yml up -d.
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:
docker compose -f docker-compose.selfhost.yml up -dOr, 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
/healthzand allows a 20 s start period. A slow first boot (cold migrations) can take longer — bumpstart_periodin 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.
EACCES: permission denied writing to /app/data
docker-compose.selfhost.yml bind-mounts ./data:/app/data by default, so
the container inherits the host directory's ownership. Some managed-deploy
platforms run the Docker daemon as root while the container process runs as
an unprivileged user, which can cause EACCES on the first write.
Two ways to fix it, without changing the documented default:
chownthe host dir to the container's user so the bind mount is writable. The image runs as thenodeuser (UID/GID 1000):sudo chown -R 1000:1000 ./data.- Opt into a Docker-managed named volume instead of the bind mount: in
docker-compose.selfhost.yml, comment out the- ./data:/app/dataline and uncomment the- omniroute-data:/app/dataalternative right below it, plus theomniroute-data:entry under the top-levelvolumes:. Note that this changes where your data lives:-
a new named volume starts empty — on an existing install, copy
./datainto it first, or OmniRoute will boot as a fresh instance; -
bin/snapshot-data.shandbin/restore-data.shread/write./dataon the host directly, so they will not see a named volume's contents unless you back it up separately, for example:docker run --rm -v omniroute-data:/data -v "$PWD":/backup alpine \ tar czf /backup/omniroute-data.tar.gz -C /data .
-
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):
docker compose --profile web up -dPort 20128 already in use
Set DASHBOARD_PORT, API_PORT, LIVE_WS_PORT in .env to free ports and
re-run up -d.
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.
Before you expose beyond loopback:
-
REQUIRE_API_KEY=truein.env - Dashboard login password set in the onboarding wizard to a strong, unique value
-
APP_BIND_HOSTleft at127.0.0.1unless 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)
-
./datavolume 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 and SUPPLY_CHAIN.
- Docker Guide — profiles, Caddy HTTPS, tunnels, image tags
- Quick Start — 3-minute path for first-time users
- Free Tiers Guide — 150+ free provider tiers
- Providers Guide — connecting and configuring providers
- Troubleshooting — deeper issue resolution