Un Docker = Un process
Lors de ma découverte de docker, il y a 3 ans, le message était assez clair sur le fait qu'un docker ne devait contenir qu'un process et un seul. Cependant, on ne comprend pas tout de suite tout ce que cela implique.
PID 1
En tant qu'admin sys, j'ai intégré depuis longtemps l'importance d'init, le pid 1 traditionnel sous Linux. Il m'a fallu, à l'époque, quelques jours pour comprendre tous les impacts de l'absence d'init dans un conteneur. Aujourd'hui, cette problématique est davantage connue et discutée. De nombreux workaround ont été mis en place comme par exemple l'utilisation de supervisord pour gérer les différents services ou encore plus récemment, l'introduction par Docker d'un init (tini) (nouvelle option --init) : https://medium.com/@nagarwal/an-init-system-inside-the-docker-container-3821ee233f4b
Construisant mes propres conteneurs, j'ai donc scrupuleusement séparé tous les services et surtout complètement modifié la façon de démarrer les dits services : oublié les scripts d'init qui passaient des variables d'environnment, les démons qui forkaient et qui changeaient d'utilisateur pour qu'un service ne tourne pas en root. Tout ce travail devait être reporté dans le Dockerfile.
Une philosophie assez peu respectée
En regardant, les images déposées sur le docker hub, on constate que
beaucoup d'entre elles utilisent Docker comme des mini VM, à savoir une
stack nginx/phpfpm/mariadb peut se retrouver dans un seul conteneur, le
tout gèrer au mieux par supervisord au pire par un
magnifique script shell passé en
entrypoint qui va se charger de démarrer tous les services.
Tout ceci ayant pour conséquence que la taille des conteneurs peut facilement atteindre plusieurs centaines de méga, pour exemple l'image de owncloud/server qui fait 721 MB. Dans la recherche d'une optimisation de la taille des conteneurs, Docker s'est mis à massivement utilisé Alpine Linux : https://thenewstack.io/alpine-linux-heart-docker/ , une distribution Linux très légère basée sur busybox et musl libc. Elle a été pensé pour fonctionner sans disque dur ce qui permet de l'utiliser dans des boitiers de type routeur, vpn ou firewall : https://fosdem.org/2017/schedule/event/building_a_distro_with_musl_libc/.
Musl libc a l'avantage d'être plus légère que la glibc (une comparaison est disponible sur le site officiel : http://www.etalabs.net/compare_libcs.html). Les conteneurs basés sur alpine seront donc plus légers, cependant comme cette librairie a été moins testé que la glibc, des bug inconnus peuvent être présents. Les images docker basées sur Alpine embarque donc dans le Dockerfile la recompilation de leur logiciel avec musl libc comme, par exemple, l'image officiel de nginx.
Docker FROM scratch
Pourtant, il existe une solution lorsque que notre objectif est de minimiser la taille du conteneur :
FROM scratch
Le but d'une image Docker from scratch est de créer une image vide (sans OS). On peut la créer de cette façon :
tar cv --files-from /dev/null | docker import - severine/scratch
On constatera que cette image fait bien 0 Kb en lançant un docker images :
REPOSITORY TAG IMAGE ID CREATED SIZE severine/scratch latest 0b3f13b3a072 2 weeks ago 0 B
Une application en GO
On va pouvoir utiliser cette image pour y lancer son binaire qui devra être compilé statiquement. Ce binaire embarquera alors toutes ses dépendances. C'est parfait pour une application en GO !
En reprenant cet exemple issu de http://blog.xebia.com/create-the-smallest-possible-docker-container/ :
package main import ( "fmt" "net/http" ) func helloHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello from Go in minimal Docker container") } func main() { http.HandleFunc("/", helloHandler) fmt.Println("Started, serving at 8080") err := http.ListenAndServe(":8080", nil) if err != nil { panic("ListenAndServe: " + err.Error()) } }
On compilera ce code en static :
go build -ldflags -linkmode external -extldflags -static hello.go
Et on créera notre Dockerfile :
FROM severine/scratch ADD hello /hello ENTRYPOINT ["/hello"]
Après le build, on pourra vérifier la taille de notre conteneur :
severine/hello latest 9e478eb422dd 2 weeks ago 7.05 MB
On le démarre ensuite en exposant le port :
docker run -p 8080:8080 severine/hello
Notre application est up dans un conteneur de 7 MB soit la taille du binaire.
Pour toute application qui peut être compilée en statique, on pourra la faire tourner dans son conteneur et donc de manière isolée sans avoir besoin d'embarquer tout un OS dans le conteneur. Si la compilation statique n'est pas possible, il faut se contraindre à séparer correctement tous les services et arrêter de considérer docker comme une VM.