Deploy Omnidev with Docker and Docker Compose. Self-host your AI developer bot with containerized security.
| File | Purpose |
|---|---|
src/web/Dockerfile | Production Next.js / API image (node server.js) |
src/worker/Dockerfile | Production job worker (node worker.cjs) |
src/web/Dockerfile.dev | Development image (bind-mount; app + worker dev) |
docker/docker-compose.yml | Base configuration (volumes, init service) |
docker/docker-compose.override.yml | Development overrides (auto-loaded) |
docker/docker-compose.prod.yml | Production overrides |
docker/docker-compose.showcase.yml | Showcase mode (read-only, no auth) |
.dockerignore | Excludes unnecessary files from Docker context |
.env.example | Environment variable template |
src/web/Dockerfile uses BuildKit cache mounts (# syntax=docker/dockerfile:1.4): shared pnpm store between the deps and prod-deps stages, Next.js cache under src/web/.next, apt package caches, and npm global cache for pnpm / @anthropic-ai/claude-code. The first build on a cold builder still does full compile and install; repeated builds benefit most when the platform reuses cache layers and cache mounts. Ensure the builder uses BuildKit (default on current Docker and most hosted builders).
docker compose -f docker/docker-compose.yml up
This auto-loads docker/docker-compose.override.yml which uses src/web/Dockerfile.dev with hot reload.
docker compose -f docker/docker-compose.yml -f docker/docker-compose.prod.yml up -d --build
docker compose -f docker/docker-compose.yml -f docker/docker-compose.showcase.yml up --build
Showcase mode is read-only with no authentication - for public demos.
docker compose -f docker/docker-compose.yml --profile init run --rm init-perms
Open http://localhost:3000 in your browser
If you deploy this repo via Coolify → Docker Compose, Coolify runs the same Docker Compose commands you would run locally — but it routes traffic through its proxy (Traefik/Caddy) instead of exposing host ports.
Set these in Coolify if it asks for custom commands (or use them when deploying manually on the server):
Build command
docker compose -f ./docker/docker-compose.yml -f ./docker/docker-compose.prod.yml build app
Start command
docker compose -f ./docker/docker-compose.yml -f ./docker/docker-compose.prod.yml up -d app
Notes:
app service depends on init-perms, so Compose will run the init container automatically.ports: to the host (Coolify docs warn it can reduce features like rolling updates).This app listens on port 3000 inside the container. If Coolify/proxy guesses the wrong upstream port (common default is 80), you’ll get 502 Bad Gateway even though the container is healthy.
In Coolify’s Domains field you can bind the domain to the container port, e.g.:
https://omnidev.example.com:3000This tells Coolify’s proxy: “route this domain to port 3000 inside the container”.
For a visual explanation, see docs/COOLIFY_PORTS_FLOW.md.
NEXTAUTH_SECRET: must be set (Coolify env var)NEXTAUTH_URL: must be your public URL (no :3000)Example:
NEXTAUTH_URL=https://omnidev.example.com
GET /api/health always returns 200 (used for container health checks / routing).GET /api/config/validate returns a JSON status object and also returns 200 even when config is incomplete (used by the UI).Start development with hot reload:
docker compose -f docker/docker-compose.yml up
This runs pnpm dev inside the container with your repo bind-mounted for fast iteration.
The app is available at http://localhost:3000.
If you see Bind for 0.0.0.0:3000 failed: port is already allocated, stop any existing containers:
docker compose -f docker/docker-compose.yml down
docker compose -f docker/docker-compose.yml up
To run in the background (logs in Docker Desktop instead of terminal):
docker compose -f docker/docker-compose.yml up -d
docker compose -f docker/docker-compose.yml -f docker/docker-compose.prod.yml up -d --build
docker compose -f docker/docker-compose.yml logs -f app
Or open the container in Docker Desktop and view the Logs tab.
docker compose -f docker/docker-compose.yml down
The application supports configuration through environment variables. You can:
docker/docker-compose.yml or via .env fileGitLab Configuration:
GITLAB_URL=https://gitlab.com GITLAB_TOKEN=your_gitlab_token_here
Claude Code Configuration:
Optional override for the sandbox wrapper path used to run Claude Code non-interactively
CLAUDE_CODE_WRAPPER=/usr/local/bin/claude-code-wrapper
Omnidev exclusively uses Claude Code CLI subscription authentication. No API key is needed. Users must log in to the Claude CLI once inside the container.
Exec into the running container:
docker compose -f docker/docker-compose.yml exec app bash
Run an interactive Claude CLI session and follow the prompts (it will typically print a URL + code):
claude --help # Start interactive mode (this is the most version-stable way to complete auth + trust): claude
Exit and restart the app container:
exit
docker compose -f docker/docker-compose.yml restart app
Claude Code stores its auth + settings under ~/.claude inside the container.
docker/docker-compose.yml mounts a named volume so your login persists:
/home/nextjs/.claude (the app runs as the nextjs user)/root/.claude (if you exec into the container as root and run claude, it may write here)Important: Claude also writes a ~/.claude.json file in the user's home directory.
The container startup scripts migrate this file into ~/.claude/.claude.json and symlink it back,
so it persists in the same named volume across restarts.
This project currently supports only token/API-key based MCP auth (for example, setting an Authorization: Bearer <token> header on the MCP server entry).
OAuth-based MCP servers are not supported yet (i.e. flows that require a browser login + redirect/callback to exchange codes for tokens). If you need an MCP integration, prefer providers that support static tokens (or manually issued API keys).
If you want a single consistent location, log in as nextjs (recommended):
docker compose -f docker/docker-compose.yml exec --user nextjs app bash
cd /app/workspaces
claude
When Claude prompts you to "trust" a directory, do it from the directory you want Claude to operate in.
For this app, that's typically /app/workspaces (or a specific repo under it).
Important nuance: this app typically runs Claude Code in non-interactive mode using -p/--print (and --output-format stream-json).
Per the Claude CLI help, the workspace trust dialog is skipped in -p mode, which is why "headless" commands can appear to work even if you haven't completed the interactive trust/setup flow yet.
If you want to "do it the right way" once and have it persist, run an interactive Claude session (no -p) from /app/workspaces and complete the trust prompt once:
docker compose -f docker/docker-compose.yml exec --user nextjs app bash
cd /app/workspaces
run any claude command once to trigger the trust prompt if needed
claude
To "log out" / reset Claude CLI credentials, remove the volume (this deletes the stored login):
docker compose -f docker/docker-compose.yml down
docker volume rm workflow_workflow_claude_config
MAX_WORKSPACE_SIZE_MB=500 TEMP_DIR_PREFIX=omnidev- LOG_LEVEL=info ALLOWED_GITLAB_HOSTS=gitlab.com MAX_CONCURRENT_WORKSPACES=3
The Docker setup includes a named volume workflow_workspaces to persist:
Start the application:
docker compose -f docker/docker-compose.yml up -d
Stop the application:
docker compose -f docker/docker-compose.yml down
View logs:
docker compose -f docker/docker-compose.yml logs -f app
Restart the application:
docker compose -f docker/docker-compose.yml restart app
Update and rebuild:
docker compose -f docker/docker-compose.yml up --build -d
Access the container shell:
docker compose -f docker/docker-compose.yml exec app bash
Run tests inside container:
docker compose -f docker/docker-compose.yml exec app pnpm test
Check application health:
docker compose -f docker/docker-compose.yml exec app wget -qO- http://localhost:3000/api/config/validate
Remove containers and networks:
docker compose -f docker/docker-compose.yml down
Remove containers, networks, and volumes:
docker compose -f docker/docker-compose.yml down -v
Remove images produced by the compose project (optional):
docker compose -f docker/docker-compose.yml down --rmi local
Clean up dangling images:
docker image prune
If you're deploying to a single VM (no Swarm), and you have a reverse proxy (Traefik/Caddy/nginx) handling TLS:
docker compose -f docker/docker-compose.yml -f docker/docker-compose.prod.yml up -d --build
Notes:
3000 to the host. The docker/docker-compose.prod.yml is set up that way by default; your reverse proxy should publish 80/443 and route internally to the Compose service app on port 3000 (e.g. http://app:3000 on the Docker network).NEXTAUTH_URL to your public URL (e.g. https://omnidev.example.com) in your .env file.Initialize swarm (if not already done):
docker swarm init
Deploy stack:
docker stack deploy -c docker/docker-compose.yml -c docker/docker-compose.prod.yml workflow
Notes:
3000 to the host. Your reverse proxy should publish 80/443 and route internally to the app on port 3000.NEXTAUTH_URL to your public URL (e.g. https://omnidev.example.com) in your .env file.You can use the Docker images with Kubernetes. Example deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: omnidev-app
spec:
replicas: 3
selector:
matchLabels:
app: omnidev-app
template:
metadata:
labels:
app: omnidev-app
spec:
containers:
- name: omnidev-app
image: omnidev:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: 'production'
Port already in use:
If you are running the app directly (no reverse proxy), change the port mapping in docker/docker-compose.yml:
ports: - '3001:3000'
If you are running behind a reverse proxy (Traefik/Caddy/nginx), the recommended fix is to not publish the app port at all:
ports: from the app service80/443 and route to the app service at app:3000 on the Docker networkPermission issues with workspaces:
Fix volume permissions:
docker compose -f docker/docker-compose.yml exec app chown -R nextjs:nodejs /app/workspaces
Build failures:
Clean build with no cache:
docker compose -f docker/docker-compose.yml build --no-cache
If you want to test webhooks (e.g. n8n callbacks) or access the app from outside your network:
Start the app locally:
docker compose -f docker/docker-compose.yml up -d
Expose port 3000:
ngrok http 3000
Use the printed URL (e.g. https://xxxx.ngrok-free.app) as your base URL:
POST https://xxxx.ngrok-free.app/api/askPOST https://xxxx.ngrok-free.app/api/editGET https://xxxx.ngrok-free.app/api/jobs/:jobIdNotes:
NEXTAUTH_URL to the ngrok URL.NEXTAUTH_URL is not required.Memory issues:
Increase Docker memory limit in Docker Desktop settings, or add memory limits to docker/docker-compose.yml:
deploy:
resources:
limits:
memory: 1G
The container includes health checks that verify:
Check health status:
docker compose -f docker/docker-compose.yml ps
docker inspect --format='{{.State.Health.Status}}' "$(docker compose -f docker/docker-compose.yml ps -q app)"nextjs user for securityAdd monitoring with tools like:
docker stats "$(docker compose -f docker/docker-compose.yml ps -q app)"curl http://localhost:3000/api/config/validateSecrets management - Use Docker secrets or external secret management
Network security - Consider using custom networks
Image scanning - Regularly scan images for vulnerabilities
Updates - Keep base images and dependencies updated
Reverse proxy (Traefik/Caddy) recommended:
Terminate TLS at the reverse proxy and forward traffic to the app over a private network.
Do not publish the app container port to the internet; only the reverse proxy should connect to it.
If you enable API IP allowlisting (ALLOWED_IPS), configure the proxy to overwrite/sanitize X-Forwarded-For / X-Real-IP so clients cannot spoof their IP.
Caddy example:
reverse_proxy app:3000 {
header_up X-Forwarded-For {remote_host}
header_up X-Real-IP {remote_host}
}
Logging hygiene:
If you're setting up Docker on a fresh Ubuntu VM (20.04+), follow these steps.
Update packages and install dependencies:
sudo apt update && sudo apt upgrade -y sudo apt install -y ca-certificates curl gnupg lsb-release
Add Docker's official GPG key:
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
Set up the Docker repository:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Install Docker:
sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Allow non-root Docker usage:
sudo usermod -aG docker $USER
Log out and back in, or run newgrp docker for the change to take effect.
Verify installation:
docker version
docker run hello-world