Merge pull request #349 from opticon454/feature/docker-compose

Docker Compose deployment: Codeman runs in a container and spawns Docker cases
as SIBLING containers through the mounted host socket (Docker-outside-of-Docker).

Resolved the README conflict (master had grown to eight CLIs since the branch
was cut) and moved the Compose blurb out of the feature bullets into Quick
Start, next to the other ways of starting Codeman.

Three review findings from the PR discussion are fixed here rather than left
for a follow-up, because two of them are shipped-image problems:

- `.dockerignore` excluded `.env` only at the ROOT. A pattern is matched against
  the whole context-relative path, so `docker/.env` — which the deployment's own
  README tells the user to fill with CODEMAN_PASSWORD and provider API keys —
  was picked up by `COPY . .` and baked into the image at
  /opt/codeman/docker/.env. Verified in both directions against a real build
  context: with a canary secret in docker/.env, the unfixed ignore file lets
  /ctx/docker/.env through, and `**/.env` (plus `**/.env.*` and a negation for
  the checked-in .env.example) leaves only the example behind.
- `CODEMAN_CASES_PATH` moved the server's CASES_DIR but not the CLI's, which
  still hardcoded ~/codeman-cases, so `codeman skill install --case <name>`
  reported "Case not found" on exactly the deployment the override exists for.
  Both now resolve through config/cases-dir.ts. state-store.ts keeps its own
  literal on purpose: that one migrates the historical ~/claudeman-cases
  directory by name and is about the old default, not the active location.
- CLAUDE.md gained the Compose paragraph (the sibling-container inversion, the
  three env vars, the .dockerignore and root-owned-bind traps) and .dockerignore
  joins the documented list of files that genuinely belong in the repo root.

The PR's `mode === 'claude'` guard on dockerResumeId is an unrelated master bug
fix riding along: appendResumeFlag() maps a resume id onto codex/gemini/pi/grok/
deepseek/omp/antigravity and RESUME_ID_SAFE accepts a UUID, so a Docker case's
lastClaudeSessionId was handed to every non-claude CLI.

Full gate green in a merge worktree: 6360 tests, lint, format, frontend syntax,
public assets, lockfile.
This commit is contained in:
Codeman maintainer
2026-09-01 11:32:03 +02:00
19 changed files with 712 additions and 21 deletions
+4 -2
View File
@@ -8,7 +8,6 @@
import { join, resolve, relative, isAbsolute } from 'node:path';
import { realpathSync, existsSync, mkdirSync } from 'node:fs';
import fs from 'node:fs/promises';
import { homedir } from 'node:os';
import type { z } from 'zod';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { Session } from '../session.js';
@@ -21,12 +20,15 @@ import type { EventPort } from './ports/event-port.js';
import type { AuthSessionRecord } from './ports/auth-port.js';
import type { StaleExpirationMap } from '../utils/index.js';
import { dataPath } from '../config/instance.js';
import { getCasesDir } from '../config/cases-dir.js';
import { isMultiUserMode, maxSessionsPerUser, userCasesDir } from '../config/multiuser.js';
import { SYNTHETIC_ADMIN, findUser } from '../user-store.js';
// Shared path constants used across route modules. CASES_DIR (project folders)
// stays shared across instances; SETTINGS_PATH is per-instance runtime state.
export const CASES_DIR = join(homedir(), 'codeman-cases');
// The cases dir is resolved in ONE place (config/cases-dir.ts) because the CLI
// resolves it too, and CODEMAN_CASES_PATH must move both or neither.
export const CASES_DIR = getCasesDir();
export const SETTINGS_PATH = dataPath('settings.json');
/**
+3 -3
View File
@@ -3076,9 +3076,9 @@ export function registerSessionRoutes(
casePath = dockerCase.hostWorkspacePath; // a REAL host dir (bind-mounted into the container)
docker = sessionDocker;
// Seed resume so a relaunch resumes the case's last conversation from the
// bind-mounted transcript (decision: resume-on-start default ON).
if (sessionDocker.resumeOnStart && dockerCase.lastClaudeSessionId) {
// Seed only Claude's resume id. Codex, Gemini, and the other CLIs have
// separate conversation stores and must never receive a Claude UUID.
if (mode === 'claude' && sessionDocker.resumeOnStart && dockerCase.lastClaudeSessionId) {
dockerResumeId = dockerCase.lastClaudeSessionId;
}
} else {