Apache HTTPD és statikus tartalmak Dockerrel

http kép pixabay.com-ról

Amióta a témával foglalkozom, hallottam olyan véleményt, hogy a Docker csak fejlesztésre jó, élesben nem. De olyat is, hogy élesben jó, de fejlesztésre alkalmatlan. Az igazság az, hogy mindkettőre jó, de egy sor olyan dolog van, amit másképp kell csinálni. Az is igaz, hogy karddal sem kenünk kenyeret és papírvágó késsel sem vágunk fát, tehát észben kell tartani a célt és ki kell választani a legjobb eszközt. Általánosan viszont nem jelenthető ki az alkalmatlanság. Ha valaki komolyabb webfejlesztéshez szeretné használni a Dockert, előbb tisztában kell lennie a működésével, az alapjaival, és azt is el kell fogadnia, hogy a komolyabb munkákra felkészüléshez talán sokat kell dolgoznia. Egy összetettebb webalkalmazás akár több konténerből is állhat, de most az Apache HTTPD konténerrel való munkát fogom bemutatni egyszerűbb, szerveroldali szkriptet és adatbázist nem igénylő statikus oldalakkal, kliensoldali alkalmazásokkal és azok jelszóval védésével.

Tartalom

Előfeltétel

[Tartalom]

Ha még nem olvastad és új vagy a Docker témában, javaslom, hogy a sorozat elejéről kezd az olvasást: Webfejlesztés Dockerrel. Ha még a "Mi a Docker" kérdésre keresed a választ, a mi-a-docker címkével ellátott cikkek olvasását javaslom.

Szükség lesz egy telepített Dockerre és Docker Compose-ra lehetőleg valamilyen desktop Linux operációs rendszeren. Grafikus felület nélküli verzió is megteszi, ha más nincs, de ott bizonyos lépéseket valamivel nehezebb lesz tesztelni. A megfelelő környezet kiválasztásához segíthet a Hol futtassam a konténereimet fejlesztés közben? című fejezet. Ha van elég erőforrásod virtuális gép futtatására és jó internetkapcsolatod egy több, mint 2 gigás gép letöltésére, akkor az A Docker telepítése Ubuntu 16.04-re Vagrant segítségével című fejezet végén található módon letölthetsz egy alkalmas virtuális gépet. A Vagranttal telepítés nem feltétel. Ha jobban szeretnéd az ott leírt rendszert magad felépíteni, azt is megteheted.

Kelleni fog egy terminál is. Az általam készített virtuális gépben az asztalon jobb egérgombbal kattintva az "Open terminal" opcióval meg is lehet nyitni egyet. Böngészőnek pedig a menüből a Firefox kiválasztható.

Webszerver indítása

[Tartalom]

Az Apache webszerver telepítése Deban 6-ra című, 2013-ban írt cikkben leírtakkal ellentétben a legtöbb esetben Dockerrel nem kell a telepítésen sokat agyalni és szinte mindegy, hogy milyen verzióról van szó. Lehet az akár 2.2 vagy 2.4. Bár a 2.2-es már rég elavultnak számít, még elérhető és letölthető az image hozzá. Mivel a Docker egyik nagy előnye éppen az, hogy egymás mellett tetszőleges verziókat tetszőleges példányban futtathatsz, nincs szükség manuálisan hálózati interfészeket konfigurálni. Minden újabb verzió automatikusan kapni fog egy belső IP címet, ami fejlesztés alatt akár elég is lehet. Az alábbi sort a terminálba beírva máris elindul egy 2.4-es Apache HTTPD webszerver.

docker container run -d --name webszerver httpd:2.4

A "-d" opció a "detached" mód, azaz a konténer a háttérben indul. Nem látszik, amit a HTTPD kiír a képernyőre. A "‑‑name" után pedig egy nevet lehet írni, amire később hivatkozni lehet. Ezzel már elindult a webszerver, amiről megbizonyosodhatsz, ha az alábbi parancsot futtatod:

docker container list

Látszik, hogy a szerver működik és az is, hogy a 80-as TCP porton figyel, de a böngészőbe beírva, hogy "localhost", nem fog bejönni semmi. Ez azért van, mert ahogy említettem, minden konténer kapni fog egy saját IP címet és annak portjait használja. A konténerben futó webszerver elérésére innentől több módszer is van.

A webszerver elérése

[Tartalom]

Az IP címet az alábbi módon lehet megtudni az előbb megadott "webszerver" nevet használva:

docker container inspect webszerver

A fenti sor egy JSON választ fog adni, amiben az "IPAddress"-t kell keresni. Linuxon ezt grep paranccsal is lehet szűrni:

docker container inspect webszerver | grep IPAddress

Több találat is lesz, de benne lesz az IP is. Az igazi Dockeres megoldás viszont az inspect "‑f" opciója után írt Go template-tel egyenesen az IP cím megjelenítése:

docker container inspect -f "{{.NetworkSettings.IPAddress}}" webszerver

Az IP a Vagrant virtuális gépben első konténerként biztos, hogy "172.17.0.2" lesz. Lehet automatizálni, hogy parancssorból a konténer nevének ismeretében megnyíljon a böngészőben a weboldal akármilyen IP címen is van. Nem sokkal jobb ez, mint manuálisan megnézni az IP-t, de valamit gyorsít a munkán. Firefoxnál ez Ubuntuban így menne:

firefox $(docker container inspect -f '{{.NetworkSettings.IPAddress}}' webszerver)

Akár így, akár beírva a böngészőbe már megjelenik, hogy "It works!". Az ezt megjelenítő index.html már benne van a httpd konténerben a /usr/local/apache2/htdocs mappában, amit le kell majd később lecserélni a saját weboldal tartalmára. Az IP-hez lehet domént is rendelni a hosts fájlban, ami linuxon az /etc/hosts útvonalon van. Persze a gond az, hogy ez az IP nem biztos, hogy állandó. Ha már több konténer van, akkor mindegyik egy új, még szabad címet kap. Tehát előfordulhat, hogy egy törlés és újra létrehozás között az IP egy másik konténerhez kerül. Eleinte ennek kicsi az esélye, de idővel nőni fog. Ezért erre később még általánosabb megoldást kell találni, de addig is több lehetőség van. Mindenek előtt ideje törölni a most futó webszerver konténert:

docker container rm -f webszerver

A "‑f" azért kellett, mert még futott a konténer és így nem kellett külön leállítani előtte a "stop"-pal. Ezt a lépést a továbbiakban minden "webszerver" nevű konténer indítása előtt végezd el, hogy ne legyen névütközés. Most az IP ismerete helyett akár kihagyható a konténer hálózatának izolációja, hogy a "localhost:80" működjön böngészőből:

docker container run -d --name webszerver --net host httpd:2.4

De ebben az esetben nem lehetne egyszerűen több verziót futtatni párhuzamosan, vagy bármit, ami szintén a 80-as porton figyel. De az sem biztos, hogy működik, ha már eleve foglalt a 80-as port. Célszerűbb a gazda operációs rendszer tetszőleges portját átirányítani a konténer 80-as portjára. Az alábbi példában a 8080-ast, így a "localhost:8080" címen lesz elérhető a konténer:

docker container run -d --name webszerver -p 8080:80 httpd:2.4

Így minden konténer egyedi portot kaphatna, de mindig figyelni kell arra, hogy a megadott port ne legyen foglalt. Azt is lehet, hogy nem adod meg a gazda portját, csak a konténerét, a Docker pedig generál egy portszámot.

docker container run -d --name webszerver -p 80 httpd:2.4

Ezzel nem sokkal vagy előrébb, mert most már nem kell lekérdezned az IP-t, de a portot igen. Le lehetne kérdezni ezt is az "inspect"-tel, de van más mód is.

docker container port webszerver 80

Ez a "webszerver" nevű konténer összes portátirányítását megmutatja, ami a 80-as belső portra mutat. Innen kileshető a port, és szintén automatizálható a weblap böngészőben megnyitása.

firefox $(docker container port webszerver 80 | head -n1)

A "| head -n1"-nek köszönhetően több port esetén csak az első lesz figyelembe véve. A porton kívül viszont a gazda IP címét is meg lehet adni az átirányításnál. Ez akkor jó, ha több külső cím van hozzárendelve, vagy valamelyik lokális címet adod meg, amiből elég sok van, tehát elvileg elég, de persze még ekkor is lehetne IP ütközés:

docker container run -d --name webszerver -p 127.0.1.5:80:80 httpd:2.4

Ha viszont a gazda 80-as portja minden IP-n foglalt, akkor ez sem fog működni. A gazda portja ilyenkor elhagyható, de a kettőspontok nem. Tehát automatikusan generált portszám fix IP mellett így nézne ki:

docker container run -d --name webszerver -p 127.0.1.5::80 httpd:2.4

Mindegyik fent írt megoldásra működik a portátirányítás lekérdezése és automatikus megnyitás, de ez azért még így sem mindig kényelmes, és jó lenne, ha domén néven is el lehetne érni az oldalakat. Az eddig leírt esetekben lehetne használni egy szolgáltatást, ami automatikusan módosítja a hosts fájlt, de ahhoz kell adminisztrátori jogosultság és érdemes egy mentést készíteni a fájlról mindenképp. Van, hogy a konténerek nem érhetők el közvetlenül IP címen. Ilyenkor kell egy szolgáltatás, egy proxy, amire a használt portok át vannak irányítva és a proxy már a Docker hálózatában rálát a többi konténerre is. Mindez pedig automatizálható, de erről majd később lesz szó.

Statikus weblap vagy kliensoldali alkalmazás fejlesztése

HTML fájlok

[Tartalom]

Az eddigiekkel egy a gond. Saját fájlokról még szó se volt. A fájlok SCP-vel vagy rsync-kel szinkronizálása nem jöhet szóba. Általában nincs is SSH démon egy konténerben. Van ugyan lehetőség fájlok másolására, de másolni csak image-be érdemes, vagy ha azonnali beavatkozásra van szükség és elkerülendő a konténer törlése, esetleg épp a konténerből kell kimásolni valamit. Az image-ekről később lesz szó és a fejlesztés közben folyamatosan változó fájloknál egyébként is jobb felcsatolni a projekt könyvtárát, vagy legalább azt a mappát, ahol a változó fájlok vannak, ha az image-ben már ott van a többi.

Most ismét az a biztos, ha törlöd az előző konténert, már most egy másik nevet fogok megadni, de a lefoglalt portokból lehetne probléma. Elvileg csak egy konténer van ezen a ponton, de akár az összes konténer is törölhető egyszerre az alábbi paranccsal a biztonság kedvéért.

docker container rm -f $(docker container list -qa)

A törlés parancsa más ismerős lehet. Utána konténerek nevét vagy azonosítót kell megadni. A dollár mögötti zárójelben levő kód pedig pontosan azt adja vissza. Az összes futó és leállított konténer azonosítóját. Most kell viszont egy projekt könyvtár a következő konténerhez.

mkdir -p projects/html && cd projects/html

Abban legyen egy egyszerű html fájl. Egy "index.html". A tartalma pedig például "Hello Docker!", de tetszőleges lehet.

echo "Hello Docker!" > index.html

Most pedig indítani kell egy konténert, amibe fel lesz csatolva az aktuális mappa a webszerver gyökér könyvtárának helyére:

docker container run -d --name html -p 8080:80 --mount type=bind,source=$PWD,target=/usr/local/apache2/htdocs,ro httpd:2.4

A fenti sor nagy része ismerős kell legyen. Most "html" néven jött létre a webszerver konténere, viszont van egy "‑‑mount" opció is. Ezután egy szóközök nélküli, vesszőkkel elválasztott rész jön. Itt az első a "type", azaz a felcsatolás típusa. A "bind" azt jelenti, "a gazda rendszer egy általam megadott mappáját szeretném felcsatolni a konténerbe". Más típusról később lesz szó. A "source" után a felcsatolandó forrás mappa jön, ami lehet változó is, mint jelen esetben a $PWD, ami az éppen aktuális könyvtár teljes útvonala Ubuntuban. Fontos, hogy teljes útvonalat kell megadni, nem relatívat! Végül a "target" után kell megadni a felcsatolandó mappa konténerben választott helyét. A negyedik rész, amivel megadható, hogy a felcsatolt mappa csak olvasható legyen a konténerben, vagy módosítható is. Ennek itt még nincs sok jelentősége, de mivel nem kell a konténerből a fájlt módosítani, megadtam az "ro"-t, azaz "read‑only" jelzőt.

A "--mount" az új lehetőség a fájlok felcsatolására. A dokumentáció ezt javasolja már arra hivatkozva, hogy logikusabb a korábbi módszernél, ugyanis itt pontosan tudni, hogy melyik a forrás mappa és melyik a cél. Bár ez igaz, cserébe viszont hosszabb az utasítás és lesz egy kötelező "type", amennyiben saját mappát csatolnál fel. A teljesség kedvéért alább mutatom a régi utasítást, ami a fentivel egyenértékű és a "‑v" vagy "‑‑volume" opcióval dolgozik, az egyes részeket pedig kettősponttal választja el egymástól. Akár így is írható:

docker container run -d --name html -p 8080:80 -v $PWD:/usr/local/apache2/htdocs:ro httpd:2.4

A lényeg, hogy ezek után az index.html és a projekt mappában bármilyen fájl szerkeszthető és azonnal elérhető lesz a konténerben, így a webszerveren keresztül. Mivel ezeket a szervernek csak olvasni kell tudni, ebből nem szokott gond lenni a jogosultságok miatt fejlesztés közben. Lehet használni htaccess-t is egyes mappák levédésére vagy URL rewrite-ot. Ezek viszont alapértelmezetten le vannak tiltva a szerverbeállításokban, ezért ilyenkor módosítani kell a konfigurációt is.

Szerverkonfiguráció

[Tartalom]

Ahogy a HTML fájlokat, úgy a konfigurációs fájlokat sem szerkesztjük közvetlenül a konténerben. A konfiguráció már fejlesztési időben is praktikusabb, ha eleve egy saját, módosított image-ben van, de semmi gond nincs azzal sem, ha azt is felcsatolod. Ahhoz viszont tudni kellene, hogy merre vannak ezek a konfigurációs fájlok, mik a jelenlegi paraméterek és min kell változtatni. Most jön az, hogy a docker "exec" utasításával a konténerben parancsokat lehet lefuttatni, ami lehet akár egy "ls", ami a fájlokat listázza, vagy valamilyen shell is indítható, ami tulajdonképpen a konténerbe való belépést jelenti.

docker container exec -it html bash

A "-it" az interaktív terminált jelenti, amire szükség van, ha shellt indítasz. Utána a konténer neve jön, végül pedig a futtatandó parancs, ami most a bash. Innentől kezdve pedig lehet nézelődni a fájlok között. A webszerver könyvtárába kerülsz belépéskor, mert ez a munkakönyvtár (workdir), ami minden konténer image-ében megadható, vagy akár a konténer létrehozásakor is. A conf/httpd.conf fájlt kellene kimásolni a konténerből, miután "exit"-tel kilépsz.

docker container cp html:/usr/local/apache2/conf/httpd.conf .

Ebben a fájlban a DocumentRoot "/usr/local/apache2/htdocs" sort kell keresni, majd alatta a "Directory" blokkban az "AllowOverride None" -t módosítani "AllowOverride All" -ra. Most pedig nincs más hátra (tényleg?), mint felcsatolni a konfigurációs fájlt a helyére.

docker container rm -f html
docker container run -d --name html -p 8080:80 \
    --mount type=bind,source=$PWD,target=/usr/local/apache2/htdocs,ro \
    --mount type=bind,source=$PWD/httpd.conf,target=/usr/local/apache2/conf/httpd.conf,ro \
    httpd:2.4

Így már működik a rewrite is és saját gépen még nagyon biztonsági kockázat sincs, de azért ugye érzed, hogy a webszerver gyökérkönyvtárában a httpd.conf nem lesz jó helyen? Ugyanis oda is fel lesz csatolva. Ha tehát vannak egyedi konfigurációs fájljaid, azt a webszerver gyökerén kívül célszerű elhelyezni. Rakjuk is helyre rögtön:

docker container rm -f html
mkdir htdocs && mv index.html htdocs/
docker container run -d --name html -p 8080:80 \
    --mount type=bind,source=$PWD/htdocs,target=/usr/local/apache2/htdocs,ro \
    --mount type=bind,source=$PWD/httpd.conf,target=/usr/local/apache2/conf/httpd.conf,ro \
    httpd:2.4

Bonyolódik a parancs, de ezen a cikk végére segítek. Most még valami htaccess kellene a fentiek tesztelésére.

Jelszavas védelem

[Tartalom]

Ami még kliensoldali alkalmazásoknál is szükséges lehet, az a jelszavas védelem. Mivel nincs szerveroldali szkript, ami az autentikációt végzi, megfelel egy egyszerű HTTP autentikáció. Ehhez első lépésben egy jelszófájlt kellene generálni. A jelszót a htpasswd nevű program tudja generálni, ami benne van a HTTPD konténerben. Nem kell viszont a konténerbe belépni és ott futtatni, mivel akkor a konténerben jönne létre a fájl és át kellene másolni egy megosztott területre, majd onnan a gazda rendszeren a helyére. Lehet viszont indítani egy új konténert, ami csak addig él, amíg lefut a program.

docker container run --rm httpd:2.4 htpasswd -nb teszt elek > .htpasswd

Itt a "htpasswd -nb teszt elek > .htpasswd" a futtatandó parancs, tehát minden, ami az image neve után van. Ez hasonló, mint amikor a "docker container exec"-et használtam, csak így egy még nem létező konténer jön létre, lefut a parancs, majd törlődik a konténer a "--rm" opció miatt. Enélkül megmaradna, csak leállna. Így a név "teszt" lesz, a jelszó pedig "elek". A jelszó viszont titkosítva kerül bele a .htpasswd nevű fájlba a dokumentum gyökéren kívül, a httpd.conf mellett. Most jöhet a ".htaccess" nevű fájl a htdocs alkönyvtárba, az index.html mellé:

AuthType Basic
AuthName "Secure Area"
AuthUserFile /usr/local/apache2/.htpasswd

Require valid-user

Megjegyzés: Parancssori szövegszerkesztőben szerkesztéshez a "nano .htaccess", ablakos szerkesztőhöz pedig a "gedit .htaccess" parancs használható a terminálból. Az ablak bezárása után pedig újra lehet gépelni a terminálba.

Végül pedig újra kellene generálni a konténert úgy, hogy a helyére tegye már a fent hivatkozott .htpasswd fájlt is.

docker container rm -f html
docker container run -d --name html -p 8080:80 \
    --mount type=bind,source=$PWD/htdocs,target=/usr/local/apache2/htdocs,ro \
    --mount type=bind,source=$PWD/httpd.conf,target=/usr/local/apache2/conf/httpd.conf,ro \
    --mount type=bind,source=$PWD/.htpasswd,target=/usr/local/apache2/.htpasswd,ro \
    httpd:2.4

Így már beírva a böngészőbe, hogy "localhost:8080", a szerver kérni fogja a nevet (teszt) és a jelszót (elek). Nagyjából minden működik, csak egy kicsit sokat kell gépelni és erre emlékezni is kéne később. Ilyenkor lesz hasznos a Docker Compose.

Fejlesztés Docker Compose-zal

[Tartalom]

A Docker Compose egy konfigurációs fájl segítségével teszi leírhatóvá és kezelhetővé a teljes projektet, akár 1, akár több konténerből áll. Innentől nem kell minden paramétert parancssorban írni. Nem kell a konténer nevét tudni leállításkor és induláskor. Valójában a Docker Compose generálja a nevet, bár felülírható. A legutolsó konténerindítás a következőképpen néz ki vele. Hozz létre egy "docker-compose.yml" nevű fájlt a htdocs mappa mellett a projekt könyvtárában, aminek a tartalma:

version: "3"

services
:
  httpd
:
    image
: httpd:2.4
    ports
:
      - "8080:80"
    volumes
:  
      - ./htdocs:/usr/local/apache2/htdocs:ro
      - ./httpd.conf:/usr/local/apache2/conf/httpd.conf:ro
      - ./.htpasswd:/usr/local/apache2/.htpasswd:ro      

Ezután futtasd a következő parancsot:

docker-compose up -d

A "-d" ugyanazt jelenti, mint a "docker container run" esetén. A háttérben indul a projekt összes konténere. "run" helyett pedig "up"-ot kell írni, hogy a fájlban leírt összes szolgáltatás elinduljon. A verzió a fájl elején kötelező. Ettől függ, a fájlban milyen paraméterek érvényesek. A verziók közti különbséget itt nem részletezném. A "services" alatt jönnek a szolgáltatások. A "httpd" nevű szolgáltatás jelen esetben gyakorlatilag egy konténert jelent, aminek a neve "html_httpd_1" lesz, ami a projekt mappájából, a szolgáltatás nevéből és egy sorszámból adódik össze. Utóbbi egyelőre mindig 1 lesz. A "containe_name" paraméterrel ez felülírható lenne. De több is történik itt. Egy részt megfigyelhetted, hogy relatív útvonalakat írtam a fájlok felcsatolásánál és a "volumes" kulcsszó alá tettem. A Docker Compose kiegészíti abszolút útvonalakra a fájlneveket, de az nagyon fontos, hogy "./" -rel kezdd a forrás fájlok elérési útját relatív útvonalaknál.

A másik, ami nem látszik, hogy a Docker Compose minden projektnek saját hálózatot hoz létre alapesetben. Ha tehát most megnéznéd a html_httpd konténer IP címét, nem 172.17-tel kezdődne, mint korábban, hanem legalább 172.18-cal. A projekt indításakor ki is kellett írja, hogy Creating network "html_default" with default driver. Ha már nem kell, törölhető a konténer és a hálózat is az alábbi utasítással:

docker-compose down

Addig pedig az IP címet vagy az egyszerű grep-pel, vagy egy módosított Go template szabállyal tudod lekérdezni a korábbiakkal ellentétben:

docker container inspect -f "{{.NetworkSettings.Networks.html_default.IPAddress}}" html_httpd_1

Befejezés

[Tartalom]

Ilyen egyszerű oldalaknál tehát nincs különösebb nehézség. A fejlesztett kódot is fel lehet csatolni a konténerbe, ahogy bármilyen statikus tartalmat, de a konfigurációs fájlokat is. Élesben ilyenkor minden szükséges fájlt érdemes egy saját image-be tenni, mert így bárhol, bármikor használhatod az image-et akárhány szerveren akárhány példányban. Ha van CSS preprocesszor vagy bármi, ami fejlesztés közben kell, de élesben nem, azokat ki lehet hagyni az image-ből és sokkal kisebb lesz az eredmény. Saját image-hez kell viszont majd saját Docker Registry is,
de a Docker Hub-on is lehet egy repository-t ingyen privátként használni.

További cikkek a Dockerről

Kategóriák: 
Megosztás/Mentés