Running OmniRoute in Docker with Persistent Storage: Step-by-Step Guide
TLDR: Mount a Docker volume to /app/data and run the container with a stop timeout of at least 40 seconds to ensure SQLite state survives restarts.
OmniRoute, available at diegosouzapw/OmniRoute, ships a multi-stage Dockerfile that isolates the runtime in a lean production image. Running OmniRoute in Docker with persistent storage requires mounting a volume onto the expected data directory so the SQLite database, encryption keys, and provider configurations survive container recreation. The repository also provides a root-level docker-compose.yml and a detailed docs/guides/DOCKER_GUIDE.md for orchestrating the server with its Redis sidecar.
Understand the OmniRoute Docker Image Targets
OmniRoute's Dockerfile defines three build targets that determine which capabilities are bundled inside the container:
runner-base— The minimal image. It contains no bundled global CLIs and is ideal when you only need the HTTP/REST API, dashboard, MCP, and A2A services.runner-cli— Extendsrunner-baseby adding global CLI tools such as@openai/codex,@anthropic-ai/claude-code,droid, andopenclaw. Select this target when OmniRoute must spawn command-line agents.runner-web— Further extendsrunner-cliwith Playwright browsers for web-cookie-based providers like Gemini-Web and Claude-Web.
At runtime, every target uses the standalone Next.js output located at .build/next/standalone and executes as the non-root node user. The entrypoint invokes /tmp/check-permissions.sh, which originates from scripts/check-permissions.sh in the repository to validate volume ownership before startup. The healthcheck runs node healthcheck.mjs to verify server availability.
Prepare Persistent Storage for /app/data
The OmniRoute server expects all durable state to live in /app/data. This path stores the SQLite database and its WAL files, encryption keys, provider configurations, combo definitions, and migration scripts. Without an external mount, this state disappears when the container is removed.
Create a named volume before launching the container:
docker volume create omniroute-data
You can also override the default path with the DATA_DIR environment variable, though /app/data is the value baked into the published image.
Deployment Options for Running OmniRoute in Docker
Depending on your infrastructure, you can start OmniRoute with a simple docker run command or use Docker Compose for full-service orchestration.
Option 1: Quick Start with Docker Run
For a minimal headless deployment, run the base image and attach the named volume:
docker run -d \
--name omniroute \
--restart unless-stopped \
--stop-timeout 40 \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
This exposes the server on port 20128 and persists data inside omniroute-data. The --stop-timeout 40 flag gives the SQLite write-ahead log enough time to checkpoint cleanly during shutdown.
Option 2: Run with an Environment File
When you need to customize REDIS_URL, NEXT_PUBLIC_BASE_URL, or API keys, copy the provided template and inject it at runtime:
cp .env.example .env
# Edit .env with your production settings
docker run -d \
--name omniroute \
--restart unless-stopped \
--stop-timeout 40 \
--env-file .env \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
According to the OmniRoute source code, .env.example serves as the canonical reference for all configurable environment variables.
Option 3: Docker Compose with Redis Sidecar (Recommended)
For production deployments, use the root docker-compose.yml to launch both OmniRoute and a redis:7-alpine sidecar. Redis powers the distributed rate-limiter and shared cache via the internal URL redis://redis:6379. Without it, OmniRoute falls back to an in-memory store that loses quota-tracking guarantees across restarts.
services:
omniroute:
image: diegosouzapw/omniroute:latest
container_name: omniroute
restart: unless-stopped
ports:
- "20128:20128"
volumes:
- omniroute-data:/app/data
environment:
- PORT=20128
- REDIS_URL=redis://redis:6379
redis:
image: redis:7-alpine
container_name: omniroute-redis
ports:
- "6379:6379"
volumes:
- omniroute-redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
volumes:
omniroute-data:
omniroute-redis-data:
Docker Compose profiles let you select additional capabilities beyond the base server:
base— Minimal headless server.cli— Includes bundled CLIs (needed for Codex, Claude Code, etc.).cliproxyapi— Adds the CLIProxyAPI sidecar on port8317.host— Mounts host-wide binaries in read-only mode.
Start the stack with the desired combination:
# Minimal headless server
docker compose --profile base up -d
# Add bundled CLIs
docker compose --profile cli up -d
# Combine CLI profile with the CLIProxyAPI sidecar
docker compose --profile cli --profile cliproxyapi up -d
Ensure Graceful Shutdowns and Data Integrity
SQLite write-ahead logging requires sufficient time to flush and checkpoint during shutdown. As noted in the Dockerfile comments, the container should receive a stop timeout of at least 40 seconds. Setting --stop-timeout 40 in docker run, or the equivalent Docker Compose stop_grace_period, prevents WAL corruption and keeps the storage.sqlite file consistent inside the mounted volume.
Upgrade OmniRoute Without Losing Data
Because all state lives in the named volume, upgrading is a matter of pulling a newer image and recreating the container:
docker pull diegosouzapw/omniroute:3.8.50
docker stop omniroute
docker rm omniroute
docker run -d \
--name omniroute \
--restart unless-stopped \
--stop-timeout 40 \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:3.8.50
The new container detects the existing SQLite file in /app/data and executes any pending migrations automatically.
Summary
- Mount a Docker volume to
/app/datato persist the SQLite database, keys, and provider configs across container restarts. - Choose the appropriate image target:
runner-basefor minimal HTTP services,runner-clifor command-line agents, orrunner-webfor browser-based providers. - Use
--stop-timeout 40(or an equivalent grace period) to guarantee clean SQLite WAL checkpoints on shutdown. - Deploy the
redis:7-alpinesidecar viadocker-compose.ymlto maintain distributed rate-limiting and cache resilience. - Upgrade by pulling a new image tag and reattaching the existing volume; migrations run automatically on startup.
Frequently Asked Questions
What directory must be mounted for persistent storage when running OmniRoute in Docker?
OmniRoute stores all runtime state in /app/data, including the SQLite database, encryption keys, provider configurations, and combo JSON. You must mount a Docker volume or bind-mount to this exact path to prevent data loss when the container is recreated.
Is Redis required for OmniRoute to function in Docker?
Redis is not strictly required, but omitting it degrades resilience. The docker-compose.yml defines a redis:7-alpine sidecar that connects via REDIS_URL=redis://redis:6379. Without Redis, OmniRoute falls back to an in-memory rate-limiter and cache that lose state on container restart.
Which Docker Compose profile should I choose for my deployment?
Select base for the minimal headless server, cli when you need bundled CLIs like @openai/codex and @anthropic-ai/claude-code, and cliproxyapi to expose the CLIProxyAPI sidecar on port 8317. The host profile is available to mount host-wide binaries in read-only mode.
How do I upgrade OmniRoute without losing my existing data?
Keep your named volume attached to /app/data, pull the desired image tag (for example, diegosouzapw/omniroute:3.8.50), remove the old container, and start a new one with the same volume mount. OmniRoute reuses the existing storage.sqlite file and runs any new migrations automatically.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →