Skip to content

erlang/docker-erlang-example

Repository files navigation

About

This file contains a step-by-step guide that demonstrates how one can create a compact docker image containing a small service written in Erlang. More complex docker examples can be found in the advanced_examples folder.

Create a Docker Image Using Alpine Linux

In this example we create a docker image containing a small Erlang application.

We use the following Dockerfile, containing two build stages:

# Build stage 0
FROM erlang:alpine

# Set working directory
RUN mkdir /buildroot
WORKDIR /buildroot

# Copy our Erlang test application
COPY dockerwatch dockerwatch

# And build the release
WORKDIR dockerwatch
RUN rebar3 as prod release

# Build stage 1
FROM alpine

# Install some libs
RUN apk add --no-cache openssl && \
    apk add --no-cache ncurses-libs && \
    apk add --no-cache libstdc++

# Install the released application
COPY --from=0 /buildroot/dockerwatch/_build/prod/rel/dockerwatch /dockerwatch

# Expose relevant ports
EXPOSE 8080
EXPOSE 8443

CMD ["/dockerwatch/bin/dockerwatch", "foreground"]

The image is built with the following command:

$ docker build -t erlang-dockerwatch .

Notice that if you need a proxy to access the internet, you must forward your proxy settings to docker, for example by giving the option --build-arg https_proxy=$HTTP_PROXY to docker build.

This is what happens:

1. Build stage 0: build

This step starts from the official erlang docker image based on alpine linux. So with the base image, a full Erlang/OTP installation and rebar3 already exists.

Our Erlang application is found in the dockerwatch directory on our local filesystem, and we use the COPY command to import this complete directory into the current working directory in the image.

Finally, we release our erlang application using rebar3 and the following rebar.config.

{deps, [{jsone,  "1.4.7"},   %% JSON Encode/Decode
        {cowboy, "2.5.0"}]}. %% HTTP Server

{relx, [{release, {dockerwatch, "1.0.0"}, [dockerwatch]},
        {vm_args, "config/vm.args"},
        {sys_config, "config/sys.config"},
        {dev_mode, true},
        {include_erts, false},
        {extended_start_script, true}
    ]}.

{profiles, [{prod, [{relx, [{dev_mode, false},
                            {include_erts, true},
                            {include_src, false}]}]}
           ]}.

2. Build stage 1: create a minimal docker image

Now that our application is released, we can discard all extras that was needed for compilation. That is, we no longer need rebar3 or the full Erlang/OTP installation, since our release already contains the required parts of Erlang/OTP, including the runtime system. So, we start a new build stage from the apline linux docker image.

Using the COPY command with option --from=0, we specify that we want to copy artifacts from build stage 0 into the current build stage. We use this to copy our released Erlang application into the final image.

We expose the relevant ports (needed by our application), and specify the command to execute when running the image.

What happened?

$ docker images
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
erlang-dockerwatch   latest              fcdd1aaa2ee7        16 seconds ago      26MB
    ...

The final docker image for the application has a size of approximately 26MB. It may be useful to know that one can get down the image size for this application even further (to approximately 13MB) by using some tricks (e.g., compiling the Erlang release without the ncurses library, stripping away debug symbols from binaries and compressing applications). An alternative docker file, which is named Dockerfile.very_small_image, illustrates how this can be done.

Generating Certificate

Generate certificates in subdirectory ssl.

$ ./create-certs

For some more details of what this command does, see README-CERTS.md

Running the Erlang Application

We start the image in a docker container by issuing the following command.

$ docker run -d -p 8443:8443 --init --volume="$PWD/ssl:/etc/ssl/certs" --log-driver=syslog erlang-dockerwatch
870f979c5b4cdb7a1ba930b020043f50fa7457bf787237706fb27eefaf5fe61d

Let's parse some of the input.

  • -d, starts the container in the background and prints the container ID.
  • -p 8443:8443, exposes port 8443 from the container to our localhost.
  • --init, use the tini initialization as PID 1 in order to prevent zombie processes.
  • --volume="$PWD/ssl:/etc/ssl/certs", mounts our local directory ($PWD/ssl) with certificates to /etc/ssl/certs the container.
  • --log-driver=syslog, will log all data from stdout in the container to our local syslog.

In /var/log/syslog we can see these entries:

Nov  7 14:55:42 elxa19vlx02 NetworkManager[1738]: <info>  [1541598942.5025] device (veth2644103): link connected
Nov  7 14:55:42 elxa19vlx02 NetworkManager[1738]: <info>  [1541598942.5026] device (docker0): link connected
Nov  7 14:55:43 elxa19vlx02 da217065cb2d[1334]: Exec: /dockerwatch/erts-10.1.1/bin/erlexec -noshell -noinput +Bd -boot /dockerwatch/releases/1.0.0/dockerwatch -mode embedded -boot_var ERTS_LIB_DIR /dockerwatch/lib -config /dockerwatch/releases/1.0.0/sys.config -args_file /dockerwatch/releases/1.0.0/vm.args -pa -- foreground
Nov  7 14:55:43 elxa19vlx02 da217065cb2d[1334]: Root: /dockerwatch
Nov  7 14:55:43 elxa19vlx02 da217065cb2d[1334]: /dockerwatch

And here is our docker container:

$ docker ps
CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS              PORTS                              NAMES
870f979c5b4c        erlang-dockerwatch   "/dockerwatch/bin/do…"   3 minutes ago       Up 2 minutes        8080/tcp, 0.0.0.0:8443->8443/tcp   nifty_heisenberg

Fetch container IP Address from container id:

$ docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 870f979c5b4c
172.17.0.2

Create a counter called cnt using https with curl:

$ curl --cacert ssl/dockerwatch-ca.pem -i -H "Content-Type: application/json" -X POST -d "" https://localhost:8443/cnt
HTTP/1.1 204 No Content
server: Cowboy
date: Wed, 22 Feb 2017 13:12:54 GMT
content-length: 0
content-type: text/html
vary: accept

Read all counters using https with curl as json:

curl --cacert ssl/dockerwatch-ca.pem -H "Accept: application/json" https://localhost:8443
["cnt"]

Read the counter cnt using https with curl as json:

curl --cacert ssl/dockerwatch-ca.pem -H "Accept: application/json" https://localhost:8443/cnt
{"cnt":0}

Increment the counter cnt using http with curl:

curl -H "Content-Type: application/json" -X POST -d '{}' http://172.17.0.2:8080/cnt

Read the counter cnt using http with curl as text:

curl -H "Accept: text/plain" http://172.17.0.2:8080/cnt
1

Increment the counter cnt by 20 using http with curl:

curl -H "Content-Type: application/json" -X POST -d '{"value":20}' http://172.17.0.2:8080/cnt

Read the counter cnt using http with curl as text:

curl -H "Accept: text/plain" http://172.17.0.2:8080/cnt
21