PHP alkalmazás fejlesztése Docker konténerekkel

php elefánt kép pixabay.com-ról

Az előző részben megmutattam, hogyan lehet statikus tartalmakat, kliensoldali webes alkalmazásokat fejleszteni és közzétenni Docker konténerben, most viszont a szerveroldali programokon a sor. Ebben az esetben az komplikálja a feladatot, hogy már nem elég a kliensnek odaadni a programfájlokat, azokat szerveroldalon kell értelmezni. Ez persze nem feltétlen jelentene önmagában problémát, mivel az értelmező és a szerver lehet egy konténerben is. PHP alkalmazásnál viszont érdemes külön konténerben futtatni a PHP értelmezőt és a webszervert. Az alábbiakban ennek mikéntjéről fogok írni.

Tartalom

Miért két konténer?

[Tartalom]

Az első kérdés biztos az, hogy mégis mi a fenének két konténer külön HTTPD-vel és PHP FPM-mel. Az Apache HTTPD szervernek van PHP modulja, tehát közvetlenül a modulon keresztül tudná értelmeztetni a PHP programot egyetlen konténerben. Ezzel pedig egy sor komplikációtól megkímélve. Nézzük az érveket.

Szokták mondani, hogy a PHP FPM jobb, mert gyorsabb. Azért ez így nem igaz. FPM esetén a HTTPD webszerver a PHP kéréseket továbbítja a PHP felé például TCP kapcsolaton keresztül, majd fogadnia kell a választ, amit majd visszaküldhet a kliensnek. Mindez nyilván időbe telik. Akkor hogy is van ez?

A HTTPD-ben a PHP modul feltétel nélkül betöltődik, akkor is, ha statikus tartalmat kell kiszolgálni. Stílusfájlokat, képeket, és így tovább. Ez persze nem azt jelenti, hogy értelmezni is fogja őket, de a modul legalábbis betölt. Ez is időbe telik, még ha önmagában ez minimális is, de minél több a statikus tartalmak forgalma, annál inkább számíthat.

Az FPM-nek van egy olyan előnye is, hogy a HTTPD szervertől független felhasználó nevében tud indulni, így a jogosultságokat is lehet egy kicsit finomítani.

A HTTPD webszervernek igazából nincs túl sok memóriára szüksége általában. Ezzel szemben a PHP-nek már sokkal inkább, de ugyanez igaz a processzorigényre is. Külön konténerben, akár külön hoszton sokkal könnyebb lehet elosztani az erőforrásokat és emellett az Apache HTTPD és PHP processzmenedzserét külön be lehet állítani az igényeknek megfelelően.

Mindehhez hozzájön a tény, hogy a külön konténerek miatt a frissítés is egymástól függetlenül történhet, vagy cserélni lehet a komponenseket. A PHP-ra nyilván szükség lesz, de hogy HTTPD vagy NginX lesz előtte, az már akár opcionális is lehet.

Gyakorlatilag szeretjük a PHP FPM-et, ezért szükséges a két konténer, de önmagában a külön konténer is praktikus, és akkor csak a PHP FPM jöhet szóba.

A HTTPD és PHP közti kommunikáció

[Tartalom]

A HTTPD-nek tudnia kell, hogy URL alapján milyen kéréseket továbbítson a PHP-nek, majd azokat TCP vagy Unix socketen keresztül szépen megbeszélik egymással. De ha már Dockernél tartunk, a Unix socketet rögtön felejtsük is el, és maradjunk a TCP-nél. Amúgy is fel kell készülni arra, hogy a fejlesztés után majd éles szerver jön, ahol az ég tudja, hol lesz az egyik vagy másik konténer.

Bár nem hálózati kommunikációval összefüggő kérdés, de nyilván nem szeretnénk, ha a PHP nem tudna válaszolni a HTTPD-nek egy kérésre, ezért komolyabb esetekben érdemes elmélyedni a processzmenedzserek lelkivilágában, amiről korábban a Processzkezelőkkel szemtől szemben: HTTPD MPM és PHP FPM változatok linux környezetben című cikkben írtam.

Az, hogy a HTTPD értse a PHP kéréseket és megfelelően továbbítsa a PHP-nek, például az alábbi konfigurációval érhető el:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
<FilesMatch "\.php(/.*)?$">
    SetHandler  "proxy:fcgi://host-vagy-ip-cim:9000"
</FilesMatch>

<Proxy "fcgi://hoszt-vagy-ip-cim:9000">
</Proxy>

A konténerek kommunikációja a gyakorlatban

[Tartalom]

Az előzőekben csak a PHP-ről és webszerverről volt szó, de konténereknél egy kicsit komplikáltabb az ügy. Ilyenkor a két komponenst úgy kell elképzelni, mint két külön gépen futó szolgáltatást, amik között hálózati kapcsolat van. A hálózatokról az "A Docker hálózatkezelése" részben írtam bővebben.

Fejlesztéskor is jó eséllyel vagy Docker Swarm-ot vagy Docke Compose-t fogsz használni, de ugye mindkettőnél nagyon hasonló yaml fájlt kell szerkeszteni. Most maradnék a Docker Compose-nál. Alább egy nagyon minimális váz látható a compose fájlra.

version: "3"

services
:
  php
:
   # ide jönnek a php konténer paraméterei
  httpd
:
   # ide jönnek a httpd konténer paraméterei

A services alatt persze nem lett volna muszáj a szolgáltatást is php-nek és httpd-nek nevezni. Az teljesen tetszőleges, de így a legegyszerűbb. Később majd szó lesz a paraméterekről is. Minden Compose projekt automatikusan saját hálózatot kap, így jól el lehet különíteni az egyes projekteket egymástól. Ám ahogy azt a hálózatoknál említettem, az egyedi Docker networkökre jellemző, hogy a konténerek nevei hosztnévként is funkcionálnak a hálózat konténerei között. Docker Compose-nál pedig a szolgáltatás neve is, amire később oda kell figyelni, amikor már nem elégszel meg egyetlen közös hálózattal, például reverse proxy esetén, de egy MySQL szervert is el lehet szeparálni az azt amúgy sem használó konténerektől.

Megjegyzés: Docker Compose-zal azért működnek hosztnévként a szolgáltatások nevei, mert a szolgáltatás minden hálózatában, annak minden konténere megkapja hálózati álnévként. Ezt a "docker network connect" utasítással bárki megteheti bármilyen konténer és hálózat esetén Compose nélkül is. A hálózatban azonos álnévvel rendelkező konténerek között pedig működik a load balancing is. MySQL-ről és proxy-ról majd később lesz szó részletesebben, de mivel élesben szinte biztos, hogy lesz reverse proxy, fontos megjegyezni, hogy a proxy hálózatához nem szabad a PHP konténereket is hozzáadni, csak a HTTPD-t, ami így a PHP-vel közös hálózaton keresztül csak a helyi PHP-t látja "php" néven. Már amennyiben így kívánsz hivatkozni a PHP konténerekre és nem fix IP-vel vagy egy speciális névvel

Mivel tehát a PHP service neve "php", ez felhasználható a korábban írt fcgi proxy konfigurációnál:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
<FilesMatch "\.php(/.*)?$">
    SetHandler  "proxy:fcgi://php:9000"
</FilesMatch>

<Proxy "fcgi://php:9000">
</Proxy>

Ezt akár be is lehetne másolni egy saját Docker image-ben a httpd.conf-ba vagy külön fájlba is, vagy lehet keresni olyan image-et, amiben ezt már megoldották, ahogy én az itsziget/httpd24-ben. Így a következőképpen folytathatod a Compose fájlt.

version: "3"

services
:
  php
:
    image
: php:7.1-fpm
    volumes
:
      - .:/var/www/html
  httpd
:
    image
: itsziget/httpd24:1.1
    volumes
:
      - .:/var/www/html
    environment
:
      SRV_DOCROOT
: /var/www/html
      SRV_PHP
: 1

Ez a konfiguráció abban az esetben működik, ha docker-compose.yml fájl mellett vannak a dokumentum gyökérbe való fájlok. Ez nyilván nem mindig van így. Olyankor az SRV_DOCROOT változó értékét és a volumes szekciókban a forrás mappát át kell írni. Van ráadásul olyan eset is, amikor a projekt gyökérben egy almappa a dokumentum gyökér. Ilyennel lehet találkozni egyes keretrendszereknél is, mint például a Symfony. A következőkben pedig egy komplett példát mutatok egy Symfony projekt beüzemelésére Docker Compose-zal.

Symfony projekt Docker Compose-zal

Symfony telepítése

[Tartalom]

Már régóta dolgozom Symfony keretrendszerrel, de ha nem tenném, akkor is nehéz lenne elkerülni a komponenseit, amit más keretrendszerek és alkalmazások is előszeretettel vetnek be. Egyik jellemzője, hogy létezik egy mappa a projekten belül, amire a szerver dokumentum gyökerét kell állítani. Ez régen "web" volt, ma már "public". A PHP látni fogja az efelett levő fájlokat, de a webszerveren keresztül azok elérhetetlenek lesznek. Ez pedig azt is jelenti, hogy ezen kívül más mappát nem is kell felcsatolni a webszerver konténerébe. Most készítsünk el egy egyszerű Symfony 3.4 projektet. Ez az LTS verzió jelenleg. Ha pedig hosszan támogatott verziót szeretnél, akkor érdemes ilyent választani.

