Docker – optymalizacja obrazu z wykorzystaniem „Multi-Stage Builds”
Co jest ważne przy budowanie kontenerów w Dockerze? Oczywiście rozmiar ich obrazów. Zawsze chcemy, aby obraz na którym zbudujemy kontener zajmował jak najmniej miejsca i był przy tym funkcjonalny. Wyobraźmy sobie jednak sytuację w której chcemy uruchomić w kontenerze naszą aplikację napisaną np. w Go. Do tego celu oczywiście wykorzystamy oficjalny obraz golang:latest
. Powiedzmy, że nasz Dockerfile
wygląda tak:
# obraz z którego wychodzimy FROM golang:latest # budujący MAINTAINER Daniel Wyrzyński # ustawiamy katalog roboczy i kopiujemy naszą aplikacje WORKDIR /go/zrodla/moje_aplikacje COPY aplikacja.go . # budujemy aplikacje RUN go build -o aplikacja . # uruchamiany naszą aplikacje CMD ["./aplikacja"]
Natomiast nasz plik aplikacja.go
, to prosty „Hello World!”:
package main import "fmt" func main() { fmt.Println("Witaj czytelniku rm-rf.blog") }
Zbudujmy kontener:
[rm-rf@localhost golang]# docker build -t aplikacja:latest . Sending build context to Docker daemon 3.072kB Step 1/6 : FROM golang:latest ---> 138bd936fa29 Step 2/6 : MAINTAINER Daniel Wyrzyński ---> Running in 35452f65a6f4 Removing intermediate container 35452f65a6f4 ---> a6aca6f3bf66 Step 3/6 : WORKDIR /go/zrodla/moje_aplikacje Removing intermediate container 498f7de6e43e ---> 620674789c3d Step 4/6 : COPY aplikacja.go . ---> c763638f7306 Step 5/6 : RUN go build -o aplikacja . ---> Running in 66849812efac Removing intermediate container 66849812efac ---> 57218db73b79 Step 6/6 : CMD ["./aplikacja"] ---> Running in b65656d7ae53 Removing intermediate container b65656d7ae53 ---> 98ee9166994d Successfully built 98ee9166994d Successfully tagged aplikacja:latest
I w sumie osiągnęliśmy to co chcieliśmy, mamy kontener w którym uruchomiona będzie nasza aplikacja,więc w czym problem? Tutaj z odpowiedzią przychodzi nam ta informacja:
[rm-rf@localhost golang]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
aplikacja latest 98ee9166994d 19 seconds ago 735MB
Nasz obraz do malutkiej aplikacji, którą chcieliśmy testować zajmuję całe 735MB. Skąd taki rozmiar? stąd, że obraz golang
jest przygotowany w taki sposób, aby znajdowały się w nim wszystkie komponenty potrzebne do budowania naszych projektów. A więc pełny toolchain, który potrzebujemy tak naprawdę tylko do zbudowania naszego kodu, a to wszystko swoje „waży” i niekoniecznie jest nam dalej potrzebne. Chcielibyśmy, żeby sama aplikacja została zbudowana w golang
a następnie uruchomiona w innym małym kontenerze jak alpine
. Do niedawna radzono sobie z tym w różny sposób, budowało się kontener np. z golang
z wystawionym wolumenem w którym była nasza aplikacją, następnie można było ją przerzucić do następnego kontenera, który chcieliśmy budować. W efekcie, potrzebowaliśmy dwa oddzielne pliki Dockerfil
, a jeśli chcieliśmy to sobie zautomatyzować to dodatkowo skrypt w bashu.
I tutaj z pomocą przychodzi nam Multi Stage Builds który jest dostępny od wersji 17.05+. Dzięki temu cały proces zmieścimy tylko w jednym Dockerfile
. Spójrzmy jak taki plik wygląda tym razem:
# obraz do zbudowania aplikacji FROM golang:latest # budujący MAINTAINER Daniel Wyrzyński # ustawiamy katalog roboczy i kopiujemy naszą aplikacje WORKDIR /go/zrodla/moje_aplikacje COPY aplikacja.go . # budujemy aplikacje RUN go build -o aplikacja . # obraz w którym będziemy testować naszą aplikację FROM alpine:latest RUN apk --no-cache add ca-certificates # katalog dla aplikacji WORKDIR /appki/ # Kopiujemy naszą zbudowaną aplikację z kontenera golang COPY --from=0 /go/zrodla/moje_aplikacje/aplikacja . # uruchamiamy aplikacje w kontenerze zbudowanym na Alpine CMD ["./aplikacja"]
Jak widzimy, możemy bez problemu użyć 2x FROM
, co wcześniej było niemożliwe. Do tego mamy możliwość skopiowania pliku bezpośrednio z pierwszego kontenera który budowaliśmy czyli:
COPY --from=0 /go/zrodla/moje_aplikacje/aplikacja .
Pojawiło się --from=0
co oznacza, że plik będzie kopiowany z kontenera 0
, czyli pierwszego użytego FROM
. Możemy też nazwać budowany kontener np:
FROM golang:latest as budowniczy
Dzięki temu w COPY
możemy podmienić 0
na tą nazwę:
COPY --from=budowniczy /go/zrodla/moje_aplikacje/aplikacja .
Zbudujmy ponownie nasz kontener:
[rm-rf@localhost aplikacja]# docker build -t aplikacja:latest . Sending build context to Docker daemon 3.584kB Step 1/10 : FROM golang:latest ---> 138bd936fa29 Step 2/10 : MAINTAINER Daniel Wyrzyński ---> Running in e709211cb8ca Removing intermediate container e709211cb8ca ---> a18e75d29133 Step 3/10 : WORKDIR /go/zrodla/moje_aplikacje Removing intermediate container d5c67db01b18 ---> 50d3c37d437e Step 4/10 : COPY aplikacja.go . ---> e9f0cc3cb231 Step 5/10 : RUN go build -o aplikacja . ---> Running in b92b731b4aee Removing intermediate container b92b731b4aee ---> 03636503fc66 Step 6/10 : FROM alpine:latest latest: Pulling from library/alpine ff3a5c916c92: Pull complete Digest: sha256:7df6db5aa61ae9480f52f0b3a06a140ab98d427f86d8d5de0bedab9b8df6b1c0 Status: Downloaded newer image for alpine:latest ---> 3fd9065eaf02 Step 7/10 : RUN apk --no-cache add ca-certificates ---> Running in eb2e7a108680 fetch http://dl-cdn.alpinelinux.org/alpine/v3.7/main/x86_64/APKINDEX.tar.gz fetch http://dl-cdn.alpinelinux.org/alpine/v3.7/community/x86_64/APKINDEX.tar.gz (1/1) Installing ca-certificates (20171114-r0) Executing busybox-1.27.2-r7.trigger Executing ca-certificates-20171114-r0.trigger OK: 5 MiB in 12 packages Removing intermediate container eb2e7a108680 ---> 9ead9d0fb133 Step 8/10 : WORKDIR /appki/ Removing intermediate container 960390685c7c ---> c3be234375f9 Step 9/10 : COPY --from=0 /go/zrodla/moje_aplikacje/aplikacja . ---> 500e024d061b Step 10/10 : CMD ["./aplikacja"] ---> Running in 0f3c3d917f86 Removing intermediate container 0f3c3d917f86 ---> 0d6b033d7e08 Successfully built 0d6b033d7e08 Successfully tagged aplikacja:latest
Jak teraz wygląda nasza kontener?
[rm-rf@localhost aplikacja]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
aplikacja latest 0d6b033d7e08 7 seconds ago 6.54MB
Oprócz tego, że nasz obraz zajmuje 6.54MB to dodatkowo cały „przepis” mamy tylko w jednym pliku Dockerfile
, a aplikacja sama trafia do malutkiego kontenera Alpine. Dzięki temu mamy dużą przejrzystość. Wcześniej potrzebowaliśmy dwóch Dockerfile
(jeden dla golang, drugi dla alpine) do tego jeśli chcieliśmy to sobie zautomatyzować, potrzebny był plik ze skryptem w bashu 🙂
Z automatu od kiedy funkcja Multi-Stage Builds pojawiła się w Dockerze trafiła do zasad best practice budowania kontenerów. Myślę, że jest to całkowicie zrozumiałe i nie trzeba nikogo przekonywać jak bardzo jest użyteczna.
Gorąco zachęcam do wprowadzenie jej w życie każdemu entuzjaście Dockera i testowania w ten sposób budowanych kontenerów 🙂