Docker Configuration

Advanced Docker setup and configuration options.

Basic Setup

The installer sets this up for you. To do it by hand:

mkdir opencode-manager && cd opencode-manager
curl -fsSL https://raw.githubusercontent.com/chriswritescode-dev/opencode-manager/main/docker-compose.release.yml -o docker-compose.yml
docker compose up -d

No .env is required. When AUTH_SECRET is not set, the container generates one on first start and stores it in the data volume at /app/data/.auth-secret, so it survives restarts and image updates. Set AUTH_SECRET yourself only to manage it externally; changing it later signs every user out and makes stored encrypted credentials unreadable.

docker-compose.yml

There are two Compose files with the same configuration:

  • docker-compose.release.yml pulls the image published for each release from ghcr.io. The installer and the commands above save it as docker-compose.yml. It sets the Compose project name ocm, so its volumes are ocm_opencode-workspace, ocm_opencode-data, and ocm_opencode-bin
  • docker-compose.yml in a source checkout builds the image from the checkout instead. See Building From Source

The release Compose file:

name: ocm

services:
  app:
    image: ghcr.io/chriswritescode-dev/opencode-manager:latest
    container_name: opencode-manager
    ports:
      - "5003:5003"
      - "5004:5004"
      - "5100:5100"
      - "5101:5101"
      - "5102:5102"
      - "5103:5103"
    environment:
      - NODE_ENV=${NODE_ENV:-production}
      - PUID=${PUID:-1000}
      - PGID=${PGID:-1000}
      - HOST=0.0.0.0
      - PORT=5003
      - OPENCODE_SERVER_PORT=5551
      - OPENCODE_HOST=127.0.0.1
      - DATABASE_PATH=/app/data/opencode.db
      - WORKSPACE_PATH=/workspace
      - PROCESS_START_WAIT_MS=2000
      - PROCESS_VERIFY_WAIT_MS=1000
      - HEALTH_CHECK_TIMEOUT_MS=30000
      - MAX_FILE_SIZE_MB=50
      - MAX_UPLOAD_SIZE_MB=50
      - DEBUG=false
      - AUTH_SECRET=${AUTH_SECRET:-}
      - AUTH_TRUSTED_ORIGINS=${AUTH_TRUSTED_ORIGINS:-http://localhost:5003}
      - ADMIN_EMAIL=${ADMIN_EMAIL:-}
      - ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
      - ADMIN_PASSWORD_RESET=${ADMIN_PASSWORD_RESET:-false}
      - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-}
      - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-}
      - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
      - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
      - DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID:-}
      - DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET:-}
      - AUTH_SECURE_COOKIES=${AUTH_SECURE_COOKIES:-false}
      - PASSKEY_RP_ID=${PASSKEY_RP_ID:-localhost}
      - PASSKEY_RP_NAME=${PASSKEY_RP_NAME:-OpenCode Manager}
      - PASSKEY_ORIGIN=${PASSKEY_ORIGIN:-http://localhost:5003}
      - PREVIEW_PUBLIC_URL=${PREVIEW_PUBLIC_URL:-}
      - VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY:-}
      - VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY:-}
      - VAPID_SUBJECT=${VAPID_SUBJECT:-}
    volumes:
      - ${OCM_WORKSPACE_HOST_PATH:-opencode-workspace}:/workspace
      - opencode-data:/app/data
      - opencode-bin:/home/node/.opencode/bin
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5003/api/health"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 40s

volumes:
  opencode-workspace:
    driver: local
  opencode-data:
    driver: local
  opencode-bin:
    driver: local

Environment Variables

Create a .env file next to docker-compose.yml. Docker Compose automatically reads variables from .env; every variable is optional:

# Optional - generated on first start and kept in the data volume when unset
AUTH_SECRET=generate-with-openssl-rand-base64-32

# Optional - pre-configured admin
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=your-secure-password

# Optional - OAuth providers
GITHUB_CLIENT_ID=your-client-id
GITHUB_CLIENT_SECRET=your-client-secret