Telepíteni több módon is lehetne a rendszert. Egy részt van hozzá egy telepítő szkript. Más részt az Composer-rel is lehet telepíteni. És a legújabb verziókat már nem is támogatja a Symfony installer. A telepítőnél apró bökkenő, hogy a szkript egy Phar fájl igazából, tehát már ahhoz is PHP-re van szükség. Ez viszont konténerbe is felcsatolható. Az alábbi példában a dokumentációban mutatott helyre másoltam a programot. Nincs nagy jelentősége, mivel úgyis konténerben fog működni.

sudo mkdir -p /usr/local/bin
sudo curl -LsS https://symfony.com/installer -o /usr/local/bin/symfony
sudo chmod a+x /usr/local/bin/symfony
# A fenti sorokat csak egyszer kell futtatni. Újabb symfony projekteknél már nem szükséges.
docker container run --rm -it -u $(id -u):$(id -g)  -v /usr/local/bin/symfony:/usr/local/bin/symfony -v $PWD:$PWD --workdir $PWD --name php php:7.1 symfony new php 3.4

Megjegyzés: Látszik, hogy nem az FPM verziót kértem a PHP konténerből, de ezen a ponton még nincs is szükség rá. Ellenben a konténerben a userid beállítására igen, hogy a fájlok ne root felhasználóval jöjjenek létre.

A Composeres verzió egy kicsit rövidebb. Mivel létezik hivatalos Composer image, ami tartalmazza a php-t is, felcsatolni sem kell semmit a munkakönyvtáron kívül.

docker container run --rm -it -u $(id -u):$(id -g) -v $PWD:$PWD --workdir $PWD --name php composer create-project symfony/skeleton php 3.4

Megjegyzés: A hivatalos Composer image használata élesben nem javasolt, de a Composer haladóbb használata Dockerrel későbbi cikkben lesz részletezve.

Ezt akár ki is lehet próbálni webszerver nélkül az alábbi utasítással, ahol még mindig nincs PHP FPM, hanem a PHP beépített szerverét használjuk. Ez inkább csak tesztelésre ajánlott viszont.

docker container run --rm -it -v $PWD:$PWD --workdir $PWD --name php php:7.1 -S 0.0.0.0:80 -t public

CTRL+C -vel ki lehet lépni. Erre már nem lesz szükség, jöhet a HTTPD szerver beállítása.

Fájlok felcsatolása

[Tartalom]

Akkor most már tényleg legyen webszerver is a projekthez.

version: "3"

services
:
  php
:
    image
: php:7.1-fpm
    volumes
:
      - .:/var/www/html
  httpd
:
    image
: itsziget/httpd24:1.1
    volumes
:
      - ./public:/var/www/html/public
    environment
:
      SRV_DOCROOT
: /var/www/html/public
      SRV_PHP
: 1
    depends_on
:
     - php

Ez már indítható is az alábbi módon:

docker-compose up -d

A webszerver elérésére több módot is találhatsz a HTTPD-ről és statikus tartalmakról szóló részben.

A példában látható, hogy a PHP konténer megkapta a teljes dokumentum gyökeret, de a HTTPD csak a public almappát. Persze fejlesztésnél ennek nagyjából semmi jelentősége nincs, így ebben a fázisban, ha úgy tetszik, nyugodtan fel lehet csatolni a teljes könyvtárat. Amit még észre kell venni, az a "depends_on", amivel a httpd-nél megadtam, hogy függ a "php"-től, tehát előbb a php szolgáltatásnak kell elindulnia. Érdemes mindig meghatározni a sorrendet, bár nem mindig számít.

Bár a docker-compose.yml-ben a "volumes" alatt van mindkettő, megkülönböztetünk volume-ot és felcsatolt hoszt könyvtárat, illetve fájlt (bind mount). Egy volume-ot a services blokkon kívül külön definiálni kell és nem is feltétlenül az adott hoszton lesz fizikailag. A bind mount-nál viszont arra kell figyelni, hogy vagy abszolút útvonal legyen a forrás vagy Docker Compose-nál legalábbis "./" vagy "../"-rel kezdődjön. De mi történik, ha a felcsatolandó könyvtár vagy fájl nem is létezik, például, mert hibás a hivatkozás, vagy nem lett létrehozva? A régi működés szerint ilyenkor automatikusan létrejön a felcsatolandó útvonal könyvtárként, ráadásul root jogosultságokkal. A program pedig ezután vagy működik vagy nem. Ez igazából a Docker működésének következménye a "-v" és a "--mount" opciók különbsége miatt. "--mount" esetén a nem létező forrás hibaüzenetet okoz. A Docker Compose ezt a funkcionalitást csak a 3.3-as Compose fájl verziótól támogatja, ami a Docker Compose 1.14-től elérhető. A verzió viszont még nem elég. Ilyenkor az alábbi módon kell a mount-ot definiálni a yml fájlban például egy, a projekten kívüli, a hosztra is felcsatolt mappa esetén:

   volumes:
      - .:/var/www/html
      - type
: bind
        source
: /mnt/data/files
        target
: /var/www/files

Figyelni kell a megfelelő indentálásra, mert itt a kötőjel utáni kulcs-érték pár miatt egy asszociatív tömb (map) keletkezik, aminek a source és a target is része. Alternatív formátum a JSON-hoz hasonló:

   volumes:
      - .:/var/www/html
      - {type
: bind, source: /mnt/data/files, target: /var/www/files}

Tudni kell, hogy a Docker image-ek gyakran saját volume definícióval készülnek, ami azt jelenti, hogy ha te nem is sorolod fel a volumes blokkban, attól az még létre fog jönni volume-ként, csak valami ilyesmi névvel fog szerepelni a "docker volume ls" utasítást futtatva:

d5d57d6d660641b561c57c2dfc153d56ec1c56b492e63a18661cd31c5eff5460

Amennyiben letörlöd a compose projektet a "docker-compose down" -nal, az újabb projektindításkor új volume fog létrejönni egy újabb, hasonlóan szép névvel. Ez viszont olyan esetben nem jó, amikor a létrejött adatoknak meg is kellene maradni. Ráadásul, ha valamiért nagyobb mennyiségű adat kerül ide, az a tárterületet is eltelíti lassan. Bár az eddigi image-ekben nem volt ilyen definíció, a teljesség kedvéért ki fogom tenni a Symfony "var" alkönyvtárát ilyen volume-ra, ami a cache-t és a logokat tartalmazza. Így az nem kerül a hosztra akkor sem, ha egyébként a dokumentum gyökeret felcsatoltam, aminek része a "var", mert a volume-ok a bind mount-ok után vannak felcsatolva.

version: "3.3"

volumes
:
  var
:

services
:
  php
:
    image
: php:7.1-fpm
    volumes
:
      - .:/var/www/html
      - var:/var/www/html/var
      - type
: bind
        source
: /mnt/data/files
        target
: /var/www/files
  httpd
:
    image
: itsziget/httpd24:1.1
    volumes
:
      - ./public:/var/www/html/public
    environment
:
      SRV_DOCROOT
: /var/www/html/public
      SRV_PHP
: 1
    depends_on
:
     - php

Itt már egy "volumes" blokk is megjelent külön a verziószám után. Az alapértelmezett volume-okat itt elég felsorolni paraméterek nélkül, csak indexként. Majd ugyanazzal a névvel lehet rá hivatkozni a szolgáltatásoknál. Tudni kell, hogy ezek a volume-ok annak a konténernek az adataival töltődnek fel, amelyik előbb elindul. Ha valamiért létezne a webszerver konténerben is a mappa, amit a php konténer adataival kellett volna feltölteni, a "depends_on" nélkül előfordulhat, hogy ez nem történik meg.

Olyan eset is lehetséges, amikor egy mappát elérhetővé kell tenni a webszerveren keresztül, de a PHP-nak is el kell érnie, például, mert az másolja bele a fájlokat feltöltésnél. Ekkor először is mind a két szolgáltatásnak fel kell csatolni a mappát, más részt adná magát, hogy a dokumentum gyökérbe történjen a csatolás, hiszen ott elérhető lesz közvetlenül a httpd szerveren keresztül. Ha viszont a dokumentum gyökér is fel van csatolva mindkét szolgáltatásnak bind mount-tal, akkor a hoszton is létre fog jönni üres mappaként a felcsatolt célkönyvtár. Ez persze nem biztos, hogy zavar, ugyanakkor felesleges is. Ám, ha ezt még a "shared" opcióval is ötvözöd valamilyen okból, akkor nem üres mappa jön létre, hanem a hoszton is fel lesz csatolva a dokumentum gyökérbe a mappa. Az ráadásul ott is marad, amíg az "umount" utasítással rendszergazdaként le nem csatolod. Ezt aztán elkezdi beolvasni az IDE (ha nincs kivételként beállítva) és a hoszton végzett műveleteknél is figyelembe kell venni, nehogy véletlenül letöröld vagy egy tartalomban keresés műveletet feleslegesen egy több gigabájtnyi képkönyvtáron is lefuttass.

Megjegyzés: A "shared" opció bind mount-nál adható meg a célkönyvtár után pl. "source:target:shared" alakban, ami annyit tesz, hogy amennyiben az ezzel megjelölt mount célkönyvtárába a konténerben fel lesz csatolva egy új fájl vagy mappa, az ugyanúgy a hoszton is felcsatolódik a mount forráskönyvtárában. volume-oknál pedig nem lehet bind opciókat megadni.

A webszerveren egy alias beállításával az egyébként dokumentum gyökéren kívülre felcsatolt mappát is elérhetővé lehet tenni az alábbi szerverkonfigurációval, amennyiben az "alias" modul engedélyezett:

Alias /files /var/www/files
<Directory /var/www/files>
Require all granted
</Directory>

Ahol az Alias után az első paraméter a böngészőben beírandó, a második a fájlrendszeren levő útvonal. Erre a mappára viszont jó eséllyel a webszerveren nincs hozzáférés engedélyezve, ezért ezt is be kell állítani a fent látható módon. Gond persze akkor van, ha a PHP program sem támogatja, hogy tetszőleges helyen lehessen a feltöltések mappája.

Fájlrendszer jogosultságok

[Tartalom]

Eddig csak a webszerver konfigurációjában megadható jogosultságról volt szó, de a fájlrendszer még ennél is gyakoribb problémát okozhat.

A Symfony 3 "var" nevű almappájáról már volt szó, ami tartalmazza a naplófájlokat és a cache-t. Alapértelmezésként a PHP a www-data felhasználóval működik, tehát a fájlokat is azzal menti. Ha volume-ként lettek létrehozva, ez nem gond, mert úgyis be kell lépni a konténerbe, ahol jó eséllyel nem lesz gond a hozzáféréssel. Ha azonban a gazda rendszerről szeretnéd elérni a fájlokat az egyszerűség kedvéért, majd akár törölni őket, csak root-ként lehet. Olvasni bármilyen felhasználóval tudod, de előfordulhatna olyan eset, amikor egy PHP program, vagy épp a fejlesztő/megrendelő speciális jogokat igényel. Például olvasási joga csak a tulajdonosnak lehet. Egy ilyen fájlt még olvasni sem fogsz tudni a hosztról, ami főleg akkor probléma, amikor Docker image-et szeretnél készíteni, és a Docker sem tudja beolvasni a fájlokat, amikor összegyűjti a .dockerignore-ban nem szereplő elemeket a fájlrendszerről.

Ilyenkor meg lehetne adni a PHP FPM konfigurációban, hogy a PHP-t futtató felhasználó azonosítói megegyezzenek a sajátjaiddal (user, group). Más részt viszont, ahogy a Symfony telepítésénél történt, a konténer felhasználója is felülbírálható egy nem root felhasználói azonosítóval, ami után a PHP nem fog tudni más nevében futni, hiszen nincs hozzá joga. Bármi is van az FPM konfigurációban, az figyelmen kívül lesz hagyva. Ilyenkor így néz ki egy compose fájl:

version: "3.3"

services
:
  php
:
    image
: php:7.1-fpm
    volumes
:
      - .:/var/www/html
      - type
: bind
        source
: /mnt/data/files
        target
: /var/www/files
    user
: ${USER_ID:-33}:${GROUP_ID:-33}
  httpd
:
    image
: itsziget/httpd24:1.1
    volumes
:
      - ./public:/var/www/html/public
    environment
:
      SRV_DOCROOT
: /var/www/html/public
      SRV_PHP
: 1
    depends_on
:
     - php

A USER_ID és GROUP_ID környezeti változók, amiket majd be kell állítani indításkor. Ha nem teszed, akkor az alapértelmezett 33-as azonosítókkal fog működni a konténer. Az indítás pedig a következőképpen történne:

USER_ID=$(id -u) GROUP_ID=$(id -g) docker-compose up -d

Persze fixen is bele lehet írni az azonosítókat a compose fájlba, ha csak te fejlesztesz és tuti nem lesz futtatva más felhasználóval. Vagy akár egy "docker-compose.override.yml" nevű fájlban mindenki beállítja a saját azonosítóit, amit a Docker Compose automatikusan összefűz a "docker-compose.yml" fájllal.

Egyedi igények

[Tartalom]

Általában a hivatalos Docker image-ekből érdemes kiindulni, de van, amikor nem elég, és kellenek a PHP-hez további modulok. Ez lehet akár egy memcached vagy redis kiterjesztés is a session-ök tárolására. Ilyenkor saját Docker image-et kell készíteni. Ezt fejlesztési időben nem muszáj feltölteni sehova. Valójában a projektben is lehet tárolni a Dockerfile-t, ami a telepítést leírja. Majd a projekt első indításakor lefut a build. Ebben az esetben az "image" helyett a docker-compose.yml-ben a "build"-et kell megadni.

# ...
  php
:
    build
:
      context
: .
      dockerfile
: php.Dockerfile
# ...

Ha csak egy Dockerfile-ra van szükség és nem kell egyedi név hozzá, akkor a "dockerfile" sor ki is hagyható. A build leírása a php.Dockerfile-ban pedig így néz ki:

FROM php:7.1-fpm

RUN apt-get update \
 && apt-get install -y --no-install-recommends libmemcached-dev \
 && pecl install memcached \
 && docker-php-ext-enable memcached \
 && apt-get remove --purge -y libmemcached-dev libhashkit-dev libsasl2-dev

