Több konténer együttese

Amikor több konténert együttesen szeretnénk kezelni, a Docker Compose eszköztárával definiálhatjuk és automatizálhatjuk a teljes alkalmazás infrastruktúráját egy YAML fájlban. Ez a módszer rendkívül hasznos az egyes szolgáltatások közötti kommunikáció kialakításában és a függőségek kezelésében is. Ebben a fejezetben bemutatjuk, hogyan építhetünk fel komplex, több összetevőből álló rendszereket úgy, hogy azok elszeparált konténerek együtteseként működjenek. Ehhez megismerjük a Docker Compose alapjait, az ehhez szükséges fájlstruktúrákat, és gyakorlati példákon keresztül próbáljuk ki a többkonténeres alkalmazások felépítését. A fejezetben az alábbiakról lesz szó:

  • A docker-compose elvi működése.

  • A docker-compose fájl tartalma, felépítése és szabályai.

  • A docker-compose használata és különböző paraméterei.

  • Gyakorlati példák.

Szükséges eszközök:

Windows, MacOS vagy Linux operációs rendszerű számítógép, telepített Docker szoftverrel.

Feldolgozási idő:

kb. 3 óra. Gyakorlati feladatok megoldására további 1 óra.

Az eddigiek alapján már szinte tetszőleges konténert fel tudunk építeni, vagy már kész konténereket is használatba tudunk venni. Egy feladat megoldásához a gyakorlatban viszont nem mindig elég egyetlen konténert használni. Egy website-nak a webszerver mellett adatbáziskezelő-rendszerre is szüksége lehet, és a megoldást nem a minden alkalmazást „egybegyúró” komplex konténerek, hanem elemi konténerek együttműködése jelenti. Ebben a felépítésben tehát a webalkalmazást futtató konténer mellett egy másik, az adatbáziskezelőt működtető konténert is el kell indítanunk és biztosítanunk kell a kommunikációt is köztük. A konténereket futtató host gép tehát nem egyetlen nagy adatbázis motort működtet, amely minden webkonténer számára adatbázis szolgáltatást nyújt, hanem minden egyes webhez önálló, konténerben futó adatbáziskezelő tartozik.

Konténerek együttese

Konténerek együttese

Felmerülhet a kérdés, hogy miért nem egyetlen konténerben tároljuk az összes olyan komponenst, amely egy adott szolgáltatás működtetéséhez szükséges összes összetevőt egyben tartalmazza? Miért nem egyetlen konténer tartalmazza a webszervert és az adatbázist is? A választ a konténerek tervezési architektúrája adja: az egyes szolgáltatások önálló konténerbe helyezésével az egyes alkotóelemek könnyen cserélhetők, frissítésük esetén önálló egységet alkotva nincsenek kihatással a rendszer más részeire.

Egy komplex rendszert alkotó konténerek együttesét multi-konténereknek nevezik. Ebben az egyes konténerek egymással is kommunikálnak, ehhez azonban egy nyilvánosan nem elérhető, csak a konténerek közt működő belső hálózatot használnak. Az együttműködő konténerek közötti belső kapcsolatokat ezért deklarálni kell. Mivel a Dockerfile csak egyetlen konténer leírására szolgál, ezért egy több konténert tartalmazó egység leírását egy másik típusú fájlban írjuk le, a felépítésükre pedig a docker-compose parancs szolgál majd.

Amikor egy több konténer együtteséből álló konténer együttest kell indítanunk, a docker-compose programot kell használnunk. Ez alapértelmezésben egy docker-compose.yaml nevű fájlt keres, és az ebben leírtak szerint építi fel az egyes konténereket és alakítja ki kapcsolataikat. Ha esetleg nem találkoztál még .yaml fájllal, ezek a .json fájlokhoz hasonló strukturált fájlok, amelyek egy sajátos szintaxis mentén adatok szervezett leírását teszik lehetővé. Bár ezek az egyszerű szövegfájlok tetszőleges text editorral szerkeszthetők, fontos szerepe van a behúzások helyes írásának – ez a .yaml fájlok szerkesztésének egyik neuralgikus pontja. A helyzetet tovább nehezíti, hogy a docker-compose.yaml fájlnak a docker fejlődésével több verziója is megjelent – ezek sajnos nem feltétlenül kompatibilisek egymással, így a .yaml fájl első sora rendszerint az alkalmazott verzió számát írja le. Azt, hogy az éppen rendelkezésre álló docker-compose program melyik .yaml verziót támogatja, a dokumentáció tartalmazza.

