Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: Grzegorz 'baziorek' Bazior
Korekta: Paweł Król
Narzędzia dla programistów C++

Docker pod kątem zastosowań programisty C++

[artykuł]

Wprowadzenie

Docker jest narzędziem, o którym wielu już słyszało, wielu go też używało pośrednio, lub bezpośrednio. Mówiąc w skrócie jest to narzędzie tworzące niezależne wirtualne izolowane środowisko. Coś jak bardzo lekka maszyna wirtualna. Narzędzie to posiada bardzo dużą ilość zaawansowanych funkcji, których nawet nie tknę w tym artykule. Najczęściej używaną funkcjonalnością jest możliwość postawienia izolowanego systemu operacyjnego zawierającego wszystkie zależności do aplikacji, której planujemy użyć bez skomplikowanej instalacji i konfiguracji. Poza samymi funkcjonalnościami tworzenia środowiska pod naszą aplikacje Docker umożliwia nam w łatwy sposób migrować między wersjami, rozpowszechniać naszą aplikacje, tworzyć migawki itp. Kluczowe jest, że raz przygotowany obraz będzie działał na różnorakich systemach, dzięki temu nie musimy pisać naszej aplikacji pod różne systemy operacyjne w różnych wersjach i dystrybucjach. Kontenery stosujemy min. w sytuacji, gdy chcemy "zainstalować" aplikacje bez troszczenia się o zależności oraz bez obaw, że coś zostanie w systemie.

Podstawowe pojęcia


Obraz dockera - plik obrazu bez możliwości edycji. Obrazy mogą być pobierane z repozytorium obrazów lub tworzone własnoręcznie. W przeciwieństwie do maszyn wirtualnych każdy kontener dockera pochodzący powstały na bazie obrazu używa jądra maszyny hosta, a nie własnego.
Instancja kontenera - plik obrazu z możliwością zapisu i odczytu. Kontenery tworzone są na bazie obrazów. Przeznaczeniem instancji kontenera jest wykonywanie pojedynczego zadania (procesu), możliwe stany kontenera: created, running, restarting, exited, paused. dead.

Aby dobrze zrozumieć czym w dockerze jest obraz wyobraźmy sobie, że mamy płytę z systemem operacyjnym (lub obraz *.iso), po zainstalowaniu mamy już instancje systemu operacyjnego, którą sobie zmieniamy a obraz zostaje bez zmian. Z jednego obrazu możemy tworzyć wiele instancji, a instancje dockera możemy zmienić na obraz. Obrazu mogą zawierać własne systemy operacyjne, zaimplementowane na pokładzie bazy danych, aplikacje, serwisy.

Instalacja Dockera

Zasadniczo możemy go zainstalować na dowolnym Linuxie (nawet Raspberry PI), możemy też na Windowsie od wersji 7 wzwyż. Instalacja na Linuxie jest dość prosta, przykładowo na debiano-pochodnych systemach wystarczy:
sudo apt-get install docker -y


W niniejszym artykule skupię się wyłącznie na używanie Dockera na Linuxach. Posiadaczy Windowsa odsyłam do innego artykułu.

Zwiększenie wygody użytkowania Dockera - docker bez roota
Po zainstalowaniu dockera zauważymy, że aby cokolwiek na nim zrobić musimy użyć
sudo
, co jest konieczne nawet jak chcemy sprawdzić które instancje działają. Możemy to zmienić przez komendę:
sudo setfacl -m user:$USER:rw /var/run/docker.sock
# jakbysmy nie mieli powyzszego:
sudo apt-get install acl -y
Jest sposób aby nie musieć używać sudo ani nie zwiększać uprawnień - zastosować alternatywę do dockera: Padmana, która używa innego mechanizmu, przez to nie potrzebuje
sudo
. Dodam, że Padman jest napisany przez firmę RedHat, co sugeruje, że jest to narzędzie do użytku produkcyjnego.

Pobieranie obrazów dockera

Obrazy możemy sobie pozyskać z m.in. z własnego archiwum (obraz utworzony wcześniej), z internetu (repozytoria kontenerów) lub wygenerować obraz własnoręcznie (uruchamiając plik z instrukcjami pozwalającego zbudować własny obraz dockera, czyli używając tzw. Dockerfile).

Strona https://hub.docker.com/ jest oficjalnym repozytorium obrazów, tam możemy wyszukiwać interesujące obrazy dostępne. Istotnym aspektem jest możliwość filtrowania wyników po "Verified publisher" lub "Official image", również mamy możliwość wyboru kategorii, przykładowo:
Jeśli nie mamy "zweryfikowanego dostawcy" lub "Official image" możemy pokusić się o pobranie obrazu jeśli jest on Open Source - tą kwestie możemy sprawdzić jeśli albo znamy dostawcę obrazu, albo możemy też próbować wejść w szczegóły obrazu i tam poszukać jakiś odniesień do Githuba, gdzie powinien być Dockerfile. Można też na Githubie kliknąć zakładkę Packages, gdzie będzie instrukcja jak pobrać konkretny obraz dla konkretnej wersji (wśród tych udostępnionych). Przykładowo dla projektu Hyperledger Iroha mamy kilka pakietów, a pobrać je możemy w następujący sposób - z Dockerhuba robimy to komendę:
docker pull nazwa_obrazu


Przykładowe uruchomienie komendy w celu pobrania obrazu:
docker pull ghcr.io/hyperledger/iroha-burrow:91970160562d16211decda909e0b9629ecc2781c684d68655ec43377b21ce6ff


zrzucanie obrazu do/z pliku archiwum TAR:
docker save/load


Polecenie poniżej wypisuje wszystkie obraz opisując stan ich uruchomienia:
docker images # listowanie obrazów dockera
sudo docker image ls

Kasowanie obrazu Dockera:
docker image rm
docker rmi nazwa_obrazu/id_obrazu --force # wtedy też powstałe z nich instancje zostaną usunięte

Obraz dockera, nawet systemu jest minimalny, co widać przykładowo na poniższej liście:
docker images
REPOSITORY                 TAG          IMAGE ID       CREATED         SIZE
postgres                   9.5-alpine   f6e71ff7ed6b   7 months ago    36.2MB
ubuntu                     21.04        1fc773f9e714   10 months ago   80.7MB
System Ubuntu, który zajmuje poniżej 100 Mb, jakże minimalistyczny musi być ten system (przykładowo nie ma
vim
 i dla równowagi również
emac
).

Zatrzymanie, uruchomienie i sprawdzenie instancji kontenerów dockera

Najprostrzym poleceniem uruchomienia kontenera jest polecenie
run
. W tym wypadku zostanie uruchomiony kontener, którego obraz zostanie automatycznie uruchomiony. W sytuacji, gdy obraz nie istnieje jest automatycznie pobierany.

