How to make a very, very, small docker image for dynamic programs.

I digging in the docker, and I found docker image containers too bigger than they need to be.

If you want a really small and functional docker image, look at "alpine", it uses actually 5.242MB, and has a bash and package manager. I really appreciate this image.

Anyway, I'm just too curious to stay on others job, I need to do it from myself, understand, and then I can accept that others has a better solution and just use it.

In this way, I did started a research to make my own basic functional image.

If you just want a basic structure to use docker with low space usage, use "docker run -ti alpine sh" and be happy. if you, as me, feel the need to learn more, keep reading.

The docker documentation show us how to create a image from scratch. http://docs.docker.com/articles/baseimages/#creating-a-simple-base-image-using-scratch

It's just

FROM scratch
COPY true-asm /true
CMD ["/true"]

If you have a statically linked program, it's all you need to know. Just copy your program and set it in CMD. A nice sample is here in this article/tutorial to create a "go" web server: http://blog.xebia.com/2014/07/04/create-the-smallest-possible-docker-container/

But what about a really clean docker image for dynamic linked programs? what about run bash in a smaller possible docker? Well, for it we need to learn/remember some concepts.

To avoid keep this post to big, you can learn this concepts here: http://www.ibm.com/developerworks/library/l-lpic1-v3-102-3/l-lpic1-v3-102-3-pdf.pdf

In theory, we just need to detect all the dependencies using "ld.so --list" and copy it to our scratch docker.

Let's do a docker image to run /bin/bash:

create a clean directory called docker_bash and enter it.

mkdir docker_bash;
cd docker_bash;

Let's create a Dockerfile to build our image, and see how much space we use:

echo "FROM scratch" > Dockerfile
docker build -t docker_bash .
Sending build context to Docker daemon 2.048 kB
Sending build context to Docker daemon 
Step 0 : FROM scratch
 ---> 
Step 1 : CMD /bin/bash
 ---> Running in 61af7327eb2e
 ---> aeb4860259ea
Removing intermediate container 61af7327eb2e
Successfully built aeb4860259ea
Now we can see that we have a empty image result zero bytes:
$ docker images  |grep docker_bash
docker_bash             latest              aeb4860259ea        8 seconds ago       0 B

My intent is to run a dynamic linked program (bash), so what my dependencies ? the ld.so can say it. To show what is your ld.so, just use:

$ ls /lib/ld-* -l
-rwxr-xr-x 1 root root 163500 Abr 10  2013 /lib/ld-2.17.so
lrwxrwxrwx 1 root root     10 Abr  8  2013 /lib/ld-linux-x86-64.so.2 -> ld-2.17.so
$ /lib/ld-2.17.so --list /bin/bash
        linux-vdso.so.1 (0x00007ffcb03ba000)
        libreadline.so.6 => /lib64/libreadline.so.6 (0x00007fbee994c000)
        libhistory.so.6 => /lib64/libhistory.so.6 (0x00007fbee9744000)
        libncursesw.so.5 => /lib64/libncursesw.so.5 (0x00007fbee94e1000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007fbee92dd000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fbee8f30000)
        /lib64/ld-linux-x86-64.so.2 => /lib/ld-2.17.so (0x00007fbee9ba8000)

Here you are, all your dependencies. You can do the same in each lib listed to ensure it has no others dependencies. I'm not sure "ld.so --list" show recursive dependencies, I don't think so. But for bash is just these.

The COPY command in Dockerfile just copy from current directory and below, so we need to copy those deps to current directory structure:

$ mkdir {lib{,64},bin}
$ cp /lib64/ld-2.17.so lib64/
$ ln -sv /lib64/ld-2.17.so lib64/ld-linux-x86-64.so.2
“lib64/ld-linux-x86-64.so.2” -> “/lib64/ld-2.17.so”
$ cp /lib64/libc.so.6 lib64/
$ cp /lib64/libdl.so.2 lib64/
$ cp /lib64/libncursesw.so.5 lib64/
$ cp /lib64/libhistory.so.6 lib64/
$ cp /lib64/libreadline.so.6 lib64/
$ cp /bin/bash bin/

Note the symlink was created to our root, but in our image it's correct.

We can use chroot to test our structure:

