Docker i docker-compose: wprowadzenie, instalacja

jaszczur.eu 4 lat temu

Docker to najpopularniejsza platforma do wirtualizacji. Jest obowiązkowym elementem wyposażenia wielu profesjonalistów i amatorów IT. Docker pozwala w prosty sposób tworzyć, współdzielić i zarządzać kontenerami, wszystko na licencji Open Source. W tym wpisie znajdziesz krótkie wprowadzenie do Dockera. Mam nadzieję iż pozwoli Ci ono zdobyć pewne intuicyjne zrozumienie tej technologii, a konkretnie poleceń docker i docker-compose. Napiszę też krótko jak zainstalować oba narzędzia pod Linuxem.

Wprowadzenie

Czym jest Docker?

Docker to platforma służąca do konteneryzacji (tzw. wirtualizacja na poziomie systemu operacyjnego). Kontener jest uruchomioną instancją obrazu Dockera, a więc, w skrócie, aplikacji przygotowanej dla tej platformy. Aplikacje działające w kontenerze czują się w nim jak na własnym, normalnym komputerze. Są dzięki temu odizolowane od innych aplikacji, działających w swoich indywidualnych kontenerach.

Kontenery komunikują się poprzez ściśle zdefiniowane interfejsy:

  • sieciowe – uruchamiając kontener podajemy porty poprzez które może się komunikować, oraz
  • system plików – kontenery mogą posiadać tzw. wolumeny, a więc fragment przestrzeni dyskowej host’a. Współdzieląc ten sam wolumen kontenery mogą się więc również komunikować.

Każdy obraz zawiera wszystkie niezbędne do funkcjonowania danej aplikacji biblioteki i zależności, we adekwatnych, przetestowanych dla danego wydania wersjach. Jest to ogromna zaleta w procesie tworzenia aplikacji i testowania. Pobieramy obraz z repozytorium, odpalamy go poleceniem docker run (czy docker-compose up) i… 90% problemów z kategorii „u mnie działało” jest wyeliminowanych. Oczywiście stanowi to też korzyść w trakcie uruchamiania i zarządzania aplikacjami „na produkcji”.

Wszystkimi kontenerami zarządza Docker Engine.

Podsumowując, najważniejsze cechy którymi ja opisałbym kontenery Docker’a to:

  • izolacja– a więc jasno definiujesz metody którymi mogą się komunikować między sobą oraz z hostem (np. wolumeny, porty sieciowe). Wszystko co jest na zewnątrz kontenera, w tym inne kontenery z tej samej aplikacji, muszą z nich korzystać w komunikacji.
  • samowystarczalność– zawierają wszystkie niezbędne do uruchomienia Twojej aplikacji zależności. W przeciwieństwie do maszyn wirtualnych Docker korzysta jednak z jądra systemu operacyjnego maszyny na której jest uruchomiony.
  • zarządzalność– możesz je uruchamiać, zatrzymywać, kasować i przenosić obrazy kontenerów. Oznacza to m.in. proste tworzenie, aktualizowanie i nadzór nad środowiskami. Daje to dużą przewagę zarówno w trakcie developmentu (aplikacje działają w przewidywalny sposób na wszystkich środowiskach – produkcja, test, development etc.). Oczywiście przydaje się też w zastosowaniach produkcyjnych, poprzez chociażby zapewnienie wysokiej dostępności dzięki Docker Swarm.
  • nietrwałość – to może oczywiste, ale wszystkie zmiany które wprowadzimy w strukturze plików kontenera zostaną stracone w momencie jego zatrzymania. Warto pamiętać, iż jeżeli chcemy wprowadzać zmiany w plikach wewnątrz kontenera, lub aby kontener mógł w trwały sposób tworzyć czy edytować pliki, to musimy skorzystać w wolumenów danych.

Jeden kontener – jedno zadanie

Z wymienionych powyżej względów kontener Dockera zwykle tworzymy w taki sposób, aby realizował jedno, podstawowe zadania. Wtedy maksymalnie wykorzystujemy zalety tej technologii. Oznacza to natomiast, iż nasza aplikacja zwykle posiada wiele kontenerów. Jeden na back-end, jeden na relacyjną bazę danych, jeden na InfluxDB, jeden na Grafanę etc.. Proces podziału aplikacji na kontenery odpowiedzialne za jedno, jasno określone zadanie określane jest jako decoupling (rozłączenie). W rezultacie, poza stworzeniem obrazu kontenera, zbudowaniem go oraz uruchomieniem będziemy musieli zarządzać także ustawieniami wielu innych kontenerów tworzących aplikację Dockerową. Do tego służy właśnie Docker Compose.

Czym są obrazy Docker’a?

Obrazy możemy tworzyć sami – dzięki pliku Dockerfile, o czym kawałek niżej, lub w sposób interaktywny, o czym na dzisiaj w ogóle . Najczęściej pobieramy gotowe obrazy z repozytorium. Warto zapamiętać, iż obraz jest definicją aplikacji, a kontener jego uruchomioną instancją. Z jednego obrazu można uruchomić wiele kontenerów (instancji).

Same obrazy zbudowane są z warstw. Każda kolejna warstwa bazuje na poprzedniej, dodając coś do niej. Przyśpiesza to znacząco proces tworzenia obrazów i zmniejsza zapotrzebowanie na przestrzeń dyskową. Dwa różne obrazy (różne aplikację) mogą mieć sporo wspólnych części (a więc warstw na których bazują). jeżeli chcesz zobaczyć ile dokładnie zajmują obrazy na Twoim systemie polecam komendę

$ docker system df -v

Tam znajdziesz rozbicie rozmiaru każdego obrazu na SIZE, SHARED SIZE i UNIQUE SIZE.

Jeżeli chcesz, możesz sprawdzić z jakich dokładnie warstw składa się dowolny obraz Docker’a, korzystając z polecenia

$ docker history nazwa_obrazu

W procesie budowania obrazu Docker musi

  1. przebudować warstwę gdzie wprowadziliśmy zmiany, np. w kodzie źródłowym naszej aplikacji, oraz
  2. przebudować wszystkie warstwy wyżej w hierarchii, a więc te które zależą od warstwy 1.

W rezultacie najlepszą praktyką aby ten proces przebiegał efektywnie jest ułożenie warstw w których najczęściej wprowadzamy zmiany możliwie najwyżej w hierarchii (tzn. jako ostatnie warstwy obrazu). O optymalizacji przeczytasz jeszcze trochę niżej, w opisie Dockerfile

W momencie uruchomienia kontenera z obrazu tworzona jest kolejna, ostatnia warstwa, tzw. warstwa kontenera. Ta warstwa istnieje tylko tak długo jak sam kontener. Wprowadzone tam zmiany zostaną zaorane w momencie jego zatrzymania, o czym wspominałem powyżej.

Repozytoria obrazów Docker’a

Tak jak wspomniałem obrazy możemy tworzyć sami lub pobrać z repozytorium. Repozytoria Docker’a działają trochę podobnie do repozytoriów git’a, składnia poleceń jest również podobna (docker pull <nazwa obrazu>, docker push <nazwa obrazu>). Pozwalają one na przechowywanie i współdzielenie obrazów

Warto podzielić repozytoria na 3 kategorie:

  • własne repozytoria, gdzie trzymamy np. obrazy nad którymi pracujemy (nasze aplikacje)
  • repozytoria firm trzecich (np. Red Hat Quay, Amazon ECR czy Google Container Registery), których celem też jest zwykle przechowywanie naszych własnych obrazów.
  • Docker Hub – oficjalne repozytorium Docker’a.

Dwa pierwsze punkty pominę, warto natomiast powiedzieć parę słów na temat Docker Hub. Znajdziemy tam ponad 100 000 obrazów, w tym wiele oficjalnych, a więc zarządzanych przez dostawców danej aplikacji. Każdy obraz posiada dokumentacją, opisującą zwykle przykładowe składnie polecenia docker run czy pliku compose. Sama lektura listy dostępnych obrazów jest bardzo ciekawa, kliknij explore aby ją przejrzeć. Można znaleźć fajne narzędzia które w kilku krokach uruchomimy i przetestujemy na własnej maszynie. Dzięki filtrom znajdziesz np. obrazy przystosowane do architektury ARM (a więc działające na np. Raspberry Pi) czy ARM64 (Raspberry Pi wersja 3 lub nowsza).

self-hosting

