You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(infra): build claude-runner via compose build-only service (#40)
* fix(infra): build claude-runner image out-of-band instead of via compose profile
The `claude-runner` image is a runtime dependency (the API spawns containers
from it per session), not a Compose service. A previous attempt shipped it as
a `profiles: [build-only]` service, but Compose excludes profiled services
from both build and run unless the profile is explicitly activated — which
Coolify does not do, causing `404 No such image: claude-runner:latest` when
the API tries to start a session.
Changes:
- Remove `claude-runner` service from infra/coolify/docker-compose.prod.yml
- Fix the Makefile `build-runner` tag (`claude-runner:latest`, no namespace)
- Make `make build` also build the runner
- Document the per-host one-time build in Quick Start, self-hosting guide,
and Coolify guide (with pre-deployment command)
- Update CLAUDE.md decisions and troubleshooting
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(infra): use build-only service pattern for claude-runner
Switches from "out-of-band make build-runner" (previous commit) to a proper
Compose build-only service:
claude-runner:
build: { context: ./infra/docker/claude-runner }
image: claude-runner:latest
entrypoint: ["/bin/true"]
restart: "no"
The container exits immediately on `docker compose up` (entrypoint is
overridden to `/bin/true`) and is not restarted, so it produces no runtime
overhead — but Compose still builds the image and leaves it tagged
`claude-runner:latest` on the host, ready for the API to spawn session
containers from via the Docker socket.
Benefits over the previous commit:
- `docker compose up --build` is the single command on every host (Coolify,
VPS, local dev, ECS EC2)
- No Coolify pre-deployment command to configure in the UI
- No `make build-runner` step for users
- Rebuilds automatically whenever `infra/docker/claude-runner/` changes
Applied to both the dev `docker-compose.yml` and the prod
`infra/coolify/docker-compose.prod.yml`. `make build-runner` stays as a
convenience shortcut for quick iteration. Docs reverted to the simpler
"just run compose" narrative.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
-**Open source target**: designed for self-hosting with own Claude licenses
138
138
-**Post-results to PR**: after session completion, the API can post score card as a PR comment — opt-in per installation via `post_results_to_pr` boolean; extraction and formatting in `container/pr_comment.py`, triggered in `_event_stream()` after `mark_completed()`
139
139
-**Coolify deployment**: Two-domain setup via Traefik: `helprs.tech` (web) and `api.helprs.tech` (API). TLS via Let's Encrypt, managed by Coolify. Domains are set in Coolify UI (General > Domains for api / Domains for web) — they may get cleared on redeploy, re-check after each deploy. "Preserve Repository During Deployment" must be enabled so skills are available on the host. The prod compose uses `./` paths (repo-root-relative) because Coolify sets `--project-directory` to the repo root.
140
-
-**claude-runner image**: built via a `profiles: [build-only]`service in the prod compose — Coolify builds the image but never starts the container. The API spawns it dynamically. Image name is `claude-runner:latest` (no namespace prefix).
140
+
-**claude-runner image**: declared in both `docker-compose.yml` and `infra/coolify/docker-compose.prod.yml` as a **build-only service** — `entrypoint: ["/bin/true"]`+ `restart: "no"` so the container exits immediately on `up`, leaving only the built image `claude-runner:latest` on the host. The API spawns containers from this image dynamically via the mounted Docker socket. Image tag is hard-coded in `container/service.py:CLAUDE_RUNNER_IMAGE` — it must stay `claude-runner:latest` (no namespace prefix). Earlier attempt with `profiles: [build-only]` was reverted: `profiles:` excludes a service from both build AND run unless the profile is explicitly activated, which Coolify does not do, causing `404 No such image` failures on session spawn.
141
141
-**Non-root API container**: production Dockerfile uses `appuser` with `chown -R appuser:appuser /app` for uv cache writes. Docker socket access requires `group_add: ["${DOCKER_GID:-994}"]` in the compose to match the host's docker group GID.
142
142
-**BYOK supports OAuth tokens**: `validate_claude_key()` accepts both API keys (`sk-ant-api03-...`) and OAuth tokens (`sk-ant-oat...`). OAuth tokens skip server-side validation (validated at runtime by Claude Code CLI). Frontend setup page guides users to `claude setup-token`.
143
143
-**VITE_* build args**: `VITE_API_URL` and `VITE_GITHUB_APP_SLUG` are build-time variables — must be passed as `args` in the compose and declared as `ARG`/`ENV` in `Dockerfile.web`. They cannot be set at runtime.
3. Start the API, Web, and DB containers (claude-runner is `profiles: [build-only]`, not started)
147
+
2. Build the API, Web, and claude-runner images (claude-runner is a **build-only service** — the container exits immediately with `/bin/true`, leaving the image available on the host for the API to spawn session containers from)
@@ -219,20 +219,13 @@ The `docker-compose.prod.yml` has `group_add: ["${DOCKER_GID:-994}"]`. If your s
219
219
docker images | grep claude-runner
220
220
# Should show: claude-runner latest <id> <date> <size>
221
221
222
-
docker ps | grep claude-runner
223
-
# Should be empty (it's only spawned on demand, not a long-lived service)
222
+
docker ps -a | grep claude-runner
223
+
# Expected: an "Exited (0)" container from the last compose up — this is
224
+
# normal, the service is declared as build-only (entrypoint /bin/true).
225
+
# The API does NOT use this container — it spawns fresh ones per session.
224
226
```
225
227
226
-
If the image is missing, the `claude-runner` service may have been excluded from the build. Verify the compose file includes:
227
-
228
-
```yaml
229
-
claude-runner:
230
-
build:
231
-
context: ./infra/docker/claude-runner
232
-
image: claude-runner
233
-
profiles:
234
-
- build-only
235
-
```
228
+
If the image is missing, the compose build step failed. Check Coolify's **Logs** tab for build errors (usually a Dockerfile issue in `infra/docker/claude-runner/`) and redeploy.
236
229
237
230
---
238
231
@@ -295,7 +288,20 @@ The `DOCKER_GID` does not match your server's Docker group. See [step 8](#8-dock
295
288
No such image: claude-runner:latest
296
289
```
297
290
298
-
Verify the compose includes the claude-runner service with `profiles: [build-only]`. Redeploy to rebuild the image.
291
+
The claude-runner service did not build. Verify `infra/coolify/docker-compose.prod.yml` includes the service block:
292
+
293
+
```yaml
294
+
claude-runner:
295
+
build:
296
+
context: ./infra/docker/claude-runner
297
+
image: claude-runner:latest
298
+
entrypoint: ["/bin/true"]
299
+
restart: "no"
300
+
```
301
+
302
+
Redeploy via Coolify. Check the deploy logs for errors in the `claude-runner` build step. The image name must be exactly `claude-runner:latest` (no namespace prefix) — the API references this tag in `apps/api/src/helprs/modules/container/service.py`.
303
+
304
+
If the image was pruned manually (`docker image prune -a`), just redeploy — Compose rebuilds it as part of the stack.
- Verify the claude-runner image exists: `docker images | grep claude-runner` — if missing, run `docker compose up --build` (the image is built as part of the normal compose up)
322
329
- Check `SKILLS_HOST_PATH` is an absolute path and the directory exists on the Docker host
323
330
- Verify the `DOCKER_GID` matches the host's Docker group: `getent group docker | cut -d: -f3`
324
331
- Check API logs for container creation errors: `docker compose logs api | grep container`
0 commit comments