🇬🇧 Go to english version of this post / Przejdź do angielskiej wersji tego wpisu
Już wcześniej pisałem, że jestem ogromnym zwolennikiem rozwiązań self-hosted, czyli takich, które uruchamia się na swoim sprzęcie lub na zarządzanym przez siebie serwerze VPS. W poprzednich wpisach napisałem już jak stworzyć swój serwer domowy, więc naturalnym następnym krokiem jest uruchomienie na nim usług. W tym wpisie przedstawię Dockera, który jest najpopularniejszym narzędziem do wirtualizacji, czyli uruchamiania wielu małych maszyn wirtualnych wewnątrz jednej maszyny fizycznej (serwera). Te maszyny nazywa się kontenerami, a są to tak naprawdę kontrolowane środowiska do uruchamiania konkretnych aplikacji. Kontenery mogą być od siebie odizolowane lub ze sobą połączone w kontrolowany sposób. Zaletą takiego rozwiązania jest to, że z poziomu jednego serwera głównego można w bardzo przejrzysty sposób zarządzać wieloma środowiskami, na których uruchomione będzie wiele usług. Dodatkową zaletą Dockera jest to, że w Internecie jest wiele gotowych obrazów kontenerów, które po szybkiej konfiguracji, która przeważnie ogranicza się do ustawienia kilku parametrów, są gotowe do uruchomienia.
EDIT: Zostałem słusznie pouczony przez jednego z Czytelników, że Docker != wirtualizacja, a kontenery to nie maszyny wirtualne. Faktycznie jest to prawda i przyznaję się do tego, że niesłusznie zastosowałem skrót myślowy, odnosząc Dockera do wirtualizacji. Dlaczego Docker to nie wirtualizacja zwięźle wytłumaczono w tym artkule.
Instalacja i podstawowa konfiguracja
Wszystko opiszę tak jakbyśmy mieli do czynienia z czystym serwerem, a każdy dopasuje poniższe instrukcje do swojej sytuacji. Rozpoczynamy od instalacji środowiska Docker.
sudo apt install docker.io docker-compose -y
Jest to pakiet normalnie dostępny w repozytorium APT. Piszę o tym dlatego, że w niektórych poradnikach dotyczących Dockera możecie się spotkać z paczką docker-ce, która w praktyce jest tym samym, ale znajduje się w zewnętrznym, ale oficjalnym, repozytorium docker.com, które wymagałoby podpięcia do serwera. Nie widzę potrzeby, aby wykonywać dodatkowe kroki, a więc korzystamy z pakietu udostępnianego przez domyślne repo. Po prawidłowej instalacji usługa powinna od razu zostać odpalona, jednak dla pewności sprawdźmy to poleceniem:
sudo systemctl status docker
Jeżeli jakimś cudem otrzymaliśmy odpowiedź zawierającą zwrot inactive to znaczy, że coś podejrzanego poszło nie tak. Czasem wystarczy jedynie ręcznie uruchomić proces:
sudo systemctl enable --now docker
Niekiedy jednak konieczne jest ponowne uruchomienie całego serwera. Jeżeli i to nie pomoże to w rezultacie wykonania powyższego polecenia powinniśmy otrzymać jakiś błąd. Błędy mogą być przeróżne, więc nie jestem w stanie opisać w tym wpisie wszystkich możliwości, natomiast aby nie zostawiać nikogo w potrzebie proponuję w takim przypadku, abyś drogi Czytelniku, napisał wtedy komentarz z wklejoną treścią błędu, a ja postaram się pomóc na tyle na ile będę mógł.
Po instalacji powinna zostać również utworzona specjalna grupa dla użytkowników dedykowanych do pracy z Dockerem. Dobrą praktyką jest upewnienie się czy taka grupa istnieje, a jeżeli nie to jej założenie:lo
sudo groupadd docker
Następnie należy dodać swojego użytkownika do tejże grupy:
sudo usermod -aG docker $USER
Teraz, aby zmiany zostały zostały wprowadzone, tj. nasz użytkownik faktycznie posiadł nowe uprawnienia, należy się rozłączyć i połączyć ponownie z serwerem. Z doświadczenia mogę dodać, że czasem konieczny jest także reboot całej maszyny.
Uruchomienie przykładowego kontenera
Spróbujmy teraz odpalić przykładowy kontener. Do tego celu wybrałem pi-hole, czyli usługę będącą serwerem DNS, który może między innymi służyć do moderowania ruchu sieciowy, który przez niego przepuścimy. To tak w ogromnym skrócie, bo o pi-hole na pewno napiszę oddzielny wpis, a tutaj posłuży nam jedynie jako przykład. Uruchamiać kontenery Dockera można na wiele sposób, ale najpopularniejsze to:
- polecenie docker run …, które jest wygodne przy uruchamianiu na szybko prostych kontenerów,
- docker-compose, które polega na stworzeniu odpowiedniego pliku konfiguracyjnego .yml i jego kompilacji (czy też skomponowania od compose), jest to preferowane przeze mnie rozwiązanie,
- skorzystanie z narzędzia z interfejsem graficznym, które pozwala zarządzać kontenerami w bardziej przyjazny (dla niektórych) sposób, bo nie wymagający korzystania z terminala, przykładem takiego narzędzia jest Portainer.
W tym wpisie posłużymy się metodą bazującą na wykorzystaniu docker-compose. Zaczynamy od utworzenia w katalogu domowym naszego użytkownika folderu dedykowanego dla Dockera i podfolderu dla kontenera, który właśnie tworzymy.
mkdir -p /home/$USER/docker/pihole
Następnie stwórzmy wewnątrz plik konfiguracyjny docker-compose.yml, który jest niezbędny do utworzenia danego kontenera i późniejszej jego aktualizacji, bo aktualizacja kontenera polega w praktyce na jego usunięciu i utworzeniu od nowa z zachowaniem odpowiednich plików i ustawień, ale o procesie aktualizacji szczegółowo później.
nano /home/$USER/docker/pihole/docker-compose.yml
Zawartość podstawowego pliku konfiguracyjnego kontenera pi-hole powinna wyglądać następująco:
version: "3"
services:
pihole: # robocza nazwa kontenera (niezbyt istotna, bo jedynie na potrzeby tego pliku)
container_name: pihole # nazwa jaką będzie identyfikował ten kontener (musi być unikatowa)
image: pihole/pihole:latest # określenie jaki obraz ma zostać użyty
ports: # sekcja, w której ustawia się przekierowania portów serwera na porty kontenera
- "53:53/tcp" # DNS przy użyciu protokołu TCP
- "53:53/udp" # DNS przy użyciu protokołu UDP
- "80:80/tcp" # HTTP
environment: # sekcja, w której określa się tzw. zmienne środowiskowe
TZ: 'Europe/Warsaw' # przykładowa zmienna określająca strefę czasową
volumes: # sekcja, w której definiuje się tzw. wolumeny, czyli foldery współdzielone pomiędzy serwerem a kontenerem
- '/home/$USER/docker/pihole/volumes/etc/pihole:/etc/pihole'
restart: unless-stopped # tzw. polityka uruchamiania ponownie, np. to restarcie serwera lub samego procesu Dockera
Jeżeli nie jest to pierwszy wpis, który czytasz na tym blogu to wiesz, że moim ulubionym sposobem opisywania kodu jest robienie tego poprzez komentarze w jego środku. Tak też zrobiłem i tym razem, a komentarze w plikach YAML rozpoczyna się znakiem #. Pliki YAML mają strukturę podobną do drzewa, większe gałęzie rozgałęziają się na mniejsze i tak dalej i tak dalej. W sekcji services mamy jedynie jeden kontener, bo przedstawiony powyżej przykład jest dosyć prosty, nie ma co przesadzać na początek. Jednak warto jest uprzedzić, że można poprzez jeden plik konfiguracyjny uruchomić grupę kontenerów, które będą ze sobą współpracować. Dla przykładu może to być usługa oraz niezbędna do jej prawidłowej pracy baza danych MySQL. Taki tandem.
Pomimo wyjaśnienia kodu poprzez komentarze chciałbym się jednak bardziej szczegółowo pochylić nad pewnymi fragmentami:
- Linia 6., w której wskazujemy jakiego obrazu chcemy użyć. Podstawowym repozytorium, z którego czerpane są obrazy kontenerów jest Docker Hub. Można także dodawać customowe repozytoria, ale nie będziemy się tutaj na tym skupiać. Format zapisu jest podobny do tego używanego na GitHubie, gdzie pierwszy człon to nazwa dewelopera stojącego za obrazem, a druga to nazwa samego obrazu. Do tego po dwukropku podaje się wersję, którą chce się użyć. W tym przypadku użyliśmy latest, czyli narzucamy kompilatorowi, że chcemy używać wersji najnowszej.
- Linie 8.-10., w których przekierowujemy porty serwera na porty kontenera. Składnia jest taka, że przed dwukropkiem podajemy port serwera, który chcemy przekierować, a po dwukropku port kontenera. Dodatkowo po ukośniku można jeszcze sprecyzować protokół TCP/UDP, ale podkreślam, że jedynie można, bo jeżeli nic nie podamy to przekierowanie będzie działało dla obu protokołów.
- Linia 11., czyli sekcja ze zmiennymi środowiskowymi. Co w zasadzie oznacza ta dumnie brzmiąca nazwa? Upraszając zagadnienie do minimum, jest to zestaw danych wejściowych na podstawie, których w prosty sposób możemy modyfikować parametry pracy kontenera, o ile oczywiście deweloper stojący za danym obrazem przewidział taką możliwość.
- Linia 14., w której utworzyliśmy przestrzeń współdzieloną pomiędzy serwerem, a kontenerem. Jest to folder, którego zawartość jest identyczna na serwerze i wewnątrz kontenera. Jest to arcyważne zagadnienie, bo taki folder współdzielony pozostaje na serwerze nawet po całkowitej anihilacji (unicestwieniu) kontenera. Wolumeny są kluczowe w procesie aktualizacji kontenerów, ale o tym już za chwileczkę.
- Linia 15., czyli polityka ponownego uruchamiania, która określa jak kontener ma się zachować w przypadku kiedy zostanie wyłączony w skutek jakiegoś wydarzenia. Są następujące wartości jakie możemy tu ustawić:
- no – nigdy nie uruchamia ponownie kontenera,
- always – uruchamia ponownie kontener bezwarunkowo aż do momentu jego całkowitego usunięcia,
- on-failure – uruchamia ponownie kontener pod warunkiem zgłoszenia statusu błędu,
- unless-stopped– uruchamia ponownie kontener pod warunkiem, że nie jest on ręcznie zatrzymany (status stop).
Wiemy już wszystko o konstrukcji pliku docker-compose.yml, więc możemy go zapisać i z niego wyjść. Teraz pozostaje nam jeszcze utworzyć wolumen, który zdefiniowaliśmy w pliku konfiguracyjnym, a także otworzyć porty w firewallu, z których będzie korzystać kontener.
mkdir -p /home/$USER/docker/pihole/volumes/etc/pihole
sudo ufw allow 53
sudo ufw allow 80/tcp
Na koniec kompilujemy i uruchamiamy kontener.
docker-compose -f /home/$USER/docker/pihole/docker-compose.yml up -d
Zarządzanie kontenerami
Pierwsze uruchomienie mamy za sobą, więc teraz powinniśmy się nauczyć jak zarządzać kontenerami. Przejdę jedynie przez podstawowe polecenia, które są fundamentalne.
Wylistowanie wszystkich uruchomionych kontenerów (dodając -a na końcu pokazane zostaną wszystkie kontenery jakie istnieją na naszym serwerze, a nie tylko te uruchomione):
docker ps
Zatrzymanie kontenera (przykład: pi-hole):
docker stop pihole
Uruchomienie zatrzymanego kontenera (przykład: pi-hole):
docker start pihole
Alternatywny sposób odpalania nowych kontenerów, który ja wykorzystuję jak muszę odpalić jakiś testowy kontener na szybko, lub odpalam usługę, która nie jest bardzo skomplikowana i nie potrzebuje wielu parametrów. W poniższym przykładzie odpalony zostanie kontener testowy hello-world, którego zasada działania polega na wyświetleniu ciągu znaków i wyłączeniu się zaraz po wykonaniu tego zadania:
docker run hello-world
Usunięcie kontenera (po ówczesnym jego zatrzymaniu przy użyciu polecenia stop):
docker rm hello-world
To w zasadzie wszystkie podstawowe polecenia, które należy znać, aby obsługiwać Dockera na akceptowalnym poziomie. Pisząc o każdym z powyższych poleceń dokleiłem też bezpośrednie linki do ich dokumentacji.
Jak dostać się do środka kontenera
Bardzo przydatną funkcją Dockera jest możliwość uzyskania dostępu do powłoki samego kontenera tak jakbyśmy logowali się do zwykłego serwera przy użyciu SSH. Realizuje się to poprzez polecenie (na przykładzie pi-hole):
docker exec -it pihole /bin/bash
Wychodzi się w taki sam sposób jak z serwera przy połączeniu SSH, czyli poleceniem exit. W podobny sposób można także wykonać wewnątrz kontenera określone polecenie, bez wchodzenia do niego:
docker exec -it pihole <polecenie>
Aktualizacja kontenerów
Przyznam szczerze, że jak zaczynałem swoją przygodę z Dockerem i dowiedziałem się w jaki sposób realizuje się aktualizację kontenerów, to początkowo byłem w lekkim szoku, pomyślałem nawet, że to głupota, ale później po przeanalizowaniu zrozumiałem, że jest to genialne w swojej prostocie. Otóż, tak jak opisałem wyżej, proces uruchamiania kontenera rozpoczynamy od tworzenia pliku konfiguracyjnego docker-compose.yml, w którym zawarte są wszystkie informacje o tym jak chcemy, aby wyglądał tenże kontener. Ten plik po skompilowaniu i uruchomieniu kontenera nigdzie nie znika i dalej jest dostępny tam gdzie go utworzyliśmy. Dodatkowo definiujemy volumes (z ang. wolumeny), w których przechowujemy wszystkie istotne pliki kontenera. Wolumeny te pozostają nienaruszone nawet w przypadku zatrzymania pracy czy też całkowitego usunięcia kontenera. Biorąc te dwie rzeczy mamy wszystkie potrzebne składniki, które są niezbędne do ponownego uruchomienia identycznego kontenera. No dobrze, ale zapytasz teraz – skoro będzie to identyczny kontener to gdzie w tym wszystkim jest aktualizacja?! Wróćmy do tworzonego przez nas pliku konfiguracyjnego i wyciągnijmy następującą linijkę:
image: pihole/pihole:latest
Słowem klucz jest tutaj latest, a więc parametr, który mówi kompilatorowi, że podczas tworzenia kontenera ma on wziąć najnowszą wersję obrazu jaka jest dostępna w momencie kompilacji. A zatem w telegraficznym skrócie, aktualizacja kontenera Dockera polega na jego zatrzymaniu, usunięciu, ponownej kompilacji z tymi samymi parametrami, ale na nowszej wersji obrazu i na koniec wypełnieniu plikami z wolumenu, z których korzystał przed aktualizacją. Proste i skuteczne. Przejdźmy w takim razie przez taki proces aktualizacji naszego przykładowego kontenera pi-hole.
1. Zatrzymujemy kontener:
docker stop pihole
2. Usuwamy kontener:
docker rm pihole
3. Kompilujemy ponownie i uruchamiamy ponownie kontener:
docker-compose -f /home/$USER/docker/pihole/docker-compose.yml up -d
4. Sprawdźmy czy uruchomił się prawidłowo:
docker ps
Gotowe. Ja przeważnie załatwiam to skryptem, który z automatu aktualizuje mi wszystkie kontenery. Powyższa instrukcja wydaje się banalna i turbo szybka, ale jak trzeba ją powtórzyć kilkanaście razy to już po chwili człowiek zaczyna się zastanawiać jak to zautomatyzować. W przyszłości podzielę się na pewno takim skryptem, ale w oddzielnym wpisie.
Tworzenie kopii zapasowych kontenerów
Zarządzanie kontenerami, w taki sposób jak opisałem, niesie za sobą ogromną zaletę. Posiadając wszystkie kontenery w jednym miejscu i do tego podzielone na podfoldery, po jednym na każdy kontener, nie ma absolutnie żadnego problemu ze zrobieniem szybkiej kopii zapasowej. Backup wszystkich kontenerów robi się poprzez skopiowanie całej zawartości folderu /home/$USER/docker/. Można do tego celu użyć funkcji tar:
tar -cvpf /home/$USER/$(date +"%FT%H%M")_docker_backup.tar.gz /home/$USER/docker
Tak sformułowane polecenie utworzy archiwum, o nazwie <data>_docker_backup.tar.gz, z kopią zapasową w środku, w katalogu domowym użytkownika. Dla pewności dobrze jest też skopiować całą zawartość folderu /var/lib/docker/, w którym znajdują się wszystkie pliki Dockera jako usługi zainstalowanej na naszym serwerze. Jednakże do zrobienia tego potrzebujemy dostępu roota.
sudo su
tar -cvpf /root/$(date +"%FT%H%M")_var_lib_docker_backup.tar.gz /var/lib/docker
Tak sformułowane polecenie utworzy archiwum, o nazwie <data>_var_lib_docker_backup.tar.gz, z kopią zapasową w środku, w katalogu domowym roota.
To tyle na dzisiaj…
🇬🇧 Go to english version of this post / Przejdź do angielskiej wersji tego wpisu
Uf, dobrnęliśmy do brzegu. Napisanie tego wpisu nie było łatwe, a i jego lektura do przyjemnych raczej też nie będzie należała. Niemniej jednak wydaje mi się, że udało się upchnąć sporo merytorycznego contentu, który mam nadzieję komuś się przyda! Przedstawiona powyżej wiedza jest raczej elementarna, ale na pewno pozwala rozpoznać się w temacie, a nawet operować w środowisku Dockera w zakresie podstawowych czynności. Taki właśnie był cel tego wpisu. Wiele razy zarzekałem się na drugim blogu – odroid.pl, że taki wpis powstanie i w końcu udało mi się usiąść i go stworzyć. Dziękuję za dziś i w razie czego jestem dostępny w komentarzach.
Zgredek
Kontener to nie maszyna wirtualna. To są dwa różne terminy. Maszyna wirtualna to emulowana platforma sprzętowa a której uruchamia się system operacyjny z własnym jądrem. W przypadku kontenerów mamy do czynienia z odizolowaną przestrzenią użytkownika wewnątrz jednego systemu. Z tego powodu nie da się wewnątrz kontenera uruchomić kompletnego systemu operacyjnego z własnym jądrem. Do stworzenia kontenera wykorzystywane są przestrzenie nazw które pozwalają stworzyć to wyizolowane środowisko i kilka innych funkcji jądra zapewniających odpowiedni poziom bezpieczeństwa lub kontrolę zużycia zasobów.
Pingback: Nowy blog – stary ja, ale gdzie indziej
janek
Dotarłem tu z newslettera zaufanej trzeciej strony i też jestem w szoku, że polecono artykuł gdzie w pierwszym akapicie jest mowa o dokerze jako wirtualizacji.
Czy ktoś to w ogóle czytał czy tylko “przyjacielska” rekomendacja to nie wiem, w każdym razie trochę razi w oczy.
Pingback: Weekendowa Lektura: odcinek 512 [2023-03-25]. Bierzcie i czytajcie | Zaufana Trzecia Strona
Pingback: Docker – one server, many services [ENG 🇬🇧] – Tomasz Dunia Blog
Pingback: Terminal z Proxmox – ambitny serwer domowy – Tomasz Dunia Blog
Pingback: Mastodon API – lista obserwujących i obserwowanych – Tomasz Dunia Blog
Pingback: Portainer – GUI dla Docker’a – Tomasz Dunia Blog
Piotr
Czy mogę prosić o pomoc w implementacji obsługi DNS over HTTPS (DoH) w kontenerze Pi-Hole tak aby w jednym pliku docker-compose.yml były zawarte dwa obrazy: Pi-hole+DoH z obsługą upstream do np. Cloudflare? Fajnie by było swój DNS oparty o Pi-hole, zabezpieczyć ruch do upstremu np. Cloudflare poprzez (DoH).
Tomasz Dunia
Wysłałem prośbę o pomoc na Mastodonie, mam nadzieję, że ktoś odpisze z cennymi radami 🙂
https://mastodon.tomaszdunia.pl/@to3k/111453886586665858#
Pingback: Uptime Kuma – monitorowanie pracy usług – Tomasz Dunia Blog