Właśnie dodałem nowy artykuł na temat self-hostingu. jeżeli rozpoczynasz zabawę z Dockerem, jest spora szansa iż Cię zainteresuje

Przejdź do artykułu

Co to Docker Compose?

Docker Compose to narzędzie pozwalające w prosty sposób zarządzać aplikacjami składającymi się z wielu kontenerów. Ma dwa najważniejsze elementy.

Pierwszy z nich to komenda docker-compose. Pozwala ona zarządzać stanem aplikacji Dockera – a więc zestawem wszystkich kontenerów opisanych w pliku z definicją. Tym poleceniem startujemy, zatrzymujemy czy sprawdzamy stan aplikacji, analogicznie do polecenia docker dla pojedynczego kontenera.

Drugi element to plik docker-compose.yml który definiuje wszystkie usługi oraz ich konfigurację w ramach jednej aplikacji Dockera. W zasadzie jego nazwa może być dowolna, natomiast jeżeli użyjesz właśnie docker-compose.yml1 to będziesz ją mógł pomijać korzystając z polecenia docker-compose co jest 1) wygodne 2) nie powoduje konfliktów, gdyż w folderze nadrzędnym aplikacji i tak zwykle chcemy mieć tylko jeden plik z ustawieniami Docker Compose.

1 Korekta (2022-04): docker-compose.yml wydaje się być obsługiwany, ale jest to nazwa deprecjonowana. Poprawna nazwa pliku dla docker-compose to compose.yaml (preferowane) lub compose.yml.

Plik ten (co podpowiada nam rozszerzenie) używa notacji YAML. Ciekawostka – YAML jest rekursywnym akronimem frazy „YAML Ain’t Markup Language”.

Jeżeli nie znasz notacji YAML, polecam po prostu poszukać przykładowych plików docker-compose.yml. Zobaczysz, iż notacja jest dosyć prosta – ważne są indentacje (czyli wcięcia kodu, a’la Python), składnia jest intuicyjna, listy działają tak jak w JSON.

Struktura pliku docker-compose.yml

Spójrzmy na przykładowy plik docker-compose.yml zawarty w dokumentacji obrazu Redis (baza danych klucz-wartość):

version: '2.0' services: web: build: . ports: - "5000:5000" volumes: - .:/code - logvolume01:/var/log links: - redis redis: image: redis volumes: logvolume01: {}

Oto kilka wniosków które można wyciągnąć z jego lektury.

  • Mamy trzy sekcje: versions (obowiązkowo w każdym docker-compose), services (mięso), volumes (persystencja danych)2.
  • Mamy dwie usługi, a więc w rezultacie uruchomienia tego pliku powstaną dwa kontenery: web, a więc prawdopodobnie jakaś aplikacja webowa oraz sam redis.
  • Usługa web bazuje na budowanym obrazie. Atrybut build ma wartość . (folder bieżący), oznacza to iż w folderze z plikiem docker-compose.yml znajduje się również plik Dockerfile przy pomocy którego ten obraz zbudujemy.
  • Usługa redis posiada tylko wskazany obraz (image: redis). Co to oznacza? o ile nie posiadamy zbudowanego obrazu o nazwie redis, Docker wyszuka taką nazwę w serwisie Docker Hub (o którym kilka słów poniżej). W tym konkretnie przypadku pobierze oficjalny obraz bazy danych Redis.
  • Oba kontenery są ze sobą połączone, natomiast tutaj istotna uwaga. Mechanizm, z którego tu skorzystano, oparty jest na atrybucie links. Ten atrybut jest deprecjonowany, czyli przestanie być wspierany przez którąś z kolejnych wersji Dockera. Docker zaleca korzystanie z user-defined networks, czyli w uproszczeniu polegamy na portach wystawianych przez poszczególne usługi oraz, jeżeli to konieczne, odrębnych, nazwanych sieciach wirtualnych.

Poza deprecjonowanym atrybutem uwagę zwraca też Dockerfile usługi web zlokalizowany w folderze nadrzędnym (na tym samym poziomie co docker-compose.yml). Nie jest to miejsce na analizę struktury aplikacji ale w moich projektach Dockerfile jest zawsze w podfolderze nazwanym tak samo jak usługa. W tym przykładzie miałby ścieżkę ~/web/Dockerfile a parametr build usługi web wyglądałby tak: build: ./web.

