In|the|first post of this series we discussed how container images are built. We talked about layers, reusing them, and how to make your Dockerfiles build faster.
We also started to build a container containing our shiny 'Hello World' C application.
What is the ultimate goal of our container? In this case, we want to be able to build our application in an automated and consistent way, and then use it in our environment.
The first Dockerfile we wrote to build our Hello world app is:
FROM ubuntu:18.04RUN apt-get updateRUN apt-get install -y build-essentialWORKDIR /appCOPY app/hello.c /app/RUN gcc -o hello hello.cENTRYPOINT [ "/app/hello" ]
After building it, you can notice the size of the final container:
$docker build -t stage0 .
$docker image lsREPOSITORY TAG IMAGE ID CREATED SIZEstage0 latest 87a0e7eb81da 4 minutes ago 319MBubuntu 18.04 2c047404e52d 7 weeks ago 63.3MB$
319 Mb (a delta of 255 Mb over the ubuntu image) for a minimal hello world C app. Not exactly optimal. Additionally, if we jump into the container, we can see all our source code, compilation artifacts, etc. Not something you want to include in your shipping product.
One of the problems with this approach is we're doing two very different things with our container: we're using it to build a product, and we're using the same one to ship it. Not optimal.
One common approach to solve this problem is to use two dockerfiles; one for building and one for shipping the product. During build, you can extract the built application to a local directory, and copy the files to the shipping container. This is called "the builder pattern".
Please read on, butdon't apply this ... yet. There are better ways!
The first dockerfile is almost identical to our first approach, except for the last line where ENTRYPOINT is defined:
Dockerfile.build:
FROM ubuntu:18.04RUN apt-get updateRUN apt-get install -y build-essentialWORKDIR /appCOPY app/hello.c /app/RUN gcc -o hello hello.c
You can now build this container and, after it's completed, extract the application from it. To build it, you need to specify the filename you want to use as your Dockerfile -Dockerfile.build on this example. Also, we're adding a name to the built image (build_step).
$docker build . -f Dockerfile.build -t build_step
$docker image lsREPOSITORY TAG IMAGE ID CREATED SIZEbuild_step latest f0e92c66ae80 About a minute ago 308MBubuntu 18.04 c090eaba6b94 10 hours ago 63.3MB$
Now you need to create a container from this image, extract the compiled application, and copy it to your filesystem:
$docker create --name extract build_step
$docker ps -aCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESf22b7931ece6 build_step "/bin/bash" 6 seconds ago Created extract$mkdir built-app
$docker cp extract:/app/hello built-app/
And finally you have your compiled application:
$ls -l built-apptotal 24-rwxr-xr-x 1 fsedanoc staff 8304 Jan 21 15:03 hello$
Now you can build a simple Dockerfile to create the minimal environment to run your app:
Dockerfile
FROM ubuntu:18.04WORKDIR /appCOPY built-app/hello /app/ENTRYPOINT ["/app/hello"]
$docker run ship_stepHello from the container!$
If you look at the size of the images, the shippable one is much smaller -Actually it's just the base image plus our final application:
$docker image lsREPOSITORY TAG IMAGE ID CREATED SIZEship_step latest 25cebe417392 5 minutes ago 63.3MBbuild_step latest f0e92c66ae80 19 minutes ago 308MBubuntu 18.04 c090eaba6b94 11 hours ago 63.3MB$
While the 'Builder pattern' works, keeping two separate Dockerfiles, plus scripts to extract the application from one to the host directory, etc, can become difficult to manage. There's a better way!
Multi-stage dockerfiles!
If you read the documentation for the Dockerfile, you can see the COPY file accepts an extra parameter:
--from
Per the documentation:
OptionallyCOPY
accepts a flag--from=<name>
that can be used to set the source location to a previous build stage (created withFROM .. AS <name>
) that will be used instead of a build context sent by the user
This flag allows you to compress the two steps in the Builder pattern in a single Dockerfile:
FROM ubuntu:18.04 as build_stepRUN apt-get updateRUN apt-get install -y build-essentialWORKDIR /appCOPY app/hello.c /app/RUN gcc -o hello hello.cFROM ubuntu:18.04WORKDIR /appCOPY --from=build_step /app/hello /appENTRYPOINT [ "/app/hello" ]
$docker build -t multistep .After the build, you can see the final image, with the correct size, and as lean as it can be. You're ready to push your final image to the repository and use it to run your app!
$docker image lsREPOSITORY TAG IMAGE ID CREATED SIZEmultistep latest 31ddecae27b8 9 minutes ago 63.3MB<none> <none> c301e0bb00e0 9 minutes ago 308MBubuntu 18.04 c090eaba6b94 11 hours ago 63.3MB
$docker run multistepHello from the container!$
Multi step builds are a great tool that will improve the quality of your containers. A smaller container means faster pushes and pulls, and that's crucial in a CI/CD/CD environment
For any question or comment, please let me know on the section below or also in Twitter or Linkedin
We'd love to hear what you think. Ask a question or leave a comment below.
And stay connected with Cisco DevNet on social!
Twitter @CiscoDevNet | Facebook | LinkedIn
Visit the new Developer Video Channel