How to Deploy OmniRoute Using Docker on a VPS and Configure a Reverse Proxy with Base Path
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:
ARG OMNIROUTE_BASE_PATH=""
ENV OMNIROUTE_BASE_PATH=$OMNIROUTE_BASE_PATH
At runtime, 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 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:
# 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 structure to persist data and manage restart behavior:
# 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:
# /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 todocker 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/_healthentry 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.tsverifies that theDockerfileexposes theOMNIROUTE_BASE_PATHbuild argument.tests/unit/docker-ensure-base-path.test.tsconfirms the build script writes the resolved base path toBUILD_OMNIROUTE_BASE_PATH.tests/unit/docker-healthcheck-base-path.test.tschecks 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=/omnirouteand resolved at runtime insrc/shared/utils/basePath.ts. - The container exposes port
20128and 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_passand forwarding standard headers. - Unit tests in
tests/unit/dockerfile-base-path-arg.test.ts,docker-ensure-base-path.test.ts, anddocker-healthcheck-base-path.test.tsenforce 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 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.
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 →