2 Poza wspomnianymi sekcjami versions, services, volumes plik docker-compose może posidać jeszcze sekcje networks, configs i secrets. Dokładną specyfikację pliku compose znajdziesz, jak zawsze, w dokumentacji Dockera.

Uruchamiać przez docker czy docker-compose?

TL;DR? Ja wolę docker-compose.

W instrukcjach do rozmaitych obrazów znajdziesz często dokładne parametry polecenia docker. jeżeli przyjrzymy się dokumentacji do aktualnej wersji oficjalnego obrazu Elasticsearch, znajdziemy tam dzisiaj następującą metodę uruchomienia:

$ docker run -d --name elasticsearch --net somenetwork -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:tag

Jeżeli potrzebujesz uruchomić Elasticsearch aby coś sprawdzić, przetestować, czy zrobić mały research, to prawdopodobnie będziesz wracał kilka razy do tego polecenia aby uruchomić je z innymi parametrami. Za każdym razem szukając poprzedniej wersji polecenia w historii powłoki (o czym pisałem tutaj -> polecenia Linux’a).

Znacznie lepiej wykorzystać plik docker-compose, choćby do uruchomienia aplikacji Docker’a składającej się z jednego kontenera. Często znajdziesz gotowego compose’a w dokumentacji obrazu, natomiast w przypadku Elasticsearch jej nie znalazłem. TBF, klikając link do dokumentacji znajdziemy bardzo fajny tutorial „Running in Production Mode” a w nim plik compose dla wielowęzłowego klastra.

Jeśli nie znajdziesz przykładowego docker-compose możesz go po prostu spróbować napisać, lub wspomóc się polecam webową aplikację Composerize która generuje plik compose z polecenia wklejonego docker run. Poniżej tak wygenerowany compose z instrukcji run dla Elasticsearch:

version: '3.3' services: elasticsearch: container_name: elasticsearch network_mode: somenetwork ports: - '9200:9200' - '9300:9300' environment: - discovery.type=single-node image: 'elasticsearch:tag'

OK ale po co to wszystko?

Przede wszystkim, posiadamy prosty dostęp do ergonomicznej edycji parametrów, korzystając ze środowisk IDE czy edytorów plików dostępnych poprzez ssh jak nano. Po drugie, pliki compose możemy zapisać, przesłać czy wersjonować przy użyciu np. git’a. Dzięki temu każda nowa wersja Twojej aplikacji może posiadać adekwatnie skonfigurowany plik compose. Dotyczy to zwłaszcza parametrów takich jak:

  • Porty
  • Wolumeny
  • Wersje obrazów
  • Zmienne środowiskowe, o których za chwilę

Zawartość pliku compose możesz wykorzystać także uruchamiając aplikację dzięki mojego ulubionego narzędzia do zarządzania kontenerami – Portainer. Tam każdy compose to tzw. stack. Na temat Portainera piszę krótko w artykule o self-hostingu.

Zmienne środowiskowe kontenera

Uruchamiając kontener możemy podać mu zmienne środowiskowe które zostaną inicjalizowane w momencie startu kontenera. W poprzedniej sekcji możemy zobaczyć jak wygląda deklaracja zmiennych środowiskowych dla Elasticsearch, zarówno dzięki komendy docker ruin jak i pliku compose (zmienna discovery.type).

Konwencja wskazuje aby nazwy zmiennych środowiskowych zawsze były pisane dużymi literami (jak widzimy Elasticsearch się jej nie trzyma ).

Dla wielu obrazów zmienne środowiskowe to podstawowy, czy często jedyny sposób ich konfiguracji. Jest to o tyle wygodne, iż zapisując plik compose, zapisujemy w zasadzie dokładną konfigurację która zostanie uruchomiona.

Często dostawcy obrazów dostarczają również przemyślane wartości domyślne zmiennych środowiskowych. Dzięki temu, możemy uruchomić standardową konfigurację złożonych aplikacji przy użyciu krótkiego polecenia. Nextcloud, którego plik compose z podstawową konfiguracją ma ~33 linie, możesz uruchomić też poleceniem

$ docker run -d -p 8080:80 nextcloud
Pliki konfiguracyjne

