- Buy Microsoft Visio Professional or Microsoft Project Professional 2024 for just $80
- Get Microsoft Office Pro and Windows 11 Pro for 87% off with this bundle
- Buy or gift a Babbel subscription for 78% off to learn a new language - new low price
- Join BJ's Wholesale Club for just $20 right now to save on holiday shopping
- This $28 'magic arm' makes taking pictures so much easier (and it's only $20 for Black Friday)
Understanding Container Images, Part 2: Optimizing Your Images
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.
Hello world app – Take 2
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.04 RUN apt-get update RUN apt-get install -y build-essential WORKDIR /app COPY app/hello.c /app/ RUN gcc -o hello hello.c ENTRYPOINT [ "/app/hello" ]
After building it, you can notice the size of the final container:
$ docker build -t stage0 .
$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE stage0 latest 87a0e7eb81da 4 minutes ago 319MB ubuntu 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.
The builder pattern
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, but don’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.04 RUN apt-get update RUN apt-get install -y build-essential WORKDIR /app COPY 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 ls REPOSITORY TAG IMAGE ID CREATED SIZE build_step latest f0e92c66ae80 About a minute ago 308MB ubuntu 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 -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES f22b7931ece6 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-app total 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.04 WORKDIR /app COPY built-app/hello /app/ ENTRYPOINT ["/app/hello"]
$ docker run ship_step Hello 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 ls REPOSITORY TAG IMAGE ID CREATED SIZE ship_step latest 25cebe417392 5 minutes ago 63.3MB build_step latest f0e92c66ae80 19 minutes ago 308MB ubuntu 18.04 c090eaba6b94 11 hours ago 63.3MB $
Simplyfing the build process
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_step RUN apt-get update RUN apt-get install -y build-essential WORKDIR /app COPY app/hello.c /app/ RUN gcc -o hello hello.c FROM ubuntu:18.04 WORKDIR /app COPY --from=build_step /app/hello /app ENTRYPOINT [ "/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 ls REPOSITORY TAG IMAGE ID CREATED SIZE multistep latest 31ddecae27b8 9 minutes ago 63.3MB <none> <none> c301e0bb00e0 9 minutes ago 308MB ubuntu 18.04 c090eaba6b94 11 hours ago 63.3MB
$ docker run multistep Hello 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
Related resources
Share: