Skip to content

Repository files navigation

ompweb-docker

Containerized packaging of oh-my-pi (the omp CLI, an AI coding agent by can1357) and ompweb (the matching web UI by kahme247), bundled into a single Docker image and auto-published to GitHub Container Registry on every push.

Use this instead of running omp on bare metal. State lives in a Docker volume; upgrades are a docker compose pull.


Why a wrapper repo?

The upstream ompweb repo has no Docker artifacts at all — it's distributed only via npm. This wrapper repo contains just the Docker infrastructure (Dockerfile, compose, workflow, docs). At image build time we:

  1. npm install @kahme247/ompweb@<version> — the published tarball ships a pre-built .next/ bundle (the package's prepack script runs next build before publishing), so no build step is needed in Docker.
  2. Download the static omp-linux-x64 binary from can1357/oh-my-pi/releases (the @oh-my-pi/pi-coding-agent npm package is Bun-only source code and not installable in plain Node).
Component Upstream Source
Web UI https://github.com/kahme247/ompweb npm @kahme247/ompweb
Agent CLI https://github.com/can1357/oh-my-pi GitHub Releases omp-linux-x64
This wrapper MIT

Quick start

Requires Docker 24+ and Compose v2.

# 1. Get the wrapper files
git clone https://github.com/<you>/ompweb-docker.git
cd ompweb-docker

# 2. Configure
cp .env.example .env
$EDITOR .env            # set OMP_WEB_PASSWORD at minimum

# 3. Run
docker compose pull
docker compose up -d

# 4. Open http://localhost:30177

The first run pulls the image from ghcr.io/<owner>/ompweb-docker:latest. Use docker compose logs -f ompweb to watch Next.js boot.


Image tags

The workflow in .github/workflows/docker.yml produces:

Event Tags pushed
Push to main main, main-<sha>, latest
Tag v1.2.3 1.2.3, 1.2, 1, v1.2.3 (and latest if this is the newest release)
Pull request (no push — build only, image is discarded)
Manual dispatch tags for the current ref

Pin to a specific tag for reproducibility:

# docker-compose.yml
image: ghcr.io/<owner>/ompweb-docker:18.1.15    # OMP_VERSION=18.1.15 build
# or for a specific ompweb npm version:
image: ghcr.io/<owner>/ompweb-docker:0.4.2       # OMPWEB_VERSION=0.4.2 build

Persistent data

The container stores two kinds of state:

What Where How to persist
omp config, models, MCP servers, session index /data/omp (mapped to ~/.omp/agent) Named volume ompweb_data (default)
Project session JSONLs, project memory The cwd being worked on Bind-mount your source code (./workspace:/workspace)

By default only ompweb_data is mounted. To let omp actually edit your code, uncomment the workspace line in docker-compose.yml and set WORKSPACE_DIR in .env to the directory you want to work on. In the ompweb UI, set the project cwd to /workspace/<your-project>.


Using a custom OpenAI-compatible endpoint

Point omp at any OpenAI-compatible service (OpenRouter, LM Studio, Together AI, Ollama with the OpenAI-compat shim, vLLM, etc.) by adding a few lines to .env. On the next container start, the entrypoint writes ~/.omp/agent/models.yml and config.yml for you — no YAML knowledge required.

Var Example
OMP_PROVIDER_LABEL openrouter
OMP_PROVIDER_BASE_URL https://openrouter.ai/api/v1
OMP_PROVIDER_API_KEY sk-or-v1-...
OMP_PROVIDER_API openai-completions (or openai-responses)
OMP_PROVIDER_MODEL_ID anthropic/claude-3.5-sonnet
OMP_PROVIDER_MODEL_NAME Claude 3.5 Sonnet (optional, defaults to MODEL_ID)
OMP_DEFAULT_MODEL openrouter/anthropic/claude-3.5-sonnet (optional; for IDs that themselves contain /)

Leave all six blank to use the built-in providers (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.).

OpenRouter:

OMP_PROVIDER_LABEL=openrouter
OMP_PROVIDER_BASE_URL=https://openrouter.ai/api/v1
OMP_PROVIDER_API_KEY=sk-or-v1-...
OMP_PROVIDER_API=openai-completions
OMP_PROVIDER_MODEL_ID=anthropic/claude-3.5-sonnet
OMP_PROVIDER_MODEL_NAME=Claude 3.5 Sonnet
# OpenRouter model IDs contain '/'; set this explicitly to avoid ambiguity.
OMP_DEFAULT_MODEL=openrouter/anthropic/claude-3.5-sonnet

LM Studio (local):

OMP_PROVIDER_LABEL=lmstudio
OMP_PROVIDER_BASE_URL=http://host.docker.internal:1234/v1
OMP_PROVIDER_API_KEY=lm-studio
OMP_PROVIDER_API=openai-completions
OMP_PROVIDER_MODEL_ID=qwen2.5-coder-7b

Ollama (local):

OMP_PROVIDER_LABEL=ollama
OMP_PROVIDER_BASE_URL=http://host.docker.internal:11434/v1
OMP_PROVIDER_API_KEY=ollama
OMP_PROVIDER_API=openai-completions
OMP_PROVIDER_MODEL_ID=qwen2.5-coder:7b

If you've already written ~/.omp/agent/models.yml by hand (e.g. via docker exec ompweb sh and a text editor), the entrypoint won't touch it — your edits survive restarts. To regenerate from env vars, delete the files first:

docker exec ompweb rm /data/omp/models.yml /data/omp/config.yml
docker compose restart

Backups

docker run --rm \
  -v ompweb_data:/data \
  -v "$PWD":/backup \
  alpine tar czf /backup/ompweb-data.tgz -C / data

Restore

docker run --rm \
  -v ompweb_data:/data \
  -v "$PWD":/backup \
  alpine tar xzf /backup/ompweb-data.tgz -C /

Exposing to the internet

Do not put this container directly on a public IP — OMP_WEB_PASSWORD is the only auth, and there is no rate limiting. Always front it with a reverse proxy.

Caddy (recommended — automatic HTTPS)

# 1. Edit Caddyfile, replace ompweb.example.com with your domain
cp Caddyfile /etc/caddy/Caddyfile.d/ompweb.caddy
$EDITOR /etc/caddy/Caddyfile.d/ompweb.caddy

# 2. Include it from your main Caddyfile:
echo "import Caddyfile.d/*.caddy" >> /etc/caddy/Caddyfile

# 3. Reload
systemctl reload caddy

A sample Caddyfile for this repo shows the right headers (X-Forwarded-*, X-Real-IP) so ompweb sees the real client IP through the proxy.

nginx

server {
    listen 443 ssl http2;
    server_name ompweb.example.com;

    ssl_certificate     /etc/letsencrypt/live/ompweb.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ompweb.example.com/privkey.pem;

    client_max_body_size 50m;
    proxy_read_timeout   3600s;
    proxy_send_timeout   3600s;

    location / {
        proxy_pass         http://127.0.0.1:30177;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
    }
}

Updating

docker compose pull
docker compose up -d

The CI rebuilds on every push to main and pushes latest. Tagged releases (vX.Y.Z) push immutable semver tags.

To force a rebuild with a pinned version of omp (instead of latest):

docker build --build-arg OMP_VERSION=18.1.15 --build-arg OMPWEB_VERSION=0.4.2 -t ompweb:custom .
docker compose up -d   # if your compose points at this local tag

Architecture

┌──────────────────────────────────────────────────┐
│ Container (ghcr.io/<owner>/ompweb-docker)       │
│                                                  │
│  ENTRYPOINT: tini → docker-entrypoint.sh         │
│      └─ validates OMP_WEB_PASSWORD (if LAN bind) │
│      └─ mkdir -p $PI_CODING_AGENT_DIR            │
│      └─ exec node node_modules/@kahme247/ompweb/bin/omp-web.js │
│              ├─ listens on 0.0.0.0:30177         │
│              └─ spawns `omp --mode rpc-ui`       │
│                 (NDJSON over stdio, per session) │
│                                                  │
│  /              rootfs (node:26-slim, Debian)    │
│  /app/node_modules  npm install of @kahme247/ompweb│
│  /usr/local/bin/omp  static omp binary (glibc)   │
│  /data          persistent volume                │
│      └─ omp/   → ~/.omp/agent  (config, etc.)    │
│  /workspace     bind-mounted from host           │
└──────────────────────────────────────────────────┘

Why bundle both?

ompweb does not embed omp. It locates the omp binary via OMP_WEB_OMP_BIN (or $PATH) and spawns it with --mode rpc-ui, exchanging NDJSON frames over stdio. There is no HTTP server in omp. So the two must run together, and the simplest deployment is one container.

Why Debian slim?

We use node:26-slim (Debian Bookworm, glibc) rather than Alpine for two reasons:

  1. Current Nodenode:26-slim tracks the latest 26.x release, which is what users coming to this project expect in 2026.
  2. glibc-compatible omp binaryoh-my-pi ships a glibc omp-linux-x64 binary that runs cleanly on Debian with no extra runtime needed (no Bun, no musl loader).

The size penalty vs Alpine (~80MB base vs ~50MB) is small compared to the bundled Next.js build and omp binary (~150MB). To pin a specific Node patch, build with --build-arg NODE_VERSION=26.8.1-slim.

Why not output: 'standalone' for Next.js?

We don't build Next.js at all in this image — we install the pre-built tarball from npm. So output: 'standalone' is moot here. If upstream ompweb ever switches to standalone, this image would automatically benefit.


Security notes

  • OMP_WEB_PASSWORD is the only authentication boundary. Pick something strong; don't reuse another service's password.
  • The container runs as UID 1001 (non-root), with cap_drop: [ALL] and no-new-privileges. Subprocess omp runs as the same user.
  • The ompweb_data volume is the only place persistent state lives. Back it up regularly.
  • API keys are passed via environment variables — never bake them into the image. See .env.example.
  • The image is scanned by Trivy on every build; results surface in the Actions run summary.

Troubleshooting

"OMP_WEB_PASSWORD must be set when OMP_WEB_HOSTNAME is not 127.0.0.1"

You bound the container to 0.0.0.0 but didn't set a password. Either set OMP_WEB_PASSWORD in .env, or bind to localhost only (OMP_WEB_HOSTNAME=127.0.0.1) and access via a reverse proxy.

Container is healthy but the UI is blank

docker compose logs ompweb. If you see bind: address already in use, something else is on port 30177. Change ports: in docker-compose.yml or stop the conflicting process.

"omp: command not found" inside the container

The omp binary lives at /usr/local/bin/omp and is set as OMP_WEB_OMP_BIN in the Dockerfile. If you've mounted over /usr/local/bin, you've hidden it. Remove the override.

Image build fails resolving "latest"

The Docker build stage calls api.github.com for the latest omp release. If GitHub rate-limits you (60/hr unauthenticated), pin a version explicitly: --build-arg OMP_VERSION=18.1.15.

I want a different version of ompweb

docker build --build-arg OMPWEB_VERSION=0.4.2 -t ompweb:custom .

Where 0.4.2 is any published version on https://www.npmjs.com/package/@kahme247/ompweb?activeTab=versions.


Development

Build locally

docker build --build-arg OMPWEB_VERSION=latest -t ompweb:dev .
docker run --rm ompweb:dev omp --version    # sanity check
docker run --rm ompweb:dev ls node_modules/@kahme247/ompweb/.next  # confirm prebuilt .next exists

Run with a local compose override

docker compose -f docker-compose.yml -f docker-compose.override.yml up -d

A typical override might point at a locally-built image:

# docker-compose.override.yml
services:
  ompweb:
    image: ompweb:dev
    build: .

Lint the workflow / Dockerfile

# Docker
docker run --rm -i hadolint/hadolint < Dockerfile

# GitHub Actions
actionlint .github/workflows/docker.yml

License

MIT for the wrapper files in this repo. See upstream projects for their own licenses.

About

Dockerized oh-my-pi (omp) AI coding agent with the ompweb web UI — single container, persistent state, auto-built and published to GHCR.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages