Skip to content

Commit 96ceb24

Browse files
fix(docker): make the git-checkout volume writable, in both stacks
A new named volume is created root-owned, and one created by an earlier image keeps that image's uid. Staging's is 100:101 at 0755 while the buildpack image runs as 1002:1001, so the agent could not create its content-addressed store: Failed to lock CAS blob 0a603c06... Caused by: java.nio.file.AccessDeniedException: /data/git-repos/cas Nothing writes there until an agent job runs, so the deployment reported healthy and the first practice review failed. chown -Rh, not a find predicate: -h retargets the dangling symlinks the tree contains instead of following them out of the volume, and the predicate it replaces skipped the group entirely, so it did not do what it claimed. The preview stack gets the same text and the same command — it had drifted to chown -R, which has the symlink hole. volume-init is a dependency of both application roles, so it runs on every start: network_mode none because it needs no networking, a logging cap because every sibling has one, and a digest-pinned base because an unpinned tag in that position can block the stack from starting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZADQeSx6zQNNqsdu7AAqZ
1 parent 5c1ee0e commit 96ceb24

3 files changed

Lines changed: 31 additions & 25 deletions

File tree

docker/compose.app.yaml

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -86,21 +86,25 @@ services:
8686
max-size: "50m"
8787
max-file: "5"
8888

89-
# A named volume is created root-owned, and one created by an earlier image keeps that image's
90-
# uid. The buildpack image runs as `cnb` (1002:1001), so anything it must write under
91-
# /data/git-repos — the git checkouts and the agent's content-addressed store — is unwritable
92-
# until the ownership matches. That surfaces as AccessDeniedException on the first practice
93-
# review rather than at startup, because nothing writes there until an agent job runs. Only
94-
# entries with the wrong owner are touched, so an already-correct volume costs a metadata walk.
89+
# A new named volume is created root-owned, and one created by an earlier image keeps that image's
90+
# uid. The buildpack image runs as `cnb` (1002:1001), so nothing under /data/git-repos is writable
91+
# until the ownership matches — and that surfaces when an agent job first writes there, not at
92+
# startup. -h so a symlink inside a checkout is retargeted rather than followed out of the volume.
9593
volume-init:
96-
image: alpine:3
94+
image: alpine:3@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
9795
command:
9896
- sh
9997
- -c
100-
- "find /data/git-repos ! -user 1002 -exec chown -h 1002:1001 {} + && chmod 0755 /data/git-repos"
98+
- "chown -Rh 1002:1001 /data/git-repos && chmod 0755 /data/git-repos"
10199
volumes:
102100
- git-repos:/data/git-repos
103101
restart: "no"
102+
network_mode: none
103+
logging:
104+
driver: "json-file"
105+
options:
106+
max-size: "50m"
107+
max-file: "5"
104108

105109
# See docs/admin/agent-image-digests.md.
106110
release-pin-fetcher:
@@ -298,10 +302,8 @@ services:
298302
volume-init:
299303
condition: service_completed_successfully
300304
restart: unless-stopped
301-
# The image runs unprivileged (the buildpack's `cnb` user) while /var/run/docker.sock is
302-
# root:docker 0660, so the sandbox runtime cannot reach the daemon without joining the host's
303-
# docker group. There is no portable value: read the host's with
304-
# `getent group docker | cut -d: -f3` and set DOCKER_GROUP_ID to it.
305+
# Joins the host's docker group so the unprivileged image can reach root:docker 0660
306+
# /var/run/docker.sock. Host-specific — see DOCKER_GROUP_ID in .env.example.
305307
group_add:
306308
- "${DOCKER_GROUP_ID:-999}"
307309
volumes:
@@ -435,10 +437,8 @@ services:
435437
volume-init:
436438
condition: service_completed_successfully
437439
restart: unless-stopped
438-
# The image runs unprivileged (the buildpack's `cnb` user) while /var/run/docker.sock is
439-
# root:docker 0660, so the sandbox runtime cannot reach the daemon without joining the host's
440-
# docker group. There is no portable value: read the host's with
441-
# `getent group docker | cut -d: -f3` and set DOCKER_GROUP_ID to it.
440+
# Joins the host's docker group so the unprivileged image can reach root:docker 0660
441+
# /var/run/docker.sock. Host-specific — see DOCKER_GROUP_ID in .env.example.
442442
group_add:
443443
- "${DOCKER_GROUP_ID:-999}"
444444
volumes:

docker/preview/compose.app.yaml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -197,18 +197,20 @@ services:
197197
cpus: "0.5"
198198
memory: 512M
199199

200-
# Docker creates named volumes as root:root; the buildpack image runs as `cnb` (1002:1001) and
201-
# needs to write git checkouts and Tomcat access logs. Every deploy gets fresh volumes, so the
202-
# ownership fix has to run on every deploy too.
200+
# A new named volume is created root-owned, and one created by an earlier image keeps that image's
201+
# uid. The buildpack image runs as `cnb` (1002:1001), so nothing under /data/git-repos is writable
202+
# until the ownership matches — and that surfaces when an agent job first writes there, not at
203+
# startup. -h so a symlink inside a checkout is retargeted rather than followed out of the volume.
203204
volume-init:
204-
image: alpine:3
205+
image: alpine:3@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
205206
command:
206207
- sh
207208
- -c
208-
- chown -R 1002:1001 /data/git-repos && chmod 0755 /data/git-repos
209+
- "chown -Rh 1002:1001 /data/git-repos && chmod 0755 /data/git-repos"
209210
volumes:
210211
- git-repos:/data/git-repos
211212
restart: "no"
213+
network_mode: none
212214

213215
# The service name must stay dash-free. Coolify stores a service's configured domain under the
214216
# service name but looks it up with dashes replaced by underscores, so a dashed name never
@@ -346,10 +348,8 @@ services:
346348
WEBHOOK_EXTERNAL_URL: ${WEBHOOK_EXTERNAL_URL:-https://staging.hephaestus.aet.cit.tum.de}
347349
THC_PORT: "8080"
348350
THC_PATH: /actuator/health/liveness
349-
# The image runs unprivileged (the buildpack's `cnb` user) while /var/run/docker.sock is
350-
# root:docker 0660, so the sandbox runtime cannot reach the daemon without joining the host's
351-
# docker group. There is no portable value: read the host's with
352-
# `getent group docker | cut -d: -f3` and set DOCKER_GROUP_ID to it.
351+
# Joins the host's docker group so the unprivileged image can reach root:docker 0660
352+
# /var/run/docker.sock. Host-specific — see DOCKER_GROUP_ID in .env.example.
353353
group_add:
354354
- "${DOCKER_GROUP_ID:-999}"
355355
volumes:

docker/self-host/.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ GH_AUTH_TOKEN=
108108
#AGENT_ENABLED=false
109109
#HEPHAESTUS_FABRIC_GC_RETENTION_DAYS=30
110110
#GIT_CHECKOUT_ENABLED=false
111+
112+
# Supplementary group the app joins so the agent sandbox can reach the Docker socket. The image runs
113+
# unprivileged and the socket is root:docker 0660, so a wrong value fails every sandbox start with
114+
# "permission denied" — at first review, not at boot. Read your host's id with
115+
# `getent group docker | cut -d: -f3`; the 999 default is right on many Debian hosts and wrong on others.
116+
#DOCKER_GROUP_ID=999
111117
#PRACTICE_REVIEW_FOR_ALL=false
112118
#SANDBOX_MAX_CONCURRENT=1
113119

0 commit comments

Comments
 (0)