Files
Codeman/docker/README.md
T
DevvynandClaude Sonnet 5 5b4878df3b fix(docker): address Ark0N's PR review on Update-Codeman.sh — fix the handoff, the build/down ordering, and default-clear the build volumes
Three blockers, all fixed and verified by actually running the script (not
just string-matching it):

1. `exec "$script_dir/Start-Codeman.sh"` failed EACCES/exit 126 on every
   checkout, since Start-Codeman.sh is committed non-executable (100644) —
   the same fact my own second commit on this branch established. Fixed to
   `exec bash "$script_dir/Start-Codeman.sh"`.

2. `down` ran before `build --no-cache`, so Codeman and every session it was
   running were offline for the entire rebuild, and a build failure left the
   stack down with nothing to bring it back — the exact ordering mistake
   Start-Codeman.sh's own "Build BEFORE taking the stack down" comment exists
   to prevent. Reordered to build, then down, then hand off.

3. The default path could throw the rebuild away: codeman-node-modules/
   codeman-dist only re-seed from the image while EMPTY, Start-Codeman.sh
   only clears them when it detects the checkout's HEAD or package-lock.json
   moved, and neither condition is true for the Dockerfile-only change this
   script exists for — so a plain `bash docker/Update-Codeman.sh` rebuilt an
   image whose fresh node_modules/dist then sat unused behind the old
   volumes. Made clearing them the default; `--keep-volumes` opts out
   (replaces the old `--volumes`/`-v` flag, which is no longer needed since
   clearing is now the default).

Smaller items from the same review, also fixed:

- The --no-cache build now derives PUID/PGID from CODEMAN_APPDATA_PATH's
  owner first, via the identical owner_of() helper Start-Codeman.sh uses
  (parity-tested) — without it, the build used Compose's default 1000:1000
  regardless of the real appdata owner (99:100 on the Unraid layout
  docker/README.md documents), and Start-Codeman.sh's own correctly-PUID'd
  build during the handoff would then rebuild those layers anyway, so the
  --no-cache image never actually shipped.
- docker/README.md's "rebuilds ... only when it detects ... moved" wrongly
  described BOTH the rebuild and the volume-clearing as conditional;
  Start-Codeman.sh rebuilds on every start, only the volume-clearing is
  conditional. Corrected, and reworded around the new default.
- --help/-h now prints usage and exits 0 instead of falling into the
  unrecognised-argument branch.
- "the ONLY named volumes this stack declares" now says docker-compose.yaml
  specifically, since a docker-compose.override.yml could add more.

New tests: PUID/PGID derivation parity with Start-Codeman.sh's owner_of(),
--help handling, and — the one that actually catches blocker #1, which five
source-string-matching tests did not — a real end-to-end smoke test: a
synthetic deployment, a stub `docker` on PATH logging every invocation, the
real script executed via a real subprocess. Confirms the real command
sequence (build --no-cache, then down --volumes or plain down, then evidence
the handoff genuinely ran Start-Codeman.sh) and that a working handoff fails
honestly at Start-Codeman.sh's own later check rather than with EACCES.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N6eadpRyqpA9PD3i139cSD
2026-09-21 15:19:00 +08:00

9.9 KiB

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 commands instead. Running Compose from inside docker/ with no -f lets it discover docker-compose.override.yml on its own (see Local customisation); naming the file with -f docker/docker-compose.yaml from the repository root silently drops the override unless it is named too.

Copy-Item docker/.env.example docker/.env
Set-Location docker
docker compose --env-file .env up --build -d

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

The container starts as root so entrypoint.sh can correct the ownership of a bind source the Docker daemon created (it creates a missing one as root:root), then drops to PUID:PGID with setpriv before the server starts, so Codeman itself never runs privileged. That drop needs cap_add: [CHOWN, DAC_OVERRIDE, KILL, SETGID, SETUID] against the file's cap_drop: ALL; a compose file written elsewhere (Unraid's Compose Manager, a hand-written unit) must carry the same additions, and the entrypoint names them when they are missing. A directory owned by neither root nor PUID:PGID is never re-owned: it is probed for writability as the runtime account and refused with a clear message if that fails. Setting user: in Compose skips the whole step.

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 codeman. 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.

Major updates

Start-Codeman.sh rebuilds the image on every start, but only clears the codeman-node-modules/codeman-dist build-artefact volumes when it detects the checkout's HEAD or package-lock.json moved — exactly right for an ordinary git pull, too narrow when a release note (or the updater's own blocker message) calls for starting over on a Dockerfile-only change, which touches neither. For that case, docker/Update-Codeman.sh force-rebuilds the image with no layer cache, clears those two volumes, stops the stack, then hands off to Start-Codeman.sh for the usual start:

bash docker/Update-Codeman.sh

Pass --keep-volumes to skip clearing them (safe only if you know the rebuilt image's node_modules/dist did not change) — the scripted default is the "Resetting the build artefacts" procedure in ../docs/docker-self-update.md. Those two are the only named volumes docker-compose.yaml itself declares; a docker-compose.override.yml could add more, and application data and case workspaces are host bind mounts, never touched either way.

Local customisation

Compose merges docker-compose.override.yml on top of docker-compose.yaml. Keep host-specific changes there rather than editing docker-compose.yaml, so this repository can be updated without losing them. Both docker-compose.override.yml and docker-compose.override.yaml are ignored by Git.

Start-Codeman.sh names the Compose file explicitly, which disables Compose's automatic discovery of the override file, so the script adds it back when one is present and prints the file it used. Running docker compose from this folder without any -f option finds it automatically. When passing -f docker/docker-compose.yaml from the repository root, add -f docker/docker-compose.override.yml as well, or the override is silently ignored.

An override file adds to and replaces individual settings. It cannot delete a key from docker-compose.yaml, and Compose concatenates rather than replaces ports, so removing a published port still requires editing docker-compose.yaml. The example below replaces the restart policy and adds a mount, leaving every other setting in place:

services:
  codeman:
    restart: always
    volumes:
      - /srv/projects:/srv/projects

Reverse-proxy host allowlist

Codeman rejects any request whose Host header is not on its own allowlist - a DNS-rebinding guard, not a Compose or Docker concern. Loopback, any IP literal, the configured --host, and a few tunnel-provider suffixes are allowed by default; a reverse-proxied domain is not, and is rejected with 403 Forbidden: host not allowed before the request reaches any handler.

Add the domain with CODEMAN_ALLOWED_HOSTS in .env:

CODEMAN_ALLOWED_HOSTS='codeman.example.com,.internal.example.com'

docker-compose.yaml forwards it into the container (Compose only passes through the environment keys it explicitly lists, and this is one of them, with an empty default so the line is optional in .env).

See the application's own docs/wiki/Remote-Access.md for the full allowlist format and the tunnel providers it accepts by default.

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/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/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 from docker-compose.yaml and add the following to the codeman service. The service and network additions can instead be placed in docker-compose.override.yml, but the ports: removal cannot, as described under Local customisation:

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.