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ę że 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.
Krótki disclaimer – ten post jest z końca 2020, więc ma już dobrze ponad 3 lata. Wiele informacji, zwłaszcza wyjaśniających koncepcję Dockera jest wciąż aktualna, natomiast instrukcja instalacji i niektóre polecenia mogą być trochę outdated.
Pracuję nad nową wersją tego posta – jeśli jesteś zainteresowana/y zostaw proszę komentarz! Na pewno pojawi się wtedy szybciej 💪😁.
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 właściwych, 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 za pomocą 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ć, że jeśli 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 zazwyczaj tworzymy w taki sposób, aby realizował jedno, podstawowe zadania. Wtedy maksymalnie wykorzystujemy zalety tej technologii. Oznacza to natomiast, że nasza aplikacja zazwyczaj 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 – za pomocą 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ć, że 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śli 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
- przebudować warstwę gdzie wprowadziliśmy zmiany, np. w kodzie źródłowym naszej aplikacji, oraz
- 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 zazwyczaj 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ą zazwyczaj 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).
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śli 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 zazwyczaj 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, że 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 zapewne jakaś aplikacja webowa oraz sam redis.
- Usługa web bazuje na budowanym obrazie. Atrybut build ma wartość . (folder bieżący), oznacza to że w folderze z plikiem
docker-compose.yml
znajduje się również plikDockerfile
przy pomocy którego ten obraz zbudujemy. - Usługa redis posiada tylko wskazany obraz (
image: redis
). Co to oznacza? Jeżeli 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 skrócie polegamy na portach wystawianych przez poszczególne usługi oraz, jeśli 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śli 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 zapewne 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
, nawet 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ć właściwie 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ę za pomocą 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 za pomocą 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, że 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śli 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 za pomocą .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śli tak jak ja pracujesz na Linuxie Mint (czyli dystrybucji bazującej na Ubuntu). Zwróć uwagę że chcemy zainstalować Docker Engine – czyli mechanizm pozwalający korzystać z technologii Docker. Odbywa się to za pomocą 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ć. Jeżeli 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 ręcznie 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ć właściwy codename dla dystrybucji nadrzędnej (Ubuntu). Jeżeli tak jak ja korzystasz z Mint – sprawdź tutaj na jakim distro Ubuntu bazuje Twoja wersja. Jeżeli chcesz zobaczyć dla jakich dystrybucji wydawany jest docker wejdź tutaj).
docker-compose
Instalacja Docker Compose jest prostsza i również polecam zapoznanie się z właściwą instrukcją na stronie oficjalnej dokumentacji. Polecenie curl
która pobiera właściwy 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 każdego 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 że zrobisz to dla samego Dockera, Docker Compose domyślnie nie wymaga uprawnień root’a.
Zweryfikuj że 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 zazwyczaj 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 że 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. Jeżeli 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ę. Obecnie fragment instrukcji realizuje następującą sekwencję:
- Skopiuj plik
requirements.txt
(wymagane biblioteki których użyłem) z folderu aplikacji - Doinstaluj kilka bibliotek do Linuxa Alpine
- Pobierz i zainstaluje biblioteki z pliku
requirements.txt
- Skopiuj resztę folderu aplikacji, ustaw zmienne środowiskowe, uruchom serwer Gunicorn.
Oznacz to że jeżeli 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ć że te 3 wcześniejsze trwają zdecydowanie dłużej. Mój proces przed tą zmianą wyglądał tak:
- Skopiuj cały folder aplikacji (w tym
requirements.txt
) - Doinstaluj, pobierz, zainstaluj etc..
- 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 jeżeli 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 obecnie 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ę że jeżeli 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
idocker-compose
, jeśli chcesz iść dalej i budować własne obrazy to szukaj w google o plikachDockerfile
- 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 że 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:

1 Comment
Polecenie z dodaniem repo dla odpowiedniej dystrybucji zawiera delikatny błąd w postaci braku ” na końcu, powinno być:
sudo add-apt-repository
„deb [arch=amd64] https://download.docker.com/linux/ubuntu
(lsb_release -cs)
stable”