Ezzel egy rétegben történik a telepítés és a pecl install után már felesleges programkönyvtárak törlése. Hogy mik feleslegesek, azokat nagyjából teszteléssel lehet megtudni. De "-dev" -re végződő csomagokat jó eséllyel biztonságosan ki lehet törölni. A telepítéskor pedig kiírja az "apt", hogy miket telepített a kért csomagokon kívül. Azokat is figyelni kell.

Megjegyzés: A hivatalos PHP image-ek adnak ugyan a modulok engedélyezéséhez és telepítéséhez szkripteket, de a függőségeket már neked kell megfejteni és előre telepíteni. Ehhez próbáltam segítséget adni a saját verziómmal, amiben előre leteszteltem több telepíthető modult, és a függőségeik telepítéséhez elkészítettem a szkripteket. A kész, minden általam támogatott modult telepítő verzió letölthető a Docker Hub-ról is.

A memcached konténer elindítása pedig már csak egy újabb sor a yaml fájlban. Ezután a PHP-t kellene konfigurálni, hogy a session-t a memcached-del kezelje, de a Symfony-ban definiálható kódból is egyedi Memcached session handler.

docker-compose.yml

version: "3.3"

services
:
  php
:
    build
:
      context
: .
      dockerfile
: php.Dockerfile
    volumes
:
     - .:/var/www/html
      - type
: bind
        source
: /mnt/data/files
        target
: /var/www/files
    user
: ${USER_ID:-33}:${GROUP_ID:-33}
  httpd
:
    image
: itsziget/httpd24:1.1
    volumes
:
     - ./public:/var/www/html/public
    environment
:
      SRV_DOCROOT
: /var/www/html/public
      SRV_PHP
: 1
  memcached
:
    image
: memcached:1.5-alpine

Session handler Symfony-ban

namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\MemcachedSessionHandler;
use Memcached;

class DefaultController
{
    public function index()
    {
        $memcached = new Memcached();
        $memcached->addServer('memcached', '11211');
        $session = new Session(new NativeSessionStorage([], new MemcachedSessionHandler($memcached)));

        $c = $session->get('c') ?? 1;
        $response = new Response('Request counter: ' . $c);
        $session->set('c', $c + 1);
        return $response;
    }
}

Ez persze megint egy olyan dolog, amire fejlesztéskor kisebb valószínűséggel lesz szükség, de ha élesben kell, akkor már fejlesztéskor is érdemes azzal dolgozni. Azt tudni kell a Memcached-ről, hogy skálázni csak úgy lehet, ha a php programban mindegyik példány címét felveszed, majd egy algoritmus alapján hol egyik, hol másik szerverre kerülnek az adatok. Ha viszont valamelyik szerver kiesik, a többi nem tudja átvenni a helyét, az azon levő adatok elvesznek. Ha ezt elkerülnéd, akkor a Redis-szel próbálkozhatsz. Ez a fejezet részletesen azt viszont már nem tárgyalja.

Ha egyedi szerverkonfigurációra is szükség van, akkor azt szintén bele lehet tenni saját Docker image-be. Persze, amíg folyamatosan szerkesztgetni kell, fel is lehet csatolni a konténerbe. Végül pedig, amire még valószínűleg szükséged lesz, az alkalmazás debugolása. az XDebug erre egy elterjedt eszköz. Ez viszont érezhetően lassíthatná a programot, ezért csak fejlesztői környezetben ajánlott telepíteni, ami a Dockerfile-hoz való következő sorok hozzáadásával történhetne:

RUN yes | pecl install xdebug \
 && echo "zend_extension=$(find /usr/local/lib/php/extensions/ -name xdebug.so)" > /usr/local/etc/php/conf.d/xdebug.ini \
 && echo "xdebug.remote_enable=on" >> /usr/local/etc/php/conf.d/xdebug.ini \
 && echo "xdebug.remote_autostart=off" >> /usr/local/etc/php/conf.d/xdebug.ini \
 && echo "xdebug.remote_connect_back=on" >> /usr/local/etc/php/conf.d/xdebug.ini \
 && echo "xdebug.idekey=XDEBUG" >> /usr/local/etc/php/conf.d/xdebug.ini

Itt még az lehet probléma, hogy amennyiben kódsoronként léptetve debugolod a programot valamilyen klienssel, a HTTPD szerveren a PHP FPM-hez definiált proxy nem fogja megvárni a program végét, ezért a böngészőben már csak hibaüzenet fogsz látni. Ez elkerülhető a timeout megnövelésével az alábbi módon:

<Proxy "fcgi://php:9000">
    ProxySet disablereuse=on
    ProxySet timeout=3600
</Proxy>

A "disablereuse" opció azért szükséges, mert a jelek szerint a HTTPD bármilyen direktíva megadásával újra felhasználná a már indított processeket, ami gyakori hibaüzenetekhez vezetne a weboldalon. Az itsziget/httpd24 image a 2.0 verzió óta alapértelmezetten tartalmazza ezt a direktívát is az állítható timeout mellett

Befejezés

Nehéz minden kérdést lefedni, mert a problémák és így a megoldásaik egyediek lehetnek, de annyit kijelenthetek, hogy sokszor a speciálisabb igényekre is viszonylag egyszerű megoldás. Talán csak rájönni nem az. Viszont minél többet foglalkozol a Dockerrel, annál könnyebb lesz. A speciálisnak tűnő igények pedig gyakran nem is olyan speciálisan, mint hinnéd, így valaki már talán fel is tette a kérdést egy fórumon, amit kis kereséssel meg is találhatsz. Azt nyilván nem állíthatom, hogy minden megoldható Dockerben, hiszen mindent még én sem próbáltam ki. Az Chuck Norris feladata... Félni ugyanakkor nem szabad a Dockerrel fejlesztéstől és üzemeltetéstől, mert az esetek többségében eddigi tapasztalataim szerint többet segít, mint amennyit esetleg akadályoz és tanulásra kényszerít.

Források

[Tartalom]

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

Hozzászólások

Kele Norbert képe

Szia,
jó volt a cikk. Pont szükségem lenne valami hasonlóra, de még csak most ismerkedek a Dockerrel és nem tudom, hogy ami nekem kellene az egyáltalán megoldható-e vagy teljesen rossz oldaltól közelítem meg a dolgot. Esetleg tudnál ebben tanácsot vagy segítséget adni?
Tehát van 2 éles szerver, amin weblapok és webalkalmazások futnak. Mindegyiken php, mysql, phpmyadmin, git és composer van. Viszont az egyik php 5-öt még a másik php 7-et futtat és a mysql verziók sem egyeznek. Nagyon jó lenne ha lehetne készíteni olyan Docker konténereket, amik a 2 éles szervert tükrözik, mindezt úgy hogy pl. a gazda gép home könyvtárában van egy szerver1 és egy szerver2 mappa, amik az adott szerveren lévő weblapokat és webalkalmazásokat tartalmazzák és ezek lennének felcsatolna az egyik és a másik konténerbe. Gondlom mivel itt több weblapról van szó egy konténeren belül, lehetőség kellene legyen arra is hogy az apache sites-enabled configjait és a hosts fájlt be lehessen állítani. Továbbá a webalkalmazások fájlokat hoznak létre és azok jogosultsága is jó kellene legyen, tehát a gazda gép és a docker konténerben futó php is el kellene érje, ezenkívül a composernek is működnie kellene a konténeren belülről és azokat a fájlokat is el kellene érni. Tehát lényegében a saját gépen menne a fejlesztés, de magát a szerver környezetet a Docker biztosítaná.
Ez megoldható vagy csak túl lenne bonyolítva és egyszerűbben is meg lehet oldani?

Rimelek képe

Szia. Ennél kevesebb is problémás lenne. Először is lehet, hogy nem értem jól a helyzetet, de ha mégis, akkor ezek szerint a Docker csak a hoszt gép csomagkezelője helyett van használva és egyébként minden weblap adott szerveren belül ugyanabban a konténerben működne. Ehhez jönne hozzá, hogy ezeken a szervereken (vagy ez már a saját gép?) lenne a fejlesztéshez szükséges forráskód is, ami meg lenne osztva valahogy a fejlesztői géppel (ha nem eleve itt van) és felcsatolva a konténerekbe is. Ezeknek pedig az éles szerver tükrének kéne lennie.

A Docker azért jó, mert egy, kettő vagy kétszáz példányban is el tudod indítani ugyanazt akárhol, amíg készítesz hozzá saját image-et. Ezen kívül a különböző alkalmazásoknak különböző igényei lehetnek és ezért különböző image-ek kellenek hozzá és külön konténerben kell futniuk. Ehhez jön még, hogy más kell élesben és más fejlesztés közben (git, composer, xdebug, stb), amikkel nem akarod lassítani az éles példányt (erről készülőben van bővebb cikk, csak nem volt időm befejezni). A Composernek ráadásul akár 2 giga memória is kellhet, miközben a PHP-ben nem biztos, hogy akarsz engedni ennyi memóriát az alkalmazásoknak.

Arról nem beszélve, hogy ha minden weboldal egy közös Apache HTTPD konténeren keresztül érhető el, akkor külön még le sem tudod őket állítani, illetve nem tudod őket frissíteni egymástól függetlenül. Ez eléggé szembe megy a Docker elveivel.

Ami a jogosultságokat illeti, arról kell gondoskodni, hogy legalább az írandó mappák és fájlok legyenek a PHP user tulajdonában. És ha külön HTTPD konténered van, akkor az is legalább olvasni tudja a dokumentum gyökeret, de ezzel nem nagyon szokott gond lenni. Főleg, ha a fájlok a PHP image-ben vannak és az image készítésekor be lett állítva a fájlok jogosultsága.

A composer user kérdésről írtam a cikkben. Azóta még rájöttem, hogy ha a PHP-t is saját userrel futtatod, más problémákat vet fel. Ha jól rémlik, olyankor nem tudott logolni az stderr-re a konténer, de legalábbis valami volt, amivel sokat szívtam, mire rájöttem, mert eleinte én is így dolgoztam a composerrel.

Jelenleg a Composert külön konténerből futtatom, ami az "dev" alkalmazás php image-en alapul (ami az éles + fejlesztői eszközök), csak le van tiltva az xdebug. A user, amivel futtatom, paraméterben adható meg, és a saját userem lesz. Előfordul, hogy tényleg helyre kell tenni az írás jogot, de azért nem futtatjuk a composert percenként általában.

Szóval, amit én javaslok, hogy minden PHP alkalmazásnak legyen saját PHP image-e, amibe beleteszed az alkalmazást. Teszel minden PHP konténer elé egy HTTPD konténert (vagy NginX-et.) Nem kell sites-enabled konfiguráció, mert minden konténer egy domainre hallgat (vagy további aliasokra, ha kell pl usereknek aldomain.) és a saját PHP konténerének küldi a PHP kéréseket.

Fejlesztés közben használhatod az éles app image-et, és felcsatolod a szerkesztendő fájlokat felülírva az eredetieket. Vagy amit én csinálok, hogy helyben build-elek új image-et, mert változhat a Dockerfile is.

Élesben mindegyik app annyi erőforrást kap, amennyit akarsz. Olyan kiterjesztéseket telepítesz bele, amikre szüksége van. Tetszőlegesen felskálázhatod csak azt, amit kell. Ráadásul, ha az egyik appodon keresztül bejutnak a konténerbe, a többi appod védve van. Teljesen mindegy hány fizikai szervered van és hány PHP vagy MySQL verzióra van szükséged. Legalábbis a fizikai szerverek száma nem befolyásolja, csak az elérhető erőforrások.

Sikerült válaszolnom a kérdésedre?

Kele Norbert képe

Az éles szerveren nincs Docker, az egy sima virtuális szerver apache, php 7, mysql stb. van rajta és van még egy virtuális szerver amin apache, php 5, mysql, stb. van. Ezeken van több webalkalmazás (ezek vannak a sites-enabled-ben konfigurálva), a régiek php5-ön az újak már a php7-es szerveren. Nekem csak annyi kellene hogy a local gépen, amin megy a webfejlesztés ugyanaz a környezet legyen mint az éles virtuális szervereken. Azért gondoltam a Dockerre, hogy azzal talán mindkét szervert tudnám szimulálni független az operációs rendszertől ami a local számítógépen van (így a többi webfejlesztőknek csak odaadnám az image-t, hogy ezt indítsd el és így fejlessz és akkor valószínűleg jó lesz élesben is). De ha jól értelek, akkor valóban elbonyolítottam és miden webalkalmazásnak csinálhatnék saját image-t localba és amit épp fejleszteni kell annak az image-t indítom el. Ha így csinálnám, akkor lényegében annyi is elég lenne, hogy docker-compose-ben megadom a kívánt php-t, mysql-t, stb. és mehet is a dolog?

Rimelek képe

Igen, de csak ha élesben is van Docker. Ha nem így csinálod, akkor nem garantálhatod az azonos környezetet, csak remélheted, hogy elég jól csináltad ahhoz, hogy közel azonos legyen. De akkor nem vagy közelebb annál, mintha virtuális gépet indítottál volna szerintem. Bár az erőforrásokkal akkor is jobban gazdálkodhatnál Dockerrel. Viszont egy Docker konténer valószínűleg nem is tartalmaz mindent, amit mondjuk egy telepített virtuális vagy rendes gép igen, tehát ez is egy lehetőség arra, hogy legyenek különbségek. Akkor pedig lehet, hogy egy Vagrant géppel még jobban is járnál.

De innen indultam én is, majd el is vetettem, mert sokkal macerásabb volt fejleszteni vele, mint Dockerrel. Még ha lenne valami Ansible konfigurációd és az éles szerver is ugyanúgy állna elő, mint mindenki saját virtuális gépe, akkor legalább az azonosság biztosítva lenne.

Végül is nem mondom, hogy ne lehetne Docker ügyesen konfigurálva csak fejlesztői környezetben, ha jelenleg ez a megoldható és a VM nem járható út, de nem lenne 100%-os.

Kele Norbert képe

Köszönöm a választ. Igen, valószínűleg nem lenne 100%-os egyezés, de még mindig jobb lenne, mint php7-el fejleszteni és azt feltenni a php5-ös szerverre. :)