# Optional - passkeys
PASSKEY_RP_ID=localhost
PASSKEY_ORIGIN=http://localhost:5003

# Optional - preview gateway behind HTTPS (see Cloudflare Tunnel)
PREVIEW_PUBLIC_URL=https://preview.example.com

# Optional - push notifications
VAPID_PUBLIC_KEY=BMx-1234567890abcdefghijklmnopqrstuv...
VAPID_PRIVATE_KEY=abcd1234567890abcdef...
VAPID_SUBJECT=mailto:you@example.com

Entrypoint Behavior

The container entrypoint (scripts/docker-entrypoint.sh) automatically:

  1. Verifies Bun is installed (installed at build time, fallback install if missing)
  2. Reconciles the persisted OpenCode home binary (/home/node/.opencode/bin/opencode):
    • a persisted binary inside the supported range is retained, including a user-selected version newer than the image-bundled OPENCODE_BUNDLED_VERSION. The supported range is a stable X.Y.Z release at or above the bundled version with the same major version (>=OPENCODE_BUNDLED_VERSION <next major);
    • a persisted binary that is malformed, unversioned, a prerelease, or outside the supported range is removed (only that binary), so PATH falls back to the image-bundled /usr/local/bin/opencode without a download
  3. Installs OpenCode only when no usable binary is present: if opencode is missing entirely, or if the surviving binary is still outside the supported range, the pinned bundled version is downloaded from https://opencode.ai/files/bin/ into the persisted bin volume
  4. Provides AUTH_SECRET: an explicit AUTH_SECRET is used as is; otherwise the secret in /app/data/.auth-secret is used, and generated (32 random bytes, base64, mode 0600) on first start
  5. Aligns the node account to PUID/PGID (default 1000) before chowning the workspace, the /app/data directory, and the node home directory. If PUID/PGID are already used by another account in the image, startup aborts with an explicit error. Group alignment runs first, so a free PGID combined with an occupied PUID mutates /etc/group before the UID collision is detected and aborts startup; realign to the original ids or pick a free pair before retrying.

Port Configuration

Main Application

The application runs on port 5003 by default:

ports:
  - "5003:5003"

Change the host port if needed:

ports:
  - "8080:5003"  # Access at localhost:8080

Dev Server Ports

Port 5004 is the preview gateway. It proxies a running dev server through an authenticated origin and supports HMR, so it is the recommended way to view a dev server:

ports:
  - "5004:5004"

Ports 5100-5103 remain exposed for running dev servers inside repositories and reaching them directly:

ports:
  - "5100:5100"
  - "5101:5101"
  - "5102:5102"
  - "5103:5103"

Exposing these raw ports bypasses the preview gateway's authentication and same-origin isolation. Prefer the gateway unless you need direct access to a dev server.

Configure your dev server to use one of these ports:

// vite.config.ts
export default {
  server: {
    port: 5100,
    host: '0.0.0.0'
  }
}
next dev -p 5100 -H 0.0.0.0
app.listen(5100, '0.0.0.0')

Volume Mounts

Workspace

Repository storage:

volumes:
  - ${OCM_WORKSPACE_HOST_PATH:-opencode-workspace}:/workspace

All cloned repositories are stored here. Defaults to a named volume for data persistence across container recreations.

Accessing Repositories From the Host

To work in the cloned repositories from the host instead of through docker exec, bind /workspace to a host directory and run the container as your host user so the files stay usable from the host without root. Add to .env:

# Output of `id -u` and `id -g` on the host
PUID=1000
PGID=1000
OCM_WORKSPACE_HOST_PATH=/absolute/path/to/opencode-workspace

OCM_WORKSPACE_HOST_PATH is consumed verbatim as the compose mount source, so an absolute or ./-relative path becomes a bind mount while the default value (opencode-workspace) stays a named volume. PUID/PGID are applied before the workspace is chowned, so agent-created files are host-owned and both sides share one uid — which also avoids git's "dubious ownership" warning.