Oczywiście pliki konfiguracyjne do których mamy dostęp poprzez wolumen hosta też bywają w użyciu, zwłaszcza tam gdzie konfiguracja jest obszerna. Przykład takiego wykorzystania wolumenów danych znajdziemy w dokumentacji do Telegraf gdzie mapujemy plik telegraf.conf z hosta na kontener:

$ docker run -d --name=telegraf --net=influxdb -v $PWD/telegraf.conf:/etc/telegraf/telegraf.conf:ro telegraf

O samym Telegraf pisałem w artykule Wprowadzenie do InfluxDB.

Plik .env

Kiedy podawanie zmiennych środowiskowych w pliku compose przestaje być ergonomiczne możemy skorzystać z plików .env. jeżeli umieścimy plik o tej nazwie w folderze z którego uruchamiamy docker-compose, wszystkie zmienne w tym pliku zostaną zainicjalizowane po starcie kontenera.

Pliki środowiska (które maja oczywiście znacznie szersze wykorzystanie niż tylko Docker) mają bardzo prostą strukturę. Każdy wiersz ma format NAZWA=Wartość, a więc np.:

ZMIENNA_JEDEN=user ZMIENNA_DWA=1402 DB_PASSWORD=hunter2

Kiedy posiadamy tak zadeklarowane zmienne możemy po prostu wskazać w pliku compose która zmienna należy do którego kontenera, bez podawania ich wartości:

web: environment: - ZMIENNA_JEDEN db: environment: - ZMIENNA_DWA

Możemy także użyć tych zmiennych korzyzstając ze składni $ZMIENNA lub ${ZMIENNA}, nie tylko w sekcji environment:

db: image: baza:${WERSJA}

Często wykorzystujemy tą składnię aby podać tą samą zmienną środowiskową z pliku .env do różnie nazwanych zmiennych w kilku kontenerach:

db: environment: - DB_USER_PASSWORD=${DB_PASSWORD} web: environment: - DB_PASSWORD=${DB_PASSWORD}

Warto także powrócić do podanego powyżej przykładu polecenia docker run dla Telegraf. Tam, przy mapowaniu wolumenu dla pliku konfiguracyjnego, korzystamy ze zmiennej $PWD. Ta zmienna nie została dostarczona w pliku – jest to zmienna środowiskowa dostępna we wszystkich powłokach kompatybilnych z Posix podająca ścieżkę aktualnego folderu.

Wracając do .env – możesz oczywiście wskazać inną nazwę pliku ze zmiennymi środowiskowymi. Dla polecenie run będzie to parametr --env-file, a więc np. docker run --env-file secrects.env. Analogicznie w docker-compose możesz użyć sekcji usługi env_file:

web: env_file: - secrets.env

Więcej ciekawych patentów na zmienne środowiskowe znajdziesz, jak zawsze, w oficjalnej dokumentacji compose’a: Environment variables in Compose.

Wrażliwe zmienne środowiskowe

Jednym ciekawym zastosowaniem dla pliku.env jest przechowywanie w nim wrażliwych zmiennych środowiskowych (hasła do bazy danych, klucze API etc.) i wyłączenia go z kontroli wersji dzięki .gitignore.

Jest to przydatne i praktykowane w mniejszych projektach, poważniejsi użytkownicy skorzystają z dedykowanych rozwiązań, takich jak chociażby Docker Secrets, będącego częścią Docker Swarm. Swarm to narzędzie do orkiestracji, do tego tematu wrócimy parę akapitów niżej.

Instalacja

docker

W skrócie – aby zainstalować Dockera polecam skorzystać z bardzo intuicyjnej i zrozumiałej instrukcji zawartej w dokumentacji Dockera. Ja często do niej wracam, opisuje jasno procedurę dla wielu systemów operacyjnych. Czego dokładnie szukać? Chociażby tego artykułu: Install Docker Engine on Ubuntu, przynajmniej jeżeli tak jak ja pracujesz na Linuxie Mint (czyli dystrybucji bazującej na Ubuntu). Zwróć uwagę iż chcemy zainstalować Docker Engine – czyli mechanizm pozwalający korzystać z technologii Docker. Odbywa się to dzięki deamon’a dockerd czyli pracującego „w tle” procesu obsługującego rozmaite zadania. Z demonem dockerd komunikujemy się poprzez API oraz CLI.