Kezdjünk megint egy egyszerű példával! Az első docker-compose.yaml fájlunk csak egyetlen konténert definiál majd, ebben először a .yaml fájl előállítására koncentrálunk. A későbbiekben ezt fogjuk újabb és újabb konténerekkel bővíteni. Először tehát készítsünk egy konténert egy egyszerű PHP alkalmazással, amelyhez használjuk a Php-be épített webszervert! A web könyvtárunk helyezkedjen el a konténeren kívül, a webroot mappában! A projekt szerkezete így az alábbi lesz:

php-webserver
  ├── webroot
  │  ├── index.php
  ├── docker-compose.yaml
  └── run

A webroot/index.html fájl most is csak egyetlen index.php fájlt tartalmaz, amely a már ismert phpinfo() függvénnyel a használt PHP verziószámát és kapcsolódó információkat jelenít meg:

<?php
phpinfo();
?>

A konténer felépítésére szolgáló docker-compose.yaml tartalma az alábbi lesz:

version: "3.0"
services:
  php:
    container_name: columbo.uni-eszterhazy.hu_www
    image: php:8.1
    ports:
      - 8080:80
    command: php -S 0.0.0.0:80 -t /var/www/html
    volumes:
      - ./webroot:/var/www/html

Ebben az egyes elemek jelentése a következő:

version:

A docker-compose.yaml verziószáma, ez példánkban 3.0.

services:

Ebben a blokkban soroljuk fel a kialakítandó konténereket. Minden konténert névvel kell ellátni, és a leírását egy egységgel (tabbal) beljebb kell kezdeni.

php:

A létrehozandó konténer neve, amelyet egy kettőspont követ. A példában egyetlen szolgáltatást (service-t) definiáltunk, amelynek a php nevet adtuk.

container_name:

az egyes konténereket el kell nevezni, amit ez a direktíva tesz lehetővé. Bár a név ezen a ponton nem tűnik túlságosan fontosnak, egy olyan szerver esetén, ami számos konténert futtat, ez teheti egyértelművé, hogy melyek tartoznak egy szolgáltatáshoz. A webszervert futtató konténer neve végződhet pl. _www-re, az adatbázis szerveré _mysql-re. Így az egy szolgáltatáshoz tartozó konténerek üzem közben is egyértelműen azonosíthatók, áthelyezésükkor, törlésükkor a módosítandó rendszerelemek egyértelműen meghatározhatók. A futó konténerek nevei most is a docker ps paranccsal jeleníthetők meg:

CONTAINER ID   IMAGE     COMMAND                  CREATED              STATUS              PORTS                  NAMES
05d37de60c5a   php:8.1   "docker-php-entrypoi…"   About a minute ago   Up About a minute   0.0.0.0:8080->80/tcp   columbo.uni-eszterhazy.hu_www
image:

ez a direktíva határozza meg, hogy melyik kész, a DockerHubról származó konténert szeretnénk felhasználni. Ez példánkban a php 8.1-es változata, amit a weblap kimenete is megmutat majd.

port:

Ahogyan az a Hálózatkezelés a konténerekben c. fejezetben már láttuk, a konténerek hálózati kommunikációjának engedélyezéséhez meg kell adnunk, hogy a konténer melyik porton fogadjon kéréseket, és azokat melyik belső portra továbbítsa. A 8080:80 beállítás azt eredményezi, hogy a „külvilágtól” a 8080-as porton jövő kéréseket a konténer belsejében működő alkalmazás 80-as portjára kell továbbítani. Ezen a porton a Php-be épített webszerver „hallgatózik”, az oda küldött kéréseket fogadja, feldolgozza, és a megfelelő tartalommal válaszol.