To migrate an existing named volume to a bind mount without losing data, copy the volume contents into the host directory with a one-off container — this works whether <host path> already exists or not, and avoids depending on Docker's internal storage path (/var/lib/docker/... differs on Docker Desktop, rootless Docker, and custom data roots). The destination must be empty; if it is not, the recipe aborts without copying anything so existing files are never overwritten:

docker compose stop
mkdir -p "<host path>"
# Abort if the destination is non-empty so we never overwrite existing files.
if [ -n "$(ls -A "<host path>")" ]; then
  echo "destination '<host path>' is not empty; aborting migration" >&2
  exit 1
fi
# `<project>` is the Compose project name, usually the directory containing docker-compose.yml.
docker run --rm \
  -v <project>_opencode-workspace:/from:ro \
  -v "<host path>":/to alpine sh -c 'cp -a /from/. /to/'
sudo chown -R "$(id -u):$(id -g)" "<host path>"
docker compose up -d
# After confirming /workspace contains your repositories, remove the old volume:
docker volume rm <project>_opencode-workspace

The quoted "<host path>" keeps paths containing spaces (for example /Users/name/My Repositories) intact across mkdir, the Docker -v argument, and chown. With the empty-destination guard in place, cp -a /from/. /to/ copies the volume's contents (not its _data directory) directly into the bind-mount root, so repositories land directly beneath the mounted /workspace. The named volume is left in place until you confirm the migration succeeded.

Data

Database and configuration:

volumes:
  - opencode-data:/app/data

Contains:

  • SQLite database
  • User settings
  • Session data
  • The generated AUTH_SECRET (.auth-secret), so docker compose down -v also resets it

Uses a named volume for data persistence.

OpenCode Binary

volumes:
  - opencode-bin:/home/node/.opencode/bin

Persists the OpenCode binary that the Manager's installer (from the UI's OpenCode settings) writes into ~/.opencode/bin, so an upgrade survives container recreations. The volume is limited to the binary and leaves existing workspace and XDG persistence behavior for config, auth, and chat state unchanged.

On startup the entrypoint reconciles the persisted binary:

  • a persisted binary inside the supported range (a stable release >=OPENCODE_BUNDLED_VERSION with the same major version) is retained, including a user-selected version newer than the image-bundled one;
  • a malformed or unversioned persisted binary is removed so PATH falls back to the image-bundled /usr/local/bin/opencode, avoiding a download;
  • a persisted binary outside the supported range (older than the bundled version, a prerelease, or a different major version) is removed so the pinned bundled OpenCode 2 binary is used; it is downloaded from https://opencode.ai/files/bin/ into this volume only when no supported binary remains on PATH.

A fresh volume starts empty and the image-bundled binary is used until an upgrade installs into the volume.

Import Existing OpenCode Chats From Your Host

If you already use standalone OpenCode on your machine and want Dockerized OpenCode Manager to show those chats on first setup, bind your host OpenCode config/state into the container and bind your repo root to the same absolute path that standalone OpenCode used. The installer offers to write this setup for you; its repositories mount is read-write so agents can work in those repositories.

Add to .env:

OCM_REPOS_HOST_PATH=/Users/you/Development
OCM_OPENCODE_CONFIG_HOST_PATH=/Users/you/.config/opencode
OCM_OPENCODE_STATE_HOST_PATH=/Users/you/.local/share/opencode

Then add a compose override:

services:
  app:
    environment:
      - OPENCODE_IMPORT_CONFIG_PATH=/import/opencode-config/opencode.json
      - OPENCODE_IMPORT_STATE_PATH=/import/opencode-state
    volumes:
      - ${OCM_REPOS_HOST_PATH}:${OCM_REPOS_HOST_PATH}:ro
      - ${OCM_OPENCODE_CONFIG_HOST_PATH}:/import/opencode-config:ro
      - ${OCM_OPENCODE_STATE_HOST_PATH}:/import/opencode-state:ro

