Skip to content

Commit 35e3313

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 3f66e70 commit 35e3313

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
@@ -75,21 +75,25 @@ services:
7575
max-size: "50m"
7676
max-file: "5"
7777

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

9498
# See docs/admin/agent-image-digests.md.
9599
release-pin-fetcher:
@@ -293,10 +297,8 @@ services:
293297
volume-init:
294298
condition: service_completed_successfully
295299
restart: unless-stopped
296-
# The image runs unprivileged (the buildpack's `cnb` user) while /var/run/docker.sock is
297-
# root:docker 0660, so the sandbox runtime cannot reach the daemon without joining the host's
298-
# docker group. There is no portable value: read the host's with
299-
# `getent group docker | cut -d: -f3` and set DOCKER_GROUP_ID to it.
300+
# Joins the host's docker group so the unprivileged image can reach root:docker 0660
301+
# /var/run/docker.sock. Host-specific — see DOCKER_GROUP_ID in .env.example.
300302
group_add:
301303
- "${DOCKER_GROUP_ID:-999}"
302304
volumes:
@@ -434,10 +436,8 @@ services:
434436
volume-init:
435437
condition: service_completed_successfully
436438
restart: unless-stopped
437-
# The image runs unprivileged (the buildpack's `cnb` user) while /var/run/docker.sock is
438-
# root:docker 0660, so the sandbox runtime cannot reach the daemon without joining the host's
439-
# docker group. There is no portable value: read the host's with
440-
# `getent group docker | cut -d: -f3` and set DOCKER_GROUP_ID to it.
439+
# Joins the host's docker group so the unprivileged image can reach root:docker 0660
440+
# /var/run/docker.sock. Host-specific — see DOCKER_GROUP_ID in .env.example.
441441
group_add:
442442
- "${DOCKER_GROUP_ID:-999}"
443443
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
@@ -347,10 +349,8 @@ services:
347349
WEBHOOK_EXTERNAL_URL: ${WEBHOOK_EXTERNAL_URL:-https://staging.hephaestus.aet.cit.tum.de}
348350
THC_PORT: "8080"
349351
THC_PATH: /actuator/health/liveness
350-
# The image runs unprivileged (the buildpack's `cnb` user) while /var/run/docker.sock is
351-
# root:docker 0660, so the sandbox runtime cannot reach the daemon without joining the host's
352-
# docker group. There is no portable value: read the host's with
353-
# `getent group docker | cut -d: -f3` and set DOCKER_GROUP_ID to it.
352+
# Joins the host's docker group so the unprivileged image can reach root:docker 0660
353+
# /var/run/docker.sock. Host-specific — see DOCKER_GROUP_ID in .env.example.
354354
group_add:
355355
- "${DOCKER_GROUP_ID:-999}"
356356
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)