Files
Codeman/docker
DevvynandClaude Opus 5 7af4dbc0f8 feat(docker): derive the agent image's npm CLI list from the catalogue
docker/agent.Dockerfile hardcoded the four npm-published CLIs it installs, one
of the several lists that had to be kept in step with the registry by hand.

It now takes them as `ARG CLI_NPM_PACKAGES`, supplied by
scripts/build-agent-image.mjs from config/clis.stock.json, with the default set
to today's list so a bare `docker build` still produces the same image. The arg
is expanded unquoted because word splitting is what turns the list into several
arguments, which is exactly why every token is validated against
^[@A-Za-z0-9][@A-Za-z0-9/._-]*$ on the producing side; a package name carrying a
space or a metacharacter is refused rather than reaching the RUN line. Verified
by building the layer: four packages in, four arguments out, and the default
still applies with no arg.

The list is filtered on each entry's `enabled` flag — the field whose absence
was the maintainer's §3 finding, where a CLI shipping disabled still got baked
into every image. No stock entry is disabled today, so that assertion would pass
vacuously; a unit test feeds the pure helper a fabricated disabled entry so the
fix is covered now rather than the first time someone ships one.

⚠️ It reads the STOCK catalogue, never the merged registry. A user's
~/.codeman/clis.json must not change what is inside an image tagged
codeman/agent:base, or two machines holding that tag hold different images.

Four CLIs keep hand-written layers because the registry cannot describe what
makes them special: pi's --ignore-scripts, deepseek's pnpm companion and dsh-tui
profile, and the three standalone installers. Rather than extend the schema for
a Docker-only benefit, the coverage test requires each to carry a written reason
AND still be present, so an exclusion cannot quietly become an omission.

There are two producers of this command line and there have to be — the .mjs
cannot import TypeScript, and src/docker-hosts.ts builds the same argv for the
in-app auto-build — so a parity test pins them together, package list, arg pairs
and rendered argv. Their order is pinned too: a different order is a different
RUN string and so a needless cache miss between the two build paths.

docker/server.Dockerfile is deliberately NOT edited (PRs #373 and #377 both
modify it); its narrower list is asserted as a declared omission list instead, so
the divergence is reviewable without touching the file.

Also fixes the in-app hint at index.html, which the new coverage test caught
still omitting omp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EMxQreQUZX5ZyybxAGh12
2026-09-13 17:43:13 +08:00
..

Codeman Docker deployment

This folder contains the Compose configuration, server image Dockerfile, and environment template for a locally built Codeman server.

Start

From the repository root, create the runtime environment file and set the required values, especially CODEMAN_PASSWORD.

cp docker/.env.example docker/.env
bash docker/Start-Codeman.sh

On PowerShell, use the following command instead.

Copy-Item docker/.env.example docker/.env
docker compose --env-file docker/.env -f docker/docker-compose.yaml up --build -d

Every required value is defined and explained in .env.example. GEMINI_API_KEY is intentionally optional and may remain blank.

On Linux, Start-Codeman.sh stops with an error when required paths are missing. It creates the application-data directory when safe, detects its numeric owner as PUID:PGID, and detects DOCKER_SOCKET_GID from the configured Docker socket. It rejects a root-owned application-data directory because Codeman and its local CLI sessions must remain unprivileged.

Codeman, Claude, OpenCode, and other local sessions run as the unprivileged account named by CODEMAN_RUNTIME_USER, which defaults to opencode. When Compose is run directly, PUID and PGID default to 1000:1000; set them in .env when the application-data directory has a different owner. The Bash start script determines them automatically instead.

To retain Docker-case support without root when running Compose directly, set DOCKER_SOCKET_GID to the numeric group ID of the host socket. On a standard Linux Docker host, obtain it with stat -c '%g' /var/run/docker.sock. The Bash start script detects it automatically.

Updating

Use App Settings → Updates in the web UI. The checkout Compose builds from is also mounted at /opt/codeman, so an update's git checkout and rebuild persist on the host, and the server exiting is what restarts the container onto the new build.

Releases that change server.Dockerfile, docker-compose.yaml, or add a key to .env.example cannot be applied that way — the updater detects them, names what changed, and asks you to run Start-Codeman.sh here on the host instead. Details: ../docs/docker-self-update.md.

Application data storage

The default configuration uses a host-folder bind mount:

volumes:
  - type: bind
    source: ${CODEMAN_APPDATA_PATH}
    target: /home/${CODEMAN_RUNTIME_USER}

Set CODEMAN_APPDATA_PATH in .env to a directory that the Docker daemon can access. The example value is /mnt/user/appdata/Coding/codeman.

CODEMAN_CASES_PATH is the separate host directory for managed case workspaces. It is mounted into Codeman at the same absolute path, allowing the host Docker daemon to bind it into an isolated case container. Set it to a child directory of CODEMAN_APPDATA_PATH unless you deliberately store workspaces elsewhere.

Compose also exposes CODEMAN_APPDATA_PATH to Codeman as CODEMAN_DOCKER_HOST_HOME. This lets Docker case seed files, CLI credentials and the hook secret be mounted using paths that exist in the host daemon's filesystem. Direct host installations do not set this variable and retain their existing behaviour.

Set CODEMAN_DOCKER_DISABLE_SWAP_LIMIT=1 when docker info reports SwapLimit=false. Codeman continues to apply the configured case memory limit, omits Docker's unsupported --memory-swap option, and filters only the daemon's exact swap-capability warning. Every other Docker create error and its exit status remain visible.

For an existing installation created by a root-running image, change ownership of the application-data directory before upgrading so the configured PUID and PGID can read the saved credentials and state:

chown -R 99:100 /mnt/user/appdata/Coding/codeman

Replace 99:100 and the path with the values from your .env file.

Do not replace this bind mount with a Docker-managed named volume when Docker cases are enabled. Codeman passes seed, credential, transcript and hook-secret bind sources to the host Docker daemon, so their source files must have stable paths in the daemon's filesystem. A named volume does not provide the required host path mapping.

Static macvlan networking

The default configuration publishes a host port. It does not use network_mode: host. To attach Codeman directly to an existing external macvlan network with a static IP address and MAC address, remove the ports: section and add the following to the codeman service:

mac_address: ${CODEMAN_MAC_ADDRESS}
networks:
  codeman_lan:
    ipv4_address: ${CODEMAN_IPV4_ADDRESS}

Then add this top-level network declaration:

networks:
  codeman_lan:
    external: true
    name: ${CODEMAN_MACVLAN_NETWORK}

Set CODEMAN_MACVLAN_NETWORK, CODEMAN_IPV4_ADDRESS, and CODEMAN_MAC_ADDRESS in .env. The values in .env.example match the supplied Unraid example network and should be changed for other hosts.

Create a managed macvlan network

If an external macvlan network does not already exist, use this top-level declaration instead. Do not use it together with the external-network declaration.

networks:
  codeman_lan:
    driver: macvlan
    driver_opts:
      parent: ${CODEMAN_MACVLAN_PARENT}
    ipam:
      config:
        - subnet: ${CODEMAN_MACVLAN_SUBNET}
          gateway: ${CODEMAN_MACVLAN_GATEWAY}

Macvlan containers are ordinarily not reachable from their Docker host without additional host-network routing. Confirm the selected address, MAC address, parent interface, and subnet are reserved and valid for the target network before starting the stack.