command:

a konténer elindítása után ezt a programot kell elindítani. A docker-compose.yaml fájlban nem kell korábban látott a tömbszerű megadást használni, a parancs és paraméterei egymás után sorolhatók fel. A Php esetében a -S-t követően a webszerver által használni kívánt IP cím és portszám szerepel, a -t után pedig a web fájlokat tartalmazó könyvtár neve olvasható. A 0.0.0.0 azt jelenti, hogy a szolgáltatás minden rendelkezésre álló IP címen elérhető lesz, használata pedig azért célszerű, mert így nem kell megadni azt a jövőbeni, jelenleg még ismeretlen IP címet, amit majd a konténer futtatásakor a felhőben kapni fog.

volumes:

itt lehet leírni a konténer egy belső könyvtárának, és a fizikai fájlrendszer könyvtárainak vagy fájljainak összerendelését. Mindkét paraméter megadásakor abszolút elérési utat kell használni. A példa webszervere ennek az összerendelésnek köszönhetően képes hozzáférni a fizikai fájlrendszerben levő webroot könyvár tartalmához.

A konténer felépítését a már említett docker-compose up paranccsal lehet elvégezni, amennyiben démonként, a háttérben szeretnénk azt futtatni, egy -d kapcsolóval kell azt kiegészíteni. Ekkor értelemszerűen a konténer üzenetei sem látszanak majd, így ezt a módot inkább a production változatban célszerű választani.

