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 \