mat
mat

Reputation: 1717

Using Docker multistage build to create multiple images

I have a project with multiple subprojects and I want to ship a separate docker image for each of them. To increase efficiency, I want to use multistage builds and am looking for a best practice pattern how this is to be done in the most efficient and intuitive way. So far I have found two possibilities, both with drawbacks:

Multiple Dockerfiles and build calls

I can make one Dockerfile for the builder image

FROM maven as builder

COPY . /build
WORKDIR /build
RUN mvn -e clean install

and separate Dockerfiles for each subproject

FROM my_builder as builder

FROM openjdk:jre-slim as proj1

COPY --from=builder /build/proj1.jar /somewhere/
CMD ["java", "-jar","/somewhere/proj1.jar"]

This works, but the drawback is, that I have to build my images in multiple steps and the Dockerfiles of the subprojects can not be built by themselves:

docker build -t my_builder .
docker build proj1/
docker build proj2/

Using a docker-compose file

I can remove this problem by using a docker-compose file:

version: "3.4"

services:
   builder:
      build:
         context: ./
   proj1:
     build:
       target: proj1
       context: ./proj1
     depends_on:
       - builder
   proj2:
     build:
       target: proj2
       context: ./proj2
     depends_on:
       - builder

This has the advantage of being able to run the build with a single command

docker-compose build

but has the drawback of creating a unneeded and artificial dependency for docker-compose which is not needed in the project.

Building the whole project in all subprojects

I could also add the buildstage to all Dockerfiles

FROM maven as builder

COPY . /build
WORKDIR /build
RUN mvn -e clean install

FROM openjdk:jre-slim as proj1

COPY --from=builder /build/proj1.jar /somewhere/
CMD ["java", "-jar","/somewhere/proj1.jar"]

This would have the advantage that I can build each project's container by itself

docker build proj1/

On the other hand, it it less efficient and violates the DRY principal (the first part of each Dockerfile is repeated over and over again).

Best Practice?

Is there a better way to do this? Preferably even one which would work with a single Dockerfile?

Upvotes: 2

Views: 2073

Answers (1)

srlm
srlm

Reputation: 3226

I had the same problem: several projects that shared some common Dockerfile lines, but had a few differences. There's a couple of ways that you can solve this with a single Dockerfile.

First, you can do a fan out method:

FROM ubuntu:18.04 as base
RUN echo "base" >> /history.txt
CMD cat /history.txt


FROM base as variant0
RUN echo "variant0" >> /history.txt

FROM base as variant1
RUN echo "variant1" >> /history.txt

Then during your build you just select which one you want using --target:

docker build --file=fan-out.dockerfile --target=variant0 --tag=fan-out/variant0 ./

Alternatively, sometimes your project has the shared steps at the end instead of the beginning. You could do something like this, what I call the fan-in method:

ARG variant

FROM ubuntu:18.04 as variant0
RUN echo "variant0" >> /history.txt

FROM ubuntu:18.04 as variant1
RUN echo "variant1" >> /history.txt

FROM ubuntu:18.04 as variant2
RUN echo "variant2" >> /history.txt

FROM $variant as join
# pass, do nothing

FROM ubuntu:18.04 as final
COPY --from=join /history.txt /
RUN echo "final" >> /history.txt
CMD cat /history.txt

And build it with a --build-arg:

docker build --file=fan-in.dockerfile --target=final --build-arg="variant=variant1" --tag=fan-in/variant1 ./

In either of these methods, you'll probably want to have a makefile or shell script to keep track of the commands for each variation.

I wrote up a blog post with some more details.

Upvotes: 3

Related Questions