by October 12, 2017
onOver the last couple of weeks I have been developing an erlang application called statser that is basically a drop-in replacement of the graphite metrics server. Now that I have established a stable version that supports most of the features I initially aimed for, I want to build a docker image for statser.
My first goal was to get something working first and iterate on proper automation and reasonable size of the resulting image after that.
So my first attempt was based on CentOS 6:
FROM centos:6
# erlang
RUN yum update -y && \
# install the build dependencies for erlang itself
yum install -y wget git gcc-c++ unzip && \
# install the erlang package from erlang-solutions
yum install -y http://packages.erlang-solutions.com/erlang-solutions-1.0-1.noarch.rpm && \
yum install -y erlang && yum clean all
# statser release
RUN wget https://github.com/kongo2002/statser/archive/master.zip && \
unzip master.zip && rm master.zip && \
cd statser-master && \
# fetch rebar3
wget https://s3.amazonaws.com/rebar3/rebar3 && chmod +x rebar3 && \
./rebar3 compile
EXPOSE 2003 8080 8125/udp
WORKDIR /statser-master
CMD ./start.sh
The Dockerfile
described above first installs the latest erlang distribution, fetches the master
version of “statser” from github and starts the build by using rebar3
(which is downloaded as well) after that.
The resulting image works fine but results in a rather large image:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
kongo2002/centos-statser latest fd9de4025ce3 2 days ago 818 MB
My next attempt is using a minimalistic base image using alpine linux. I have used that base image already before and it serves really well to produce small docker images.
At first we will need a base image that contains a working erlang build and runtime environment:
FROM alpine:3.6
# install relang distribution from source
RUN apk add --no-cache build-base autoconf openssl git openssl-dev && \
wget http://erlang.org/download/otp_src_20.1.tar.gz && \
tar xvf otp_src_20.1.tar.gz && rm otp_src_20.1.tar.gz && \
cd otp_src_20.1 && \
./otp_build autoconf && \
./configure --disable-hipe --without-termcap --without-javac && \
make -j8 && make install && \
cd .. && rm -rf otp_src_20.1
# install rebar3
ADD https://s3.amazonaws.com/rebar3/rebar3 /usr/bin/rebar3
RUN chmod a+x /usr/bin/rebar3
# install post-build triggers for depending images
ONBUILD COPY src src
# prepare build invocation
CMD rebar3 as production release -o /build
This docker image will serve as our stable erlang base image (in this case using erlang OTP 20.1). Additionally we installed ONBUILD
triggers (see docker documentation) that allow us to create new build images containing the “statser” sources we actually want to create a docker image of.
Next we will create another docker image that actually contains the “statser” sources to package a release of:
Pretty simple, right? The trick is actually the building of the image that looks somewhat like the following:
$ docker build -t kongo2002/alpine-statser-build --file alpine-statser-build-base/Dockerfile src
Now that we built a new image based on the kongo2002/alpine-statser-build-base
we just created before the build procedure will execute the ONBUILD
triggers and copy the src
files into the working directory of the new image.
Starting a new container of that image will build the actual “statser” release and put the build results in a mounted volume:
$ docker run --rm -v "$PWD/build:/build" kongo2002/alpine-statser-build
As soon as the container terminated with success we should have an erlang release of “statser” in the /build
folder.
Now all we have to do is to build a new “runtime” container that just contains the release files from the previous step:
FROM alpine:3.6
# jiffy runtime dependency
RUN apk add --no-cache libstdc++
COPY build/statser /statser
EXPOSE 2003
EXPOSE 8080
EXPOSE 8125/udp
CMD ["/statser/bin/statser", "foreground"]
That’s it! The reward for those few steps is a docker image that is much smaller with a total size of 37 MB:
REPOSITORY TAG IMAGE ID CREATED SIZE
kongo2002/statser latest 6931953373f6 56 minutes ago 36.7 MB
And it even works as well :D
$ docker run -it kongo2002/statser
Exec: /statser/erts-9.1/bin/erlexec -noshell -noinput +Bd -boot /statser/releases/1.0.0/statser -mode embedded -boot_var ERTS_LIB_DIR /statser/lib -config /statser/releases/1.0.0/sys.config -args_file /statser/releases/1.0.0/vm.args -pa -- foreground
Root: /statser
/statser
21:28:03.004 [info] Application lager started on node statser@325f4ca71a44
21:28:03.004 [info] initial load of configuration
21:28:03.005 [info] start listening for API requests on port 8080
21:28:03.005 [info] start listening for metrics on port 2003
21:28:03.005 [info] starting health service with update interval of 60 sec
21:28:03.005 [info] starting instrumentation service at <0.489.0>
21:28:03.005 [info] starting rate limiter [create_limiter] with limit 10/sec
21:28:03.005 [info] preparing instrumentation service timer with interval of 60000 ms
21:28:03.005 [info] starting rate limiter [update_limiter] with limit 500/sec
21:28:03.005 [info] Application statser started on node statser@325f4ca71a44
...
The steps described above might seem intimidating at first but after you understood the basic mechanism of ONBUILD
triggers it is actually pretty straightforward. In fact the whole process can be “automated” in a simple script file in about 6 lines (without comments).
Please keep in mind I just finished this process and there may still be some flaws or ways to improve these steps for sure. Anyways, I am pretty satisfied so far - the next step will be to integrate this (or a similar mechanism) into my github build/publish.