Search code examples
dockerdockerfiledocker-build

Build .deb package in one Docker build stage, install in another stage, but without the .deb package itself taking up space in the final image?


I have a multistage Docker build file. The first stage creates an installable package (e.g. a .deb file). The second stage needs only to install the .deb package.

Here's an example:

FROM debian:buster AS build_layer
COPY src/ /src/
WORKDIR /src
RUN ./build_deb.sh
# A lot of stuff happens here and a big huge .deb package pops out. 
# Suppose the package is 300MB.

FROM debian:buster AS app_layer
COPY --from=build_layer /src/myapp.deb /
RUN dpkg -i /myapp.deb && rm /myapp.deb
ENTRYPOINT ["/usr/bin/myapp"]

# This image will be well over 600MB, since it contains both the
# installed package as well as the deleted copy of the .deb file.

The problem with this is that the COPY stage runs in its own layer, and drops the large .deb package into the final build context. Then, the next step installs the package and removes the .deb file. However, since the COPY stage has to execute independently, the .deb package still takes up room in the final image. If it were a small package you might just deal with it, but in my case the package file is hundreds of MB, so its presence in the final build layers does increase the image size appreciably with no benefit.

There are related posts on SO, such as this one which discusses files containing secrets, and this one which is for copying a large installer from outside the container into it (and the solution for that one is still kinda janky, requiring you to run a temporary local http server). However neither of these address the situation of needing to copy from another build stage but not retain the copied file in the final package.

The only way I could think of to do this would be to extend the idea of a web server and make available an SFTP or similar server so that the build layer can upload the package somewhere. But this also requires extra infrastructure, and now you're also dealing with SSH secrets and such, and this starts to get real complex and is a lot less reproducible on another developer's system or in a CI/CD environment.

Alternatively I could use the --squash option in BuildKit, but this ends up killing the advantages of the layer system. I can't then reuse similar layers across multiple images (e.g. the image now can't take advantage of the fact that the Debian base image might exist on the end user's system already). This would minimize space usage, but wouldn't be ideal for a lot of other reasons.

What's the recommended way to approach this?


Solution

  • You can mount the build layer just for the dpkg command:

    FROM debian:buster AS build_layer
    COPY src/ /src/
    WORKDIR /src
    RUN ./build_deb.sh
    
    FROM debian:buster AS app_layer
    RUN --mount=type=bind,from=build_layer,target=/mnt dpkg -i /mnt/src/myapp.deb
    ENTRYPOINT ["/usr/bin/myapp"]