Saturday, October 24, 2015

create a slim and lean docker image for your go/golang application

all well and dandy - we have our app, it is running inside a docker image - yet there is still something we could do better.

hit 'docker images' and take a look at the output

REPOSITORY   TAG        IMAGE ID       CREATED         VIRTUAL SIZE
deligo       latest     11e4c299c1e0   39 minutes ago  755.8 MB
anything bothering you? this 755MB for the image kindda bothered ME.
so much space for an executable, which is less then 1% of this.
lets see how we can improve this

of course, this problem is not something new and many have looked and found solution that satisfies their case. in most cases the solution was using an image of an extremely slim linux distribution - alpine linux

it comes with only about 5 megabytes and through its packaging system - apk - allows you to only install packages you need

now, in my original Dockerfile i was actually building the executable inside the container, but we don't really need this right now, do we? in many cases it would probably make sense - thus the image will be able to grab latest code and build and run the latest executable, but on the other hand you may not wish to have your source code out, even in a secure environment like a docker image.

so, we will just ADD our executable to docker and it will run it away.
here is one of the benefits of compiled go executables - the practically have no dependencies (alright, this is only in my silly case, but i think this is also pointed out by golang dev community as well).

so lets do this.

if we sticked with building our app inside the image we'd have put in the Dockerfile something like

RUN apk add --update go && \
    rm /var/cache/apk/*
but we won't, so we won't: we're are only adding our executable with an ADD command

ADD deligo /go/bin/

and setting the entrypoint

ENTRYPOINT /go/bin/deligo

build and run away!
by the way - here are two useful switches to docker build cmd:
--no-cache=true
--rm
no-cache is especially useful when your executable has changed. i had some problems until realized that docker uses a cache for each step when building the image - which is good, but this cache doesn't seem too smart if it cannot realize a file has changed. so now i use this cmd arg for every build
rm of course removes not needed working data created during the build, and i believe essentially not needed

so, we built the image, we start it with docker run (don't forget to supply -p 8080:8088 or whatever, if your app has IO over ports), and then...
well in my case it was something like
deligo not found
but how it may not be found when i see it in the folder on the image and i can even sh to the image and run the file and..not found!? oh

so while we copied our executable to the image, there still seem to be some dependencies to syslibs which our slim alpine linux doesn't include. we can probably research which and add them, but this will inevitably make our image bigger. may be lets just sigh heavily and do it - include the whole go build chain etc - but turns out this might not even work, because at least for me alpine's
apk --update add go
only added go v1.4, even though i see go v 1.5.1 at their packages site. i found some workarounds for this, but none worked for me.
yet i found an excellent tip on codeship.com 's site, on how to build our go app with all libs statically linked into the executable:
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o deligo .
further in the article there were some discussions on SSL which i didn't bother to read, instead hurrying to build my app with the tip above and rebuild the docker image, and start it and...yes, it runs!

actually, no - it doesn't :( when my app tries to read/write through the ports, some mysterious message was dumped:
x509: failed to load system roots and no roots provided
what?

nicely, the first search result was this - https://github.com/docker/docker/issues/3825, and mentioning SSL, i remembered the text on codeship's site and so quickly switched to it, to find the final solution: you need to provide certificates to your app, in order to satisfy go’s x509 library
ADD ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
phew
(by the way - is it safe giving out certificates like that, any ideas?)

with this done - the rest is safe sails, and favorable winds...

yes, btw - our docker image got from 750MB to only 15 (15!!!!!) MB!!!
not too bad, eh?


No comments :