Docker - 3. rész: Egyszerűbb adattárolás és adatkötés

Attila | 2022. 12. 27. 10:18 | Olvasási idő: 5 perc

Címkék: #Adatbázis (Database) #Container #Docker #Image #SQLite #Virtualizáció (Virtualization) #Volume

A bejegyzésben áttekintem az egyszerűbb adattárolási módszereket: a fájl alapú adattárolást egy Ubuntu Linux alapú konténerben, vagy egy SQLite alapú adattároló használatát egy Docker Volume-ban. De a forráskód változásait figyelő alkalmazást is láthatunk itt (nodemon), a frissített forráskódot pedig be tudjuk majd kötni egy konténerhez, amivel leteszteltük a működését, ha pedig megfelelő volt, akkor az image-ét is újraépíthetjük, így már a megváltoztatott, frissebb forráskóddal.
docker-volume-vs-bind-mounts

Bevezetés

Nézzük meg, hogy hol is tartunk:

  • Kezdésnek átvettük az alapokat és telepítettük a Docker virtualizációs környezetet.
  • Felépítettük az alkalmazásunk Docker Image-ét, amely tartalmazza a Node.js alapú alkalmazás forráskódját.
  • Futtattuk az alkalmazás konténerjét és ki is próbáltuk a böngészőnkben.
  • Megosztottuk az image-t egy nyilvános helyen, a Docker Hub-on, majd onnan publikáltuk is egy ideiglenes kiszolgálóra.

Mi vár ránk most?

  • Korábban az adataink mindig elvesztek, amikor egy-egy új konténert indítottunk. Ezt orvosoljuk most azzal, hogy eltároljuk őket egy adattárolóban.
  • A Docker-es adattárolás többféle módját fogjuk áttekinteni ebben a bejegyzésben.
  • Olyan kulcsszavakkal ismerkedünk meg a bejegyzésben, mint a Volumes és a Bind mounts volume.
  • De kezdjük a legegyszerűbbel, nézzük meg, hogy hogyan lehet egy Ubuntu Linux alapú konténerben fájlban tárolni adatot.


Tároljuk el az adatainkat!

Ahogy azt már láttuk, minden egyes új konténer létrehozásakor a "todo" listánk üres volt, mivel egyetlen adatot sem tároltunk egy-egy "munkamenetnél" tovább.
Amikor egy konténer fut, akkor számos réteget (róluk majd részletesebben később lesz szó) használ az image-ből a fájlrendszere számára. Emellett minden egyes konténernek megvan a saját területe, ahova fájlokat tud létrehozni, frissíteni és törölni is tudja őket. A konténerek nem látnak rá egymás dolgaira, így azt sem tudják alapból, hogy egy másik konténerben létrehoztunk-e valamit, például egy új feladatot a listánkban, még akkor sem, ha a két konténer ugyanazt az image-t használja alapjául.

Nézzük meg ezt a gyakorlatban!

A példában azt láthatjuk, hogy ha indítunk két konténert, akkor az egyik nem fogja látni a másik konténerben létrehozott fájlt.

Először hozzunk létre egy új konténert, ami egy Ubuntu Linux alapú konténer lesz:

docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"

A benne futó shell script pedig generál nekünk egy random számot, amit utána kiír a /data.txt fájlba. Az utána lévő rész pedig figyeli a fájlt és megtartja, ameddig a konténer fut. Mindössze ennyi.

Ellenőrizzük mindezt! A Docker Desktop vezérlőpultján keressük meg a konténerek között az új Ubuntu alapú konténert.

A sorának az "Actions" részében a három pontot nyissuk le és kattintsunk az "Open in terminal" menüpontra. Adjuk itt ki a parancsot:

cat /data.txt

Eredményül pedig vissza kell kapnunk egy 1 és 10000 közötti véletlen számot (nálam épp 7554 a szám).

Alternatívan megtehetjük ezt úgy is, ha csak a saját terminálunkban a docker exec parancsot használjuk. Ehhez viszont szükség van a konténer azonosítóra, amelyben ki akarunk adni valamilyen parancsot. Így tudjuk kilistázni a konténerjeinket:

docker ps

Eredménye:

Innen kinyerhető a "Container ID", ami szükséges a parancs futtatásához.

Így alternatív módon is hozzáférhettem a konténerhez és kinyertem egy fájl tartalmát.

Most létrehozok egy másik Ubuntu image alapú új konténert.

