Diversity w polskim IT
Dariusz Luber
Coders Lab
Dariusz LuberMentor / wykładowca @ Coders Lab

Jak tworzyć wieloetapowe obrazy Docker

Dowiedz się, jak tworzyć wieloetapowe obrazy Docker, budować różne wersje programu oraz tworzyć podstawowe procesy CI wewnątrz pliku Dockerfile.
1.02.20214 min
Jak tworzyć wieloetapowe obrazy Docker

Utrzymywanie wielu wersji pliku Dockerfile w projekcie na cele developmentu, testowania i produkcji, jest dość uciążliwe. Na szczęście mamy możliwość wieloetapowej budowy obrazów Docker. Wykorzystując poszczególne etapy, możemy następnie budować różne wersje programu oraz tworzyć podstawowe procesy CI wewnątrz pliku Dockerfile.

Wiele wersji pliku, czyli co nas czeka bez Multistage Dockerfile

Weźmy na tapet aplikację napisaną w React.

Jest to prosta aplikacja internetowa "Shopping List" i na jej potrzeby stworzymy dwa obrazy:

  • wersja developerska- w procesie developmentu musimy uruchomić proces do obserwowania zmian plików i przebudowy aplikacji npm start
  • wersja produkcyjna- potrzebujemy zbudować aplikację i zaserwować ją z wykorzystaniem paczki serve i uruchomienie procesu serve -s  -l 3000


Przy podejściu utrzymywania wielu plików Dockerfile, mielibyśmy takie dwa pliki:

Pierwszy plik: 

FROM node:14-alpine

RUN apk update && apk upgrade

RUN mkdir /code

WORKDIR /code

EXPOSE 3000

CMD ls && \
    npm i && \
    npm start


Drugi plik: 

FROM node:14-alpine

RUN apk update && apk upgrade

RUN npm install -g serve && \
    mkdir /code && \
    mkdir /code/tmp

COPY ./ /code/tmp

RUN cd /code/tmp && \
    npm i && \
    npm run build && \
    mv /code/tmp/build/* /code && \
    rm -r /code/tmp

WORKDIR /code

EXPOSE 3000

CMD serve -s -l 3000

Tworzenie Multistage Dockerfile

Spróbujmy teraz połączyć to w jeden plik z wieloetapową budową.

Trzeba pamiętać, że w pliku Dockerfile może być wiele sekcji FROM i każda sekcja jest wykonywana po kolei, dzięki temu możemy tworzyć obrazy pośrednie wykorzystywane przez kolejne etapy.

Jeżeli na jednym z etapów dojdzie do błędu, to kolejne kroki nie zostaną uruchomione i cały proces zostanie przerwany, więc mamy pewność, że jeżeli np. testy nie przejdą pozytywnie, to nie zostanie zbudowany nowy obraz z aplikacją.

Przeanalizujmy kolejne etapy w takim pliku:

  • Pierwszy etap 
    FROM node:14-alpine as base-image
    RUN apk update && apk upgrade && \
    npm install -g serve
    RUN mkdir /code
    WORKDIR /code


Pobieramy obraz bazowy i nazywamy ten etap jako **base-image**, dzięki czemu na kolejnych etapach będziemy mogli jako bazę wybrać to, co teraz zbudowaliśmy.

  Dodatkowo w obrazie bazowym zainstalowaliśmy niezbędne elementy i przygotowaliśmy katalog na nasz kod z aplikacją.

  • Drugi etap 
    FROM base-image as code
    COPY . /code
    RUN npm i

 
Wykorzystaliśmy poprzedni obraz i skopiowaliśmy kod naszej aplikacji, a następnie zainstalowaliśmy dla niej niezbędne pakiety. Dzięki temu na kolejnych etapach będziemy mieli przygotowany kod wraz z zależnościami oraz będziemy mieli pewność, że na każdym etapie CI będziemy wykorzystywać ten sam kod i pakiety.

  • Trzeci etap
    FROM code as dev-image
    CMD npm start

 
Tutaj przygotowujemy sobie możliwość zbudowania obrazu developerskiego - dla procesu CI jest to zbędny krok, ale - jak widać - nie jest to długi proces, bo bazuje na zbudowanym wcześniej obrazie. Dokładamy tylko odpowiednie polecenie do uruchomienia procesu przy starcie kontenera.

  • Czwarty etap
    FROM code as test-image
    ENV CI=true
    RUN npm run test


Kolejny etap to uruchomienie testów.

W przypadku aplikacji Reactowej konieczne jest dla tego procesu ustawienie zmiennej środowiskowej CI na true, aby proces się zakończył po wykonaniu testów.

  • Piąty etap
    FROM code as tmp-build
    RUN npm build


Gdy testy przeszły, pora zbudować wersję docelową w oparciu o przetestowany kod.

Robimy to w osobnym kroku, ponieważ chcemy, aby w końcowym obrazie były tylko niezbędne pliki.

  • Szósty etap
    FROM base-image as prod-image
    COPY --from=tmp-build /code/build /code
    EXPOSE 3000
    CMD serve -s -l 3000


Ostatni etap to już zbudowanie docelowego obrazu, gdzie kopiujemy z poprzedniego kroku tylko wymagane pliki, oraz ustalamy jaki proces ma się uruchomić przy starcie kontenera.

Mając tak przygotowany plik Dockerfile, mamy pewność, że przy aktualizacji procesu budowy nie przegapimy aktualizacji któregoś z plików do budowy obrazów.

Budowanie obrazu Docker z wyborem docelowego etapu

Przejdźmy teraz do procesu budowy, bo - jak zostało już wspomniane - możemy w oparciu o ten plik zbudować różne obrazy, i tak np. w celu zbudowania tylko obrazu developerskiego możemy w poleceniu docker build podać docelowy etap budowy:

 docker build --target dev-image -t shopping-list-dev 


Oczywiście w celu ułatwienia pracy w praktyce wykorzystamy np. docker-compose:

    version: '3'

    services:
    shopping-list:
        build:
        context: ..
        dockerfile: .docker-multistage/Dockerfile
        target: dev-image
        container_name: shopping-list-dev
        ports:
        - "80:3000"
        restart: always
        tty: true
        volumes:
        - ../:/code


Oczywiście, gdy będziemy chcieli uruchomić cały proces to uruchamiamy polecenie docker build już bez parametru --target.

Wieloetapowa konfiguracja Dockerfile - podsumowanie

Jak widać na powyższym przykładzie, wykorzystanie wieloetapowej konfiguracji Dockerfile pozwala w przyjemny sposób zarządzać procesem tworzenia obrazów.

Oczywiście nie jest to podejście w 100% idealne, bo niestety nie pozwala na np. równoległe uruchamianie testów w trakcie procesu CI/CD. 

W sytuacji, gdy należy uruchomić dużo testów, można rozbudować powyższy proces o podejście GitOps, czyli budowanie pipelinów w oparciu o np. akcje na Githubie, czy CI/CD na Gitlabie. To podejście z wykorzystaniem plików konfiguracyjnych yaml usprawni i zautomatyzuje procesy CI/CD. Jest to jednak osobny temat. Poruszamy go na kursie: Docker - wstęp do konteneryzacji.

Wszystkie pliki można znaleźć w repozytorium na gałęzi docker-localStorage w odpowiednich katalogach zaczynających się od .docker-.

<p>Loading...</p>