mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
Three changes to how the Compose container starts as root and drops to PUID:PGID, each reproduced on Docker 29.1.3 / Compose v5.5.0 with a minimal image of the same shape as server.Dockerfile. - cap_add gains KILL. `init: true` makes tini PID 1, and tini stays root while the entrypoint drops the server to PUID. Signalling a process of a different uid needs CAP_KILL, and `cap_drop: ALL` had removed it, so every `docker compose down`/`restart` ended in `[FATAL tini (1)] Unexpected error when forwarding signal: 'Operation not permitted'` and the server being SIGKILLed instead of running `server.stop()`. Measured: without KILL the trap never fires, with it the child logs `GOT SIGTERM`. - /opt/codeman-cli/bin is appended to PATH, never prepended, and entrypoint.sh pins its own PATH to the system directories before its first command. The prefix is chowned to the runtime account so sessions can update the agent CLIs in place, and the root entrypoint resolved stat/chown/setpriv by bare name through it: a `setpriv` planted there by the unprivileged uid ran as uid 0 at the next start. The image's full PATH is handed back to the server at the exec (`env PATH=...`), since Codeman resolves the CLIs through it. - The ownership gate becomes a writability probe. A directory owned by neither root nor PUID:PGID is no longer refused on ownership alone; it is tested with `setpriv --reuid PUID --regid PGID --groups <same groups> test -w`, the exact identity the server gets, so a group-writable tree, an ACL or a CIFS/NFS mount reporting some unrelated uid all pass, and the refusal names path, owner and PUID:PGID. Root-owned directories are still chowned first. Also: a pre-flight runs the drop before touching anything and, when it fails, prints the cap_add list the compose file needs, so an out-of-tree compose file (Unraid's Compose Manager) gets a one-line diagnosis instead of a restart loop; `--bounding-set -all` is gone, since it is a silent no-op without CAP_SETPCAP; a root:root Docker socket now produces a warning that Docker cases will not work rather than silently losing group 0 at the drop; and CODEMAN_ALLOWED_HOSTS is forwarded from .env with an empty default (documented as a commented entry in .env.example so the parity test and the updater's env gate both stay quiet). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
133 lines
6.1 KiB
YAML
133 lines
6.1 KiB
YAML
name: codeman
|
|
|
|
services:
|
|
codeman:
|
|
build:
|
|
context: ..
|
|
dockerfile: docker/server.Dockerfile
|
|
args:
|
|
CODEMAN_RUNTIME_USER: ${CODEMAN_RUNTIME_USER}
|
|
PGID: ${PGID:-1000}
|
|
PUID: ${PUID:-1000}
|
|
image: ${CODEMAN_IMAGE}
|
|
init: true
|
|
restart: unless-stopped
|
|
ports:
|
|
- "${CODEMAN_PORT}:${CODEMAN_PORT}"
|
|
environment:
|
|
# Tells the self-updater to restart by exiting (the restart policy below
|
|
# relaunches it) rather than by looking for an init system that is not
|
|
# here. Also set in the image; repeated so a container started without the
|
|
# image default still self-identifies.
|
|
CODEMAN_IN_CONTAINER: "1"
|
|
# This file sets `restart: unless-stopped` below, so the updater may restart
|
|
# the server by EXITING. Declared here and only here, never in the image: a
|
|
# container started by plain `docker run` has no restart policy unless the
|
|
# operator gave it one, and there the updater asks the daemon instead and
|
|
# stages the update for a manual restart when it cannot get an answer.
|
|
CODEMAN_RESTART_BY_EXIT: "1"
|
|
CODEMAN_DOCKER_BRIDGE_HOOKS: ${CODEMAN_DOCKER_BRIDGE_HOOKS}
|
|
# Host-side equivalent of the runtime user's HOME. Docker case seed,
|
|
# credential and hook mounts are translated into the daemon namespace.
|
|
CODEMAN_DOCKER_HOST_HOME: ${CODEMAN_APPDATA_PATH}
|
|
CODEMAN_DOCKER_DISABLE_SWAP_LIMIT: ${CODEMAN_DOCKER_DISABLE_SWAP_LIMIT}
|
|
CODEMAN_CASES_PATH: ${CODEMAN_CASES_PATH}
|
|
# Extra Host-header allowlist entries for a reverse-proxied deployment
|
|
# (docker/README.md, "Reverse-proxy host allowlist"). Optional, so it
|
|
# defaults to empty rather than requiring a line in every .env.
|
|
CODEMAN_ALLOWED_HOSTS: ${CODEMAN_ALLOWED_HOSTS:-}
|
|
CODEMAN_HOST: ${CODEMAN_HOST}
|
|
CODEMAN_PASSWORD: ${CODEMAN_PASSWORD}
|
|
CODEMAN_PORT: ${CODEMAN_PORT}
|
|
CODEMAN_USERNAME: ${CODEMAN_USERNAME}
|
|
GEMINI_API_KEY: ${GEMINI_API_KEY}
|
|
PGID: ${PGID:-1000}
|
|
PUID: ${PUID:-1000}
|
|
TZ: ${TZ}
|
|
group_add:
|
|
# Retain access to the host Docker socket without running as root.
|
|
- ${DOCKER_SOCKET_GID:-999}
|
|
volumes:
|
|
# Application data and CLI credentials persist on the configured host
|
|
# path, rather than in a Docker-managed volume.
|
|
- type: bind
|
|
source: ${CODEMAN_APPDATA_PATH}
|
|
target: /home/${CODEMAN_RUNTIME_USER}
|
|
# Docker cases are sibling containers on the host daemon. Their workspace
|
|
# must be visible to Codeman at the same absolute path used by that daemon.
|
|
- type: bind
|
|
source: ${CODEMAN_CASES_PATH}
|
|
target: ${CODEMAN_CASES_PATH}
|
|
# Codeman uses the host daemon to create isolated Docker cases. This is
|
|
# Docker-outside-of-Docker, not Docker-in-Docker.
|
|
- type: bind
|
|
source: ${DOCKER_SOCKET}
|
|
target: /var/run/docker.sock
|
|
# The application source, so App Settings -> Updates can update in place.
|
|
# This is the SAME checkout used as the build context above, mounted over
|
|
# the image's baked copy: a `git checkout` performed inside the container
|
|
# then lands on the host and survives the container being recreated.
|
|
# Without it the pull would go to the container's writable layer and be
|
|
# silently discarded by the next `up`. See docs/docker-self-update.md.
|
|
# Defaults to `..` — the build context above — which Compose resolves
|
|
# against the project directory, so plain `docker compose up` works with
|
|
# no extra configuration. Set CODEMAN_REPO_PATH only to point elsewhere.
|
|
- type: bind
|
|
source: ${CODEMAN_REPO_PATH:-..}
|
|
target: /opt/codeman
|
|
# Build artefacts live in named volumes layered OVER the repo bind mount,
|
|
# so `npm install` and `npm run build` inside the container never write
|
|
# into the host checkout. That keeps container-compiled native modules
|
|
# (node-pty is built from source here) out of a checkout that may also be
|
|
# used to run Codeman natively, and keeps `git status` clean. Docker seeds
|
|
# an EMPTY named volume from the image, so the first start inherits the
|
|
# image's already-built node_modules and dist rather than paying for a
|
|
# bootstrap build.
|
|
- type: volume
|
|
source: codeman-node-modules
|
|
target: /opt/codeman/node_modules
|
|
- type: volume
|
|
source: codeman-dist
|
|
target: /opt/codeman/dist
|
|
extra_hosts:
|
|
- "host.docker.internal:host-gateway"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
cap_drop:
|
|
- ALL
|
|
cap_add:
|
|
# The entrypoint corrects bind-mount ownership as root before dropping to
|
|
# PUID:PGID. Everything not listed here remains dropped by cap_drop above.
|
|
# test/docker-entrypoint.test.ts pins this list against what the
|
|
# entrypoint and `init: true` actually need, so a capability cannot go
|
|
# missing silently again.
|
|
- CHOWN
|
|
- DAC_OVERRIDE
|
|
# `init: true` makes tini PID 1, and tini stays ROOT while the entrypoint
|
|
# drops the server to PUID. Signalling a process of a different uid needs
|
|
# CAP_KILL; without it tini's SIGTERM forward fails ("Unexpected error
|
|
# when forwarding signal: 'Operation not permitted'"), tini dies, and the
|
|
# PID namespace teardown SIGKILLs the server instead of letting
|
|
# `server.stop()` flush state on every `docker compose down`/`restart`.
|
|
- KILL
|
|
- SETGID
|
|
- SETUID
|
|
healthcheck:
|
|
test:
|
|
- CMD-SHELL
|
|
- >-
|
|
node -e "fetch('http://127.0.0.1:${CODEMAN_PORT}/api/status').then((response) => process.exit(response.status < 500 ? 0 : 1)).catch(() => process.exit(1))"
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 30s
|
|
|
|
volumes:
|
|
# Container-owned build artefacts. They persist across container recreation,
|
|
# so an in-app update's `npm install` output is not thrown away by the next
|
|
# `up`, and they are seeded from the image on first use. Removing them (or
|
|
# `docker compose down -v`) is the supported reset: the next start rebuilds
|
|
# from the image.
|
|
codeman-node-modules:
|
|
codeman-dist:
|