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
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>
-**Open source target**: designed for self-hosting with own Claude licenses
139
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()`
140
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.
141
-
-**claude-runner image**: NOT a Compose service — it is a runtime dependency built once per Docker host with `make build-runner` (or a pre-deploy command on managed platforms). The API spawns containers from it dynamically via the mounted Docker socket. Image name is `claude-runner:latest` (no namespace prefix — hard-coded in `container/service.py:CLAUDE_RUNNER_IMAGE`). Previous attempt to include it as a `profiles: [build-only]`compose service was removed because `profiles:` excludes the service from both build and run unless the profile is explicitly activated — which Coolify does not do, causing `404 No such image` failures on spawn.
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.
142
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.
143
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`.
144
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.
-**Preserve Repository During Deployment**: **Enable this**. Without it, Coolify removes the cloned repo after building images. The `skills/` directory must remain on the host because the API mounts it into claude-runner containers at runtime.
135
135
136
-
### Pre-deployment Command (required)
137
-
138
-
The API spawns ephemeral containers from the `claude-runner` image. This image is **not** a Compose service — it is a runtime dependency that must exist on the Docker host before the API can start a session.
Replace `<uuid>` with your Coolify application directory (same one you use for `SKILLS_HOST_PATH` in step 7). The image is tagged `claude-runner:latest` with no namespace prefix — this is the exact tag the API references (`CLAUDE_RUNNER_IMAGE` in `apps/api/src/helprs/modules/container/service.py`).
147
-
148
-
!!! tip "Why a pre-deploy command?"
149
-
Coolify runs `docker compose up --build`, which only builds services listed in the compose file. Keeping `claude-runner` out of the compose avoids two footguns: Compose trying to start a container that has no long-lived process, and `profiles:` silently excluding the service from the build. A one-line pre-deploy command is explicit and portable to any host. Re-runs are cheap (Docker caches the layers).
150
-
151
136
### Advanced > General
152
137
153
138
-**Auto Deploy**: Enable for automatic redeployment on push to `main`.
@@ -159,10 +144,9 @@ Replace `<uuid>` with your Coolify application directory (same one you use for `
159
144
Click **Deploy**. Coolify will:
160
145
161
146
1. Clone the repo
162
-
2. Run the pre-deploy command to build `claude-runner:latest` on the host
163
-
3. Build the API and Web images
164
-
4. Start the API, Web, and DB containers (claude-runner is not a service — it is spawned per session by the API)
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)
Then re-check the pre-deployment command in Coolify (step 5) so future deploys rebuild it automatically. An image pruning operation (`docker image prune -a`) will also remove it — the pre-deploy command will rebuild it on the next deploy.
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.
249
229
250
230
---
251
231
@@ -308,13 +288,20 @@ The `DOCKER_GID` does not match your server's Docker group. See [step 8](#8-dock
308
288
No such image: claude-runner:latest
309
289
```
310
290
311
-
This happens when the pre-deployment command (step 5) did not run or the image was pruned. Build it manually on the host:
291
+
The claude-runner service did not build. Verify `infra/coolify/docker-compose.prod.yml` includes the service block:
Then verify the pre-deployment command is configured in Coolify so it rebuilds on every deploy. 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`.
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.
!!! important "Why `claude-runner`is not in the compose file"
183
-
The `claude-runner`image is a **runtime dependency**, not a service. The API
184
-
spawns a new container from it for each session (via the mounted Docker socket)
185
-
— it is never run as a long-lived process. Putting it in the compose file would
186
-
either cause Compose to try to start it (and crash), or require a `profiles:`
187
-
flag that silently excludes it from automated builds. A plain `docker build` is
188
-
explicit and works on every Docker host.
180
+
!!! note "About the `claude-runner`"Exited" container"
181
+
The `claude-runner`service is a **build-only service**: `docker compose up`
182
+
builds the image, starts the container, which exits immediately (its entrypoint
183
+
is overridden to `/bin/true`) with `restart: "no"`. The container stays in
184
+
`Exited (0)` state and is not restarted — but the image `claude-runner:latest`
185
+
remains available on the host. The API spawns new containers from this image
186
+
per session via the Docker socket.
189
187
190
-
Re-run `make build-runner` after editing anything under `infra/docker/claude-runner/`
191
-
or after a `docker image prune -a`.
188
+
This pattern keeps `docker compose up --build` as the single source of truth
189
+
for the whole stack, including the runner image.
192
190
193
191
The API runs on port 8000, the frontend on port 80. You need a reverse proxy (nginx, Caddy, Traefik) in front to handle TLS and route traffic.
194
192
@@ -220,10 +218,7 @@ cd helprs
220
218
cp .env.example .env
221
219
# Edit .env with your values...
222
220
223
-
# Build the claude-runner image (one-time per host)
224
-
make build-runner
225
-
226
-
# Start services
221
+
# Start services (the claude-runner image is built as a build-only service)
227
222
docker compose -f infra/coolify/docker-compose.prod.yml up -d --build
228
223
229
224
# Set up your preferred reverse proxy (Caddy example)
@@ -330,7 +325,7 @@ If you enabled **Post results to PR** in installation settings, the score card i
330
325
### Container won't start
331
326
332
327
- Verify Docker socket is mounted: check that `/var/run/docker.sock` is accessible to the API container
333
-
- Verify the claude-runner image exists: `docker images | grep claude-runner` — if missing, run `make build-runner` (the image is a per-host one-time build, not a Compose service)
328
+
- 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)
334
329
- Check `SKILLS_HOST_PATH` is an absolute path and the directory exists on the Docker host
335
330
- Verify the `DOCKER_GID` matches the host's Docker group: `getent group docker | cut -d: -f3`
336
331
- Check API logs for container creation errors: `docker compose logs api | grep container`
0 commit comments