Multi-Stage Builds
By the end of this exercise, you should be able to:
- Write a Dockerfile that describes multiple images, which can copy files from one image to the next.
Defining a multi-stage build
Make a fresh folder
multi-stageto do this exercise in, andcdinto it.Add a file
hello.goto themulti-stagefolder containing Hello World in Go:package main import "fmt" func main() { fmt.Println("hello world") }Now let's Dockerize our hello world application. Add a
Dockerfileto themulti-stagefolder with this content:FROM golang:nanoserver COPY . /code WORKDIR /code RUN go build hello.go CMD ["\\code\\hello.exe"]Build the image and observe its size:
PS: node-0 multi-stage> docker image build -t my-app-large . PS: node-0 multi-stage> docker image ls | select-string my-app-large REPOSITORY TAG IMAGE ID CREATED SIZE my-app-large latest 7c95f4e0112e 11 minutes ago 1.54GBTest the image to confirm it actually works:
PS: node-0 multi-stage> docker container run my-app-largeIt should print "hello world" in the console.
Update your Dockerfile to use an
ASclause on the first line, and add a second stanza describing a second build stage:FROM golang:nanoserver AS gobuild COPY . /code WORKDIR /code RUN go build hello.go FROM microsoft/nanoserver COPY --from=gobuild /code/hello.exe /hello.exe CMD ["\\hello.exe"]Build the image again, test it and compare the size with the previous version:
PS: node-0 multi-stage> docker image build -t my-app-small . PS: node-0 multi-stage> docker container run my-app-small PS: node-0 multi-stage> docker image ls | select-string 'my-app-' REPOSITORY TAG IMAGE ID CREATED SIZE my-app-small latest 13a42c43f45f 11 minutes ago 1.14GB my-app-large latest 7c95f4e0112e 13 minutes ago 1.54GBAs expected, the size of the multi-stage build is much smaller than the large one since it does not contain the .NET SDK.
Finally, make sure the app actually works:
PS: node-0 multi-stage> docker container run my-app-smallYou should get the expected 'hello world' output from the container with just the required executable.
Building Intermediate Images
In the previous step, we took our compiled executable from the first build stage, but that image wasn't tagged as a regular image we can use to start containers with; only the final FROM statement generated a tagged image. In this step, we'll see how to persist whichever build stage we like.
Build an image from the
buildstage in your Dockerfile using the--targetflag:PS: node-0 multi-stage> docker image build -t my-build-stage --target gobuild .Run a container from this image and make sure it yields the expected result:
PS: node-0 multi-stage> docker container run -it --rm my-build-stage hello.exeList your images again to see the size of
my-build-stagecompared to the small version of the app.
Conclusion
In this exercise, you created a Dockerfile defining multiple build stages. Being able to take artifacts like compiled binaries from one image and insert them into another allows you to create very lightweight images that do not include developer tools or other unnecessary components in your production-ready images, just like how you currently probably have separate build and run environments for your software. This will result in containers that start faster, and are less vulnerable to attack.