# How to Deploy OmniRoute Using Docker on a VPS and Configure a Reverse Proxy with Base Path

> Learn to deploy OmniRoute with Docker on a VPS. Configure your reverse proxy and basePath for seamless integration. Get started today!

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

---

**OmniRoute ships as a multi-stage Docker image that bakes the base path into the build at compile time, listens internally on port 20128, and is designed to run behind an nginx reverse proxy after you pass `--build-arg OMNIROUTE_BASE_PATH=/omniroute` during the Docker build.**

OmniRoute is an open-source routing and provider aggregation platform available at `diegosouzapw/OmniRoute`. Deploying it to a VPS using Docker requires understanding its multi-stage image pipeline, the build-time base path injection, and the minimal reverse-proxy rules needed to serve the app under a sub-path. The following guide walks through the repository's official Dockerfile, Docker Compose orchestration, and nginx configuration exactly as implemented in the source code.

## How OmniRoute's Multi-Stage Docker Build Works

The `Dockerfile` in the root of the repository defines a three-phase build that keeps the final runtime image lean while supporting Next.js, optional Playwright browsers, and CLI tooling.

### The Base Stage

The first stage uses a Node 26 Slim image and applies only OS-level security updates plus a patched npm toolchain. This layer is intentionally minimal to reduce the attack surface and final image size.

### The Builder Stage

The `builder` stage installs the monorepo workspace, runs `npm ci --ignore-scripts`, rebuilds native modules such as `better-sqlite3`, executes the post-install script for `tls-client-node`, and finally compiles the Next.js application. According to the source, you can toggle the bundler via the `OMNIROUTE_USE_TURBOPACK` build argument; by default the build uses Turbopack, but you can fall back to Webpack by setting this argument accordingly.

### The Runner Stages

The pipeline splits into three final targets: `runner-base`, `runner-web`, and `runner-cli`. The production-ready standalone bundle is copied into a minimal runtime image. If you need web-cookie providers, select the `runner-web` target, which installs the optional Playwright browsers. Every runner stage sets the non-root `node` user as the default runtime user for security.

## Baking the Base Path Into the Image

A critical design decision in OmniRoute is that the **base path is embedded at build time**, not configured at runtime. In `Dockerfile`, the build argument `OMNIROUTE_BASE_PATH` defaults to an empty string and is propagated into the image as an environment variable:

```dockerfile
ARG OMNIROUTE_BASE_PATH=""
ENV OMNIROUTE_BASE_PATH=$OMNIROUTE_BASE_PATH

```

At runtime, [`src/shared/utils/basePath.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/utils/basePath.ts) resolves the effective base path by preferring the public variable `NEXT_PUBLIC_OMNIROUTE_BASE_PATH` and falling back to `OMNIROUTE_BASE_PATH`. Because the prefix is baked into the bundle, every generated URL—including health-check endpoints, API routes, and static assets—automatically includes the configured sub-path. The server-side fetch wrapper in [`src/shared/utils/basePathFetch.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/utils/basePathFetch.ts) also prepends this path when making internal requests, which eliminates the need for fragile runtime URL rewrites.

## Deploying OmniRoute on a VPS

### Single-Container Docker Deployment

For a quick VPS deployment, build the image with your desired sub-path and run the container directly:

```bash

# Build the image with a sub-path of /omniroute

docker build \
  --build-arg OMNIROUTE_BASE_PATH=/omniroute \
  -t omniroute:latest .

# Run the container (exposes port 20128 internally)

docker run -d \
  --name omniroute \
  -p 8080:20128 \
  -e OMNIROUTE_MEMORY_MB=2048 \
  omniroute:latest

```

The container listens on `PORT=20128`. The `-p 8080:20128` mapping lets you expose the app on host port 8080, which you will later proxy through nginx.

### Docker Compose Deployment (Recommended)

