Docker

Niedawno zacząłem prowadzić Grupę .NET na Uniwersytecie Warszawskim i, ponieważ niedługo zaczniemy pierwszy projekt, postanowiłem założyć serwis Git. Dotychczas pracowałem z GitLabem, ale nie przypadł mi specjalnie do gustu. Dlatego gdy odkryłem Go Git Service postanowiłem go wdrożyć.

Gogs ma kilka metod instalacji, a jedna z nich to użycie kontenerów Dockera. Od pewnego czasu słyszałem o tej technologii, ale nie wiedziałem jak działa, ani do czego dokładnie służy. Teraz kiedy udało mi się opanować ją w stopniu jaki potrzebny był do postawienia serwera opartego o kontenery, postanowiłem opisać tu jak jej używać.

Jak to działa?

Docker posługuje się technologią konteneryzacji zaimplementowaną w kernelu Linuxa. Każdy obraz to zbiór plików user-space’u Linuxa. Tworząc kontener bierzemy pliki z obrazu i używamy ich jako ,,wirtualnej maszyny”, która korzysta z naszego działającego kernela, który zapewnia odseparowanie procesów tego kontenera od procesów hosta, czyli naszego systemu. Zaletą tego rozwiązania w stosunku do używania maszyn wirtualnych jest to, że nie mamy duplikacji kernela - całego systemu zarządzania pamięcią i dyskiem. Więc nasze kontenery są mniejsze i szybsze.

Docker architecture

Dodatkowo, każdy obraz jest skonstruowany z warstw połączonych przez UnionFS, więc pobierając dwie aplikacje oparte o Ubuntu, ściągniemy warstwę zawierającą pliki Ubuntu tylko raz. W trakcie działania aplikacji wszelkie modyfikacje plików i folderów systemowych są kolejną warstwą, więc nie dochodzi do kopiowania wszystkich plików do nowego kontenera, tylko ich współdzielenia.

Instalacja

Po pierwsze chcemy zainstalować Docker. Jest on dostępny przede wszystkim na Linuxie, ale także na Windowsie i Mac OS X, gdzie jest oparty o VirtualBox. Ponieważ ja pracowałem na maszynie wirtualnej z Debianem, będę opisywał proces ze strony Linuxa.

Postępując zgodnie z Getting Started: Step 1, uruchomiłem

curl -fsSL https://get.docker.com/ | sh

i po chwili miałem zainstalowany Docker. Aby uruchamiał się on ze startem systemu, zrobiłem

sudo systemctl enable docker
sudo systemctl start docker

Normalnie Docker potrzebuje uprawnień administratora do działania i żeby nie musieć ciągle wpisywać sudo, należy dodać użytkownika do grupy docker

sudo usermod -aG docker user

Uruchamianie aplikacji

Zaczniemy od standardowej aplikacji hello-world. Aby uruchomić aplikację użyjemy

docker run [OPTIONS] image[:version] [COMMAND]

W naszym przypadku będzie to po prostu

docker run hello-world

Jeśli nie podamy wersji obrazu to automatycznie będzie to latest. Jeśli nie posiadamy obrazu w naszym systemie zobaczymy komunikat

Unable to find image `hello-world:latest` locally

Po czym zostanie od ściągnięty z domyślnego rejestru hub.docker.com i uruchomiony.

Kilka opcji

Po pierwsze, wiele aplikacji, które będziemy uruchamiać to będą daemony i nie będziemy chcieli aby zajmowały nasz terminal. Aby aplikacja działała w tle, należy ją uruchomić z opcją -d, czyli ,,detached”. W kolejnych przykładach będziemy posługiwali się obrazem Ubuntu.

docker run -d ubuntu

Jak sprawdzić czy aplikacja działa?

docker ps

Powyższe polecenie pokaże nam działające kontery

CONTAINER ID	IMAGE	COMMAND	CREATED	STATUS	PORTS	NAMES

Ale jest pusto. Czy Ubuntu się nie uruchomił? Uruchomił się, nie miał nic do roboty więc się zakończył. A co może się dziać? Możemy wejść w tryb interaktywny, o czym niżej, lub wywołać jedno polecenie.

$ docker run ubuntu ls /dev
core	fd	full	fuse	mqueue	null	ptmx	pts	random	shm	stderr	stdin	stdout	tty	urandom	zero

Dodając parametr -a do polecenia ps zobaczymy wszystkie kontenery, także te które się zatrzymały.

$ docker ps -a
CONTAINER ID	IMAGE	COMMAND	CREATED	STATUS	PORTS	NAMES
6d27601a1401	ubuntu	"/bin/bash"	About a minute ago	Exited (0) About a minute ago	dreamy_franklin
3d8188660fe2	hello-world	"/hello"	11 minutes ago	Exited (0) 11 minutes ago	jovial_yalow

Jeśli będziemy chcieli zrobić coś więcej w aplikacji, będziemy potrzebowali dwóch innych argumentów

  • -i - ,,interactive” przekazuje dane z klawiatury na STDIN
  • -t - ,,tty” uruchamia terminal

Spróbujmy teraz

$ docker run -it ubuntu
[email protected]:/ #

Żeby przekonać się, że naprawdę mamy do czynienia z systemem zróbmy

[email protected]:/ # ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

Dalej przekonajmy się, że pracujemy na tym samym kernelu

[email protected]:/ # uname -a
Linux  3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt25-1 (2016-03-06) x86_64 x86_64 x86_64 GNU/Linux

I to samo na hoście

$ uname -a
Linux  3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt25-1 (2016-03-06) x86_64 GNU/Linux