Dla Windowsa oraz macOS Docker Engine dostępny jest w pakiecie Docker Desktop, wcześniej znanego jako Docker for Windows i Docker for Mac. Moje doświadczenia z Docker Desktop są kiepskie, chociaż dawno już nie musiałem z tego rozwiązania korzystać. o ile nie masz Linuxa polecam odpalenie wirtualnej maszyny w chmurze Google Cloud Platform, Microsoft Azure czy Amazon Web Services. O chmurze Amazona pisałem dwukrotnie, w temacie prywatnego, darmowego serwera VPN oraz zdalnego repozytorium GIT. W obu wpisach znajdziesz informacje jak stworzyć i połączyć się z instancją EC2 na chmurze Amazona.

Problem z dystrybucją Linuxa

Kiedy instalujesz Dockera na Linuxie jest jedno zastrzeżenie. Dodając repozytorium Docker’a (Set up the repository, krok 3 z instrukcji powyżej) masz wywołać następujące polecenie:

$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu (lsb_release -cs) stable

To polecenie doda dwa wiersze do pliku /etc/apt/sources.list.d/additional-repositories.list zawierającego listę dodatkowych repozytoriów. U mnie ten plik wygląda tak:

kuba@local:~$ cat /etc/apt/sources.list.d/additional-repositories.list deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic test

W obu wierszach musiałem manualnie zmienić codename dystrybucji (tutaj wartość poprawna – bionic). W moim Linuxie Mint rezultat polecenia lsb_release -cs podającego tzw. krótki codename dystrybucji to tricia. Dla tej dystrybucji Docker nie przygotował wydania, dlatego należy odszukać adekwatny codename dla dystrybucji nadrzędnej (Ubuntu). o ile tak jak ja korzystasz z Mint – sprawdź tutaj na jakim distro Ubuntu bazuje Twoja wersja. o ile chcesz zobaczyć dla jakich dystrybucji wydawany jest docker wejdź tutaj).

docker-compose

Instalacja Docker Compose jest prostsza i również polecam zapoznanie się z adekwatną instrukcją na stronie oficjalnej dokumentacji. Polecenie curl która pobiera adekwatny plik binarny na Twoją maszynę korzysta z komend uname -s i uname -m które zwracają odpowiednio nazwę kernela (u mnie Linux) oraz architektury (x86_64). A więc nie ma wspomnianego problemu z dystrybucjami.

Po instalacji

Jeżeli pracujesz na Linuxie i nie chcesz dodawać sudo dla wszystkich wywołania komendy docker i docker-compose to powinieneś zadbać o możliwość wykonywania Dockera przez zwykłego użytkownika (nie root’a). Tutaj również oficjalna dokumentacja jest bardzo pomocna. Wystarczy iż zrobisz to dla samego Dockera, Docker Compose domyślnie nie wymaga uprawnień root’a.

Zweryfikuj iż Docker jest zainstalowany poprawnie uruchamiając obraz „hello-world” :

kuba@local:~$ docker run hello-world Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world 0e03bdcc26d7: Pull complete Digest: sha256:4cf9c47f86df71d48364001ede3a4fcd85ae80ce02ebad74156906caff5378bc Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly. (...)

Teraz przetestujmy jeszcze Docker Compose. Stwórz nowy folder oraz dodaj w nim następujący plik docker-compose.yml:

version: '2.0' services: hello: image: hello-world

Jak widzisz ma on bardzo prostą strukturę, z jedną usługą (a w zasadzie aplikacją) – opartą również na obrazie testowym hello-world. Teraz możemy wywołać komendę docker-compose up (pomijamy zwykle wykorzystywany parametr -d, ponieważ 1) chcemy na bieżąco zobaczyć output kontenera hello a 2) po wydrukowaniu powitalnego tekstu aplikacja się zatrzyma):

kuba@local:~/test$ docker-compose up Creating network "test2_default" with the default driver Creating test2_hello_1 ... done Attaching to test2_hello_1 hello_1 | hello_1 | Hello from Docker! hello_1 | This message shows that your installation appears to be working correctly. (...) hello_1 | For more examples and ideas, visit: hello_1 | https://docs.docker.com/get-started/ hello_1 | test2_hello_1 exited with code 0 kuba@local:~/test$ cat docker-compose.yml

