gmile
Smallest docker container with elixir app
After docker released native support for multi-stage builds, I decided to make my own small docker container with app.
Below container is based on the one from @bitwalker. It is the smallest one I could come up with in one sitting, given quite limited knowledge of docker & distillery releases:
# Dockerfile
FROM bitwalker/alpine-elixir:1.4.4 as builder
ADD . /app
WORKDIR /app
ENV MIX_ENV=prod
RUN mix do deps.get, deps.compile, release
FROM alpine:3.6
RUN apk add --no-cache \
ca-certificates \
openssl-dev \
ncurses-dev \
unixodbc-dev \
zlib-dev
WORKDIR /app
COPY --from=builder /app/_build/prod/rel/ritm/releases/0.0.1/ritm.tar.gz /app
ENV PORT=4000
RUN tar -xzf ritm.tar.gz; rm ritm.tar.gz
CMD ["bin/ritm", "foreground"]
It’s around 64Mb when pushed to docker hub. That’s a lot, although I don’t expect this to grow significantly (more app code & deps shouldn’t contribute that much).
I am aware guys from @Nerves-Core-Team build super-small linux images with the app installed, but I am not sure what could be derived from their work in order to build my own small container with docker.
How can I reduce the size of container even further?
Most Liked
gmile
In case anyone is interested, this is the container I’m building right now. It uses:
-
alpine:3.6as a builder/runner image, Erlang 20.1.1Elixir 1.5.2
I like to build my own image instead of a pre-backed image in this case. I like to install elixir/erlang, as well as other packages, instead of compiling them. By looking at base image, I know that there’s no magic going on here.
It’s size, including my apps compiled code and dependencies is 55.5 Mb.
FROM alpine:3.6 as builder
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/community/" >> /etc/apk/repositories
RUN apk add --update \
git \
erlang=20.1.1-r0 \
elixir=1.5.2-r0 \
erlang-crypto \
erlang-parsetools \
erlang-syntax-tools \
erlang-runtime-tools
ADD . /app
WORKDIR /app
ENV MIX_ENV=prod
RUN mix do local.hex --force, \
local.rebar --force, \
deps.get, \
deps.compile, \
release
FROM alpine:3.6
RUN apk add --no-cache \
ncurses-libs \
zlib \
ca-certificates \
openssl \
bash
WORKDIR /app
COPY --from=builder /app/_build/prod/rel/my_app/releases/0.0.1/my_app.tar.gz /app
RUN tar -xzf my_app.tar.gz; rm my_app.tar.gz
I’m relying on distillery for building releases. After the container is built, the app must be started by running bin/my_app foreground in the container:
docker run my_app_image bin/my_app foreground
gmile
So, the Dockerfile I’m using has changed significantly. I’m now using two of them:
- a base one, which is updated much less frequently,
- and the apps one, which is ran every time I need to pack the app into a container.
This is how they look like:
Base:
# edenlabllc/elixir:1.5.2
FROM alpine:edge
ENV REFRESHED_AT=2017-11-23
RUN apk add --update \
erlang=20.1.7-r0 \
elixir=1.5.2-r0 \
erlang-crypto \
erlang-parsetools \
erlang-syntax-tools \
erlang-runtime-tools \
git \
make
Everyday use:
FROM edenlabllc/elixir:1.5.2 as builder
ARG APP_NAME
ARG APP_VERSION
ADD . /app
WORKDIR /app
ENV MIX_ENV=prod
RUN mix do \
local.hex --force, \
local.rebar --force, \
deps.get, \
deps.compile, \
release
FROM alpine:edge
ARG APP_NAME
ARG APP_VERSION
RUN apk add --no-cache \
ncurses-libs \
zlib \
ca-certificates \
openssl \
bash
WORKDIR /app
COPY --from=builder /app/_build/prod/rel/${APP_NAME}/releases/${APP_VERSION}/${APP_NAME}.tar.gz /app
RUN tar -xzf ${APP_NAME}.tar.gz; rm ${APP_NAME}.tar.gz
ENV REPLACE_OS_VARS=true \
APP=${APP_NAME}
CMD ./bin/${APP} foreground
In order to build this, I run:
docker build --tag "my_account/my_app:1.2.3" \
--file Dockerfile \
--build-arg APP_VERSION=1.2.3. \
--build-arg APP_NAME=my_app .
I could merge both Dockerfiles, but it just didn’t make sense to re-install the compile-time dependencies every time I needed to build the app. After all, elixir and erlang release versions much less frequently.
melpon
I think openssl package is not needed if Erlang is built with ./configure --disable-dynamic-ssl-lib ....
bitwalker
If you depend on additional system libs that can add quite a bit of weight, depending on what they are. You can also double check to see which Erlang applications are being included, it should only be the subset your application depends on, not all of them.
A medium-sized application would probably depend on enough Erlang libs to hit that limit before any code from their own application was counted. A simple test project of mine hits 23mb with just libs, and here are the ones included in the release:
asn1-5.0/
certifi-1.1.0/
combine-0.9.6/
compiler-7.1/
crypto-4.0/
elixir-1.4.5/
gettext-0.13.1/
hackney-1.8.0/
idna-4.0.0/
iex-1.4.5/
kernel-5.3/
logger-1.4.5/
metrics-1.0.1/
mimerl-1.0.2/
poison-3.1.0/
public_key-1.4.1/
sasl-3.0.4/
ssl-8.2/
ssl_verify_fun-1.1.1/
stdlib-3.4/
test-0.1.0/
timex-3.1.13/
tzdata-0.5.12/
So either your app was extremely small or didn’t depend on much - in any case, I suspect most real-world applications will be much larger than 18mb. ERTS itself is only like 6mb, and anything in priv of your application or your dependencies will also be included.
I strongly recommend not stripping debug info - those cases you mentioned are some of the most powerful capabilities of the runtime, and are one of the reasons why OTP is such a powerful tool operationally. You are trading a small amount of space for a significant amount of functionality, to me it’s not even close to worth it to sacrifice that.
bitwalker
You’d have to show what is taking up that space in the ERTS folder - on my laptop, a release packed with ERTS included only shows 5.4mb in the erts-9.0 folder via du -hs <path>. Either there is more in the ERTS folder on Alpine which can be stripped (and I can tweak that in the Alpine images I produce if so) or it’s just bigger on Linux for some reason, but I can’t imagine why that would be.







