Learning Docker create your own micro-image

In the last article I wrote about how you can create your own small image using docker scratch image. The scratch image has the ability to execute basic binary files. I assume that you will have some code base that is then compiled to inserted into the scratch image. In order to do this, you can have a build machine that you maintain to create Linux executables, or you can use another docker image to create the binary and copy it to the scratch image. This is known as a multi-stage build that produces the smallest possible end state container. This whole process can be done in a single Dockerfile. Let’s start with a basic c program that prints out Hello from Docker when executed:

#include <stdio.h>
  
int main() {
    printf("Hello from Docker\n");
    return 0;
}

This should be saved in the current director as hello.c. We then need to build a machine with gcc to compile the c program into a binary. We will call this machine builder. The Dockerfile for builder looks like this:

FROM ubuntu:latest AS builder
# Install gcc
RUN apt-get update -qy
RUN apt-get upgrade -qy
RUN apt-get install build-essential -qy
COPY hello.c .
# Build binary saved as a.out
RUN gcc -o hello -static hello.c

This does the following:

  • Use ubuntu:latest as the image
  • RUN the commands to update and upgrade base operating system (-qy is to run quiet (-q) and answer yes (-y) to all questions)
  • RUN the command to install build-essential which includes the gcc binary and libraries
  • COPY the file hello.c from the local file system into current directory
  • RUN gcc to compile hello.c into hello – This step is critical because we are using the compiler to include all required libraries with the static line without this the executable will fail while looking for a dynamically liked library

Let’s manually build this container to test the static linking using a small docker file:

FROM ubuntu:latest

Now let’s turn this into a container and test our commands to ensure we have the correct commands and order to create our builder container:

docker build -t builder .

This will build the container image called builder from ubuntu:latest from docker hub. Now lets run an instance of this container and give it a try.

docker run -it builder /bin/bash

You are now connected to the container and you can test all your commands to ensure they work

apt-get update -qy
apt-get upgrade -qy
apt-get install build-essential -qy
#We cannot run the next command we need to copy the code using vi so we will install vi only in our test case
apt-get install vim -qy
#Copy the contents of hello.c into file named hello.c
#COPY hello.c .
# Build binary saved as a.out
gcc -o hello hello.c

Let’s check if hello has dependancies on dynamic linked libraries:

root@917d6b3c9ea9:/# ldd hello
	linux-vdso.so.1 (0x00007ffc35dbe000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa76c376000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fa76c969000)

As you can see it has dynamically linked libraries those will not work in scratch because they will not exist. Lets static link them using this command:

gcc -o hello -static hello.c
root@917d6b3c9ea9:/# ldd hello
	not a dynamic executable

As you can see making sure we are not dynamically linking executables is critical. Now we know we have a working builder we can just take the executable and copy it to the scratch container for a very small container job. As you can see this process could be used to make very fast acting functions as a service on demand.

FROM scratch
# Copy our static executable.
COPY --from=builder hello /
# Run the hello binary.
ENTRYPOINT ["/hello"]

This takes the hello binary from the builder and puts it into our final image. Put them together in a single Dockerfile like this:

FROM ubuntu:latest AS builder
# Install gcc
RUN apt-get update -qy
RUN apt-get upgrade -qy
RUN apt-get install build-essential -qy
#COPY the hello.c file from OS
COPY hello.c .
# Build the binary.
RUN gcc -o hello -static hello.c
FROM scratch
# Copy our static executable.
COPY --from=builder hello /
# Run the hello binary.
ENTRYPOINT ["/hello"]

Build the container which we will call csample using this command:

docker build -t csample .

Sending build context to Docker daemon  3.584kB
Step 1/9 : FROM ubuntu:latest AS builder
 ---> 7698f282e524
Step 2/9 : RUN apt-get update -qy
 ---> Using cache
 ---> 04915027a821
Step 3/9 : RUN apt-get upgrade -qy
 ---> Using cache
 ---> 998ea043503f
Step 4/9 : RUN apt-get install build-essential -qy
 ---> Using cache
 ---> e8e3631eaba6
Step 5/9 : COPY hello.c .
 ---> Using cache
 ---> 406ad6aafe8f
Step 6/9 : RUN gcc -o hello -static hello.c
 ---> Using cache
 ---> 3ebd38451f71
Step 7/9 : FROM scratch
 ---> 
Step 8/9 : COPY --from=builder hello /
 ---> Using cache
 ---> 8e1bcbc0d012
Step 9/9 : ENTRYPOINT ["/hello"]
 ---> Using cache
 ---> 5beac5519b31
Successfully built 5beac5519b31
Successfully tagged csample:latest

Try starting csample with docker:

docker run csample
Hello from Docker

As you can see we have now used a container to build the executable for our container.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.