koczka@columbo:~/docker-php$ docker-compose up
Starting columbo.uni-eszterhazy.hu_www ... done
Attaching to columbo.uni-eszterhazy.hu_www
columbo.uni-eszterhazy.hu_www | [Thu May 16 14:40:09 2024] PHP 8.1.28 Development Server (http://0.0.0.0:80) started

A futtatáshoz szükséges parancsokat esetleg egy scriptben is összegezhetjük, ez a későbbiekben kényelmesebbé teszi a fejlesztés alatt működő konténer újraindítását azzal, hogy az indítást megelőzően leállítja a korábbi futó változatot is.

#!/bin/bash
docker stop columbo.uni-eszterhazy.hu_www
docker rm columbo.uni-eszterhazy.hu_www
for I in $(docker image ls -q)
  do docker image rm $I --force
done
docker-compose up

A következő példában ezt a web konténert kiegészítjük egy mySql adatbázis szerverrel is.

Php szerver mySql adatbázissal

A docker-compose igazi ereje tehát azokban az esetekben mutatkozik meg, amikor egy szolgáltatást több konténer együttes működése valósít meg. Ilyenkor a docker-compose.yaml nem csak egy service blokkot tartalmaz, hanem többet is: minden egyes szolgáltatást megvalósító konténerhez tartozik majd egy.

A példánkban a Php és mySql együttműködését valamelyest megnehezíti a már bemutatott korlát: a php értelmező a mySql adatbázisokhoz csak egy extensionön (bővítményen) keresztül tud kapcsolódni, emiatt a Dockerhubból letöltött változat önmagában most sem lesz használható. Ezért most a kész konténer helyett egy Dockerfile-lal felépített egyedi konténert fogunk alkalmazni, ezt ugyanúgy fogjuk felépíteni, ahogyan azt már a Saját konténer készítése fejezetben láttuk. Az egyetlen különbséget a konténer .yaml fájlba történő beépítése jelenti.

A projektünk struktúrája most az alábbiak szerint épül fel. A webroot könyvtárban továbbra is a webszerver fájljait tároljuk. A mySql-képes php motor előállítását egy alkalmas Dockerfile alapján végezzük, amit a php könyvtárban helyezünk el. A mySql adatbázis fájljait nem tárolhatjuk a konténer belsejében, hiszen akkor a konténer eldobásakor és újraépítésekor az abban tárolt adatok elvesznének – ezért ezeket a host gép fájlrendszerében, a mysql-data könyvtárban helyezzük el. (Ezt nem kell manuálisan létrehozni, a folyamat során automatikusan elkészül majd.) Az initdb.sql a kezdeti adatbázis tartalmát definiálja majd, a neve tetszőlegesen megválasztható. A docker-compose.yaml pedig a projekt könyvtár gyökerébe kerül.

php-webserver
  ├── webroot
  │   └── index.php
  ├── php
  │   └── Dockerfile
  ├── mysql-data
  ├── initdb.sql
  ├── docker-compose.yaml
  └──run

Először foglalkozzunk a mySql-képes PHP értelmező előállításával! Az ehhez szükséges Dockerfile felépítése meglehetősen egyszerű, de érdemes odafigyelni arra, hogy azt az erre a célra létrehozott php könyvtárban helyezzük el. Most a php 8.1-es változatából indulunk ki, és az abban levő docker-php-ext-install programmal elvégezzük a mysqli extension hozzáadását. Az így létrejövő konténerben működő php interpreter már képes lesz kapcsolódni egy mySql adatbázishoz.

FROM php:8.1
RUN docker-php-ext-install mysqli

A docker-compose.yaml most két konténert definiál, a már ismert php -t és a mysql-t. A php esetében viszont most nem használhatjuk a korábban látott image direktívát, mert az nem adná hozzá a mysql extensiont az értelmezőhöz. Ehelyett most a build: php direktívát használjuk, ennek hatására a build folyamat belép a php könyvtárba, és az ott található Dockerfile könyvtár alapján felépíti a php konténert.

A mySql szerver konténerének előállítása is tartalmaz néhány új elemet. A konténer neve, az image és a használt port jelentése megegyezik az eddig látottakkal. Az image beállítására itt is szükség van: a konténer belsejében szereplő /var/lib/mysql könyvtár fájljait a konténeren kívül, a mysql-data könyvtárban kell tárolnunk azért, hogy az a konténertől függetlenül megőrizhető legyen.

version: "3.0"
services:
  php:
    container_name: columbo.uni-eszterhazy.hu_www
    build: php
    ports:
      - "8080:80"
    command: php -S 0.0.0.0:80 -t /var/www/html
    volumes:
      - ./webroot:/var/www/html

  mysql:
    container_name: columbo.uni-eszterhazy.hu_mysql
    image: mysql:8.0
    ports:
      - "3306:3306"
    volumes:
        - ./mysql-data:/var/lib/mysql
        - ./initdb.sql:/docker-entrypoint-initdb.d/initdb.sql
    environment:
        MYSQL_USER: webdb
        MYSQL_PASSWORD: webdb
        MYSQL_DATABASE: webdb
        MYSQL_ROOT_PASSWORD: nagyonTitKosJelszo
        MYSQL_ROOT_HOST: "0.0.0.0/0"

A konfigurációs fájl új eleme az environment blokk, amely a konténeren belül létrehozandó környezeti változókat definiálja. Mivel a változók definiálásának ez meglehetősen egyszerű módszere, gyakran használjuk különféle kezdeti paraméterek meghatározására, esetünkben az adatbázis hozzáférés paramétereinek beállítására. A használható változók neveit a konténer dokumentációja tartalmazza, a MySql esetében ezek a következők:

MYSQL_USER:

a konténer az első indulásakor létrehozza ez a felhasználót.

MYSQL_PASSWORD:

definiálja a MYSQL_USER felhasználó jelszavát.

MYSQL_DATABASE:

a konténer az első indulásakor létrehozza az itt megadott adatbázist, és hozzáférést biztosít az imént létrehozott felhasználó számára.

MYSQL_ROOT_PASSWORD:

amennyiben szükséges, ebben a változóban lehet megadni az adatbázis szerver rendszergazdai jelszavát, de erre a legtöbb esetben nincs szükség.

MYSQL_ROOT_HOST:

a root hozzáférés az itt megadott hálózatra korlátozható, így más hálózatokból még érvényes név és jelszó birtokában sem lehetséges a hozzáférés – ennek az adatbázis védelmében van szerepe.

A konténer számos más, egyéb környezeti változón keresztül beállítható paramétert fogadhat, ezekről részletes információ a DockerHub mySql oldalán található.

Szintén új elem az initdb.sql fájl összerendelése a konténeren belül levő docker-entrypoint-initdb.d/initdb.sql fájllal. A konténer az első indulásakor ellenőrzi a docker-entrypoint-initdb.d/ könyvtár tartalmát, és az ott található összes fájlt a frissen létrehozott adatbázison lefuttatja. Amennyiben a példában szereplő db.sql fájlban SQL mondatok, tábla definíciók, tipikusan INSERT sorok vannak, ezekkel a webdb adatbázis táblái létrehozhatók és kezdeti adatokkal feltölthetők. Ezt a módszert kell tehát alkalmazni ahhoz, hogy a konténer már a létrehozásakor egy előre előkészített és adatokkal feltöltött adatbázist tartalmazzon. Példánkban az initdb.sql fájl egy tábla definíciót és egy rekord beszúrását tartalmazza:

CREATE TABLE people (
  id    serial,
  name  VARCHAR(64) NOT NULL
);
INSERT INTO people (name) VALUES ('Szabo Andras');
INSERT INTO people (name) VALUES ('Kiss Lajos');

Már csak egy feladat van hátra, egy olyan webalkalmazás készítése, amely kapcsolódik a mysql adatbázisunkhoz és képes azon műveleteket végezni. Az alábbi program, a webroot/index.php fájlban van, és a webdb adatbázis people táblájából kéri le és írja ki a name oszlop mezőit:

<?php
$dbhost = "mysql";
$username = "webdb";
$password = "webdb";
$db = "webdb";

$conn = new mysqli($dbhost, $username, $password, $db);
if ($conn->connect_error) {
    die("Kapcsolódási hiba: " . $conn->connect_error);
}

$sql = "SELECT * FROM people";
$result = $conn->query($sql);

echo "<table border='1'>";
echo "<tr><th>Name</th>";
while($row = $result->fetch_assoc()) {
    echo "<tr><td>".$row["name"]."</td>";
}
echo "</table>";
$conn->close();
?>

Fontos tudni, hogy ebben a struktúrában nem tudhatjuk előre, hogy az adatbázis szervert tartalmazó konténer milyen IP címet kap majd akkor, amikor azt a saját gépünk helyett a szolgáltató szerverén működtetjük. Az adatbázis kapcsolat kiépítéséhez ezért nem használhatjuk a korábban esetleg megszokott localhost-ot sem, mert az a webszerver címét jelenti, az adatbázis viszont nem ezen van. Azért, hogy a konténerek közti kapcsolat ne függjön attól, hogy azok éppen milyen IP címeket kaptak, azok a konténercsoporton belül a szolgáltatás nevével kell hivatkozhatniuk egymásra. A fenti php programban ezért adtuk a $dbhost változónak értékül a mySql szerver docker-compose.yaml-ban használt szolgáltatás nevét, a mysql-t.

A konténer felépítését alapjában véve ismét a docker-compose up paranccsal végezzük. A tesztelés során azonban hasznos lehet, ha a javításokat követően leállítjuk, majd töröljük a konténereket, töröljük az adatbázis fájlokat a mysql-data könyvtár eltávolításával:

#!/bin/bash
docker stop columbo.uni-eszterhazy.hu_www
docker stop columbo.uni-eszterhazy.hu_mysql
docker rm columbo.uni-eszterhazy.hu_www
docker rm columbo.uni-eszterhazy.hu_mysql
for I in $(docker image ls -q)
  do docker image rm $I --force
done
rm -r mysql-data
docker-compose up

A docker-compose parancs számos egyéb paraméterrel is rendelkezik, ezeket érdemes lehet a dokumentációban áttekinteni. A teljesség igénye nélkül lássunk még néhány példát!

A docker-compose build parancs újraépíti az egyes konténer elemeket. A --scale web=3 paraméter a web nevű szolgáltatásból három példányt indít el, ami akkor hasznos, ha a szolgáltatásunkat több konténerrel szeretnénk kiszolgálni. A docker-compose down pedig a docker-compose.yaml fájl alapján leállítja az abban létrehozott konténereket. Ezt a folymatot az alábbi példa egyetlen sorban végzi el. Emlékeztetőül: a && karakterpár után írt programok csak akkor indulnak el, ha az őket megelőző program visszatérési értéke 0, azaz azok hiba nélkül futottak le.

1 docker-compose build && docker-compose up -d --scale web=3 && docker-compose down

Wordpress konténerizálása

A Wordpress az egyik legeltejedtebb CMS (Content Management System), mely bizonyos mértékig szintén jól konténerizálható. Az alábbi docker-compose.yaml fájl erre mutat példát, amit egyfajta „Wordpress-gyárként” használhatunk, ezért annak paramétereit egy .env file-ba emeltük ki. Ennek tartalma VALTOZO=ertek formájú sorokból áll, a konténerek felépítése előtt a docker-compose parancs ezt automatikusan betölti. Az .env fájlunk tartalma példánkban az alábbi:

EXTERNAL_PORT=8013
WEBHOST_URL=www.erlauni.hu
MYSQL_DATABASE=wordpress
MYSQL_USER=wordpress
MYSQL_PASSWORD=geeThu9aiR0u
MYSQL_ROOT_PASSWORD=aeT0phahboo7
WP_HOME="https://{$WEBHOST_URL}"
WP_SITEURL="https://{$WEBHOST_URL}"
STORAGE_LIMIT=2G
MEM_LIMIT=512M
MEM_SWAP_LIMIT=512M

A docker-compose.yaml fájl felépítése az alábbi:

Összefoglalás

Ebben a fejezetben a komplex feladatokat megoldó, több konténerrel működő alkalmazás létrehozását tanultuk meg. Ennek központi eleme a docker-compose.yaml fájl volt, melyben az egyes konténereket definiáltuk a már megszokott módon. Megismertük azokat a parancsokat, amelyekkel a konténerek felépíthetők, indíthatók és leállíthatók. Láttuk, hogy a konténerek belső hálózatában az egyes elemekre a szolgáltatás nevével kell hivatkozni, ennek figyelmen kívül hagyása tipikus probléma a gyakorlati alkalmazás során.

Feladatok

  1. Tekintsd át az alábbi docker-compose.yaml fájlt, és adj választ a következőkre:

  • Milyen szolgáltatást valósít meg ez a file?

  • Milyen célt szolgálhat a depends_on definíció?

  • Milyen célt szolgálhat a deploy definíció?

version: '3.8'
services:
  db:
    image: mysql:latest
    environment:
      MYSQL_ROOT_PASSWORD: dbuser
      MYSQL_DATABASE: webdb
    volumes:
      - db_data:/var/lib/mysql
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    depends_on:
      - db
    deploy:
      replicas: 3

volumes:
  db_data:
  1. Hozz létre egy docker-compose.yaml fájlt, amely egy egyszerű webalkalmazást indít egy nginx konténerben! Helyezz el benne egy weblapot, és biztosítsd, hogy az a 80-as porton legyen elérhető!

  2. Készíts egy docker-compose.yaml fájlt, amely egy web és egy db szolgáltatást indít. A web a működéséhez szükséges adatokat az adatbázisból nyeri. Használd az php és mysql image-eket.

  3. Módosítsd a docker-compose.yaml fájlt, hogy a web szolgáltatás használjon egy kötetet az adatbázis fájlok megőrzéséhez.

  4. Készíts egy docker-compose.yaml fájlt, amely a web szolgáltatás három példányát és egy db szolgáltatást.

  5. Módosítsd a docker-compose.yaml fájlt, hogy a db szolgáltatás egyedi belépési parancsot használjon az indításkor.

  6. Hozz létre egy docker-compose.yaml fájlt, amelyben a web szolgáltatás csak akkor indul el, ha a db service már fut, és fogadja a kapcsolati kéréseket. Használd a depends_on-t!

  7. Az előző feladatban létrehozott szolgáltatást indítsd el daemon módban (háttérben futó szolgáltatásként)!

  8. Állítsd le előző feladatban létrehozott szolgáltatást!