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.
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:
npm install @kahme247/ompweb@<version>— the published tarball ships a pre-built.next/bundle (the package'sprepackscript runsnext buildbefore publishing), so no build step is needed in Docker.- Download the static
omp-linux-x64binary fromcan1357/oh-my-pi/releases(the@oh-my-pi/pi-coding-agentnpm 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 |
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:30177The first run pulls the image from ghcr.io/<owner>/ompweb-docker:latest. Use docker compose logs -f ompweb to watch Next.js boot.
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 buildThe 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>.
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-sonnetLM 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-7bOllama (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:7bIf 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 restartdocker run --rm \
-v ompweb_data:/data \
-v "$PWD":/backup \
alpine tar czf /backup/ompweb-data.tgz -C / datadocker run --rm \
-v ompweb_data:/data \
-v "$PWD":/backup \
alpine tar xzf /backup/ompweb-data.tgz -C /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.
# 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 caddyA sample Caddyfile for this repo shows the right headers (X-Forwarded-*, X-Real-IP) so ompweb sees the real client IP through the proxy.
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;
}
}docker compose pull
docker compose up -dThe 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┌──────────────────────────────────────────────────┐
│ 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 │
└──────────────────────────────────────────────────┘
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.
We use node:26-slim (Debian Bookworm, glibc) rather than Alpine for two reasons:
- Current Node —
node:26-slimtracks the latest 26.x release, which is what users coming to this project expect in 2026. - glibc-compatible omp binary —
oh-my-piships a glibcomp-linux-x64binary 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.
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.
OMP_WEB_PASSWORDis 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]andno-new-privileges. Subprocessompruns as the same user. - The
ompweb_datavolume 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.
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.
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.
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.
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.
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.
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 existsdocker compose -f docker-compose.yml -f docker-compose.override.yml up -dA typical override might point at a locally-built image:
# docker-compose.override.yml
services:
ompweb:
image: ompweb:dev
build: .# Docker
docker run --rm -i hadolint/hadolint < Dockerfile
# GitHub Actions
actionlint .github/workflows/docker.ymlMIT for the wrapper files in this repo. See upstream projects for their own licenses.