Jeżeli u Ciebie wygląda to podobnie to gratulacje! Wiem iż jest to nieco powierzchowne wprowadzenie ale mam nadzieję znalazłeś tu coś pomocnego.

Co dalej?

Dockerfile

Dockerfile o którym już wcześniej wspominałem jest sposobem na tworzenie własnych obrazów, a w zasadzie pisanie instrukcji ich tworzenia. Naturalne skojarzenie z Makefile (jeśli kiedyś miałeś tą przyjemność) jest zatem jak najbardziej trafne. W oparciu o instrukcję zawartą w Dockerfile polecenie docker build zbuduje Twój obraz który po uruchomieniu stanie się kontenerem. Nie jest to artykuł o Dockerfile natomiast wspomnę tu o dwóch jego najważniejszych aspektach – strukturze i optymalizacji.

Struktura Dockerfile

Dockerfile to instrukcja wykonania, krok-po-kroku. Spójrzmy na przykładowy plik (na marginesie, wyjęty z moje własnego projektu więc nie jest to state-of-the-art):

# pull official base image FROM python:3.8.3-alpine # set work directory WORKDIR /projekt ADD ./requirements.txt /projekt/requirements.txt RUN apk --update add --virtual build-dependencies libffi-dev openssl-dev python3-dev py-pip build-base RUN pip install -r requirements.txt ADD . /projekt # set environment variables ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 CMD ["gunicorn", "-c", "gunicorn_config.py", "run:app"]

Każda linia to jedna instrukcja którą Docker zrealizuje w trakcie budowania. Cały plik opisuje proces od skopiowania obrazu python:alpine (a więc popularnego obrazu minimalnej dystrybucji Linuxa z zainstalowanym Pythonem) do uruchomienia mojej własnej aplikacji na serwerze Gunicorn.

Jeżeli interesuje Cię tworzenie obrazów z aplikacjami Pythona to warto świadomie podejść do wyboru obrazu bazowego. Alpine jest bardzo chudy, co ma swoje zalety (o czym za chwilę) ale ma też wiele wad. Niektórzy wręcz wprost odradzają użycie Alpine do tworzenia obrazów w Pythonie.

Optymalizacja i multi-stage build

W Dockerze ważna jest optymalizacja rozmiaru obrazu (w rezultacie również czasu jego budowania). Każda instrukcja tworzy warstwę (layer) w obrazie. o ile w folderze z aplikacją wprowadzimy drobną zmianę, a folder ten skopiowaliśmy w instrukcji nr 3 z 15, to pomimo braku zależności pomiędzy tą zmianą i kolejnymi instrukcjami Docker i tak przebuduje 12 następnych warstw. Dlatego kiedy piszemy Dockerfile warto wczytać się w zagadnienie optymalizacji i wieloetapowego budowania (multi-stage build) dzięki którym oszczędzimy sobie wiele czasu i zasobów. W moim przykładowym pliku mam prostą, zdawałoby się oczywistą, optymalizację. w tej chwili fragment instrukcji realizuje następującą sekwencję:

  1. Skopiuj plik requirements.txt (wymagane biblioteki których użyłem) z folderu aplikacji
  2. Doinstaluj kilka bibliotek do Linuxa Alpine
  3. Pobierz i zainstaluje biblioteki z pliku requirements.txt
  4. Skopiuj resztę folderu aplikacji, ustaw zmienne środowiskowe, uruchom serwer Gunicorn.

Oznacz to iż o ile zmienię kod aplikacji (nie zmieniając użytych modułów) to przy budowaniu obrazu (a więc przeniesieniu wprowadzonych zmian na aplikację Dockera) zostanie wykonany tylko krok 4. Nie muszę chyba pisać iż te 3 wcześniejsze realizowane są zdecydowanie dłużej. Mój proces przed tą zmianą wyglądał tak:

  1. Skopiuj cały folder aplikacji (w tym requirements.txt)
  2. Doinstaluj, pobierz, zainstaluj etc..
  3. Ustaw zmienne i uruchom serwer Gunicorn

