Skip to content

build(docker): stop duplicating /app to fix its ownership - #9301

Open
klesh wants to merge 1 commit into
mainfrom
build/shrink-image-chown
Open

klesh wants to merge 1 commit into
mainfrom
build/shrink-image-chown

Conversation

@klesh

@klesh klesh commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Summary

chgrp -R 0 /app && chmod -R g=u /app runs as a final layer in devlake-base, after python packages, the uv binary, the python plugins and the server binary have already been written by earlier layers. Overlayfs has to copy every file it mutates into the new layer, so the published image carries a second full copy of /app: 186.5 MB of the 516.6 MB total, measured on apache/devlake:v1.0.3-beta19.

This applies the ownership where the files are created instead, so no second copy is written.

Change Why
useradd … -G 0 the user has to belong to group 0 to chgrp its own files to group 0; the primary group stays devlake, so the runtime GID does not change
pip and uv installs folded into one RUN, ending with chgrp/chmod /app/.local they share /app/.local; normalising it in a later layer would copy it up again
… build.sh && chgrp -R 0 /app/python && chmod -R g=u /app/python same for the venvs the plugin build creates
mkdir logs && chgrp 0 logs && chmod g=u logs /app/logs is the only writable path the app needs at startup
COPY --chown=0:0 for lake and resources they are read-only at runtime; the modes they already have (755 binary, 644 docs) are enough for a random UID in group 0
mkdir -p -m 0775 /app/bin/plugins the sideload mount point, and the only directory created in the final stage

Resulting ownership and modes are identical to the old recursive pass (devlake:root, 775 directories / 664 files), which is what the OpenShift arbitrary-UID support added in #8657 relies on.

Verification

I could not build the full image locally: this machine has no buildx, and the legacy builder rejects FROM --platform=$BUILDPLATFORM in the earlier stage before it reaches these. So:

1. Mechanism, with a replica image. Same instructions, 40 MB of payload: the old ordering produces a 41.9 MB content layer plus a 42.0 MB duplicate; the new ordering produces a single 42.0 MB layer. Ownership and modes are identical (devlake:root 775/664).

2. The real base stage, built from a trimmed copy of the Dockerfile (--target base, builder stage removed):

layers        24 -> 23      (the separate chgrp layer is gone)
/app/logs     devlake:root 775
/app/python   devlake:root 775
/app/.local   devlake:root 775
/app          devlake:devlake 755

3. The arbitrary-UID contract still holds. Running as -u 1000120000:0 (what OpenShift does):

/app/logs writable
/app/python readable, /app/.local/bin/uv executable

id inside the image reports gid=1010(devlake) groups=1010(devlake),0(root) — the primary GID is unchanged; group 0 is supplementary, which is what lets the user chgrp.

Expected size: the published beta19 image spends 186.5 MB of its 516.6 MB on that one layer. Folding the chgrp into the layers that create the files removes it, so the image should land around 330 MB. That number is arithmetic from the published manifest, not a local measurement — the final stage can only be built by CI.

Does this close any open issues?

Refs #9276.

Other Information

config-ui/Dockerfile has the same late chgrp -R pattern over nginx and /run. It is worth the same treatment, but those trees are far smaller and messing with nginx permissions deserves its own PR.

`chgrp -R 0 /app && chmod -R g=u /app` ran as a final layer, after python
packages, the uv binary, the python plugins and the server binary had already
been written by earlier layers. Overlayfs has to copy every file it mutates
into the new layer, so the image shipped a second full copy of /app: measured
at 186.5 MB of a 516.6 MB image.

Apply the ownership where the files are created instead:

- add devlake to group 0 so it may chgrp the files it owns; its primary group
  stays devlake, so the runtime GID is unchanged
- fold the pip and uv installs into one RUN and normalise /app/.local at the
  end of it
- normalise /app/python at the end of the plugin build
- create logs with group 0 up front

Everything copied into the final stage is read-only at runtime, so `--chown=0:0`
is enough; the modes it already has (755 binary, 644 docs) let any GID 0 process
read and execute it.

Verified with a replica image that the ownership and modes are byte-identical
to the old pattern (devlake:root 775/664) while the duplicate layer disappears.
Expected result: ~330 MB instead of 516.6 MB.
@klesh klesh added the pr-type/refactor This PR refactors existing features label Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-type/refactor This PR refactors existing features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant