Saját konténer készítése

A fenti példákban kész konténereket használtam, amelyeket a DockerHubról töltöttem le. Természetesen lehetőség van saját konténerek készítésére is (persze, hiszen a DockerHub konténereit is fel kellett valahogyan építeni). Ebben a fejezetben megtanuljuk, hogyan hozhatjuk létre a saját alkalmazásainkat működtető konténereket.

  • Saját konténer létrehozása.

  • Saját konténer paraméterezése, működésének beállítása.

  • Docker image-ek építése Dockerfile segítségével.

  • A Dockerfile-ban használható beállítások ismerete.

  • Jógyakorlatok megismerése példákon keresztül.

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 2 óra.

Üdvözlet a konténerből!

Egy konténer készítése két (plusz egy) lépésben történik:

  1. Először egy szövegfájlban le kell írni a konténerrel kapcsolatos követelményeket.

  2. Az elkészített konténerdefiníciós fájl alapján fel lehet építeni a konténer image-et. A „build” folyamata (hasonlóan a programozási nyelvek fordítási eljáráshoz) a képernyőn nyomon követhető, hiba esetén megismételhető.

  3. Az elkészült image alapján pedig a már ismert módon létrehozható a konténer egy futtatható példánya. Amennyiben a tesztelés során hibát tapasztalunk, a konténert eldobjuk, az image-et pedig a javítás után újra kell építeni.

Egy új konténer image létrehozásához először célszerű egy új projekt könyvtárat létrehozni, és a konténer definíciós fájlt ebben létrehozni. A fájl alapértelmezett neve Dockerfile, ha ezt használod, akkor a fordítási parancsban nem kell azt külön megadni, ezért a legtöbb példában és leírásban ezt találod.

Az első példánkban egy egyszerű konténert készítünk, ami csak egy üdvözlő szöveget ír ki! Most a legegyszerűbb megoldásra törekszünk, ezért nem írunk programot sem, csak egy Ubuntu Linuxot tartalmazó környezetben adunk ki egy echo "Üdvözlet a konténerből!" parancsot. Először tehát elkészítjük a konténerdefiníciós fájlt, a Dockerfile-t:

FROM ubuntu:latest
LABEL maintainer="koczka.ferenc@uni-eszterhazy.hu"
CMD ["echo", "Üdvözlet a konténerből!"]

Az egyes sorok jelentése a következő:

1. FROM:

Szinte alig van olyan eset, hogy egy konténert úgymond a semmiből építünk fel, szinte mindig valamilyen alaprendszerből indulunk ki. Ez tipikusan valamilyen, a konténerben futó program számára szükséges környezet, példánkban egy minimális Linux alaprendszer. Webalkalmazásokat tartalmazó konténerekben jellemzően valamilyen webszerver van (erre láttunk példát korábban az nginx-szel) dinamikus webek esetén gyakori alap a Php vagy a Python. Esetünkben az alaprendszer tehát egy Ubuntu Linux. Az alaprendszerek a legtöbbször több verzióban is elérhetők, ezért a szükséges változatot rendszerint egy paraméterben megadjuk. Az Ubuntu Linux esetében ezek jelenleg a 16.04, 20.04 és 22.04 lehetnek, de idővel újabbak is megjelenhetnek. A példában szereplő latest az utolsó, legfrissebb elérhető változatot jelenti, de ennek használatával kapcsolatban érdemes óvatosnak lenni: ha egy konténer szoftverének fejlesztését és tesztelését egy adott környezetben végezted el, a működése során célszerű mindvégig megtartani azt. Amennyiben nincs szükség egyetlen alap csomagra sem, a FROM scratch beállítást kell használni. Ez olyan alkalmazások konténerizáciinak esetében lehet hasznos, amelyek csak egyetlen, statikusan linkelt bináris állományt tartalmaznak, amelyek működéséhez egyetlen más fájl sem szükséges.

2. LABEL:

A Dockerfile készítőjének elérhetősége, összetett rendszerek építésekor ez az adat fontos lehet. Ez a korábban használt MAINTAINER taget váltja le, amellyel korábbi forráskódok elemzése során gyakran lehet találkozni.

3. CMD:

Amikor az image alapján létrehozott konténer elindul, ismernie kell annak az alkalmazásnak a nevét és paramétereit, amit abban indítania kell, erre szolgál a CMD parancs. Paramétereként egy tömböt kell megadni, amelynek első eleme a végrehajtandó parancs, a továbbiak pedig az egyes paraméterek.

Javaslat

  • Bizonyos Linux változatok elsősorban a rendkívül kis méretük és erőforrás igényük miatt különösen ideálisak arra, hogy konténerben működtessük őket. Egy ilyen pl. az összesen 7.3Mbyte-os Alpine Linux.

Az elkészített definíciós fájl alapján felépíthető a konténer image, amihez a docker build parancsot kell használni. Ennek lesz néhány kötelező paramétere: a -t kapcsolóval be kell állítani a konténer nevét és verziószámát, ez a példában hello:1 lesz. Emellett meg kell adni a Dockerfile helyét és nevét is, de mivel ez példánkban az aktuális könyvtár, és előrelátóan az alapértelmezett fájlnevet használtuk, csak egy pontot kell tennünk a sor végére. A parancs indításával megtörténik a konténer felépítése. Ehhez először a a DockerHubról letöltődnek a szükséges fájlok (ubuntu:latest), majd nyomon követhető a Dockerfile feldolgozása.

kf@imac:~/docker$ docker build -t hello:1 .
[+] Building 5.8s (6/6) FINISHED
=> [internal] load build definition from Dockerfile                                  0.0s
=> => transferring Dockerfile: 155B                                                  0.0s
=> [internal] load .dockerignore                                                     0.0s
=> => transferring context: 2B                                                       0.0s
=> [internal] load metadata for docker.io/library/ubuntu:latest                      2.1s
=> [auth] library/ubuntu:pull token for registry-1.docker.io                         0.0s
=> [1/1] FROM docker.io/library/ubuntu:latest@sha256:f9d633ff6640178c2d0525017174a6  3.5s
=> => resolve docker.io/library/ubuntu:latest@sha256:f9d633ff6640178c2d0525017174a6  0.0s
=> => sha256:f9d633ff6640178c2d0525017174a688e2c1aef28f0a0130b26bd5 1.13kB / 1.13kB  0.0s
=> => sha256:81bba8d1dde7fc1883b6e95cd46d6c9f4874374f2b360c8db82620b33f 424B / 424B  0.0s
=> => sha256:3db8720ecbf5f5927d409cc61f9b4f7ffe23283917caaa992f847c 2.30kB / 2.30kB  0.0s
=> => sha256:01007420e9b005dc14a8c8b0f996a2ad8e0d4af6c3d01e62f123 29.54MB / 29.54MB  1.0s
=> => extracting sha256:01007420e9b005dc14a8c8b0f996a2ad8e0d4af6c3d01e62f123be14fe4  2.2s
=> exporting to image                                                                0.0s
=> => exporting layers                                                               0.0s
=> => writing image sha256:0fcc37f6b4e3cb0e5eb154eed2d85c13dfe16a4af09c224cdb50ab20  0.0s
=> => naming to docker.io/library/hello:1                                            0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

Az így elkészített konténer image alapján nem készül működő példány, tehát nem jelenik meg a kiírandó szöveg, a parancs csupán az image fájlt építi fel, amit a docker images paranccsal lehet ellenőrizni. Itt látható, hogy a képfájl mérete rendkívül kicsi, mindössze 77.9 Mbyte.

kf@imac:~/docker$ docker images
REPOSITORY              TAG       IMAGE ID       CREATED        SIZE
hello                   1         0fcc37f6b4e3   2 weeks ago    77.9MB

Végül, hozzunk létre egy konténer példányt, és indítsuk azt el! A parancs már ismert:

kf@imac:~/docker$ docker run hello:1
Üdvözlet a konténerből!

A konténer indítása a már megszokott docker run paranccsal történik, amihez hozzáadtam a fejlesztés során hasznos --rm paramétert is azért, hogy megelőzzem a tesztelés során létrejövő újabb és újabb példányok felhalmozódását a rendszerben. A megjelenő üzenet szerint az első konténerünk hiba nélkül fut le:

kf@imac:~/docker$ docker run --rm hello:1
Üdvözlet a konténerből!

Az üzenet megjelenésével elkészítettük az első saját konténerünket, de ahhoz, hogy a gyakorlatban is jól használhatókat építsünk, további funkciókra lesz szükségünk, ezért a Dockerfile további elemeit kell használnunk. Ezeket további példákon keresztül fogjuk megismerni.

Játsszunk!

Lássunk egy újabb példát! Készítsünk egy konténert, amelyben egy már ismert játékprogram, a Ninvaders fut! Lehet, hogy találnánk kész ilyet, de inkább készítsünk egy sajátot!

A Ninvaders Ubuntu Linuxon is elérhető, de az alaprendszernek nem része, ezért azt a konténerben is telepíteni kell. Ubuntu Linuxban egy szoftver telepítését, ahogyan azt már korábban tanultuk, az apt-get install paranccsal kell végezni, melyet egy apt-get update előz meg. Ezeket a parancsokat kell kiadni a konténer image létrehozása során is, ezzel egy olyan image keletkezik, amely tartalmazza a játékot is. (Tehát nem a futó konténer példány tölti majd le azt.) Most is egy 22.04-es Linuxból indulunk ki, de bővítjük a konténer image definícióját: a Dockerfile-ban további parancsokat kell végrehajtanunk a játék telepítéséhez:

FROM ubuntu:22.04
LABEL maintainer="koczka.ferenc@uni-eszterhazy.hu"
RUN apt-get update && apt-get -y install ninvaders
WORKDIR /usr/games
CMD ["./ninvaders"]

A RUN-t követő parancsok a konténer felépítése során futnak le, a már készülő konténerben. Az apt-get update frissíti a telepíthető programok listáját, az ezt követő apt-get -y install ninvaders pedig telepíti a játékot. A -y hatására a telepítési folyamat kérdés nélkül zajlik le, ez azért szükséges, mert a build folyamat alatt sajnos nincs lehetőség felhasználói interakciókra. Az && pedig a két önálló parancs egymás utáni futtatását biztosítja, a második csak az első sikeres befejeződése után fog elindulni. (Amennyiben ez a témakör nem ismerős számodra, látogass el az oprendszer.koczka.com oldalra, ahol ezekről már részletesen tanultunk.)

WORKDIR a konténeren belüli aktuális könyvtárként a /usr/games-t állítja be. A CMD végül elindítja az aktuális könyvtárban (ami tehát a /usr/games) levő ninvaders programot (az aktuális könyvtár leírására a ./ használatos – ez szintén a használt oprendszer függvénye).

A konténer felépítéséhez használjuk a docker build -t ninvaders:1 . parancsot:

kf@imac:~/docker$ docker build -t ninvaders:1 .
[+] Building 1.6s (8/8) FINISHED
=> [internal] load build definition from Dockerfile                                                                   0.0s
=> => transferring Dockerfile: 37B                                                                                    0.0s
=> [internal] load .dockerignore                                                                                      0.0s
=> => transferring context: 2B                                                               0.0s
=> [internal] load metadata for docker.io/library/ubuntu:22.04                               1.4s
=> [auth] library/ubuntu:pull token for registry-1.docker.io                                 0.0s
=> [1/3] FROM docker.io/library/ubuntu:22.04@sha256:f9d633ff6640178c2d0525017174a688e2c1aef  0.0s
=> CACHED [2/3] RUN apt-get update && apt-get -y install ninvaders                           0.0s
=> CACHED [3/3] WORKDIR /usr/games                                                           0.0s
=> exporting to image                                                                        0.0s
=> => exporting layers                                                                       0.0s
=> => writing image sha256:bcefd0ba0478e54addb57f52a3d3c56d6f0dedeca9b49609e849d8c91d7d6981  0.0s
=> => naming to docker.io/library/ninvaders:1                                                0.0s
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

Az elkészült image alapján pedig indítsunk el egy példányt a docker run -it --rm ninvaders:1 paranccsal! A --rm paraméter használata megint csak azért célszerű, mert egy játékra szolgáló konténert nem érdemes folyamatosan a rendszerben tartani, a kilépést követően a konténer példány eldobható.

A Ninvaders

A Ninvaders

A gyakorlatban a RUN parancsok számát érdemes a lehetőségekhez képes minimalizálni. Az egyes parancsok hatására ugyanis a konténer fájlrendszerében újabb és újabb rétegek jönnek létre, amely a UnionFS működését valamelyest lassítja. Bár a gyakorlatban ez csak minimális teljesítménycsökkenést okoz, kis odafigyeléssel ez könnyen elkerülhető. Ez az oka annak, hogy az előző példában a apt-get update && apt-get -y install ninvaders parancsokat egy sorban, összevonva írtuk le: ekkor a konténerben csak egy kétrétegű fájlrendszer alakul ki. Ha ezt a két parancsot két, különálló RUN sorban írtuk volna le, a fájlrendszer három rétegből épült volna fel, ami – igaz minimális mértékben, de – negatív hatással lenne a teljesítményre.

A RUN parancsok újabb és újabb rétegek létrehozását eredményezik.

A RUN parancsok újabb és újabb rétegek létrehozását eredményezik.

Egy konténer létrehozásának története utólag is lekérdezhető a docker history paranccsal. Ebben nyomon követhető a konténer image előállítási folyamata, az egyes állapotok mérete és néhány, ezekhez kapcsolódó comment is. Az image file neve helyett most annak ID-jét használjuk:

 1kf@imac:~/docker$ docker history 7ec0134e593e
 2IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
 37ec0134e593e   13 seconds ago   CMD ["./ninvaders"]                             0B        buildkit.Dockerfile.v0
 4<missing>      13 seconds ago   WORKDIR /usr/games                              0B        buildkit.Dockerfile.v0
 5<missing>      13 seconds ago   RUN /bin/sh -c apt-get update && apt-get -y …   48.4MB    buildkit.Dockerfile.v0
 6<missing>      13 seconds ago   LABEL maintainer=koczka.ferenc@uni-eszterhaz…   0B        buildkit.Dockerfile.v0
 7<missing>      3 weeks ago      /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
 8<missing>      3 weeks ago      /bin/sh -c #(nop) ADD file:18035d0a8c59e3306…   69.2MB
 9<missing>      3 weeks ago      /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B
10<missing>      3 weeks ago      /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B
11<missing>      3 weeks ago      /bin/sh -c #(nop)  ARG LAUNCHPAD_BUILD_ARCH     0B
12<missing>      3 weeks ago      /bin/sh -c #(nop)  ARG RELEASE                  0B

A parancs amellett, hogy lehetővé teszi egy konténer image ellenőrzését, jól használható a saját image-ek építésének tanulása során is.

Statikus webalkalmazás

Tegyük fel, hogy megbízást kaptál egy új motorkerékpár modell weblapjának elkészítésére. Az oldal csak statikus html elemeket, szövegeket, képeket tartalmaz, amelynek ízléses megjelenítését egy css fájl szabályozza. A megrendelő a weboldalakat a Google felhőjében akarja működtetni, ezért az elkészült munkát konténerizálva kell átadnod. Alakítsd ki a munkakörnyezetedet, készítsd el a site-ot és add át a megrendelőnek!

Ebben a példában tehát egy konténerben működő webszervert fogunk kialakítani. Ez a fejlesztés során nem tartalmazza a weboldal fájljait, azokat egy csatoláson keresztül éri majd el azért, hogy a site fájljai közvetlenül szerkeszthetők legyenek, és az eredményt is folyamatosan ellenőrizhessük. A production változatban, amit a felhőbe kell majd feltölteni, a web fájlokat már a konténer belsejében helyezzük át, így az egyetlen csomagként kerülhet továbbításra.

Egy Apache webszervert már korábban készítettünk, most ismerjünk meg egy másikat, az nginx-et (ejtsd: endzsinex). Ez a kisebb, egyes vélemények szerint gyorsabb és hatékonyabb webszervert gyakran alkalmazzák konténerekben is. Kezdjük a projekt könyvtár kialakításával! Ennek gyökérkönyvtárában helyezzük el a Dockerfile-t, a web fájljainak pedig hozzunk létre egy webroot nevű könyvtárat! Ebben a css fájloknak és képeknek nyissunk egy-egy könyvtárat, hozzuk létre a style.css és az index.html fájlt, valamint tegyünk egy képet az images könyvtárba! A teljes struktúránk tehát így épül fel:

.
├── Dockerfile
└── webroot
    ├── css
    │   └── style.css
    ├── images
    │   └── ktm.png
    └── index.html

Az index.html fájlban betöltjük a style.css-t, megadjuk az oldal nyelvét és címét, készítünk egy címsort, egy rövid bekezdést és elhelyezünk egy képet:

<!DOCTYPE html>
<html lang="hu">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>KTM Duke 200</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <h1>KTM Duke 200</h1>
    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Pariatur, laborum?</p>
    <img src="images/ktm.png" alt="Duke200">
</body>
</html>

A style.css-ben a h1 színét pirosra állítjuk, és a képek méretét a böngésző ablakának szélességében maximalizáljuk:

h1 {
    color: red;
}
img {
    max-width: 100%;
}

Az elkészített website megtekinthető pl. a vscode live serverével:

Az előkészített web projekt

Az előkészített web projekt

Konténerizáljuk a site-ot, melynek első lépése a Dockerfile elkészítse. A fejlesztői változatban tulajdonképp csak egy webszerverre lesz szükségünk, ebben csak egy nginx webszerver lesz; ennek alapértelmezett beállításai tökéletesen megfelelnek a céljainknak. A Dockerfile új eleme az EXPOSE, amivel a konténerben futó alkalmazás hálózati portszámát állítjuk be, ezzel lesz a webszerver a konténeren kívülről elérhető (ez egyfajta hálózati „ajtónak” is tekinthető). Mivel az nginx alapértelmezetten a 80-as porton „figyel”, ezért ezt a portot kell a külső kérések számára elérhetővé tenni. A külvilág számára csak az EXPOSE után felsorolt portok érhetők el, de a konténer belsejében futó alkalmazások egymással más portokon is kommunikálhatnak.

FROM nginx
LABEL maintainer="koczka.ferenc@uni-eszterhazy.hu"
EXPOSE 80

A konténerben futó webszerver belső portja a 80-as, a konténeren belül ezt tehát ezen a porton lehetne elérni. Amennyiben tehát a konténer belsejében futó egyéb alkalmazásnak erre szüksége lenne, erre a portra kellene csatlakoznia. A külső eléréshez azonban egy másik portot szokás használni azért, mert egy gépen számos más konténer is futhat, és az ütközések elkerülése érdekében azok nem használhatnak azonos port számokat. Ezért a belső port szám mellett meg kell adni egy külső portot is, ez lesz az, aminek forgalmát a Docker a konténer belsejében futó alkalmazás portjára irányítja. Az EXPOSE 8200:80 hatására a konténer 80-as belső portja a külvilág számára a 8200-as külső porton lesz elérhető, amit tehát a Docker a konténer belsejében futó nginx számára továbbít.

Építsük fel a konténer image-et, a neve legyen ktmweb, a verziószáma pedig 1! (A build folyamatát már nem jelenítjük meg.)

kf@imac:~/docker$ docker build -t ktmweb:1 .

Ez után indítsuk el a konténer egy példányát! Itt viszont számos követelménynek kell megfelelnünk, ezért a parancsunk meglehetősen sok további paramétert tartalmaz. A forráskönyvtár megadásakor a Windows és Unix-származékok eltérő formát követelnek meg, ezért most mindkettőre látunk egy-egy példát:

# Linuxon és Mac-en:
kf@imac:~/docker$ docker run --rm -v $(pwd)/webroot:/usr/share/nginx/html -p 8200:80 --name www.ktmweb.com ktmweb:1

# Windows-on:
C:\docker$ docker run --rm -v .\webroot:/usr/share/nginx/html -p 8200:80 --name www.ktmweb.com ktmweb:1

Bár már minden paraméterről volt szó, álljon itt egy kis emlékeztető:

--rm:

a konténer annak leállításakor automatikusan törlődik, a leállított konténer példány nem marad meg a rendszerben.

-v:

a -v $(pwd)/webroot/:/usr/share/nginx/html/ hatására az aktuális könyvtárból nyíló webroot a konténer belsejében a /usr/share/nginx/html/ könyvtárba lesz mountolva. Ezzel a webszerver számára elérhetővé tesszük a konténeren kívül elhelyezett web fájlokat.

-p 8200:80:

a külső 8200-as port összerendelése a konténer belsejében futó alkalmazás 80-as portjával. Ez biztosítja, hogy a böngésző címsorába írt http://localhost:8200 hatására a helyi gép 8200-as portjának forgalma a konténer 80-as belső portján figyelő nginx-hez kerül.

--name:

a --name www.ktmweb.com a konténer példány nevét állítja be. Erre a gyakorlatban tehát azért van szükség, mert egy több webkonténert futtató kiszolgáló esetén a név segítségével azok egyértelműen megkülönböztethetők, könnyen menedzselhetők. Ez látható az alábbi ábrán:

kf@imac:~/docker$ docker ps -a
CONTAINER ID   IMAGE     COMMAND                CREATED        STATUS        PORTS                 NAMES
d6918386fa10   ktmweb:1  "/docker-entrypoint.…" 3 seconds ago  Up 2 seconds  0.0.0.0:8200->80/tcp  www.ktmweb.com

Az oldal tehát a http://localhost:8200 webcím megnyitásával tekinthető meg, a web kérések kiszolgálását pedig a futó konténer üzenetei jelzik. A webszerver minden oldal és elem betöltését standard outputra „logolja”, ahogyan azt alábbi eseménynapló sorok mutatják. Az 1-es sorban az index.html, a következőben a /css/style.css, a 3-asban pedig a /images/ktm.png letöltésének napló sorai láthatók.

1172.17.0.1 - - [02/Mar/2024:16:56:59 +0000] "GET / HTTP/1.1" 200 470 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15" "-"
2172.17.0.1 - - [02/Mar/2024:16:56:59 +0000] "GET /css/style.css HTTP/1.1" 304 0 "http://localhost:8200/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15" "-"
3172.17.0.1 - - [02/Mar/2024:16:56:59 +0000] "GET /images/ktm.png HTTP/1.1" 304 0 "http://localhost:8200/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15" "-"

A megoldás világossá teszi, hogy a konténer használata feleslegessé teszi az állandó fejlesztési környezet működtetését a gépeden. Ezzel a munkakörnyezettel nem lesz szükség az Xampp alkalmazásra, mert a webszervert ki tudtuk alakítani egy apró konténerrel. Nem lesz ez másképp a Php-vel és az adatbázis szerverrel sem.

Futtatás a felhőszolgáltató rendszerében

A weboldalak „éles szerveren” történő működtetése némiképp eltér a fejlesztési környezetétől. Az ún. production változat esetében nem célszerű a website fájljait azon kívül tartani, azokat a konténer belsejébe másoljuk, így egy egységet képeznek. Másrészt arra sincs semmilyen garancia, hogy a helyi gépen kijelölt 8200-as port a felhőszolgáltató számára is megfelelő, ezért azt egy környezeti változón keresztül fogjuk megadni. Végül, éles üzemben nem töröljük az esetleg leállított konténer példányt sem, valamint azt nem a konzolon, hanem háttérszolgáltatásként indítjuk el.

Először alakítsuk át a Dockerfile-t! Az ADD webroot /usr/share/nginx/html egy másolást hajt végre, a webroot könyvtár teljes tartalmát a konténer belsejében levő /usr/share/nginx/html könyvtárba másolja, ahol az nginx a website fájljait keresi (ezt megint csak a DockerHubon, az nginx leírásából néztem ki). Módosítsuk ennek megfelelően a Dockerfile-t!

FROM nginx
LABEL maintainer="koczka.ferenc@uni-eszterhazy.hu"
ADD webroot /usr/share/nginx/html
EXPOSE 80

Építsük újra a konténert az új beállításokkal, ebben a verziószámot 2-re emeljük:

kf@imac:~/docker$ docker build -t ktmweb:2 .

A konténer indítása során a szolgáltatótól azt kérjük, hogy a EXTERNALPORT értékét a saját rendszerének megfelelően állítsa be. Ezért létrehozzuk ezt a környezeti változót, és értékeként a példában 8300-at állítottuk be. A konténer indításakor pedig a külső port „bedrótozása” helyett ezt a változót használjuk. Értelemszerűen a konténer belsejében futó alkalmazások port használata a konténer „belső ügye”, az másokra nincs kihatással, ezért annak értékét a production változatnak sem kell módosítani.

kf@imac:~/docker$ export EXTERNALPORT=8300
kf@imac:~/docker$ docker run -d -p $EXTERNALPORT:80 --name www.ktmweb.com ktmweb:2
5d5eaa6fd2334e7419ce09ae4713a80608b297592e6f80c8dd48501ddc48721d

Az indító parancsból tehát elhagytuk a --rm paramétert, és beállítottuk a -d-t, utóbbi hatására a konténer a háttérszolgáltatásként indul el. Ilyenkor a rendszer a keletkező üzeneteket is a háttérben gyűjti. Egy háttérben indított konténer már nem állítható le a Ctrl-C-vel sem, a működése a továbbiakban a docker ps -a paranccsal követhető nyomon. Miután az EXTERNALPORT értékét is megváltoztattuk, a konténer által kiszolgált website most már a http://localhost:8300 URL-en tekinthető meg.

1CONTAINER ID  IMAGE     COMMAND                 CREATED        STATUS        PORTS                NAMES
25d5eaa6fd233  ktmweb:2  "/docker-entrypoint.…"  7 minutes ago  Up 7 minutes  0.0.0.0:8300->80/tcp www.ktmweb.com

Az így elkészített konténer image a konténereket támogató webszolgáltatók szervereibe egyszerűen feltölthetők és üzembe helyezhetők. Emellett a DockerHub is lehetővé teszi kész konténerek tárolását, ahogyan azt a Publikálás a DockerHubban c. fejezetben látni fogjuk.

Php webalkalmazás

Az nginx önmagában csak az egyszerű webalkalmazások számára felel meg, a dinamikus webalkalmazások valamilyen script nyelv rendelkezésre állását kívánják meg. Az egyik legnépszerűbb ilyen nyelv a Php, ezért most egy ilyen környezetet építünk fel. A feladatunkat nagyban leegyszerűsíti, hogy maga a Php értelmező is rendelkezik egy beépített webszerverrel, így a feladat (most) nem egy Php-képes nginx építése lesz, a Php saját webszerverével dolgozunk majd.

A projektünk struktúrája hasonló az előző feladatban használthoz: a Dockerfile mellett egy webroot könyvtár tartalmazza az index.php-t. A web projektet tehát ebben a könyvtárban kell felépíteni.

kf@imac:~/docker$: ~/docker-php tree
.
├── Dockerfile
└── webroot
    └── index.php

Az előző példában bemutatott módszert követjük most is: a web könyvtárunk a fejlesztés alatt a helyi fájlrendszerben lesz, amit a production változatban a konténerbe másolunk majd. Az egyszerűség kedvéért az index.php mindössze egyetlen phpinfo() függvény hívást tartalmaz, amely a php értelmező részletes adatlapját jeleníti meg:

<?php
phpinfo();

A Dockerfile-ban az elvárt php verziójából indulunk, ez példánkban egy régebbi, 7.4-es változat lesz. A WORKDIR-rel az aktuális könyvtárat a Php értelmező webroot-jára állítjuk be, ez a /var/www/html. A Php beépített webszerverét használjuk majd, ennek port számát 80-ra állítjuk, ezért az EXPOSE értéke 80 lesz. Végül elindítjuk a Php-t a CMD paranccsal, melynek a -S 0.0.0.0:80 paramétert kell megadni ahhoz, hogy az minden rendelkezésre álló interfész 80-as portján működjön. (A CMD számára átadásra kerülő paramétereket továbbra is egy tömbben kell megadni.)

FROM php:7.4
LABEL maintainer="koczka.ferenc@uni-eszterhazy.hu"
WORKDIR /var/www/html
EXPOSE 80
CMD ["php", "-S", "0.0.0.0:80"]

Építsük fel a konténer image-et, a neve legyen phpweb, a verziószáma 1!

1kf@imac:~/docker$: docker build -t phpweb:1 .

Hozzuk létre a konténer egy példányát! A külső port, amin keresztül a konténerben futó alkalmazás 80-as belső portja elérhető lesz, legyen 8400, a konténer neve pedig www.phpweb.hu!

# Windows:
docker run --rm -p 8400:80 -v .\webroot:/var/www/html --name www.phpweb.hu phpweb:1

# Linux:
kf@imac:~/docker$:  docker run --rm -p 8400:80 -v $(pwd)/webroot:/var/www/html --name www.phpweb.hu phpweb:1
Php kód futtatása a konténerben

Php kód futtatása a konténerben

Érdemes lehet kipróbálni egyéb php verziókat: a régi kódokhoz rendelkezésre áll az 5.4-es, de jelen sorok írásakor legfrissebb 8.2 is. A phpinfo() függvény pontos információt ad a futó változatról.

Készítsük el ennek a konténernek is a production változatát, ami magában foglalja a website fájljait is! Ehhez az ADD parancsnak megadjuk a bemásolandó forrás és célkönyvtárat. A forrás a webroot, a cél az előző sorban a WORKDIR parancsban már beállított /var/www/html könyvtár. Mivel a célkönyvtár az aktuális könyvtár, a teljes megadási út helyett használható a . is. Mivel a webtartalom így a konténer image-be kerül, annak indításakor már nem kell a kötet összerendelést megadni.

FROM php:7.4
LABEL maintainer="koczka.ferenc@uni-eszterhazy.hu"
WORKDIR /var/www/html
ADD webroot .
EXPOSE 80
CMD ["php", "-S", "0.0.0.0:80"]

Építsük újra a konténer image-et:

1kf@imac:~/docker$: docker build -t phpweb:2 .

Majd indítsuk el a korábban már látott módon, az --rm nélkül és a háttérben futtatást lehetővé tevő -d-vel a 8500-as porton:

# Windows:
docker run -d -p 8500:80 --name www.phpweb.hu phpweb:2

# Linux:
kf@imac:~/docker$: docker run -d -p 8500:80 --name www.phpweb.hu phpweb:2

Ez az a működési forma, amelyet már egy production változatban, egy felhőszolgáltató infrastruktúráján is használhatunk.

Python Flask alkalmazás

Nem csak a Php alkalmazások működtethetők konténerekben, Pythonban is készíthetők webalkalmazások. A Pythonhoz elérhető Flask framework nagyszerű környezetet biztosít komplex website-ok kialakításához – ezek ráadásul könnyen konténerizálhatók. Alakítsunk ki egy ilyen környezetet, és készítsük el a Hello World üzenetet tartalmazó oldalunkat!

A fejlesztésünk projektkönyvtára az alábbi lesz. Az app könyvtár tartalmazza majd a Flask programot, és ebbe tettem a Flask telepítését leíró requirements.txt fájlt is. A konténer image felépítésére szolgáló Dockerfile a projektkönyvtárban, az app könyvtáron kívül helyezkedik el.

.
├── app
│   ├── app.py
│   └── requirements.txt
└── Dockerfile

Először lássuk, hogyan működik egy Flask alkalmazás! Az alábbi program egy Flask váz, egy Flask „hello world!”. Ez a Flask importálása és példányosítása után egy függvényt definiál, amely a site / URL-jének meghívásakor fut le. Mivel most az egyszerűségre törekszünk, ezért nincsenek további oldalaink, de ilyeneket további függvények létrehozásával és URL összerendelésével lehet előállítani. A program utolsó soraiban az alkalmazásba fordított webszerver indítását végezzük, amely minden rendelkezésre álló IP címen, az (egyébként alapértelmezett) 5000-es porton érhető el, és megjeleníti a debug üzeneteket is. Utóbbit a production alkalmazásokban melegen ajánlott kikapcsolni.

from flask import Flask                # Importáljuk a Flask-et
app = Flask(__name__)                  # Példányosítjuk

@app.route('/')                        # A / URL-re lefutó függvény
def index():
    return "<h1>Hello world!</h1>"

if __name__ == '__main__':             # A webszerver indítása
    app.run(host="0.0.0.0",
        port="5000", debug=True)

Bár a célunk nem a program natív futtatása, hiszen konténerizálni szeretnénk, de amennyiben a Python-t és a szükséges könyvtárait telepítetted a gépedre, az alkalmazás tulajdonképpen el is indítható, és a sph http://localhost:5000 URL-en a tartalma meg is tekinthető:

kf@imac:~/docker$: python3 app/app.py
* Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment.
  Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.24.60:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 120-733-884

Készítsük el a Dockerfile-t! Ebben induljunk ki a Python 3-as verziójából, majd a teljes app könyvtárat másoljuk be a konténerbe, amit a munkakönyvtár beállítása követ. Új elem a RUN sora, ez a Python szoftverek telepítésekor megszokott módon beolvassa a requirements.txt fájl tartalmát, és az abban felsorolt programkönyvtárakat a pip segítségével telepíti. A requirements.txt példánkban egyetlen sort tartalmaz, ez a Flask telepítését eredményezi. Mivel a pip futása a konténer image-ben történik meg, így a Flask a konténerben elérhető lesz.

kf@imac:~/docker$: cat app/requirements.txt
Flask

Tekintettel arra, hogy az alkalmazásban a webszerver port számát 5000-ben határoztuk meg, az EXPOSE értékeként ezt kell megadni. A Dockerfile utolsó sorában pedig a már szokásos módon elindítjuk az alkalmazást a CMD paranccsal.

FROM python:3
ADD app /app
WORKDIR /app
RUN pip install -r requirements.txt
EXPOSE 5000
CMD ["python3","app.py"]

A konténer image neve legyen flaskdemo, amit a már megszokott módon építünk fel. A kimenet 13. sora jelzi a Flask telepítését:

 1kf@imac:~/docker$: docker build -t flaskdemo .
 2[+] Building 5.7s (9/9) FINISHED
 3=> [internal] load build definition from Dockerfile                                0.0s
 4=> => transferring Dockerfile: 36B                                                 0.0s
 5=> [internal] load .dockerignore                                                   0.0s
 6=> => transferring context: 2B                                                     0.0s
 7=> [internal] load metadata for docker.io/library/python:3                         0.5s
 8=> [internal] load build context                                                   0.0s
 9=> => transferring context: 110B                                                   0.0s
10=> CACHED [1/4] FROM docker.io/library/python:3@sha256:e83d1f4d0c735c7a54fc9da...  0.0s
11=> [2/4] ADD app /app                                                              0.0s
12=> [3/4] WORKDIR /app                                                              0.0s
13=> [4/4] RUN pip install -r requirements.txt                                       4.7s
14=> exporting to image                                                              0.3s
15=> => exporting layers                                                             0.3s
16=> => writing image sha256:04489b5d4ae87ac23353989f15a8f18c777c760d21c487a4d02...  0.0s
17=> => naming to docker.io/library/flaskdemo

Indítsuk el a konténert:

kf@imac:~/docker$: docker run --rm --name www.flaskdemo.hu -p 8500:5000 flaskdemo
 * Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment.
  Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.17.0.5:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 343-913-642

Végül ellenőrizzük, hogyan működik! Az alkalmazás URL-je: http://localhost:8500, amit a böngészőbe írva a weboldalunk meg is jelenik:

Konténerizált Python-Flask alkalmazás.

Konténerizált Python-Flask alkalmazás.

A log üzenetek közt pedig látható, hogy a Flask kiszolgálja a bejövő kéréseket:

172.17.0.1 - - [03/Mar/2024 06:16:18] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [03/Mar/2024 06:16:18] "GET /favicon.ico HTTP/1.1" 404 -

C program konténerizálása

Egy C program konténerizálása egy ponton jelentősen eltér a korábbiaktól: míg a PHP és a Python nyelveken megírt programok interpretált módon futnak, azaz a forrásnyelvi program közvetlenül értelmezhető és végrehajtható az interpreter számára, egy C-ben írt programot előbb le kell fordítani. Ehhez egy fordítóprogramra is szükségünk lesz, ami a példánkban egy parancssorból indítható változat, a gcc lesz.

Fontos, hogy világos legyen: nem lenne jó a fordítást a saját gépünkön elvégezni, és az így kapott binárist a konténerbe másolni. A lefordított programok ugyanis hardver- (processzor-) és operációs rendszer függőek, így egy ilyen bináris alkalmazásával elvesztenénk az egyik legfontosabb előnyt, a platformfüggetlenséget. A konténerünket ezért úgy építjük fel, hogy a forráskód lefordítása majd a cél számítógépen, valahol a felhőben, vagy a kiválasztott szerveren történjen meg. Ezzel a módszerrel megtartjuk a platformfüggetlenséget, mivel az adott gép fordítóprogramja az aktuális processzoron és operációs rendszeren futó binárist fogja elkészíteni.

A feladat megoldása most tehát három lépésből áll. Elsőként elkészítjük C forrásnyelvi programot. A Dockerfile-unk viszont most két blokkból áll majd. Az első lefordítja a forráskódot, a második pedig a fordítási környezetet eldobva kialakítja a futtatást végző, a fordítóprogramot már nem tartalmazó konténert. Ehhez az alábbi projekt struktúrában fogunk dolgozni:

.
├── Dockerfile
├── run
└── src
    └── helloworld.c

Először írjuk meg a helloworld.c programot!

#include <stdio.h>
int main() {
    printf("Hello, World!\n");
    return 0;
}

Készítsük el a Dockerfile-t, ami tehát most két részből áll. Az első, a fordítási fázis egy C fordítóból indul ki, amelyet egy belső névvel is ellát, ez a compiler. A fordításhoz a forrás fájlt, a helloworld.c-t bemásoljuk a konténer /src könyvtárba, majd a gcc-vel lefordítjuk. Az így kapott bináris a konténer /src/hello könyvtárába kerül. A gcc paramétereként megadott -static hatására a bináris futásához nem szükséges semmilyen külső library, így egy minimális Linux környezetben is képes lesz futni. A második, futási fázisban így egy egészen kicsi képet használunk, az Alpine-t. A lefordított bináris fájlt bemásoljuk az első, fordítási fázisból, és beállítjuk az alapértelmezett parancsot a konténer futtatásához. A másolás során a forrást a --from=compiler paraméterrel írjuk le, ahol a compiler az előző fázisban alkalmazott név, ezzel tudunk hivatkozni az ott előállított tartalmakra.

# 1. blokk: fordítási fázis
FROM gcc:latest AS compiler
COPY src/helloworld.c /src/helloworld.c
RUN gcc /src/helloworld.c -static -o /src/helloworld

# 2. blokk: futttatási fázis
FROM alpine:latest
COPY --from=compiler /src/helloworld /helloworld
CMD ["/helloworld"]

Végül egy apró script a fordításhoz és a futtatáshoz (run):

docker build -t helloworld-in-c .
docker run --rm helloworld-in-c

Fáradozásunk gyümölcseként a konténerünk első lépésben elvégzi a fordítást, majd a fordítási környezetet eldobva lefuttatja az így kapott binárist. Az Alpine alkalmazásával pedig a konténerünk helyfoglalása elképesztően kicsi, mindössze 8,76 MByte:

1kf@imac:~/cDemo$: docker image ls
2REPOSITORY               TAG       IMAGE ID       CREATED          SIZE
3helloworld-in-c          latest    902c44118434   9 minutes ago    8.76MB

Egy C# konzol alkalmazás konténerizálása

Mivel a C# a legelterjedtebb programozási nyelvek egyike, érdemes megismerni az ebben készített alkalmazások konténerizálhatóságát is. Természetesen itt nem egy grafikus felületen futó interaktív program átalakítását célozzuk meg (ilyen típusú szoftvereket nem annyira működnek konténerizált formában), itt is a konzol alkalmazásokra koncentrálunk. Tekintettel a C# nyelv rendkívüli komplexitására, most nem merülünk el a részletekben, most is csupán egy egyszerű „Hello world!” alkalmazást fogunk készíteni.

A C# programok konténerizálása is eltér az interpreteres példákétól, itt is az előzőhöz hasonló módszerrel dolgozunk. Szerencsére az ezen a nyelven írt programokat nem csak Windows-on lehet futtatni, a Microsoft elérhetővé tette ezt Linux rendszerekre is. Példánkban egy Ubuntu Linux 20.04-es gépen dolgozunk majd, amelyre az alábbi módon telepíthetjük a Dotnet SDK 6-os verzióját:

apt-get update
apt-get install -y dotnet-sdk-6.0

A telepítésről részletesebb leírás érhető el a Microsoft weboldalán.

A sikeres telepítést követően készítsük el a programunk munkakönyvtárát – ehhez a C# egy parancssori eszközt is kínál: a dotnet new console -n Helloworld hatására a munkakönyvtárunk a szükséges fájlokkal együtt automatikusan létrejön. A Program.cs fájl már eleve egy Helloworld programot tartalmaz, így akár azzal is dolgozhatunk. Az így kapott könyvtárat egyetlen fájllal egészítjük majd ki, a konténer felépítésének lépéseit leíró Dockerfile-lal:

.
├── Dockerfile
├── Helloworld.csproj
├── obj
│   ├── Helloworld.csproj.nuget.dgspec.json
│   ├── Helloworld.csproj.nuget.g.props
│   ├── Helloworld.csproj.nuget.g.targets
│   ├── project.assets.json
│   └── project.nuget.cache
└── Program.cs

Bár az automatikusan generált, eredeti Program.cs is megfelelő lehet, készítsünk inkább egy minimalista, tradicionális programot:

using System;
namespace Helloworld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

A Dockerfile viszont most sokkal összetettebb, mivel annak működése két lépésből áll. Első lépésként elvégzi a konténerbe kerülő program fordítását, majd a konténer image felépítését.

# Először elkészítjük az alkalmazás binárisát
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env
WORKDIR /app
COPY *.csproj ./
RUN dotnet restore
COPY . ./
RUN dotnet publish -c Release -o out

# Felépítjük a futásidejű változatot
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "Helloworld.dll"]

A konténer felépítése és futtatása már a szokásos módon történik:

koczka@columbo:~$ docker build -t helloworld .
...

koczka@columbo:~$ docker run --rm helloworld
Hello, World!

Láttuk, hogy egy C# programot tartalmazó konténer előállítása nem feltétlenül nehéz feladat, egy megfelelően működő kétlépéses Dockerfile előállításával itt már többet kell foglalkozni. Az első működő példány előállítását követően az újabbak létrehozása már egyszerű feladat, amit a korábbi minták alapján már könnyen elvégezhetünk – a megismert módszert pedig más, a felépítés során compilert (fordítóprogramot) igénylő alkalmazás esetén alkalmazni lehet. Ha pedig a fordítást manuális úton végezzük el, akkor a korábban látott út is célravezető lehet.

Referencia

Parancs

Leírás

FROM

Meghatározza, hogy a konténer image melyik alap fájlból épüljön fel. Kötelező elem, minden Dockerfile-nak tartalmaznia kell.

LABEL

Metaadatokat ad a konténer image-hez (pl. verzió, leírás, szerző stb.).

CMD

Meghatározza azt a parancsot, amelyet a konténer elindulásakor automatikusan lefut.

WORKDIR

Beállítja az aktuális könyvtárat a RUN, CMD, ENTRYPOINT, COPY, és ADD utasításokhoz.

COPY

Fájlokat vagy könyvtárakat másol az image fájlrendszerébe.

ADD

A COPY-hoz hasonlóan működik, de helyi archívumok kicsomagolására és adott URL-en elérhető fájlok letöltésére is képes.

ENTRYPOINT

A CMD-hez hasonlóan a konténer indulásakor végrehajtandó parancsot határozza meg, de míg az a parancssorból nem írható felül, az ENTRYPOINT lehetőséget ad erre.

ENV

Környezeti változókat állít be amelyeket a konténeren belül futó folyamatok is elérnek.

EXPOSE

Megadja, hogy a konténer mely portok forgalmát kezeli.

Összefoglalás

Ebben a fejezetben megismertük azt a módszert, amellyel azokat a DockerHub konténereket hozták létre, amelyeket már eddig is használtunk. Láttuk, hogy konténer image-eket mi is építhetünk - a módszert alkalmazhatjuk saját, teljes egészében saját igényeink szerint működő és konfigurálható konténer image létrehozásakor. Ennek egyik kulcseleme a Dockerfile, amivel egy konténer image felépítése részletesen definiálható. Megismertük ennek felépítését és az abban használható legfontosabb kulcsszavakat. Láttuk az UnionFS szerepét, ami réteges felépítésével támogatja a konténer saját, belső fájlrendszerének, és az abban tárolt fájlok és könyvtárszerkezet kialakítását. A konténer image létrehozására szolgáló parancs különböző változatainak áttekintése után konkrét példákon keresztül gyakoroltuk be a konténerek építését, az itt bemutatott Php, Python és C# alkalmazások pedig valós fejlesztési környezetek alapjaira nyújtottak példát.

Feladatok

  1. Készíts egy konténert, amiben egy Python program minden indításkor eggyel nagyobb számot ír ki!

  2. Készíts két konténert, amik egy-egy Php programot tartalmaznak! Az első jelenítse meg a Hello, ez a web-1.pelda.hu szerver, a második pedig a Hello, ez a web-2.pelda.hu szerver szöveget! Mindkét konténerben futó Php használja a belső 80-as portot! A konténer image-ek neve legyen web-1.pelda.hu és web-2.pelda.hu!

  3. Indítsd el mindkét konténert úgy, hogy az elsőt a http://localhost:5000, a másodikat pedig a http://localhost:5001 URL-en lehessen elérni!

  4. Őrizd meg ezeket a konténer image-eket, mert a további fejezetekben még szükség lesz rájuk!

  5. Készíts egy konténerizált PostgreSQL adatbáziskezelőt! Gondoskodj róla, hogy a program adatkönyvtára ne vesszen el a konténer eldobása és újraépítése után sem! Az adatbázisokat a program a /var/lib/postgresql/data könyvtárban tárolja.

  6. Készíts egy konténerizált Php programot, amely egy SQLite adatbázis egy táblájában neveket tárol, abba 3 rekordot beszúr, majd megjeleníti őket! A feladatot úgy végezd el, hogy az adatbázisnak túl kell élnie a konténer esetleges újraépítését is!

  7. Oldd meg az előző feladatot Pythonban!