Reputation: 1717
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:
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/
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.
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).
Is there a better way to do this? Preferably even one which would work with a single Dockerfile?
Upvotes: 2
Views: 2073
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