---
title: "Deploy OmniRoute | (Just Updated) AI Gateway Whose Providers and Keys Survive a Redeploy"
category: "Other"
url: https://railway.com/deploy/omniroute-v3849-or-ai-gateway-whose-prov
---

# Deploy OmniRoute | (Just Updated) AI Gateway Whose Providers and Keys Survive a Redeploy

**[Deploy OmniRoute | (Just Updated) AI Gateway Whose Providers and Keys Survive a Redeploy on Railway](https://railway.com/template/omniroute-v3849-or-ai-gateway-whose-prov)**

- **Creator:** SuperSlowSloth
- **Category:** Other
- **Total deploys:** 20

## Template content

### omniroute

- **Image:** ghcr.io/bon5co/omniroute-railway:latest
- **Health check:** /api/monitoring/health
- **Public domain:** Yes

## Documentation

# Deploy and Host OmniRoute AI Gateway on Railway

OmniRoute is a self-hosted AI gateway: one OpenAI-compatible `/v1` endpoint in front of many
LLM providers, with a dashboard for provider credentials, generated API keys, model aliases,
routing rules and automatic fallback. Point Claude Code, Codex, Cursor, Cline, OpenWebUI or any
OpenAI-compatible SDK at a single base URL and change providers behind it without touching the
client.

## About Hosting OmniRoute AI Gateway

This template runs `ghcr.io/bon5co/omniroute-railway`, a thin wrapper over the official upstream
image pinned at **3.8.49**. No fork and no patches — only the configuration a Railway deployment
needs, plus three corrections that are wrong by default on this platform. One service and one
volume; nothing else to pay for.

**Your providers, endpoints and API keys survive a redeploy.** This is the whole reason the
template exists. OmniRoute keeps every piece of its state in one directory — `storage.sqlite`
holds the provider connections, generated API keys and routing rules, and `server.env` holds the
auto-generated `JWT_SECRET` and `STORAGE_ENCRYPTION_KEY` that those stored credentials are
encrypted with. The upstream image is explicit about where that directory is:

```dockerfile
# Data directory inside Docker — must match the volume mount in docker-compose.yml
ENV DATA_DIR=/app/data
```

A volume mounted anywhere else persists nothing at all, and it does so quietly — the service
deploys, reports healthy, and serves traffic. Measured on 3.8.49 with the volume on `/data` and
the container then recreated the way a redeploy does: a brand-new `storage.sqlite`, a brand-new
`JWT_SECRET`, a brand-new `STORAGE_ENCRYPTION_KEY`, and the volume still empty. Every provider
you connected is gone, and the key that could have decrypted the old credentials went with them.

Mounting on the correct path is not enough either. Railway mounts volumes owned by `uid 0` while
this image runs as `node` (uid 1000), so the app cannot write into its own data directory — and
it does not crash. It reports `healthy`, logs `[DB] SQLite database ready`, and fails every
write underneath (`[bootstrap] Could not persist secrets … EACCES`, `[sqljsAdapter] save failed
… EACCES`). This template mounts the volume at `/app/data` and repairs its ownership before the
app's privilege drop, so the writes actually land. Verified on a live Railway deployment: the
first boot applied 123 database migrations, and after a redeploy it applied **zero** — the
database was read back off the volume rather than rebuilt.

**The dashboard is password-protected from the first boot.** With no password configured,
OmniRoute leaves `POST /api/settings/require-login` outside its auth check so a first-run user
can finish onboarding — and `isAuthRequired()` tests `requireLogin === false` before anything
else. On a public URL those two compose. Measured against 3.8.49 with no initial password, every
call anonymous and from off-box:

```
GET  /api/keys                    -&gt; 401      (looks secure)
POST /api/settings/require-login   -&gt; 200      {"success":true}
GET  /api/keys                    -&gt; 200      auth is now off, for everyone, permanently
```

Whoever finds the URL can then read and mint API keys and add providers against your credits.
This template generates a per-deploy password as `${{secret(24)}}`, which Railway shows in the
service's variables panel. The same three calls against this template's live deployment return
`401 / 401 / 401`.

**The heap is sized from your container, not from the image.** Upstream bakes
`--max-old-space-size=1024` in, with its own note that 1024 MB "can be tight for large
fusion-combo panels (many models fanned out in parallel, each response buffered in full)". On
Railway the container size is a deploy-time choice, so the entrypoint reads
`/sys/fs/cgroup/memory.max` and takes 70% of it. Measured `heap_size_limit`: **1120 MB stock vs
5830 MB** in an 8 GB container, and it scales down correctly too (1481 MB in a 2 GB one). A
`OMNIROUTE_MEMORY_MB` you set yourself still overrides it.

The image tag is pinned rather than tracking `latest`, because OmniRoute applies forward-only
SQLite migrations on boot — 123 of them at this version — and an unpinned image upgrades your
database on a redeploy you did not ask for.

There is deliberately no Redis service. `REDIS_URL` is read only by OmniRoute's rate limiter,
which falls back to in-memory limiting when it is unset (upstream issue #2357, "Redis is
optional"), and a template deploy is single-replica, so a shared rate-limit backend buys nothing
a process-local one does not already give. `CLOUD_URL` is left unset for a related reason: the
cloud-sync path POSTs a bundle containing `providers` and `apiKeys` to
`${CLOUD_URL}/sync/`, so it should point somewhere you chose, not somewhere a
template chose for you.

## Common Use Cases

- One OpenAI-compatible endpoint in front of OpenAI, Anthropic, Gemini, OpenRouter and local models
- Keeping provider API keys on infrastructure you own instead of pasting them into every tool
- Giving coding agents (Claude Code, Codex, Cursor, Cline) a base URL you can re-point without reconfiguring them
- Per-key quotas, rate limits and spend visibility across a team
- Automatic fallback when a provider rate-limits or errors

## Why Deploy OmniRoute on Railway

Railway gives the gateway a public HTTPS URL, a persistent volume and a private network without
any of it being configured by hand. Deploy this template, open the URL, log in with the
generated password from the variables panel, add your providers, and point your tools at
`https://your-domain/v1`.

Because it is a gateway rather than a batch job, it is always on — which is exactly the shape
Railway's usage billing suits, and exactly the shape that makes losing its database on every
redeploy expensive rather than annoying.

## Dependencies for OmniRoute AI Gateway

- API keys for whichever LLM providers you want to route to (added from the dashboard after deploy)

### Deployment Dependencies

- Wrapper image: [`ghcr.io/bon5co/omniroute-railway`](https://github.com/bon5co/omniroute-railway) — `FROM diegosouzapw/omniroute:3.8.49`
- Upstream project: [OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- A Railway volume mounted at **`/app/data`**
- `INITIAL_PASSWORD`, generated per deploy as `${{secret(24)}}`
- Public HTTP networking with the healthcheck at `/api/monitoring/health`


## Similar templates

- [Rocky Linux](https://railway.com/deploy/rocky-linux) — [Jul'26] Hosted Rocky Linux 9 workspace with SSH and persistent storage. 🚀
- [Foundry Virtual Tabletop](https://railway.com/deploy/X5tR6G) — A Self-Hosted & Modern Roleplaying Platform
- [Letta Code Remote](https://railway.com/deploy/letta-code-remote) — Run a Letta Code agent 24/7. No inbound ports, just deploy.

Open this page in a browser: https://railway.com/deploy/omniroute-v3849-or-ai-gateway-whose-prov