Przykładowo jak chcemy zainstalować sobie bazę danych Postgres to po zainstalowaniu dockera możemy wywołać np.:
docker run --name przykladowy_postgres \
    -e POSTGRES_USER=uzytkownik \
    -e POSTGRES_PASSWORD=haslo \
    --network=host \
    -d \
    postgres

W powyższym przykładzie uruchomiona zostanie instancja dockera z przygotowanego obrazu
postgres
, użytkownik i hasło będzie odpowiednio
uzytkownik
 i
haslo
. Nazwa instancji
przykladowy_postgres
, co prawda możemy jej nie podawać, wtedy nazwa będzie losowa. Parametr
--network=host
 oznacza, że do bazy danych możemy się dostać przed adres serwera
localhost
. Bez parametru
-d
 po wywołaniu powyższej komendy byśmy byli podpięci do
stdout
 bazy danych, zupełnie tak jakbyśmy ją uruchomili ręcznie. W ramach instancji dockera będzie uruchomiona komenda odpowiedzialna za uruchomienie procesu serwera bazy danych.

 Dla nie-programistów przy pomocy Dockera można szybko wejść do internetu anonimowo przy pomocy protokołu Tor:
docker run --network=host --rm  -it --shm-size=512m -p 6901:6901 -e VNC_PW=password kasmweb/tor-browser:1.10.0

Wtedy możemy wejść przez przeglądarkę na adres https://localhost:6901 i zobaczymy jakby okno pulpitu wewnątrz przeglądarki z włączonym oknem programu Tor-Browser, loginem i hasłem są odpowiednio:
kasm_user/password
.

Można sterować tak uruchomionym kontenerem. Poniżej opisano listę komend, które można wykorzystać w tym zadaniu.


Uruchomienie instancji kontenera
docker start nazwa_kontenera
docker start id_instancji

Restart instancji kontenera:
docker restart nazwa_kontenera
docker restart id_instancji

Zatrzymanie instancji kontenera
docker stop nazwa_kontenera
# lub
docker stop id_instancji

Usunięcie instancji kontenera
docker rm nazwa_kontenera
# lub
docker rm id_instancji
# lub jeśli kotener jest wciąż uruchomiony:
docker rm ... --force

Ogólny przegląd stanu instancji kontenera
docker ps # wyświetlenie uruchomionych instancji
docker ps -a # wyświetlenie wszystkich instancji
docker ps --all # alternatywnie do powyższego
docker container ls # dokładnie to samo co docker ps
docker ps -a--format "{{.ID}} \t {{.Names}} \t {{.Status}}" # inna forma wyświetlania

Przegląd szczegółów instancji kontenera
Są to takie szczegóły jak m.in. adres IP, wolumeny (o tym może później będzie), argumenty uruchomienia, zmapowane ścieżki i wiele innych, można zwrócić przy pomocy komendy:
docker inspect # zwraca wszystkie szczegóły instancji kontenera
# można też zwrócić poszczególne składowe np.:
docker inspect --format='{{.LogPath}}' nazwa_kontenera/id_instancji # plik z logami instancji
docker inspect -f '{{ .Mounts }}' nazwa_kontenera/id_instancji # lista zmapowanych katalogów
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nazwa_kontenera/id_instancji # adresy IP kontenera

Aktualizacja szczegółów instancji kontenera
Zdarza się, że w uruchomionej instancji zmienić konfigurację kontenera. Do tego celu używamy:
docker update opcje_kontenera nazwa_kontenera

Np. jeśli chcemy ustawić aby kontener po awarii był automatycznie ponownie uruchamiany:
docker update --restart=always id_instancji/nazwa_kontenera

poza powyższym możliwe jest m.in. ograniczenie zasobów używanych przez dockera (np. ilość pamięci RAM itp)

Wyświetlenie statystyk (użycia zasobów) instancji kontenera
Robimy to komendą:
docker stats

I otrzymujemy podobne wyjście:
CONTAINER ID   NAME           CPU %     MEM USAGE / LIMIT     MEM %     NET I/O   BLOCK I/O         PIDS
918db77597be   postgres_tmp   0.01%     51.92MiB / 31.24GiB   0.16%     0B / 0B   79.5MB / 41.8MB   7

Wywołanie komendy na instancji kontenera
Bardzo często na uruchomionej instancji dockera chcemy zawołać jakąś komendę, nr. doinstalować brakujące wtyczki. Robimy to przy pomocy polecenia:
docker exec -it nazwa_kontenera/id_instancji komenda
# powyższe inaczej:
docker exec --interactive --tty nazwa_kontenera/id_instancji komenda
Gdy chcemy dostać się do konsoli bashowej należy wprowadzić polecenie:
docker exec -it postgres_tmp /bin/bash

dzięki
-it
podpinamy się do stdout i stdin.

Analogicznie do powyższego możemy podpiąć się do stdout i stdin uruchomionej w kontenerze komendy przez zawołanie:
docker attach nazwa_kontenera/id_instancji

W przypadku wcześniej uruchomionej bazy danych postgres podepniemy się tym sposobem do głównego procesu bazy.

Komendy dokumentacji
Możliwości konfiguracyjne dockera, oraz zestaw komend jest znacznie obszerniejszy, dlatego wiele możemy przejrzeć przez powszechnie znaną komendę
--help
, której możemy użyć na różnych szczeblach zagnieżdzenia:
docker --help
docker komenda --help
# np.:
docker logs --help

Jak chcemy posprzątać jedną komendą
docker system prune # usuwa nieużywane instancje oraz wiszące obrazy (czyli starsze wersje zaktualizowanych)
docker system prune -all # ta usunie wszystkie nieużywane obrazy

System operacyjny w instancji dockera

Przykładową instancje systemu operacyjnego z obrazu Ubuntu możemy pobrać zawołaniem:
docker run --interactive --tty --name ubuntu_dla_zabawy ubuntu:21.04
# tożsame z powyższym:
docker run -it --name ubuntu_dla_zabawy ubuntu:21.04
Gdzie:
  • --interactive
     oznacza tryb interaktywny, czyli STDIN oczekujące na wejście
  • --tty
    dzięki temu STDOUT jest widoczne
  • --name
    nazwa kontenera, po której możemy operować na kontenerze

W odpowiedzi pojawi się nam bashowa konsola roota obrazu, gdzie możemy zacząć działać, coś zainstalować, zbudować itp.
Za pierwszym razem zostanie nam ściągnięty obraz
ubuntu:21.04
, przez co będzie to chwilę trwało, natomiast mając ściągnięty obraz kolejne instancje tworzone są momentalnie.

Dostęp do wnętrza kontenera poprzez SSH

Doszliśmy do tego jak uruchomić obraz dockera, teraz warto sobie wejść na niego przez SSH. Jak pamiętamy mamy obecnie tylko użytkownika
root
, pamiętajmy też, że nie mamy jeszcze openssh, dlatego musimy wykonać kilka komend (poniżej zakładam, że chcemy się logować przez SSH na roota).
apt-get update && apt-get install -y ssh --no-install-recommends
echo 'root:111' | chpasswd # ustawienia hasła do logowania
sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
service ssh restart
ssh localhost # to na potrzeby testowania
Mając powyższe trzeba by pobrać adres naszej instancji, w tym celu wołamy:
docker inspect ubuntu_dla_zabawy

a następnie szukamy:
IPAddress
, albo po prostu wołamy:
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'  ubuntu_dla_zabawy

Mając adres łączymy się przez ssh:
ssh root@$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'  ubuntu_dla_zabawy)


Postawienie klienta SSH można zrobić prościej, wystarczy tylko jedna komenda, zamiast tego co powyżej. Aby więc szybko postawić kontener z gotowym klientem SSH wołamy:
docker run --rm -it --entrypoint /keygen.sh linuxserver/openssh-server


Przykład gotowego kontenera

Poniżej przedstawiono listingi dla przykładowej instancji kontenera. Warto powiedzieć, że w opisanym powyżej listingu numer
918db77597be
 to skrócone id_instancji oraz
postgres_tmp
 nazwa_kontenera.
docker ps
CONTAINER ID   IMAGE      COMMAND                  CREATED        STATUS        PORTS     NAMES
918db77597be   postgres   "docker-entrypoint.s…"   1 second ago   Up 1 second             postgres_tmp
Możemy też chcieć zobaczyć pełną uruchomioną komendę, wtedy dodajemy dodatkowy parametr wyłączający przycinanie:
docker ps --no-trunc
CONTAINER ID                                                       IMAGE      COMMAND                                                                CREATED          STATUS         PORTS     NAMES
918db77597beed9206cce66f25e06a149d4f557f0224a527621dd7b2e399a227   postgres   "docker-entrypoint.sh -c max_prepared_transactions=100 -c port=5432"   10 seconds ago   Up 9 seconds             postgres_tmp

Dostęp do stdout i stderr instancji kontenera
docker logs nazwa_kontenera/id_instancji # uwaga: wyświetli to wszystkie logi
docker logs nazwa_kontenera/id_instancji --tail 100 # wyświetli ostatnie 100 logów
docker logs nazwa_kontenera/id_instancji -f # będzie wyświetlać logi na bieżąco
docker logs nazwa_kontenera/id_instancji --since 2021-12-01T00:00:00Z --until 2021-12-31T23:59:59Z # logi w zakresie czasowym
docker logs nazwa_kontenera/id_instancji --since 1h --until 20m

przykład wszystkich informacji zwróconych przez instancje dockera uruchomienego poleceniem
docker inspect postgres_tmp
:
[
    {
        "Id": "918db77597beed9206cce66f25e06a149d4f557f0224a527621dd7b2e399a227",
        "Created": "2021-12-05T09:05:26.112508793Z",
        "Path": "docker-entrypoint.sh",
        "Args": [
            "-c",
            "max_prepared_transactions=100",
            "-c",
            "port=5432"
        ],
        "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": false,
            "Dead": false,
            "Pid": 39725,
            "ExitCode": 0,
            "Error": "",
            "StartedAt": "2021-12-05T09:05:26.555335177Z",
            "FinishedAt": "0001-01-01T00:00:00Z"
        },
        "Image": "sha256:a6cd86e1dfceceaf14408dbd29f6e5001296db8dbe2c34686a0432b5d102a869",
        "ResolvConfPath": "/var/lib/docker/containers/918db77597beed9206cce66f25e06a149d4f557f0224a527621dd7b2e399a227/resolv.conf",
        "HostnamePath": "/var/lib/docker/containers/918db77597beed9206cce66f25e06a149d4f557f0224a527621dd7b2e399a227/hostname",
        "HostsPath": "/var/lib/docker/containers/918db77597beed9206cce66f25e06a149d4f557f0224a527621dd7b2e399a227/hosts",
        "LogPath": "/var/lib/docker/containers/918db77597beed9206cce66f25e06a149d4f557f0224a527621dd7b2e399a227/918db77597beed9206cce66f25e06a149d4f557f0224a527621dd7b2e399a227-json.log",
        "Name": "/postgres_tmp",
        "RestartCount": 0,
        "Driver": "overlay2",
        "Platform": "linux",
        "MountLabel": "",
        "ProcessLabel": "",
        "AppArmorProfile": "docker-default",
        "ExecIDs": null,
        "HostConfig": {
            "Binds": null,
            "ContainerIDFile": "",
            "LogConfig": {
                "Type": "json-file",
                "Config": {}
            },
            "NetworkMode": "host",
            "PortBindings": {},
            "RestartPolicy": {
                "Name": "no",
                "MaximumRetryCount": 0
            },
            "AutoRemove": true,
            "VolumeDriver": "",
            "VolumesFrom": null,
            "CapAdd": null,
            "CapDrop": null,
            "CgroupnsMode": "private",
            "Dns": [],
            "DnsOptions": [],
            "DnsSearch": [],
            "ExtraHosts": null,
            "GroupAdd": null,
            "IpcMode": "private",
            "Cgroup": "",
            "Links": null,
            "OomScoreAdj": 0,
            "PidMode": "",
            "Privileged": false,
            "PublishAllPorts": false,
            "ReadonlyRootfs": false,
            "SecurityOpt": null,
            "UTSMode": "",
            "UsernsMode": "",
            "ShmSize": 67108864,
            "Runtime": "runc",
            "ConsoleSize": [
                0,
                0
            ],
            "Isolation": "",
            "CpuShares": 0,
            "Memory": 0,
            "NanoCpus": 0,
            "CgroupParent": "",
            "BlkioWeight": 0,
            "BlkioWeightDevice": [],
            "BlkioDeviceReadBps": null,
            "BlkioDeviceWriteBps": null,
            "BlkioDeviceReadIOps": null,
            "BlkioDeviceWriteIOps": null,
            "CpuPeriod": 0,
            "CpuQuota": 0,
            "CpuRealtimePeriod": 0,
            "CpuRealtimeRuntime": 0,
            "CpusetCpus": "",
            "CpusetMems": "",
            "Devices": [],
            "DeviceCgroupRules": null,
            "DeviceRequests": null,
            "KernelMemory": 0,
            "KernelMemoryTCP": 0,
            "MemoryReservation": 0,
            "MemorySwap": 0,
            "MemorySwappiness": null,
            "OomKillDisable": null,
            "PidsLimit": null,
            "Ulimits": null,
            "CpuCount": 0,
            "CpuPercent": 0,
            "IOMaximumIOps": 0,
            "IOMaximumBandwidth": 0,
            "MaskedPaths": [
                "/proc/asound",
                "/proc/acpi",
                "/proc/kcore",
                "/proc/keys",
                "/proc/latency_stats",
                "/proc/timer_list",
                "/proc/timer_stats",
                "/proc/sched_debug",
                "/proc/scsi",
                "/sys/firmware"
            ],
            "ReadonlyPaths": [
                "/proc/bus",
                "/proc/fs",
                "/proc/irq",
                "/proc/sys",
                "/proc/sysrq-trigger"
            ]
        },
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/65e8469709b15d6a76fbcbe2043cc90a2d68f3a53a105b39dcb34174bdfb82cb-init/diff:/var/lib/docker/overlay2/39ca13ad2c073bbe1d4ee7197917b44d2c5d6e57519f4f6a4784b53f4f2f19e1/diff:/var/lib/docker/overlay2/b80ecb598baa61c325cd6f9798487c5d5704b991dffeafcc1f43e0b8afc16d27/diff:/var/lib/docker/overlay2/ee39214669038c1f9f99ae27f0552ec1790bd7db9e8ea2ef29c7a8d26922c87e/diff:/var/lib/docker/overlay2/aa733eb5d5d012de24ce84239fd6a6448926c320ab0ce05e6a31d5104a308a7a/diff:/var/lib/docker/overlay2/6f6e56a278c8c84543e5e92d41166cae4f4377d5ee31b8c6cf0f7212ceb8f323/diff:/var/lib/docker/overlay2/dae4ac994edd81ffec298a5de4b918426b58a4b2c376fbbd19a4a95e7403d207/diff:/var/lib/docker/overlay2/01152ae4c246521d1d48b751afa471d7fae19cacc0f9833e97718a3a1a7d6dce/diff:/var/lib/docker/overlay2/b33fc10d8a163508fba4b5070c69107315e1758a97fcd20e2443690057646e3b/diff:/var/lib/docker/overlay2/4bcce04d2d6220b0ccca59c457963e867f531756f3e2a751b26589db0c3051af/diff:/var/lib/docker/overlay2/64c482e0edf1153645f1e9f8a7ece0e4e418b8170d6b906ba73ec3f9e77c4b1c/diff:/var/lib/docker/overlay2/c7d7485082f746e35735d34e6a540258ed40f998cc4586792ac433864b7ac469/diff:/var/lib/docker/overlay2/ec060a3b502da55694ae1b955abe9ea0452e3df6d0b32cdc4eccbeb94bc8ffe4/diff:/var/lib/docker/overlay2/ad241eabbab4257976fe25a1c6fcb77c90f62b905b19f2556a9c0012245d595d/diff:/var/lib/docker/overlay2/e7c7a9c794e34948917a7a92404795ec2cf74ad8f12e09be93a912453dcaabba/diff",
                "MergedDir": "/var/lib/docker/overlay2/65e8469709b15d6a76fbcbe2043cc90a2d68f3a53a105b39dcb34174bdfb82cb/merged",
                "UpperDir": "/var/lib/docker/overlay2/65e8469709b15d6a76fbcbe2043cc90a2d68f3a53a105b39dcb34174bdfb82cb/diff",
                "WorkDir": "/var/lib/docker/overlay2/65e8469709b15d6a76fbcbe2043cc90a2d68f3a53a105b39dcb34174bdfb82cb/work"
            },
            "Name": "overlay2"
        },
        "Mounts": [
            {
                "Type": "volume",
                "Name": "f7c392129a6560cb9b32576a2aa1ac532631b1dd3ee2d5882a3acd540aff8f76",
                "Source": "/var/lib/docker/volumes/f7c392129a6560cb9b32576a2aa1ac532631b1dd3ee2d5882a3acd540aff8f76/_data",
                "Destination": "/var/lib/postgresql/data",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ],
        "Config": {
            "Hostname": "agh-pc",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "5432/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "POSTGRES_USER=postgres",
                "POSTGRES_PASSWORD=mysecretpassword",
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/postgresql/13/bin",
                "GOSU_VERSION=1.12",
                "LANG=en_US.utf8",
                "PG_MAJOR=13",
                "PG_VERSION=13.1-1.pgdg100+1",
                "PGDATA=/var/lib/postgresql/data"
            ],
            "Cmd": [
                "-c",
                "max_prepared_transactions=100",
                "-c",
                "port=5432"
            ],
            "Image": "postgres",
            "Volumes": {
                "/var/lib/postgresql/data": {}
            },
            "WorkingDir": "",
            "Entrypoint": [
                "docker-entrypoint.sh"
            ],
            "OnBuild": null,
            "Labels": {},
            "StopSignal": "SIGINT"
        },
        "NetworkSettings": {
            "Bridge": "",
            "SandboxID": "edba2e11a769386f5d9000bb57cde4b43bb39ca2d253ec533ec7cb37abeb685e",
            "HairpinMode": false,
            "LinkLocalIPv6Address": "",
            "LinkLocalIPv6PrefixLen": 0,
            "Ports": {},
            "SandboxKey": "/var/run/docker/netns/default",
            "SecondaryIPAddresses": null,
            "SecondaryIPv6Addresses": null,
            "EndpointID": "",
            "Gateway": "",
            "GlobalIPv6Address": "",
            "GlobalIPv6PrefixLen": 0,
            "IPAddress": "",
            "IPPrefixLen": 0,
            "IPv6Gateway": "",
            "MacAddress": "",
            "Networks": {
                "host": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": null,
                    "NetworkID": "dc6ad81e1addd0ff7b524907c6555837bf32a458348d02efba106678c6c88735",
                    "EndpointID": "1f72587045b51043fc5b0dd9039c79e1035c05e88ceb07b7cd21d08608a9551a",
                    "Gateway": "",
                    "IPAddress": "",
                    "IPPrefixLen": 0,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "MacAddress": "",
                    "DriverOpts": null
                }
            }
        }
    }
]
Jak zapewne zauważyłeś wywołując
docker inspect nazwa_kontenera/id_instancji
 możemy podglądnąć  wszystkie parametry danej instancji kontenera.


Zagrożenia konteneryzacji z Dockerem

Nie jest tak, że zawsze należy używać dockera do wszystkiego. Kiedy jest to odradzane zostało opisane w stosownym artykule. Jednakże ja wychodzę z założenia, że warto przeczytać do końca ten artykuł i potem zdecydować czy warto pójść w dockery czy nie. Wszak dopóki czegoś nie znamy wiemy jak żyć bez tego, a odkrywszy możemy sobie ułatwić życie.

Docker mimo iż jest izolowany niesie pewne zagrożenia, najpopularniejszym jest możliwość zapchania zasobów kontenera (CPU, RAM, dysk, ilość procesów) jak i też możliwość zablokowania określonych portów. Ale jeśli dokonamy mapowania ścieżek systemu operacyjnego dla instancji kontenera to mogą one być wykorzystane przez złośliwe oprogramowanie lub nasze omyłkowe
sudo rm -rf /
 (taka sytuacja miała miejsce w pewnej firmie i backupy też usunęło). Poza tym docker ma dostęp do pełnych zasobów (o ile nie ograniczymy ich dla kontenera), w tym do RAMu, czyli może wpisać coś złośliwego do RAMu, dlatego też warto limitować dostęp do pamięci dla kontenera. Inne częste problemy z bezpieczeństwem dockera znajdują się w innym artykule.

Obraz dockera z instancji kontenera

Dzięki tej możliwości jak będziemy mieli już skonfigurowaną instancje kontenera dockera możemy ją przerobić na obraz. Aby to zrobić piszemy:
docker commit nazwa_kontenera/id_instancji nazwa_obrazu

co dla skonfigurowanego klienta SSH może wyglądać tak:
docker commit ubuntu_dla_zabawy ubuntu_ssh
#lub z uwzględnieniem wersji i kategorii:
docker commit ubuntu_dla_zabawy ubuntu_ssh/ubuntu:20.04
Taki obraz możemy zrzucić do pliku:
docker save -o nazwaPliku.tar nazwa_obrazu

czyli dla powyższego
docker save -o ubuntu_ssh_20.04.tar ubuntu_ssh/ubuntu:20.04

Obraz taki oczywiście można komuś wysłać i potem zainportować:
docker load -i plik.tar

W razie jeśli dysponujemy szybkimi maszynami, ale wciąż jeszcze wolnym/ograniczonym dostępem do internetu możemy się pokusić o kopiowanie z kompresją z którego to przeklejam kod:
docker save <image> | bzip2 | pv | ssh user@host docker load


Komunikacja kontenera ze światem

Powyżej dowiedzieliśmy się, że jest to środowisko izolowane, no ale co jeśli chcemy przekazać pewne dane z serwera goszczącego? Właśnie temu zagadnieniu będzie poruszony ten rozdział.

Mapowanie katalogów między systemem gospodarzem a systemem gościem

Robi się to przez dodanie argumentu
-v
 w następujący sposób:
-v katalog/gospodarza:/katalog/w/instancji/dockera
# alternatywnie:
--volume katalog/gospodarza:/katalog/w/instancji/dockera
# możemy też w formacie read-only:
-v katalog/gospodarza:/katalog/w/instancji/dockera:ro

Przykładowo możemy instancje uruchomić w następujący sposób:
docker run --rm --name ubuntu_na_jeden_numerek -v "$(pwd):/home:ro" --workdir /home  ubuntu:21.04 ls

Powyższa komenda uruchamia instancje obrazu
ubuntu:21.04
 o nazwie
ubuntu_na_jeden_numerek
 która na zmapowanym katalogu
$(pwd)
 w ścieżce roboczej
--workdir /home
 wykona komendę
ls
, po czym dzięki fladze
--rm
 przestanie istnieć po wykonaniu polecenia. A tak prościej: w ramach chwilowej instancji dockera zostanie w nim zawołana komenda
ls
.

Można ten mechanizm wykorzystać do skompilowania i uruchomienia projektu bez szkody dla systemu operacyjnego, ale wtedy zamiast
ls
 trzeba by było dołożyć komendy instalujące
g++
, dlatego też warto na potrzeby jednorazowej kompilacji skorzystać z obrazu, który już ma kompilator (bo np. sami takowy przygotowaliśmy, lub z gotowego).
Niestety jeśli byśmy chcieli oficjalny obraz wydany przez zweryfikowanego dostawcę to dla C++ na moment pisania tego artykułu mamy jedynie:
gdy jednak odznaczymy te dwie opcje rzuca się w oczy obraz przygotowany dla pewnego amerykańskiego uniwesrytetu, przy jego pomocy możemy skompilować dany kod w następujący sposób:
docker run -it --rm -v `pwd`:/data --workdir /data byubean/cs235-env:latest bash -c 'g++ -o main main.cpp && ./main ; rm -rf ./main'


Katalogów możemy mapować "niemalże dowolną ilość", czyli dopóki długość komendy nie przekroczy dopuszczalnej długości

Podobnym sposobem możemy skorzystać z gotowego, oficjalnego obrazu Pythona, który wołamy w następujący sposób:
docker run -it -v "$PWD":/usr/src/app python:latest python /usr/src/app/skrypt.py


Volumeny - lepszy mechanizm od mapowania katalogów

Bardziej zalecanym mechanizmem od mapowania katalogów są volumeny, ich zalety nad mapowaniem katalogów są wymienione w oficjalnej dokumentacji, poniżej kilka przykładowych komend:
docker volume create artykul # tworzenie volumenu
docker volume ls # listowanie istniejacych volumenow
docker volume rm  artykul # usuwanie volumenu

# mozemy tez zobaczyc szczegóły volumenu (w tym gdzie się znajdują dane):
docker volume inspect artykul
[
    {
        "CreatedAt": "2021-12-12T11:39:32+01:00",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/artykul/_data",
        "Name": "artykul",
        "Options": {},
        "Scope": "local"
    }
]
Powyższy volumen możemy podpiąć podając jego nazwę np.:
-v nazwa_volumenu:/katalog/w/instancji/dockera
# alternatywnie:
--volume nazwa_volumenu:/katalog/w/instancji/dockera
# możemy też w sposób bardziej human-readable:
--mount source=nazwa_volumenu,destination=/katalog/w/instancji/dockera,readonly

Mapowanie urządzeń

Zamontowany penrdive można zmapować z kontenerem, ale jest też inny sposób.


Mapowanie portów i konfiguracja sieci między kontenerami a gospodarzem

Pierwszym zagadnieniem w tym temacie jest konfiguracja sieci między instancją kontenera a systemem gospodarza, robimy ją przy pomocy
--network
. Przykładowo jeśli chcemy zmapować instancje kontenera tak jakby wszystkie usługi sieciowe były uruchomione na systemie gospodarzu to używamy opcji
--network=host
. Mamy też inne możliwości, ale zagadnienie jest na tyle rozległe i skomplikowane, że odsyłam do dokumentacji.

Bardzo często nasza aplikacja udostępnia pewne porty, część z nich chcemy wystawić na zewnątrz, część przekierować, a część aby była widoczna tylko dla komputera gospodarza lub dla wszystkich. Przykładowo jeśli napiszemy serwer www w WT możemy wystawić port w momencie uruchamiania (
docker run ...
) w taki spobów:
-p 8080:8080 # zmapuje port 8080 gospodarza z portem 8080 instancji - wtedy każdy kto "uderzy" w port 8080 naszego komputera trafi bezpośrednio do portu w kontenerze
-p 127.0.0.1:8080:8080 # jw. ale tylko host może "uderzać" do portu w kontenerze
--publish 12345:8080 # zmapuje port 12345 gospodarza z portem 8080 instancji, jest to forma bardziej human-readable niż -p
-p 12345:8080/tcp # zmapuje port 12345 gospodarza z portem 8080 instancji, zaznaczając że jest to port protokołu TCP
-P # mapuje wszystkie porty wystawione przez kontener do losowych wolnych portów gospodarza


Sterowanie kontenerem przez zmienne środowiskowe

Jest to mechanizm stosowany do sterowania programem w C++ jako alternatywa do argumentów uruchomienia programu. Zmienne środowiskowe przekazujemy komendą
-e
 w następujący sposób:
docker run ... -e KEY=Value ...
# alternatywnie:
docker run ... --env KEY=Value ...
Mając wiele zmiennych można je przekazać przez plik ze zmiennymi środowiskowymi stosując argument:
--env-file ścieżka/do/pliku

Przykładowy plik ze zmiennymi środowiskami wygląda tak:
# komentarz
COMPILER_FLAGS=-Wall -Wextra -pedantic -Werror
LINKER_FLAGS=-lboost_system -lboost_filesystem
CYTAT1=Pamiętajcie: "Musicie od siebie wymagać, nawet gdyby inni od was nie wymagali." - Św. Jan Paweł II
CYTAT2=To też warto pamiętać: "Najbardziej twórczą ze wszystkich prac jest praca nad sobą, która pozwala odnajdywać urok młodości. Nie ma większego bogactwa w narodzie nad światłych obywateli." - Św. Jan Paweł II
ich czytanie można szybko sprawdzić:
docker run --rm -it --env-file .env ubuntu:21.04 bash -c 'echo $CYTAT2'

Zmienne środowiskowe można przekazać chociażby dla oficjalnego obrazu Ubuntu umożliwiające wybór języka.


Dockerfile dla programisty C++


W poprzednich rozdziałach dowiedzieliśmy się coś o obrazach, o tworzeniu instancji kontenera z tychże obrazów, dowiedzieliśmy się również, że po wejściu do instancji kontenera możemy zainstalować potrzebne zależności. W ten sposób można przygotować własny obraz, który po wyeksportowaniu do pliku można udostępnić. Jednakże taki kontener w pliku zajmuje trochę miejsca. Istnieje dużo lepsza możliwość! Wystarczy przygotować plik Dockerfile z tekstowymi instrukcjami jak zbudować własny kontener od zera. Zawiera on instrukcje jak zbudować obraz dockera, jakie pliki przenieść, jakie komendy wywołać, czy w końcu jakie porty wystawia i jaka jest komenda uruchomionej instancji kontenera (tzw. entrypoint).

Jako, że niniejszy portal jest poświęcony językowi C++ skupię się na pisaniu prostych aplikacji w C++ i przygotowywaniu ich na dockerze, oraz tego co z funkcjonalności dockera jest nam potrzebne. Artykuł ten jest zainspirowany wystąpieniem na konferencji CppNow w roku 2018 (slajdy). Aby nie pisać tylko teoretycznie, dobrym przykładem programu w C++, który jest dobrze zdockeryzowany jest Hyperledger Iroha, który ponadto jest tak zrobiony, że można ją zbudować i uruchomić również bez dockera.

Napiszmy więc przykładowy plik
Dockerfile
 dla kompilatora g++:
FROM ubuntu:21.04

#### instalacja zaleznosci:
# RUN odpowiada za uruchomienie komendy podczas tworzenia obrazu, pamiętajmy, że mamy roota
RUN apt-get update && apt-get install -y --no-install-recommends g++
# flaga --no-install-recommends jest bardzo przydatna w instalacjach nieinteraktywnych, bez niej musieli byśmy podać pewne dane z konsoli, np. wybrać strefę czasową

WORKDIR /home/build/

ENTRYPOINT ["g++", "-Wall", "-Wextra"]
Powyższy plik
Dockerfile
 pobiera obraz Ubuntu:21.04 (
FROM ubuntu:21.04
), na nim instaluje g++ (
RUN apt-get update && apt-get install -y --no-install-recommends g++
), następnie ustawia ścieżkę roboczą (
WORKDIR /home/build/
), która jak nie istnieje zostanie utworzona. Ostatnim elementem
Dockerfile
 jest wystawienie tzw. entrypoint, czyli komendy uruchamianej w ramach dockera wraz z argumentami. Obraz z tego możemy utworzyć (komenda wołana w tym samym katalogu co
Dockerfile
):
docker build --tag gplusplus:1 .
# alternatywnie jeśli używamy innej nazwy niż Dockerfile
docker build --tag gplusplus:10.1 --file Dockerfile_v1 .
W ramach obrazu mamy już kompilator, więc aby skompilować plik wystarczy zawołać (dla skompilowania pliku
main.cpp
, który znajduje się w aktualnym katalogu
pwd
):
docker run -it --rm -v "$(pwd):/home/build/"  gplusplus:1 main.cpp

Powyższe skompiluje w ramach instancji dockera plik źródłowy, natomiast jak chcemy uruchomić wynikowy plik już robimy to na komputerze gospodarza, aby móc uruchomić to również w ramach dockera musimy podmienić entrypoint:
docker run -it --rm -v "$(pwd):/home/build/" --entrypoint "./a.out" gplusplus:1
# możemy też podać argumenty do ./a.out:
docker run -it --rm -v "$(pwd):/home/build/" --entrypoint "./a.out" gplusplus:1 argumenty do funkcji main czyli odczytywalne przez argv i argc

Jak widzimy instancje kontenera są usuwane od razu po wykonaniu (flaga
--rm
), natomiast jakbyśmy chcieli usunąć obraz wołamy:
docker rmi gplusplus


Teraz spróbujmy ciut bardziej zaawansowany
Dockerfile
, który będzie zawierał jeszcze cmake'a:
FROM ubuntu:21.04

MAINTAINER Grzegorz Prowadzacy

#### instalacja zaleznosci:
# RUN odpowiada za uruchomienie komendy podczas tworzenia obrazu, pamiętajmy, że mamy roota
RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y --no-install-recommends build-essential cmake
# flagi -y --no-install-recommends jest bardzo przydatna w instalacjach nieinteraktywnych, bez niej musieli byśmy podać pewne dane

#### Przygotowanie plikow
RUN mkdir -p /home/trunc/build