For production VPS environments, use the provided [`docker-compose.yml`](https://github.com/diegosouzapw/OmniRoute/blob/main/docker-compose.yml) structure to persist data and manage restart behavior:

```yaml

# File: docker-compose.yml

version: "3.9"
services:
  omniroute:
    build:
      context: .
      target: runner-web
      args:
        OMNIROUTE_BASE_PATH: /omniroute
    ports:
      - "8080:20128"
    environment:
      - OMNIROUTE_MEMORY_MB=2048
    restart: unless-stopped
    volumes:
      - omniroute-data:/app/data
volumes:
  omniroute-data:

```

The `runner-web` target is selected here because it includes the Playwright browsers required for web-cookie providers. The named volume `omniroute-data` persists the SQLite database and migrations across container restarts.

## Configuring the Nginx Reverse Proxy with Base Path

Because OmniRoute already knows its base path, the reverse proxy only needs to strip the prefix when forwarding requests to the container; the application handles the base path in its own generated URLs and responses. A typical nginx configuration on your VPS looks like this:

```nginx

# /etc/nginx/conf.d/omniroute.conf

server {
    listen 80;
    server_name example.com;

    access_log /var/log/nginx/omniroute.access.log;

    location /omniroute/ {
        proxy_pass http://localhost:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /omniroute/_health {
        proxy_pass http://localhost:8080/_health;
    }
}

```

Key behaviors in this block:

- `location /omniroute/` matches the base path passed to `docker build`.
- `proxy_pass http://localhost:8080/` includes a trailing slash, which strips the `/omniroute/` prefix before the request reaches the container. The internal OmniRoute server receives clean paths such as `/api/v1/chat/completions`.
- Standard forwarding headers preserve the original client IP and protocol.
- The explicit `location /omniroute/_health` entry ensures the health-check route remains accessible and correctly proxied.

## How the Repository Validates Base Path Integration

The OmniRoute test suite includes unit tests that guarantee the Docker build and base path logic function correctly:

- [`tests/unit/dockerfile-base-path-arg.test.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/tests/unit/dockerfile-base-path-arg.test.ts) verifies that the `Dockerfile` exposes the `OMNIROUTE_BASE_PATH` build argument.
- [`tests/unit/docker-ensure-base-path.test.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/tests/unit/docker-ensure-base-path.test.ts) confirms the build script writes the resolved base path to `BUILD_OMNIROUTE_BASE_PATH`.
- [`tests/unit/docker-healthcheck-base-path.test.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/tests/unit/docker-healthcheck-base-path.test.ts) checks that health-check URLs are generated with the proper prefix.

These tests demonstrate that the base path is not merely a runtime configuration but an integral part of the build and deployment pipeline.

## Summary

- OmniRoute uses a multi-stage Dockerfile (`base` → `builder` → `runner-web`) based on Node 26 Slim to produce a minimal, secure runtime image.
- The base path is injected at build time via `--build-arg OMNIROUTE_BASE_PATH=/omniroute` and resolved at runtime in [`src/shared/utils/basePath.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/utils/basePath.ts).
- The container exposes port `20128` and should be orchestrated on a VPS with Docker Compose for persistent storage and automatic restarts.
- An nginx reverse proxy serves the app under its sub-path by stripping the prefix with a trailing slash in `proxy_pass` and forwarding standard headers.
- Unit tests in [`tests/unit/dockerfile-base-path-arg.test.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/tests/unit/dockerfile-base-path-arg.test.ts), [`docker-ensure-base-path.test.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/docker-ensure-base-path.test.ts), and [`docker-healthcheck-base-path.test.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/docker-healthcheck-base-path.test.ts) enforce correctness.

## Frequently Asked Questions

### What happens if I change the base path after the image is already built?

You must rebuild the Docker image. Because `OMNIROUTE_BASE_PATH` is processed during the Next.js build and embedded into the standalone bundle, it cannot be overridden at runtime with a simple environment variable change. Re-run `docker build` with the new `--build-arg OMNIROUTE_BASE_PATH=/new-path` value.

### Why does the nginx `proxy_pass` URL end with a trailing slash?

The trailing slash in `proxy_pass http://localhost:8080/;` tells nginx to replace the matched location prefix (`/omniroute/`) with the proxy URL path. This means the OmniRoute container receives requests at root-relative paths like `/_health` instead of `/omniroute/_health`, which matches the baked-in base path expectations.

### Do I need the `runner-web` target if I only use API providers?

No. If you do not require Playwright-based web-cookie providers, you can target `runner-base` or `runner-cli` in your [`docker-compose.yml`](https://github.com/diegosouzapw/OmniRoute/blob/main/docker-compose.yml) to avoid installing browser binaries and reduce the final image size. Select `runner-web` only when the web-cookie automation features are necessary.

### How do I persist data between container restarts?

Mount a named volume or host directory to `/app/data` as shown in the Docker Compose example. This path stores the SQLite database and migration state, ensuring that provider configurations and conversation history survive container updates and host reboots.