Jak się łatwo domyślić – najdrobniejsza zmiana w kodzie powodowała przebudowanie całego obrazu „od zera”, ponowne pobranie dziesiątek bibliotek itd.. Zmiana pozwoliła mi to skrócić czas z kilku minut do kilku sekund.

Multi-stage z kolei, to proces w którym, w dużym skrócie, nasz Dockerfile wielokrotnie używa słowa kluczowego FROM. A więc tworzymy obraz w wersji full tylko po to aby np. zbudować w nim aplikację i skopiować ją do drugiego, odchudzonego obrazu. Możemy w ten sposób pozbyć się wielu artefaktów. Gdybym przepisał swój własny plik zgodnie z podejściem multi-stage, mógłbym np. wyeliminować wszystkie zależności których użyłem do zbudowania bibliotek z finalnego obrazu. Ale to chyba zagadnienie na inny artykuł…

Orkiestracja

Tematem którego w tym wpisie nie poruszyłem jest tzw. orkiestracja (orchestration). Tak jak Docker Compose pozwala nam zarządzać aplikacjami złożonymi z wielu kontenerów uruchomionych na jednej maszynie (lub, aby być bardziej precyzyjnym, na jednym Docker Engine), tak orkiestracja jest technologią pozwalającą na zarządzanie aplikacjami Docker działającymi na wielu maszynach, w rozproszonych środowiskach. Jest ona więc kluczowa do osiągnięcia skalowalności i wysokiej dostępności Twojej aplikacji. W związku z tym ten temat ma znacznie o ile Twoje aplikacje są już „na produkcji”.

Najpopularniejszymi narzędziami służącymi do orkiestracji są Docker Swarm (a więc kolejny system autorstwa Dockera) oraz Kubernetes (w skrócie – K8s). Kubernetes wywodzi się ze stajni Google, natomiast w tej chwili jest zarządzany przez Linux Foundation.

Kubernetes posiada opinie systemu bardzo złożonego i raczej trudnego w opanowaniu, zwłaszcza bez wcześniejszej wiedzy z dziedziny wirtualizacji, konteneryzacji czy właśnie orkiestracji. Docker Swarm, przynajmniej sądząc po lekturze dokumentacji, posiada nieco mniejszą barierę wejścia.

Na koniec wspomnę iż o ile jesteś użytkownikiem chmury, to każdy z większych dostawców posiada swoje własne narzędzia do zarządzania orkiestracją kontenerów:

  • Amazon Elastic Container Service (Amazon ECS )
  • Google Kubernetes Engine (GKI)
  • Azure Container Instances, Azure Container Services czy Azure Kubernetes Services (ACI, ACS, AKS)

Oczywiście pod maską (co jest ewidentne patrząc na ich nazwy) te usługi korzystają często z Swarm lub K8s, natomiast ich przewagą jest ergonomia, integracja oraz jednolity, w odniesieniu do pozostałych modułów chmury, interfejs użytkownika.

TL;DR?

  • Naucz się podstaw komend docker i docker-compose, jeżeli chcesz iść dalej i budować własne obrazy to szukaj w google o plikach Dockerfile
  • Czytaj oficjalną dokumentację Dockera
  • Testuj oficjalne obrazy dostępne na Docker Hub. Z odrobiną znajomości docker-compose można w prosty sposób zbudować złożone aplikacje.
  • Sprawdzaj listę najpopularniejszych repozytoriów na GitHubie. Możesz ją filtrować po języku – zarówno programistycznym jak i tym mówionym. Jest to o tyle ważne iż ostatnio spora część czołówki to pliki w języku chińskim.

Na koniec warto wspomnieć o repozytorium awesome-docker na GitHubie w którym znajdziesz całą masę produktów, obrazów, źródeł, zasobów itd. przydatnych w pracy i nauce Dockera. W tej chwili repozytorium posiada ponad 18000 więc z pewnością jest tam wiele cennych informacji.

P.S.

Zdjęcie tytułowe zrobiłem aparatem Nikon D200 gdzieś w koło 2010 roku, w okolicach Alamosa w Kolorado, USA . Zdjęcie w środku artykułu to trasa 50, biegnąca wzdłuż rzeki Arkansas, gdzieś pomiędzy Cañon City i Texas Creek . Poniżej bonus, zdjęcie ponownie trasa w okolicy Alamosy, innego dnia:

Idź do oryginalnego materiału