# kopiowanie katalogu gospodarza do katalogiem hosta, dzięki temu zmiany nie będą widoczne po usunięciu instancji kontenera
COPY . /home/trunc/

WORKDIR /home/trunc/build/

#### cmake generuje co trzeba
RUN cmake ..
RUN make -j4

# wystawienie komendy na zewnatrz:
CMD make -j4 && /home/trunc/build/bin/lab
Powyższy plik
Dockerfile
 może posłużyć do wytworzenia obrazu:
docker build --tag gplusplus:10.3 .
# alternatywnie jeśli używamy innej nazwy niż Dockerfile
docker build --tag gplusplus:10.3 --file Dockerfile.newest .
Po skutecznym zbudowaniu obrazu możemy jednokrotnie uruchomić rezultat (uruchomienie
make
 i uruchomienie aplikacji):
docker run --rm gplusplus
 - po uruchomieniu instancja kontenera zostanie usunięta (flaga
--rm
). A jakbyśmy chcieli usunąć obraz wołamy:
docker rmi gplusplus


Zaletą dockera jest to, że nawet jak wiele razy będziemy musieli budować obraz, to nie będziemy za każdym razem czekali od samego początku, gdyż pewne rezultaty są cachowane, poprzez tworzenie wielu warstw dla komend, dzięki temu zyskamy na czekaniu. Nawet można skorzystać z cachowania zewnętrznego. Użycie cache'a też można wyłączyć przez argument
--no-cache=true
. Aby dobrze wykorzystać ten mechanizm warto zacząć nasz Dockerfile od dłużej trwających komend, które rzadko będziemy zmieniać.

Powyższy Dockerfile jest dość skąpy, zwłaszcza w sytuacji gdy chcemy go lepiej skonfigurować, w tym celu możemy użyć komend
ARG
, która jest zmienną w trakcie budowania obrazu, a w momencie uruchamiania instancji już jej nie ma, oraz
ENV
, która jest dostępna zarówno w trakcie budowania jak i uruchamiania instancji, ale ustawić ją można bezpośrednio tylko w momencie uruchamiania. Aby to zobrazować zmodyfikujmy powyższy plik:
FROM ubuntu:21.04

MAINTAINER Grzegorz Prowadzacy

ARG ADDITIONAL_DEPENDENCIES
ARG THREADS_BUILDTIME=4

ENV THREADS $THREADS_BUILDTIME
ENV BINARY binary

LABEL reason.of.the.file="cpp0x.pl" main.programming.language="c++"

#### instalacja zaleznosci:
RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y --no-install-recommends build-essential cmake $ADDITIONAL_DEPENDENCIES

RUN echo "******** g++ --version:" $(g++ --version | head -n1)

#### Przygotowanie plikow i katalogow
RUN mkdir -p /home/trunc/build
COPY . /home/trunc/

WORKDIR /home/trunc/build/

RUN cmake ..
RUN make -j${THREADS_BUILDTIME}

CMD make -j${THREADS} && /home/trunc/build/bin/${BINARY}
Aby zbudować obraz z wykorzystaniem opcji konfiguracyjnych przekazujemy parametry po fladze
--build-arg
 przykładowo tak:
docker build --tag gplusplus . --build-arg  'ADDITIONAL_DEPENDENCIES=libgtest-dev git' --build-arg THREADS_BUILDTIME=8
Natomiast w trakcie tworzenia instancji kontenera przekazujemy argumenty flagą:
-e
 lub
--env
 np. w taki sposób:
docker run --rm -e BINARY=lab -e THREADS=100  gplusplus


Z innych możliwości dockerfile (więcej w dokumentacji):
ENTRYPOINT
 - jest podobne do CMD, z tą różnicą, że domyślne polecenie nie może być wtedy nadpisane (chyba, że użyjemy flagi
--entrypoint
).
EXPOSE port
EXPOSE port/protokół
 - Jest to opcja, która tak naprawdę jest tylko informacją na temat pliku portów wystawionych przez instancje kontenera, ale w praktyce należy uzyć jeszcze -p
LABEL
 - pewna informacja, którą można pobrać przez docker image inspect --format='' myimage
STOPSIGNAL
 - sygnał stopujący kontener (nazwa, lub id) w razie niepodania SIGTERM
HEALTHCHECK
 - komenda przy pomocy której się sprawdza czy wszystko w porządku z kontenerem
SHELL
 - zmiana powłoki od danego momentu
USER
 - ustawia innego użytkownika i opcjonalnie grupę do wykonywania dalszych komend
ONBUILD
 - opcja, w ramach której możemy dodać pewne dodatkowe komendy wykonane w momencie, gdyby nasz obraz poslużył do zbudowania innego obrazu
VOLUME
 - tworzy punkt montowania w obrazie, który w razie czego zostanie utworzony jako oddzielne volume??????

Jak sobie nie zaśmiecić dockerfile - czyli jeden obraz buduje drugi (tzw. multistage-build)

Multistage build to bardzo wygodny mechanizm pozwalający aby sobie nie zaśmiecić obrazu dodatkowymi pakietami, które potem będziemy chcieli usunąć. Mechanizm ten stosujemy przez użycie wielokrotnie klauzuli
FROM
.
Przykładowo aby kompilować pliki w C++ w ramach jednej instancji dockera, a uruchamiać w ramach innej możemy utworzyć następujący plik:

FROM ubuntu:21.04

MAINTAINER Grzegorz Prowadzacy

ARG ADDITIONAL_DEPENDENCIES
ARG FILES_TO_COMPILE
ARG BINARY=binary

RUN apt-get update && apt-get install -y --no-install-recommends g++ $ADDITIONAL_DEPENDENCIES

WORKDIR /home/build/

COPY . .

RUN g++ $FILES_TO_COMPILE -o $BINARY

###############################################################
FROM ubuntu:21.04

ARG BINARY=binary
ENV BINARY ${BINARY}

WORKDIR /home/
COPY --from=0 /home/build/${BINARY} /home/

CMD /home/${BINARY}
Przykładowe zbudowanie obrazu i uruchomienie (dla pliku
main.cpp
):
docker build --tag gplusplus:multi . --build-arg --build-arg FILES_TO_COMPILE=main.cpp
docker run -it --rm gplusplus:multi

Uruchamianie aplikacji okienkowej z dockera

Jest to jedno z odradzanych zastosowań dockera, niemniej jednak jest to możliwe. Niestety wiele zależy od systemu goszczącego i goszczonego, przykładowo dla Ubuntu na Ubuntu przez dockera można ten efekt osiągnąć tak:
docker run --rm -it --rm -e DISPLAY=$DISPLAY -v /tmp/.X11-unix/:/tmp/.X11-unix/ --name ubuntu_dla_zabawy ubuntu:21.04

