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?
How big is the actual release tar file you copy onto the image? The
reason I am asking is that I built alpine from scratch with our erlang
release and it was 18MB where 2/3 (if I remember correctly) was our release.
Normally you don’t. You can exclude debug information which will make it
much smaller (half the size?). Without debug information you cannot
do things like tracing and other useful introspection in production. But
if you need the smallest available this is an option.
Generally the release size is the size your image will be + 5MB or so
for alpine.
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:
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.
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.
Yes, it didn’t have many dependencies. My point was that the
release is what should take up the most space. You cannot get the docker image
smaller than the release and that the alpine linux image should have
very little overhead in terms of space.
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.
Agreed. I don’t think the trade-off is worth it either but if you want
the smallest image possible you should know about the option.
We generally encrypt the debug info (we ship our software to clients).
In this case you need the key to be able to do any debugging but it
makes it a little bit harder to reconstruct the source code.
Upgrading the base image to bitwalker/alpine-elixir:1.4.5 (with erlang 20.0.1 underneath) shaved off another 12 Mb down to 48 Mb. After looking at the size stats, the shrink is due to erts loosing some weight:
docker run -it gmile/ritm:0.0.10 du /app -h -d 1
21.1M /app/erts-9.0.1
52.0K /app/bin
380.0K /app/releases
22.2M /app/lib
43.7M /app
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.
Interesting, so maybe on Alpine it’s statically linked? I’ll have to review how I’m compiling Erlang in the base image, maybe I have some options set that aren’t needed.
Most images I usually deploy are +500MB… (not my fault, but many people tend to just take Ubuntu as a base image and add a lot of unneeded crap to it…)
Congrats for trying out multi-stage, it’s a really nice feature I’ll start to use once our CI is running the latest Docker version.
My own alpine image built through edib with a bit of tweaking are around 23 to 40 MB depending on js and assets most of the time. With few assets, 23 MB is a classic.
~20 MB will be the lower boundary I think (without stripping or zipping the source code; empty hello world project).
But I also think this is okay enough and mostly better than 500MB to 1GB sized images I currently see floating around in Docker based setups of some companies.
Yeah I dug around in my alpine-erlang build today, it looks like the bulk of the extra space in beam.smp is in the .debug_loc and .debug_info sections of the binary (~11mb) - so I assume one could strip those easily enough, but whether that’s ideal or not is a different question I suppose.