Docker – zmniejszanie rozmiaru obrazów

18 październik 2014

Od ponad miesiąca zajmuję się różnymi z eksperymentami z Dockerem, oprogramowaniem umożliwiającym banalne tworzenie całkowicie przenośnych, „jednozadaniowych” kontenerów. Każdy taki kontener to instancja gotowego obrazu, który pod maską jest union mountem, czyli w dużym uproszczeniu zbiorem systemów plików (warstw) nakładających się na siebie, widocznym dla systemu jako jeden wypadkowy.

Obrazów nie przynosi bocian – tworzone są na podstawie Dockerfile. Każda linia tego pliku jest wykonywana poprzez utworzenie tymczasowego kontenera opartego o poprzednie warstwy, wykonanie polecenia z Dockerfile i zapisanie kolejnej warstwy. Proces powtarza się aż do końca pliku, tworząc końcowy obraz. Niezmodyfikowane, tymczasowe warstwy zostaja usunięte, a pozostałe przechowywane są w charakterze cache – w przypadku uruchomienia zmodyfikowanego Dockerfile'a zregenerowane zostaną jedynie warstwy odpowiadające zmienionym liniom. Chociaż w teorii brzmi to sensownie, w praktyce wszystko zależy od tego jak został napisany Dockerfile.

FROM uggedal/alpine

RUN apk add -U python nodejs build-base
RUN addgroup -g 503 shout && \
        adduser -s /bin/false -u 503 -G shout -h /.shout -S shout

ENV VER 0.37.4
RUN npm install --production -g https://github.com/erming/shout/archive/$VER.tar.gz
RUN apk del build-base

USER shout
EXPOSE 9000
ENTRYPOINT ["shout", "--private"]

Powyższy raczej nieskomplikowany Dockerfile tworzy obraz z Shoutem, przeglądarkowym klientem IRC. Pierwsza linia pobiera obraz Alpine Linux, kolejna instaluje pakiety niezbędne do zbudowania Shouta, następna przeprowadza proces instalacji z rejestru Node.js, na koniec usuwając metapakiet build-base. Wynikowy obraz zajmuje… 187.3 MB. docker history szybko podpowiada dlaczego:

IMAGE               CREATED             CREATED BY                                      SIZE
19b70db2b483        2 weeks ago         /bin/sh -c #(nop) ENTRYPOINT [shout --private   0 B
94c9562dfe9d        2 weeks ago         /bin/sh -c #(nop) EXPOSE map[9000/tcp:{}]       0 B
af18e49996f5        2 weeks ago         /bin/sh -c #(nop) USER shout                    0 B
bf6baa2528a7        2 weeks ago         /bin/sh -c apk del build-base                   251.8 kB
0a4f4dd537ef        2 weeks ago         /bin/sh -c npm install --production -g https:   18.95 MB
4a004ac1fc0b        2 weeks ago         /bin/sh -c addgroup -g 503 shout &&  adduser    5.941 kB
394d5dec3c28        2 weeks ago         /bin/sh -c apk add -U python nodejs build-bas   162.9 MB
6551dee45f28        2 weeks ago         /bin/sh -c #(nop) ENV VER=0.37.4                0 B
517fe50895a9        10 weeks ago        /bin/sh -c #(nop) ADD file:4bb32cfcab19076d23   5.139 MB
c21551823489        10 weeks ago        /bin/sh -c #(nop) MAINTAINER Eivind Uggedal <   0 B
511136ea3c5a        16 months ago                                                       0 B

Warstwy od 511136ea3c5a do 517fe50895a9 to oryginalny obraz, na którym oparty jest ten nasz. 394d5dec3c28 to aż 163MB narzędzi potrzebnych do pomyślnej instalacji Shouta (0a4f4dd537ef). Usunięcie build-base w bf6baa2528a7 nie powoduje magicznego zmniejszenia obrazu. Tak jak napisałem w przydługawym wstępie do wpisu, każda linia Dockerfile to osobna warstwa. Spróbujmy zatem nieco zreorganizować instrukcje:

FROM uggedal/alpine

RUN addgroup -g 503 shout && \
        adduser -s /bin/false -u 503 -G shout -h /.shout -S shout

ENV VER 0.45.2

RUN apk upgrade -U && apk add python nodejs build-base && \
        npm install --production -g https://github.com/erming/shout/archive/$VER.tar.gz && \
        apk del build-base python

USER shout
EXPOSE 9000
ENTRYPOINT ["shout", "--private"]

Poza zaktualizowaniem Shouta, polecenia związane z jego instalacją zostały zbite w jedną dyrektywę RUN. W ten sposób rozmiar warstwy instalującej build-base, Shouta, a następnie robiącej porządki spadł do zaledwie 35.62 MB, a samego obrazu do 40.76 MB. Jest zatem znacznie lepiej, ale 40MB to nadal mało przyjemny plik do wrzucenia na Hub albo serwer przy moim uploadzie.

Ostatnią deską ratunku w zmniejszaniu obrazów jest spłaszczenie ich do jednej warstwy. Najłatwiej można tego dokonać poleceniem w stylu docker export <id> | docker import -. Wadą takiego rozwiązania jest utrata atrybutów takich jak PORT czy VOLUME, co powoduje więcej problemów niż jest warte zachodu. Z tego powodu powstało narzędzie docker-squash autorstwa Jonathana Wildera. Po instalacji opisanej na stronie projektu wystarczy uruchomić docker save <id> | sudo ./docker-squash -t <tag> | docker load i chwilę poczekać. W przypadku obrazu z Shoutem rozmiar spadł do 35.76 MB, który jestem skłonny uznać za akceptowalny.