Opis jak coś takiego zrobić na windowsie w oddzielnym artykule.

Jeśli chcemy bardziej przenośne rozwiązanie to możemy się pokusić o użycie protokołu Nomachine. Konfiguracja tego jest dość zaawansowana i jest jej poświęcony oddzielny artykuł.

CppDock narzędzie tworzące obrazy dockera z zależnościami w konkratnych wersjach
Pod koniec  wystąpienia konferencyjnego, będącego inspiracją dla tego artykułu zostało przedstawione narzędzie CppDock, które w oparciu o plik konfiguracyjny w formacie JSON wygeneruje obraz dockera zawierający wszystkie zależności C++owe w konkretnych wersjach. Po przejrzeniu się temu rozwiązaniu przyznaje, że pomysł szczytny, przydatność jest, jednakże lista przepisów, jak zbudować daną bibliotekę, jest bardzo ograniczona, dlatego wg mnie lepiej użyć managera zależności cieszącego się większą społecznością np. vcpkg, który to jeszcze załatwia multiplatformowość.

Narzędzia wspierające zarządzanie aplikacjami na wielu dockerach równocześnie

Poznaliśmy podstawy ogólnego zastosowania. Jednakże pojawiają się pytania jak używać tego gdy m.in. chcemy zarządzać dużą ilością kontenerów. Poniżej popularne narzędzia, o których warto wiedzieć

Docker-compose

Nauczyliśmy się używać Dockera, widzimy tego zalety, ale pozostaje pytanie co jeśli chcemy aby wiele instancji ze sobą rozmawiało, przykładowo: aplikacja, baza danych, oraz command-line do aplikacji. Wtedy musieli byśmy mieć trzy obrazy i uruchamiać je w odpowiedniej kolejności pilnując aby wszystkie dane konfiguracyjne były jednolite. W takich sytuacjach przydaje się narzędzie
docker-compose
, które to zarządza kilkoma instancjami równocześnie pilnując wszystkiego, a konfigurujemy przy pomocy plików YAML.

Zasadniczo narzędzie ma podobne możliwości jak Dockerfile, czyli konfigurowanie argumentów uruchomienia, zmiennych środowiskowych, sieci, a poza tym możliwość wskazania, który kontener wymaga którego innego do uruchomienia.

Przykładowy plik ocker-compose możemy zobaczyć w projekcie Hyperledger Iroha, którego treść znajduje się tutaj:
version: '3'

services:
  node:
    image: hyperledger/iroha:develop-build
    ports:
      - "${IROHA_PORT}:50051"
      - "${IROHA_TLS_PORT}:55552"
      - "${DEBUGGER_PORT}:20000"
    environment:
      - IROHA_POSTGRES_HOST=${COMPOSE_PROJECT_NAME}_postgres_1
      - IROHA_POSTGRES_PORT=5432
      - IROHA_POSTGRES_USER=iroha
      - IROHA_POSTGRES_PASSWORD=helloworld
      - CCACHE_DIR=/tmp/ccache
    user: ${U_ID:-0}:${G_ID:-0}
    depends_on:
      - postgres
    tty: true
    volumes:
      - ../:/opt/iroha:delegated
      - ccache-data:/tmp/ccache:delegated
    working_dir: /opt/iroha
    cap_add:
      - SYS_PTRACE
    security_opt:
      - seccomp:unconfined

  postgres:
    image: postgres:9.5
    environment:
      - POSTGRES_USER=iroha
      - POSTGRES_PASSWORD=helloworld
    command: -c 'max_prepared_transactions=100'

volumes:
  ccache-data:
Jak widzimy w powyższym mamy tworzone volume, oprócz tego bazę danych, a następnie węzeł blockchain Hyperledger Iroha.

Mając powyższy plik
docker-compose.yml
 możemy zarządzać wszystkimi instancjami w następujący sposób:
docker-compose up -d # uruchamia w trybie detached wszystko z pliku
docker-compose up -d -f docker-compose.yml # jw. ale z podaniem pliku
docker-compose down # zatrzymuje
docker-compose rm # usuwa instancje (muszą być zatrzymane)
docker-compose rm -f # jw. zatrzymuje, jeśli nie są zatrzymane
docker-compose down -v --rmi all --remove-orphans # czyści wszystkie pozostałości po tym co z danego pliku powstało

Docker Swarn - zarządzanie dużą ilością konrenerów dockera

Docker Swarn jest to rozwiązanie klastrowe zarządzające dużą ilością kontenerów dostarczane wraz z Dockerem. Posiada własne api, dobrze jest zintegrowane z Dockerem i docker-compose. Niestety pewne funkcjonalności wymagają wersji Enterprice. Jest to narzędzie mniej popularne niż niżej opisany Kubernetes, mimo iż jest prostrze lecz posiada mniej możliwości. Osoby zainteresowane dowiedzeniem się więcej odsyłam do oddzielnego atykułu opisującego jak skonfigurować i uruchomić Docker Swarn na wielu serwerach.


Kubernetes

Kubernetes jest to platforma open-source do zarządzania kontenerami. Jest ona na tyle popularna, że wiele dostawców serwerów wirtualnych (ang. VPS) zawierają wsparcie i instrukcje jak uruchomić na ich serwerach klaster Kubernetes. W porównaniu z Docker Swarn narzędzie Kubernetes posiada znacznie więcej możliwości, bardziej szczegółowe porównanie tych dwóch narzędzi w artykule: Swarn vs Kubernetes.

Alternatywy dockera

Docker jest najpopularniejszym narzędziem do konteneryzacji.

Dla użytkowników istnieją alternatywy (lub lista oprogramowania na stronie Wikipedii) m.in.: OpenVz, LDC (Linux Containers), ZeroVM.

Spośród alternatyw na szczególną uwagę zasługuje podman, który jest na tyle kompatybilny z dockerem, że można go używać jako
alias docker=padman
. Zaletą Podmana jest, że nie trzeba używać go jako użytkownik z uprawnieniami. Za wadę należy zuznać fakt, że niestety można go używać jedynie na Linuxie.

Przykładowo poniżej pokazano w jaki sposób pokstawić bazę danych postgres. Zwróć uwagę, że opcje polecenia są zgodne z tymi użytymi przy poleceniu docker.
podman run --name przykladowy_postgres \
    -e POSTGRES_USER=uzytkownik \
    -e POSTGRES_PASSWORD=haslo \
    --network=host \
    -d \
    postgres

Bibliografia + literatura uzupełniająca


Autorzy artykułu

Autor Zakres zmian Afiliacja
Bazior Grzegorz Utworzenie artykułu Pracownik AGH w Krakowie
Paweł Król Korekta Pracownik Politechniki Krakowskiej