$ sudo chroot $PWD/ /lib64/ld-linux-x86-64.so.2 
Usage: ld.so [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...]
You have invoked `ld.so', the helper program for shared library executables.
This program usually lives in the file `/lib/ld.so', and special directives
in executable files using ELF shared libraries tell the system's program
loader to load the helper program from this file.  This helper program loads
the shared libraries needed by the program executable, prepares the program
to run, and runs it.  You may invoke this helper program directly from the
command line to load and run an ELF executable file; this is like executing
that file itself, but always uses this helper program from the file you
specified, instead of the helper program file specified in the executable
file you run.  This is mostly of use for maintainers to test new versions
of this helper program; chances are you did not intend to run this program.

  --list                list all dependencies and how they are resolved
  --verify              verify that given object really is a dynamically linked
                        object we can handle
  --inhibit-cache       Do not use /etc/ld.so.cache
  --library-path PATH   use given PATH instead of content of the environment
                        variable LD_LIBRARY_PATH
  --inhibit-rpath LIST  ignore RUNPATH and RPATH information in object names
                        in LIST
  --audit LIST          use objects named in LIST as auditors

ld.so is static and worked in chroot. What about bash ?

$ sudo chroot $PWD/ /bin/bash
I have no name!:/$ 

You can play with bash bultin commands. When you ready to continue, just exit.

Time to build our image and see how it works.

$ cat >Dockerfile<<EOF
FROM scratch
COPY . /
CMD /bin/bash
EOF
$ docker build -t "docker_bash" .
Sending build context to Docker daemon 3.808 MB
Sending build context to Docker daemon 
Step 0 : FROM scratch
 ---> 
Step 1 : COPY . /
 ---> 8539cef0990d
Removing intermediate container 937d46fb9fa5
Step 2 : CMD /bin/bash
 ---> Running in a95d91c15f87
 ---> 259060ab8e92
Removing intermediate container a95d91c15f87
Successfully built 259060ab8e92
$ docker images  |grep docker_bash
docker_bash             latest              259060ab8e92        About a minute ago   3.799 MB

Our image has about 4MB. runnable ?

$ docker run -ti docker_bash
exec: "/bin/sh": stat /bin/sh: no such file or directory
FATA[0000] Error response from daemon: Cannot start container 8f3ced2cc7fd7c68b9afbf8f884f89ded8342ef58e576baccdc2be72782d5011: [8] System error: exec: "/bin/sh": stat /bin/sh: no such file or directory

the ENTRYPOINT is defalt to /bin/sh (which we don't have) so let's replace it:

$ docker run -ti --entrypoint /bin/bash docker_bash
bash-4.2#

To be more complete, let's replace the ENTRYPOINT on Dockerfile to use ld.so, my final Dockerfile is:

$ cat Dockerfile 
FROM scratch
COPY . /
ENTRYPOINT [ "/lib64/ld-2.17.so" ]
CMD [ "/bin/bash" ]
this way you can build and run it:
$ docker build -t "docker_bash" .
Sending build context to Docker daemon 3.808 MB
Sending build context to Docker daemon 
Step 0 : FROM scratch
 ---> 
Step 1 : COPY . /
 ---> 7f4d4645dacc
Removing intermediate container d44cea18c5e1
Step 2 : ENTRYPOINT /lib64/ld-2.17.so
 ---> Running in fd0b71a445e9
 ---> d8655fc72a5b
Removing intermediate container fd0b71a445e9
Step 3 : CMD /bin/bash
 ---> Running in 7bda8673eb93
 ---> 057518a04f80
Removing intermediate container 7bda8673eb93
Successfully built 057518a04f80
$ docker run -ti docker_bash
bash-4.2# 

Note: The ENTRYPOINT and CMD commands in Dockerfile accepts both syntax "ENTRYPOINT /lib64/ld-2.17.so" and "ENTRYPOINT [ "/lib64/ld-2.17.so" ]" in Dockerfile but in the first the command are prepended with "/bin/sh -c". Ref: http://kimh.github.io/blog/en/docker/gotchas-in-writing-dockerfile-en/

But I just did it!

I hope this post can be useful for anyone. (let me know).

Postar um comentário

Postagens mais visitadas