OPENCODE_IMPORT_CONFIG_PATH imports a single file. For a host with multiple config files (opencode.json, opencode.jsonc, and a legacy config.json), omit it and mount ${OCM_OPENCODE_CONFIG_HOST_PATH} writable at the container's OpenCode config directory (/home/node/.config/opencode) instead. Import mirrors the host's files into the workspace, removes workspace copies absent on the host, and folds a mirrored legacy config.json into the workspace config.

Why the repo mount uses the host path as the container path:

  • standalone OpenCode stores chats against absolute directory paths
  • mounting ${OCM_REPOS_HOST_PATH} to the same path inside the container preserves those paths exactly
  • OpenCode Manager can then discover that folder and create its normal workspace links under /workspace/repos

With a fresh Docker volume, first startup imports the host OpenCode config and state, and after you add ${OCM_REPOS_HOST_PATH} in the Manager UI, previously existing chats appear under the discovered repositories.

Agent Sandboxing Overlay

Optional KVM-backed agent sandboxing (see Agent Sandboxing). The sandbox overlay (docker-compose.sandbox.yml) grants the container KVM and guest-networking access, passes sandbox tuning through from .env, and persists microsandbox state. It deliberately avoids privileged: true, granting only the specific devices and capability msb needs:

services:
  app:
    devices:
      - "/dev/kvm:/dev/kvm"
      - "/dev/net/tun:/dev/net/tun"
    cap_add:
      - NET_ADMIN
    environment:
      - SANDBOX_IMAGE=${SANDBOX_IMAGE:-docker.io/cstechdev/ocm-sandbox@sha256:9df035cfb1a7c367bac6edbfd660d084b08e02faa051a93e0a6e381272b85342}
      - SANDBOX_MEMORY=${SANDBOX_MEMORY:-4G}
      - SANDBOX_CPUS=${SANDBOX_CPUS:-2}
      - SANDBOX_EXEC_USER=${SANDBOX_EXEC_USER:-${PUID:-1000}}
      - SANDBOX_NET=${SANDBOX_NET:-public}
      - SANDBOX_START_TIMEOUT_MS=${SANDBOX_START_TIMEOUT_MS:-300000}
      - SANDBOX_EXEC_TIMEOUT_MS=${SANDBOX_EXEC_TIMEOUT_MS:-600000}
    volumes:
      - microsandbox-data:/home/node/.microsandbox

volumes:
  microsandbox-data:
    driver: local

Start the Manager with the overlay:

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

The overlay requires a Linux host with /dev/kvm. Docker Desktop on macOS and Windows cannot provide /dev/kvm, so the sandbox toggle in Settings stays disabled there. /dev/net/tun and NET_ADMIN provide guest networking for SANDBOX_NET=public; if msb reports a missing device or capability on a particular host, add that specific entry rather than enabling full privilege.

