Skip to content

The Docker image ships the build toolchain it tries to remove #814

Description

@andy5995

Posted by Claude (Opus 5), an LLM made by Anthropic, at andy5995's direction.

docker/Dockerfile installs a compiler toolchain to build the native modules, then removes it in a later RUN:

RUN apt-get update && apt-get install -y ... build-essential python3 libssl-dev ... \
    && cd /enigma-bbs && npm ci

COPY . /enigma-bbs

RUN ... && apt-get remove build-essential python3 libssl-dev git curl -y \
    && apt-get autoremove -y && apt-get clean

The removal cannot reclaim anything. Each RUN is a layer, and a layer can only add to the image; deleting a file in a later one records a whiteout over a copy that is still shipped and still pulled.

The published :latest shows it. Compressed layer sizes from the amd64 manifest, read from the registry API:

Layer Size What it is
6 320.9 MB the apt-get install + npm ci run
7 4.5 MB COPY . /enigma-bbs
8 0.3 MB the run that removes the toolchain

Total compressed is 401.9 MB, so layer 6 is about four fifths of the image. Layer 8, which is where the toolchain is supposedly removed, costs 0.3 MB and gives nothing back.

I cannot say how much of layer 6 is toolchain and how much is node_modules, because apt-get install and npm ci share one RUN and therefore one layer. Splitting them would at least make the two measurable separately.

A builder stage

The shape that fixes it is a multi-stage build: a build stage that keeps the toolchain, and a runtime stage that copies only what is needed.

FROM node:22-bookworm-slim AS build
RUN apt-get install -y build-essential python3 libssl-dev ...
COPY package.json package-lock.json /enigma-bbs/
RUN cd /enigma-bbs && npm ci

FROM node:22-bookworm-slim
RUN apt-get install -y lrzsz arj lhasa unrar-free p7zip-full ...
COPY --from=build /enigma-bbs/node_modules /enigma-bbs/node_modules
COPY . /enigma-bbs

Two things to get right, and the first is the reason this is not a five minute change:

  • node_modules carries compiled native modules -- node-pty, cpu-features, dtrace-provider. Copying them between stages is only sound because both stages share a base image and the build is native per platform rather than cross compiled, which is what fix(docker): build each architecture for real, not amd64 four times #794 established. A later move to cross compilation would break the copy, so it is worth a comment where the COPY --from lands.
  • The archivers stay in the runtime stage. lrzsz, arj, lhasa, unrar-free and p7zip-full are used at runtime, not at build time, and only build-essential, python3, libssl-dev, git and curl belong to the build.

Why now

It also decides a question on #800. cache-to: type=gha was set to mode=max there; mode=max exports intermediate layers from discarded stages, and with one stage there are none, so min and max cache identical content today. A builder stage is what makes max worth asking for.

I have not built the restructured image, so the saving is an inference from the layer sizes above rather than a measurement.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions