# Running OmniRoute in Docker with Persistent Storage: Step-by-Step Guide

> Easily run OmniRoute in Docker with persistent storage. This guide shows how to mount a Docker volume for data persistence and configure stop timeouts for reliable restarts. Get your OmniRoute setup running smoothly with our st...

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: how-to-guide
- Published: 2026-07-27

---

**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`](https://github.com/diegosouzapw/OmniRoute/blob/main/docker-compose.yml) and a detailed [`docs/guides/DOCKER_GUIDE.md`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`** — Extends `runner-base` by adding global CLI tools such as `@openai/codex`, `@anthropic-ai/claude-code`, `droid`, and `openclaw`. Select this target when OmniRoute must spawn command-line agents.
- **`runner-web`** — Further extends `runner-cli` with 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`](https://github.com/diegosouzapw/OmniRoute/blob/main//tmp/check-permissions.sh), which originates from [`scripts/check-permissions.sh`](https://github.com/diegosouzapw/OmniRoute/blob/main/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:

```bash
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:

```bash
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:

```bash
cp .env.example .env

# Edit .env with your production settings

```

```bash
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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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.

```yaml
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 port `8317`.
- **`host`** — Mounts host-wide binaries in read-only mode.

Start the stack with the desired combination:

```bash

# 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:

```bash
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/data`** to persist the SQLite database, keys, and provider configs across container restarts.
- Choose the appropriate image target: **`runner-base`** for minimal HTTP services, **`runner-cli`** for command-line agents, or **`runner-web`** for browser-based providers.
- Use **`--stop-timeout 40`** (or an equivalent grace period) to guarantee clean SQLite WAL checkpoints on shutdown.
- Deploy the **`redis:7-alpine`** sidecar via [`docker-compose.yml`](https://github.com/diegosouzapw/OmniRoute/blob/main/docker-compose.yml) to 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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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.