Jeśli uruchomiliśmy kontener w trybie detached i chcemy jednak uzyskać nad nim kontrolę to możemy posłużyć się

docker attach [OPTIONS] CONTAINER

Natomiast jeśli nie chcemy uzyskiwać pełnej kontroli, bo np. aplikacja uniemożliwia nam zrobienie czegokolwiek, możemy wywołać jakieś polecenie w kontenerze przez

docker exec [OPTIONS] CONTAINER COMMAND [ARGS...]

Usuwanie i nazwy

Mamy w tej chwili 3 wyłączone kontenery i chcielibyśmy się ich pozbyć. Do tego służy polecenie rm, któremu należy podać nazwę lub hash kontenera

docker rm 3d8188660fe2 #usuwamy hello-world
docker rm dreamy_franklin #usuwamy pierwszy kontener z Ubuntu

Co to za nazwa kontenera? Domyślnie jest to losowa nazwa złożona z przymiotnika i nazwiska jakiegoś znanego naukowca. Ale możemy nazywać samemu nasze kontenery

docker run --name my_ubuntu ubuntu

Aby usunąć wszystkie nasze kontenery posłużymy się

docker rm $(docker ps -aq)

Gdzie polecenie w nawiasach wypisuje nam wszystkie kontenery, ale tylko ich hashe (-q).

Dlaczego chcielibyśmy nazywać nasze kontenery? Bo łatwiej się ich wtedy używa. Za pomocą polecenia inspect możemy zobaczyć ustawienia naszego kontenera, a za pomocą polecenia logs przejrzeć wyjście naszej aplikacji. Oba potrzebują nazwy lub hashu kontenera.

Wolumeny

Czasem możemy mieć potrzebę aby ten nasz kontener nie był całkowicie odizolowany od naszego systemu. Więc możemy współdzielić z nim foldery lub pliki. Powiedzmy, że mam folder ~/html i chciałbym go mieć w moim Ubuntu w folderze /var/www.

$ docker run -it -v ~/html:/var/www/html ubuntu
/ # cd /var/www/html; touch file.html
/ # exit
$ ls ~/html
file.html

Więcej o wolumenach.

Publiczne porty

Możemy być zainteresowani aby udostępnić pewne porty kontenera na zewnątrz. Np chcemy opublikować stronę internetową. Pobierzemy aplikację NGINX

docker pull nginx

następnie utworzymy plik konfiguracyjny ~/nginx.conf, w którym wpiszemy

events {}

http {
 server {
    listen 80;

    location / {
	root /var/www/html;
    }
 }
}

A następnie uruchamiamy

docker run -d -p 8080:80 -v ~/nginx.conf:/etc/nginx/nginx.conf -v ~/html:/var/www/html  --name webserver   nginx

Parametr -p, czyli publish, określa mapowanie portu 80 kontenera do portu 8080 hosta. Następnie mamy dwa połączenia wolumenów: pliku konfiguracyjnego i folderu z zawartością strony. Teraz wystarczy utworzyć prosty plik index.html, wrzucić go do ~/html, wejść w przeglądarce na http://localhost:8080/ i voila.

Gy będziemy chcieli zatrzymać nasz serwer wystarczy

docker stop webserver

A jeśli będziemy chcieli uruchomić go ponownie

docker start webserver

Łączenie aplikacji

Każdy kontener w trakcie tworzenie ma przypisany adres IP, który możemy sprawdzić przez docker inspect. Używanie go nie jest jednak specjalnie wygodne, szczególnie jeśli ten adres będzie się zmieniał.

Jako przykład wezmę dwa kontenery: serwer http z php i serwer smtp.

docker run -d --name smtp namshi/smtp
docker run -d --name webserver [...] --link smtp:smtp nginx

--link {name|hash}:alias tworzy połączenie sieciowe między kontenerami i dodaje wpis do pliku /etc/hosts o nazwie alias i wskazuje na adres IP wskazanego kontenera.

Dzięki temu nie musimy ustawiać portów jako publicznych dla aplikacji, które porozumiewają się lokalnie.

Więcej o możliwościach sieciowych.

Praktyczne zastosowanie

Kiedy warto korzystać z kontenerów? Jeśli chcemy szybko zainstalować aplikację wraz z jej zależnościami. Kiedy musimy korzystać z dwóch niekompatybilnych ze sobą aplikacji. Kiedy to jedyna droga.

Odkąd usłyszałem, że Microsoft udostępnia open-source’owy .NET Core, chciałem wypróbować jak to działa. Ponieważ na co dzień pracuję na Manjaro opartym o Arch Linux, który nie jest wspierany przez MS, miałem z tym trudności. Dzięki Dockerowi wystarczyło jedno polecenie aby ściągnąć gotowy obraz oparty o Ubuntu z zainstalowanym .NET Core

docker pull microsoft/dotnet

I mogę teraz bawić bez konieczności instalacji drugiego systemu lub wirtualizacji.

Podsumowanie

Kontenery ułatwiają życie głównie ze względu na prostotę użycia. Jeśli skonfiguruję sobie kontener mogę utworzyć własny obraz i zuploadować go do rejestru, aby inni mogli z niego korzystać.

Cały ten blog post to tylko zalążek możliwości i API Dockera, który jest wystarczający aby zacząć się kontenerami bawić. Nazwałbym go minimalistycznym szerokim tutorialem :) Większość informacji jest ze strony docs.docker.com, w której wszystkie funkcje Dockera są podzielone na tematy i omówione dokładniej.