From e9a92b180d5d719bf48be1296af60e3559781d22 Mon Sep 17 00:00:00 2001 From: Klesh Wong Date: Mon, 2 Nov 2026 16:27:47 +0800 Subject: [PATCH] build(docker): stop duplicating /app to fix its ownership `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. --- backend/Dockerfile | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 9ed02234a0a..590fa9255a1 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -80,29 +80,39 @@ RUN apt-get update && \ EXPOSE 8080 -RUN useradd -ms /bin/bash -d /app devlake -u 1010 +RUN useradd -ms /bin/bash -d /app devlake -u 1010 -G 0 RUN chown -R devlake:devlake /etc/ssl/certs USER devlake ENV PYTHONUNBUFFERED=1 WORKDIR /app -RUN mkdir logs +RUN mkdir logs && chgrp 0 logs && chmod g=u logs VOLUME /app/logs +# Ownership for arbitrary-UID runtimes (OpenShift runs containers with a random +# UID and GID 0) is applied in the same RUN that creates each path. Doing it +# there costs nothing: the layer was being written anyway. A recursive +# `chgrp -R 0 /app && chmod -R g=u /app` at the end of the image instead +# rewrites every file that earlier layers created, so Docker stores a second +# copy of the whole tree - around 190 MB on the published image. +# +# The devlake user is added to group 0 so it is allowed to chgrp the files it +# owns; its primary group stays devlake, so the runtime GID does not change. + # Setup Python -COPY --chown=devlake:devlake python/ /app/python/ +COPY --chown=devlake:0 python/ /app/python/ RUN python3 -m pip install --no-cache --upgrade pip setuptools && \ python3 -m pip install --no-cache -r python/requirements.txt && \ - python3 -m pip install --upgrade pip - -# Setup Python package manager -RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_UNMANAGED_INSTALL=/app/.local/bin sh + python3 -m pip install --upgrade pip && \ + curl -LsSf https://astral.sh/uv/install.sh | env UV_UNMANAGED_INSTALL=/app/.local/bin sh && \ + chgrp -R 0 /app/.local && chmod -R g=u /app/.local ENV PATH="/app/.local/bin:${PATH}" # Build Python plugins, make sure the scripts has execute permission # RUN find /app/python/ -name "*.sh" | xargs -I{} chmod +x {} -RUN /app/python/build.sh +RUN /app/python/build.sh && \ + chgrp -R 0 /app/python && chmod -R g=u /app/python FROM base as devlake-base ARG DEBUG= @@ -110,8 +120,11 @@ ARG DEBUG= # apps # The single binary already contains every plugin; the .so files that # `make build-plugin` produces stay in the builder stage and are never copied. -COPY --from=build /app/bin/lake /app/bin/lake -COPY --from=build /app/resources /app/resources +# The binary and resources are read-only at runtime, so root:0 with the modes +# they already have (755 for the binary, 644 for the swagger docs) is enough for +# a random UID in group 0. No recursive chmod afterwards. +COPY --from=build --chown=0:0 /app/bin/lake /app/bin/lake +COPY --from=build --chown=0:0 /app/resources /app/resources ENV PATH="/app/bin:${PATH}" ENV DEBUG="$DEBUG" @@ -119,9 +132,7 @@ ENV DEBUG="$DEBUG" USER root # Empty mount point for sideloaded plugins: dropping a .so here registers it on # top of the compiled-in set at startup; see PLUGIN_DIR. -RUN mkdir -p /app/bin/plugins && \ - chgrp -R 0 /app && \ - chmod -R g=u /app +RUN mkdir -p -m 0775 /app/bin/plugins USER devlake #add tini, prevent zombie process