docker run -it ubuntu ls /

Utána ez rögtön kilistázza a fájlrendszerének tartalmát a mappákkal, de data.txt fájl itt nincs benne, helyesen.

Végül a Docker Desktop vezérlőpulton törölhetjük is ezt az új Ubuntu alapú konténert. Vagy megtehetjük így is a törlést:

docker rm -f <container-id>

Ehhez persze megint kell a parancs végén látható konténer azonosító.

Ez a példa megmutatta nekünk, hogy a konténerek, még ha ugyanarra az image-re is épülnek, egymástól függetlenek, izoláltan, elszigetelten léteznek és nem látják egymás fájljait. Amikor pedig a konténer megszűnik, akkor ezek a fájlok, amiket létrehoztunk bennük, szintén megszűnnek. Nekünk viszont valamilyen maradandó adattároló kellene, ami a konténer megszűnése után is megtartja az adatokat…

Container Volumes (Adattároló kötegek)

Ezek a kötegek (volumes) támogatják (részletesen itt olvashatunk róluk: https://docs.docker.com/storage/volumes/), hogy olyan specifikus fájlrendszert működtessünk a konténerhez kapcsolódóan, amelyek a gazda gépen léteznek. Ha például egy könyvtárat felcsatlakoztatunk a konténerhez, akkor a könyvtár változásai láthatóak lesznek a gazda gépen is. Ha ugyanazt a könyvtárat csatlakoztatjuk fel a konténerekhez majd újraindítjuk például, akkor megtartja ugyanazt a tartalmát, fájljait, struktúráját és nem fog törlődni. Pont ez kell nekünk!

Tároljuk el a „todo” lista adatainkat!

Alapértelmezetten egy SQLite adatbázist hozunk erre létre az /etc/todos/todo.db helyen és névvel. Ez a mi kevés adatunknak éppen megfelelő lesz, később majd megnézünk más adatbáziskezelőket is (például a MySQL-t), amelyek nagyobb adathalmazok tárolására optimálisabbak.

Ez az adatbázis (SQLite) egy darab fájl, ami a gazda gépen tud majd létrejönni és elérhetővé válik a konténereknek. A kötegek felcsatolását "mounting"-nak hívják ebben a virtualizációs szakzsargonban. Ezután majd a konténer írni fogja ezt a felcsatolt köteget, ami a todo.db fájlt tartalmazza.

A kötegek két fajtája közül először az elsővel ismerkedünk meg, ezek az „elnevezett kötegek” (named volumes), gondoljunk rá úgy, mint ha egy egyszerű adattároló lenne. A Docker kezeli és karban tartja a fizikai helyét a diszken, és nekünk csak annyit kell tudnunk, hogy mi is a kötegnek a neve. Amikor csak használni szeretnénk a köteget, a Docker fog gondoskodni arról, hogy a helyes adatokhoz jussunk hozzá.

Először hozzunk létre egy ilyen named volume-t:

docker volume create todo-db

Válaszként megkapjuk a todo-db nevet, ami azt jelzi, hogy hibamentesen létrehozta nekünk. Ezt ellenőrizhetjük is a Docker Desktop vezérlőpultjában. A bal oldali menüben a harmadik menüpont: Volumes

A listában pedig látható is az új todo-db volume-unk.

Ezután a konténereknél állítsuk le a "todo" alkalmazásunkért felelős konténert (Stop action alkalmazása a getting-started:latest image-t konténerizáló 3000-es portokhoz kötött konténeren). Ha most ráfrissítünk a böngészőnkben a localhost:3000 oldalra, akkor láthatjuk, hogy már nem fut a kiszolgáló konténer.

Ezután úgy fogunk konténert indítani, hogy már mount-oljuk a frissen létrehozott volume-unkat. Erre a -v kapcsoló fog szolgálni a parancsban.

docker run -dp 3000:3000 -v todo-db:/etc/todos <felhasználóneved>/getting-started

Megjegyzés: itt, ahogy írtam is, szükség van a Docker Hub-os felhasználónevünkre, mert a szükséges image neve már tartalmazza azt a korábbi munkáink miatt.

Ha megfelelően elindult, akkor nyissuk meg a localhost:3000 oldalt a böngészőben, vagy csak kattintsunk rá a vezérlőpultban a konténereknél a frissen elindított konténer sorának port oszlopában lévő 3000:3000 számokat tartalmazó linkre és meg is nyílik a böngészőnkben.

Adjunk hozzá néhány feladatot a listánkhoz.

Itt látható négy feladat. Most Stop-oljuk le a konténert és kukázhatjuk is Actions oszlopban a vezérlőpulton. Vagy alternatív módon, ha lekérjük a konténer azonosítóit a docker ps paranccsal, akkor utána következhet ez a kukázó (törlő) utasítás is:

docker rm -f <container-id>

A lényeg, hogy valamelyik módszer szerint töröljük.

A terminálunk "memóriájában" viszont szerencsére még ott van az az utasítás, amellyel korábban létrehoztuk azt a konténert, amelyhez felcsatoltuk a todo-db volume-unkat is (kétszer kell csak a felfelé nyilat megnyomni és már ott is van, de ha nincsen, akkor innen is kimásolható újra).

Nyissuk meg az alkalmazást újra a böngészőnkben és ott kell lennie a korábban létrehozott feladatainknak, hiszen a todo-db, mint adatbázis, eltárolta a feltöltött adatainkat és most azokat jeleníti meg nekünk.

Hurrá! Örülünk, Vincent?

Tipp a jövőre nézve: a Docker alapértelmezetten támogatja a most használt named volume technikát és az eddig még nem használt, de mindjárt felhasználásra kerülő bind mounts volume típust, viszont létezik még számos olyan driver, ami plugin-ként elérhető ahhoz, hogy tudjunk más volume típust (NFS, SFTP, NetApp stb.) használni.

Sokan meg szokták kérdezni, hogy hol is tárolja a Docker aktuálisan az adatainkat? (amikor ilyen named volume-t csatolunk fel a konténerünkhöz) Ennek kiderítéséhez használhatjuk a következő parancsot: docker volume inspect <volume-neve> A mi példánkban ez így néz ki konkrétan:

docker volume inspect todo-db

Ha például így megvizsgáljuk a todo-db volume-unkat, akkor egy JSON választ kapunk, amely tartalmazza a tulajdonságait.

A Mountpoint pedig megmutatja, hogy konkrétan melyik könyvtárban is érhető el ez az adattároló. (Leggyakrabban ennek az eléréséhez adminisztrátori – root – jogosultság szükségeltetik, ha el akarjuk érni a gazdagépről.)

Adatokhoz való hozzáférés a Docker Desktop alkalmazással

Amíg futtatjuk a Docker Desktop-ot, addig a Docker parancsok egy kis virtuális gépen futnak a számítógépünkön. Ha bele szeretnénk nézni a Mountpoint-ban jelzett könyvtár tartalmába, akkor először be kell lépnünk ebbe a virtuális gépbe… viszont meg tudjuk ezt oldani sokkal egyszerűbben is!

A vezérlőpulton válasszuk ki a Volumes menüpontot és kattintsunk rá a todo-db volume-ra. Megjelenik az a lista, hogy melyik konténerek használják ezt a volume-ot, viszont van ott egy Data lapfül is. Ha rákattintunk és kiválasztjuk a todo.db-t az egerünkkel, akkor a sor jobb szélén megjelenik a három pont, amivel a helyi menü hozható elő, kattintsunk rá. Ezután simán le tudjuk menteni (Save As…) az SQLite adatbázist.

Lementés után meg tudjuk nyitni az adatbázist egy SQLite menedzserrel, például a DB Browser alkalmazással és már böngészhetjük is az adatbázis tartalmát:

A todo_items táblában pedig ott csücsülnek az adataink:

Ha eddig eljutottunk, akkor most már van egy alkalmazásunk, amelynek adatai "túlélték" a konténer újraindítását, perzisztensen (tartósan) léteznek.

Következhet a korábbi másik problémás eset feloldása: amikor minden apró (forrás)kódsor változtatás miatt újra kellett építenünk az image-ünket. Megnézzük, hogy hogyan lehet ezt egyszerűbben. Ehhez már az újabb volume típust fogjuk használni, a bind mounts volume-ot.


Használjuk a "Bind mounts volume"-ot!

Az eddig megismert "named volume" tökéletesen megfelelt nekünk arra, hogy ha csak adatokat akartunk tárolni és nem érdekelt minket, hogy hol is tárolódik az, egyszerűen csak működött.

Az itt bemutatásra kerülő bind mounts (részletesen itt olvashatunk róluk: https://docs.docker.com/storage/bind-mounts/) nagyobb kontrollt enged meg nekünk a csatolt kötegeken (például mi határozhatjuk meg a volume helyét, ahova becsatolja a Docker). Használhatjuk ezt is adattárolásra, de gyakran használják ezt akkor is, amikor adatokat akarunk hozzáadni a konténereinkhez… Amikor dolgozunk egy alkalmazáson, akkor megtehetjük azt, hogy "bind mount"-ként csatoljuk az alkalmazás forráskódját a konténerhez és így engedjük neki, hogy lásson minden egyes változást a kódsorokon.

Mivel a "todo" alkalmazásunk egy Node-alapú alkalmazás, ezért ehhez használhatunk egy erre a célra kifejlesztett eszközt, a nodemon-t (https://npmjs.com/package/nodemon), amely figyeli a fájlok változásait, és ha változnak, akkor újra is indítja rögtön az alkalmazást. Természetesen más nyelvekben és keretrendszerekben is létezik ugyanilyen eszköz, csak utána kell néznünk, hogy nekünk melyik is lenne a megfelelő, a Laravel-nél például a Laravel Sail lesz a segítségünkre (https://github.com/laravel/sail).

Indítsunk el egy új konténert fejlesztői módban!

A folyamat, amit véghez viszünk:

  1. Becsatoljuk (mount) a forráskódunkat a konténerbe.
  2. Telepítjük a függőségeket, beleértve a fejlesztői függőségeket is.
  3. Elindítjuk a nodemon-t, hogy figyelje a fájlrendszer változásait.

Kezdetben bizonyosodjunk meg róla, hogy nincsen egy darab „getting-started” típusú konténerünk sem: ne is fussanak, de törölhetjük is őket, ha léteznek.

Ezután térjünk vissza a VSCode-ba, ahol meg van nyitva a Node alapú projektünk és a terminál részében a megfelelő mappában is vagyunk (nálam ez a C:\xampp\htdocs\app mappa, de nálatok lehet, hogy máshol van).

Futtassunk itt a PowerShell-ben egy hosszabb (több sorból álló) parancsot, amelyben a sorokat ` jellel (Alt Gr + 7 billentyűkombinációval) törjük meg és jelezzük a rendszer számára, hogy még nem ért véget a parancs, hanem a következő sorban folytatódik.

docker run -dp 3000:3000 `
  -w /app -v "$(pwd):/app" `
  node:18-alpine `
  sh -c "yarn install && yarn run dev"

Megjegyzés: a tabulálásnak és a szóközöknek a sorok elején nincsen jelentősége, csak a jobb átláthatóság miatt lehet érdemes betenni őket.

Értelmezzük a parancsot soronként:

  • a docker run parancsot ismerjük, elindítja a konténert izoláltan és összekapcsolja a gazdagép portját a konténer belső portjával (3000 mindkettő)
  • -w /app: beállítja a konténer munkakönyvtárát, ahol a parancs futtatásra kerül majd
    • -v "$(pwd):/app": ez a bind mount kapcsolat, amely a gazdagépen lévő alkalmazás mappáját (ahova elnavigáltunk a VSCode termináljában, nálam ez a C:\xampp\htdocs\app mappa) a konténer /app mappájával.
      • Megjegyzés: a Docker-nak abszolút hivatkozásokra van szüksége a binding mounts-ok létrehozásához, emiatt ebben a példában a pwd-t használtuk, hogy kiírja a munkakönyvtár abszolút útvonalát, ahelyett, hogy ezt manuálisan megtettük volna mi magunk.
  • node:18-alpine: az image, amit használunk.
    • Megjegyzés: ez az alkalmazásunk alap image-e az ottani Dockerfile alapján.
  • sh -c "yarn install && yarn run dev": ez a parancsrészlet egy shell script, mivel az alpine-ban nincsen bash script lehetőség, az utasítás pedig a yarn csomagkezelővel telepít minden alkalmazás függőséget (csomagot) és futtatja. Ha megnézzük a package.json fájlt, akkor látni fogjuk, hogy a "dev" script elindítja a nodemon-t.

Itt látható a 9. sorban, hogy a nodemon az src mappában lévő index.js változásait fogja figyelni.

Most (és később is bármikor) meg tudjuk nézni a konténerünk naplózását (log). Ehhez először listázzuk ki a konténereket a docker ps paranccsal, majd az új konténerünk azonosítóját felhasználva kiadhatjuk a következő parancsot:

docker logs -f <container-id>

Itt láthatjuk, hogy a yarn telepítette a függőségi csomagokat és futtatja is az alkalmazást. A nodemon figyeli az src/index.js fájlt. Az alkalmazás használja az /etc/todos.todo.db SQLite adatbázist és az alkalmazást kiszolgáló webszerver figyel a 3000-es porton.

A naplózást követését a CTRL + C billentyűkombinációval tudjuk megállítani.
Most változtatást fogunk végrehajtani a VSCode-ban egy fájl forráskódjában: src/static/js/app.js fájl lesz az érintett. Az új feladat hozzáadását végző gomb feliratát fogjuk megváltoztatni, „Add Item” helyett csak „Add” fog maradni. Menjünk a 109. sorba és írjuk át ezt, majd mentsük el. Ha pedig most frissen megnyitjuk az oldalt, vagy a már megnyitottat frissítjük, akkor rögtön látszódik is az eredmény (előfordulhat, hogy a Node szerver újraindításához kell néhány másodperc, de utána már jónak kell lennie).

Végezzünk el nyugodtan bármilyen módosítást, amit szeretnénk, és ha végeztünk, ha minden megfelelően fog már menni, akkor megállíthatjuk a konténert és újraépíthetjük az image-ünket így:

docker build -t <felhasználóneved>/getting-started .

Kiegészítés: az alkalmazás a böngészőben nem mutatja a todo.db adatbázisba elhelyezett feladatainkat. Ez azért van, mert a hosszabb (4 soros) indítási parancs első sorában a konténer létrehozásakor nem csatoltuk oda -v kapcsolóval a már létező named volume-ot, így egy üres adatbázissal indult el a konténer futtatása. Ha most leállítjuk és kukázzuk a konténert a vezérlőpulton, majd újra futtatjuk az alábbi, bővített létrehozó utasítást, akkor már a meglévő adatainkat fogjuk látni futtatáskor.

docker run -dp 3000:3000 -v todo-db:/etc/todos `
  -w /app -v "$(pwd):/app" `
  node:18-alpine `
  sh -c "yarn install && yarn run dev"

A bind mounts nagyon hasznos és népszerű helyi szintű fejlesztői beállítások alkalmazásokhoz, mivel ugye rögtön látjuk az eredményét a módosításainknak, mindössze egy docker run parancsot futtatunk és mehet is minden, nem kell telepítenünk hozzá semmit.


Összegzés és továbblépés

Ebben a bejegyzésben áttekintettük az egyszerűbb adattárolási technikákat: szöveges fájlszinten és egy SQLite adatbázis segítségével, így gyakorlatilag a named volume technika használatát megismertük. De a forráskód változásait figyelő alkalmazást is láthattunk itt (nodemon), a frissített forráskódot pedig be tudtuk kötni egy konténerhez, amivel leteszteltük a működését, ha pedig megfelelő volt, akkor az image-ét is újraépíthettük, így már a megváltoztatott, frissebb forráskóddal.

Melyiket használjuk inkább a named volume-ot vagy a bind mounts-ot?

  • a Volume-okat könnyebb háttértárra kitárolni, elmenteni (backup-olni) és később helyreállítani (restore).
  • a Volume-okat könnyebb a Docker parancsokkal (CLI) és a Docker Desktop vezérlőpultján keresztül is kezelni.
  • a Volume-ok működnek Windows és Linux alapú konténereken is.
  • a Volume-ok biztonságosabbak, amikor adatokat akarunk megosztani több konténerrel is.
  • a Volume-ok hozzáférési driver-e lehetővé teszi, hogy távoli szolgáltatóknál vagy a felhőben kódoltan eltároljuk őket és akár még funkcionalitásokat is hozzáadhatunk.

A Volume-ok tehát adattárolásra megfelelőbbek, jobb teljesítményt is nyújtanak, úgyhogy érdemes arra használni őket. A bind mounts-ot pedig használjuk a forráskódok frissítésének lekezelésére Docker-es környezetben.

A következő bejegyzésben lecseréljük az SQLite adatbáziskezelőnket MySQL-re és megnézzük, hogy hogyan tudnak kommunikálni egymással a konténerek.

Kövessetek a Facebook-on, és ha tetszik a munkám, akkor támogassátok néhány euróval a blog és az oldal fennmaradását a "buymeacoffee" (kávé) ikon útmutatásait követve.