SANDBOX_EXEC_USER defaults to the numeric PUID (falling back to 1000), and the Manager runs every sandboxed command as that numeric uid (with the Manager's gid). Because the entrypoint realigns the container's node account to PUID/PGID and re-owns /workspace, a non-1000 PUID (for example PUID=1001) writes to the mounted repositories with the same identity as the workspace owner. If a configured SANDBOX_EXEC_USER cannot match the workspace owner, the toggle reports enforcement as unavailable instead of running broken commands.

The microsandbox-data volume persists microsandbox's own state (downloaded images, firmware cache) across container recreations, alongside the workspace and data volumes.

Health Checks

The container includes health checks:

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:5003/api/health"]
  interval: 30s
  timeout: 3s
  retries: 3
  start_period: 40s

Check health status:

docker inspect --format='{{.State.Health.Status}}' opencode-manager

Resource Limits

Limit container resources:

services:
  app:
    # ... other config
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 4G
        reservations:
          cpus: '0.5'
          memory: 1G

Networking

Custom Network

Create an isolated network:

services:
  app:
    networks:
      - opencode-net

networks:
  opencode-net:
    driver: bridge

Host Network

Use host networking (Linux only):

services:
  app:
    network_mode: host

Commands

Basic Operations

# Start
docker-compose up -d

# Stop (containers removed; named volumes are preserved)
docker-compose down

# Stop and remove containers and all named volumes (workspace, database, OpenCode binary)
docker-compose down -v

# Restart
docker-compose restart

# View logs
docker-compose logs -f

# View logs (last 100 lines)
docker-compose logs --tail 100

The package scripts mirror these: pnpm docker:down stops and removes containers while preserving all named volumes, and pnpm docker:reset is the destructive variant that also deletes the workspace, database, and OpenCode binary volumes.

Updating

For the release Compose file, re-run the installer, or pull the latest release yourself:

docker compose pull
docker compose up -d

The installer replaces docker-compose.yml on update, so keep your changes in .env or docker-compose.override.yml, which Compose merges automatically.

Building From Source

In a clone of the repository, docker-compose.yml is the release Compose file without name: and with a build: section in place of image:, so it builds from the checkout:

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        TOOLS_CACHEBUST: ${TOOLS_CACHEBUST:-0}
# Rebuild image
docker-compose build

# Rebuild without cache
docker-compose build --no-cache

# Update and restart (run from the repo root)
./scripts/docker-upgrade.sh          # cached rebuild, pulls first (~1-2 min)
./scripts/docker-upgrade.sh --tools  # cached rebuild with a fresh uv/opencode install (~17 min)
./scripts/docker-upgrade.sh --full   # full --no-cache rebuild (~25 min)
# Add --no-pull to skip `git pull`, or --logs to follow logs after starting.

# Or manually (equivalent to --full):
docker-compose down
git pull
docker-compose build --no-cache
docker-compose up -d

Its Compose project name comes from the checkout directory (usually opencode-manager), so its volumes stay separate from a release install's ocm_* volumes.

Debugging

# Access shell
docker exec -it opencode-manager sh

# View running processes
docker exec opencode-manager ps aux

# Check disk usage
docker exec opencode-manager df -h

# View environment
docker exec opencode-manager env

Global Agent Instructions

The container creates a default AGENTS.md file at /workspace/.config/opencode/AGENTS.md.

Default Content

Instructions for AI agents working in the container:

  • Reserved ports information
  • Available dev server ports
  • Docker-specific guidelines
  • Pre-installed browser automation (Playwright and Chromium)

Editing

Via UI: Settings > OpenCode > Global Agent Instructions

Via File:

docker exec -it opencode-manager vi /workspace/.config/opencode/AGENTS.md

Precedence

Global instructions merge with repository-specific AGENTS.md files. Repository instructions take precedence.

Exposing the OpenCode Server (Advanced)

By default, the OpenCode server binds to 127.0.0.1 inside the container and is not reachable from outside the container. This is the correct and safe default for nearly all users.

When to Expose Externally

You only need to expose the OpenCode server on an external interface if you have a specific use case that requires other services or machines to connect directly to it.

How to Expose Safely

To expose the OpenCode server on the host network:

  1. Set OPENCODE_HOST=0.0.0.0 in your environment
  2. Add port 5551:5551 to the compose ports
  3. Set OPENCODE_SERVER_PASSWORD — recommended so the exposed server uses a password you control; without it OpenCode Manager generates and persists one

Example compose override:

services:
  app:
    ports:
      - "5551:5551"
    environment:
      - OPENCODE_HOST=0.0.0.0
      - OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD:?Set OPENCODE_SERVER_PASSWORD before exposing OpenCode on port 5551}

Password Configuration

OpenCode 2 always requires Basic Auth on the managed server, so a password is always in effect. It is resolved in this order:

  1. Via UI: Use Settings → OpenCode → Server Auth to set a password at runtime
  2. Environment variable: Set OPENCODE_SERVER_PASSWORD in the compose environment: block. The default docker-compose.yml does not forward it from .env, so setting it there alone has no effect in Docker; use .env only for local runs outside Docker
  3. Auto-generated: When neither is configured, OpenCode Manager generates a random password and persists it in its database

DB-stored passwords take precedence over the environment variable, which takes precedence over the auto-generated password.