Composer helyes használata Dockerrel

composer logo a getcomposer.com-ról

A Composer a PHP függőségkezelője, ami szintén PHP nyelven lett megírva, és jellemzően egy composer.phar nevű fájl telepítésével használjuk a projekt gyökeréből vagy rendszermappából. Éles környezetben nem szükséges a webszerveren lennie, csak a függőségek letöltéséhez szükséges. De akkor miért is kérdés egyáltalán a Dockerrel használata? Ha tovább olvasod a cikket, hamarosan megtudod.

Környezet, mint függőség

[Tartalom]

Egy PHP alkalmazásnak lehetnek függőségei más PHP alkalmazások, mint osztálykönyvtárak, vagy akár program modulok, de ezen kívül nyilván az egész magától a PHP értelmezőtől is függ. Annak verziójától, illetve telepített kiterjesztéseitől. A Composer programot tehát a lehető leghasonlóbb környezetben kell futtatni, amilyenben majd a PHP alkalmazás élesben is működni fog, mert a Composer így tudja figyelembe venni az alkalmazás igényeit, és figyelmeztetni, ha valami hiányzik. Ha egy modulnak függősége egy újabb PHP verzió, akkor azt a csomagot nem fogod tudni letölteni, ami úgysem működne adott verzióval. Ha viszont eltérő PHP-val dolgozol a Composer használatakor, mint ami az alkalmazásod konténerében lenne, akkor végül a weboldal nem fog működni, vagy olyan hibákat produkál majd, amit nem értesz.

Olyan is előfordulhat, hogy valamit a Composerhez használt PHP-val nem tudsz telepíteni, mert ott hiányoznak kiterjesztések, vagy nem megfelelő a verzió. Igaz, hogy lehetne használni az "‑‑ignore‑platform‑reqs" opciót, ezt viszont nem javaslom, mert ilyenkor pont azt fogja figyelmen kívül hagyni, ami befolyásolja, hogy milyen csomagok lesznek telepítve.

Futtatás a PHP alkalmazáséval egyenértékű konténerben

[Tartalom]

Abban tehát már megegyezhetünk, hogy konténerben kell futtatni a Composert, hiszen a PHP program is konténerben fog működni, de még ekkor is több lehetőség közül lehetne választani. Ismét hangsúlyozom, hogy a cél a PHP alkalmazás konténeréhez leghasonlóbb konténert tenni a Composer alá. Ez nem jelenti azt, hogy teljesen megegyezőnek kell lenniük, tehát nem muszáj ugyanabban a konténerben sem futtatni, de akár azt is lehet. Most pedig nézzük a lehetőségeket.

A hivatalos Composer image használatával

[Tartalom]

Nagyon jó dolog, hogy a Docker Hub-on több eszköz már hivatalos image-ből is elérhető, de a hivatalos forrás ebben az esetben nem jó választás. Még ha meg is tudnád választani a PHP verziót a Composer alatt, amit jelenleg nem tudsz, akkor is külön bele kellene telepíteni minden PHP kiterjesztést, ami a programodnak szükséges.

Ez vagy dupla konfigurációt, illetve telepítést jelentene, vagy pedig egy telepítő szkript letöltését mindkét image elkészítésénél. Ám ne felejtsük el, hogy a image-ekben levő Linux disztribúció sem feltétlenül lesz azonos. Az egyik lehet Alpine Linux, a másik pedig Debian alapú, tehát a közös telepítő nem működik.

Egyik előnye azonban ennek az image-nek, hogy a Composer futtatásához megteremti a szükséges feltételeket, és tulajdonképpen ez a Docker célja.

A hivatalos image tehát már tartalmazza azokat a programokat, amiket a Composerrel általában használni szoktunk. Ilyenek például a verziókezelő kliensek, valamint az SSH kliens. Ezzel tehát nem kellene bajlódni már, ugyanakkor az sem biztos, hogy a programhoz szükséged van Mercurial vagy Subversion kliensekre, de akár az SSH is opcionális lehet, bár javasolt.

Nem utolsósorban a PHP memória limit is ki van kapcsolva ebben az image-ben, ami lényeges, mert a Composer ma már akár másfél giga memóriát is igényelhet. Szomorú, de ez van, el kell fogadni és élvezni az előnyöket.

Az alkalmazás fejlesztői image-ből indított konténerében

[Tartalom]

A legbiztosabb módja annak, hogy a Composer és a PHP alkalmazás ugyanolyan konténerben fussanak, ha ugyanabban a konténerben futnak. Ez igaz, viszont a Composer-nek és a PHP alkalmazásnak eltérőek lesznek az igényei. Egy részt bele kellene telepítened az alkalmazás mellé például a Git-et és SSH-t, amire nem biztos, hogy szüksége van, bár a dev és a production verziók ennyiben eltérhetnek egymástól. Más részt nem feltétlenül adnál másfél gigánál több memóriát neki, végtelent pedig semmiképp. Ha mégis emellett döntesz, akkor van néhány módszer, amit érdemes követni.

Ahogy azt írtam, nem muszáj teljesen megegyezni a prod és dev image-nek. A különbséget nyilván minimumon kell tartani, de production-szerű környezetben is tesztelni kell a programot, tehát, ha valami valószínűtlen hiba miatt fejlesztői módban jobban működne a program, mint élesben, az legkésőbb akkor kiderül. Viszont felesleges programokkal teleszórni egy Docker image-et megint csak nem tanácsos. Ezt a kis különbséget pedig a development és production image között két módon lehetne leírni:

  • Örökléssel: A fejlesztői image leírható egy új Dockerfile-ban, amiben a FROM kulcsszó után az élesnek szánt image áll, tehát mindent tartalmaz, amit az élesnek kell, de beletelepíthetsz további programokat. Így például a Composert, Git-et, SSH-t és XDebug-ot is. Ezzel az a probléma, hogy fejlesztés közben az eredetileg élesnek szánt verzió is változhat, és nem örököltethetsz egy korábbi verzióból, illetve az sem igazán kényelmes, ha menet közben újabb és újabb dev image-eket hozol létre és töltesz fel egy regsitry-be. Ha előnyt kellene mondanom, az talán az, hogy ha egyszer működött az image, akkor másnak, másnap vagy máshol is működni fog. Igen, mert van, amikor ez nem garantált.

  • Paraméterezett build-del: A Dockerfile tartalmazhat "ARG" kulcsszavakat is. Ezek hasonlók az "ENV"-hez, amik a futó konténerben hoznak létre környezeti változót, viszont build időben fognak létezni, tehát eltérő image-et is lehet készíteni ugyanazzal a Dockerfile-lal. Ezt a build parancsnak a "‑‑build‑arg" opcióval lehet átadni. Így nem kell duplikálni a fájl tartalmát, vagy kitenni shell szkriptbe, ami a rétegek kezelését nehezítené meg. Közben pedig egy vagy több eltérés is megoldható például egy BUILD_ENV nevű argumentummal. Az értékek akár lehetnek dev, composer és prod, ahol a composer megegyezhet a dev-vel leszámítva, hogy van benne Composer is. De akár külön argumentum is jelezheti, hogy szükség van-e rá vagy sem. Ha ezt a megoldást választod, a fejlesztői image akár csak lokálisan is létezhet, nem muszáj feltölteni egy központi tárolóba. Megeshet, hogy egy új gépen is le kell futtatni a fejlesztői build-et. Ha van olyan nélkülözhetetlen fejlesztői eszköz, aminek fennáll az esélye, hogy egy újabb build-nél hibásan fut le valamilyen külső függés miatt, és ennek javítása a még elfogadhatónál több időt vehet igénybe, akkor azért érdemes mégis tartani ebből is egy távoli image-et. Viszont, mivel nem készíthetsz új build-et, változtatni sem tudsz rajta, ami azért felvethet némi problémát a fejlesztésnél. Ilyenkor maximum a konténerben telepíthetsz és elmentheted a "docker commit" utasítással.

Példát a paraméterekre mutatok, mert az öröklést talán nem kell magyarázni, az utasítások pedig a feltételt leszámítva ugyanazok. Az alábbi részlettel a Dockerfile-ból igyekszem a lényeget mutatni.

FROM php:7.1-fpm

# ...

ARG BUILD_ENV="prod"
ARG COMPOSER_VERSION="1.6.5"

# ...

ENV PHP_ENV="${BUILD_ENV}" \
    XDEBUG_REMOTE_PORT="9000" \
    XDEBUG_REMOTE_CONNECT_BACK="1" \
    XDEBUG_IDEKEY="PHPSTORM" \
    XDEBUG_AUTOSTART="off" \
    COMPOSER_HOME=/tmp \
    COMPOSER_MEMORY_LIMIT=2G

# ...

RUN if [ "${BUILD_ENV}" = "dev" ]; then \
                apt-get update && apt-get install -y --no-install-recommends git zlib1g-dev openssh-client \
         && docker-php-ext-install zip \
         && apt-get remove --purge -y zlib1g-dev \
         && php -r "readfile('https://raw.githubusercontent.com/composer/getcomposer.org/1b137f8bf6db3e79a38a5bc45324414a6b1f9df2/web/installer');" | php -- --install-dir=/usr/bin/ --filename=composer --version=${COMPOSER_VERSION} \
         &&  yes | pecl install xdebug \
         && echo "zend_extension=$(find /usr/local/lib/php/extensions/ -name xdebug.so)" > ${XDEBUG_INI}  \
         && echo "xdebug.remote_enable=on" >> ${XDEBUG_INI} \
         && echo "xdebug.remote_autostart=\${XDEBUG_AUTOSTART}" >> ${XDEBUG_INI} \
         && echo "xdebug.remote_port=\${XDEBUG_REMOTE_PORT}" >> ${XDEBUG_INI} \
         && echo "xdebug.remote_connect_back=\${XDEBUG_REMOTE_CONNECT_BACK}" >> ${XDEBUG_INI} \
         && echo "xdebug.idekey=\${XDEBUG_IDEKEY}" >> ${XDEBUG_INI}; \
   fi

# ...

Látható, hogy

  • Két argumentumot is felvettem.

    • A BUILD_ENV értéke alapból "prod", de ha "dev"-re állítom, települhetnek a fejlesztéshez szükséges eszközök.

    • A COMPOSER_VERSION tartalmazza a telepítendő Composer verzióját. Fontos, hogy fix verzió legyen telepítve, mert szeretnénk, ha minél kevesebb meglepetés érne minket, igaz?

  • A GitHub-ról egy egészen konkrét Composer telepítő szkript töltődik le a commit hash-re hivatkozva, hogy még a telepítő különbsége se okozzon problémát az újabb buildeknél.

  • Vannak változók definiálva az XDebug konfigurációjához. Az xdebug.ini-be nem a változók értéke kerül, hanem maga a változó. Ezért a backslash a dollár előtt. Tehát a konténer indításakor változtathatók a paraméterek. Észre kell venni, hogy ezzel a módszerrel az éles image-ben is benne lesznek az XDebug változók, de nem lesz semmilyen hatásuk. Ha a felesleges változók zavarnak, akkor muszáj külön fájlban leírni a fejlesztői módot, vagy argumentumként is felveheted a változókat, és más módon kell gondoskodnod róla, hogy a PHP számára elérhetők legyenek alapértékeikkel. Például egy egyedi indító szkript létrehozásával, amiben definiálva vannak a változók.

  • Feltűnhetett, hogy memórialimitet nem állítottam be globálisan, de van egy COMPOSER_MEMORY_LIMIT változó, ami a Composer 1.6.0 óta használható a memória limit növelésére, és értelemszerűen nincs hatással a teljes alkalmazás korlátaira. Persze, ha a weblapnak van szüksége fejlesztői módban több memóriára, akkor azt is módosítani kell.

Muszáj megemlítenem, hogy pecl programmal 7.0-nál régebbi PHP-hez nem lehet telepíteni az XDebug-ot. Az elv akkor is ugyanez, csak más a módszer.

Ezt kellene tehát az előző részben már elkészített projektbe implementálni. Először is másold a teljes projekt mappát egy új "composer" nevű mappába. Az eredetit is lehet módosítani. Így viszont utólag látod a különbségeket.

Az új php.Dockerfile tehát a következő lesz:

FROM php:7.1-fpm

ARG BUILD_ENV="prod"
ARG COMPOSER_VERSION="1.6.5"

ENV PHP_ENV="${BUILD_ENV}" \
    XDEBUG_REMOTE_PORT="9000" \
    XDEBUG_REMOTE_CONNECT_BACK="1" \
    XDEBUG_IDEKEY="PHPSTORM" \
    XDEBUG_AUTOSTART="off" \
    XDEBUG_INI="/usr/local/etc/php/conf.d/xdebug.ini" \
    COMPOSER_HOME=/tmp \
    COMPOSER_MEMORY_LIMIT=2G

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

RUN if [ "${BUILD_ENV}" = "dev" ]; then \
                apt-get update && apt-get install -y --no-install-recommends git zlib1g-dev openssh-client \
         && docker-php-ext-install zip \
         && apt-get remove --purge -y zlib1g-dev \
         && php -r "readfile('https://raw.githubusercontent.com/composer/getcomposer.org/1b137f8bf6db3e79a38a5bc45324414a6b1f9df2/web/installer');" | php -- --install-dir=/usr/bin/ --filename=composer --$
         &&  yes | pecl install xdebug \
         && echo "zend_extension=$(find /usr/local/lib/php/extensions/ -name xdebug.so)" > ${XDEBUG_INI}  \
         && echo "xdebug.remote_enable=on" >> ${XDEBUG_INI} \
         && echo "xdebug.remote_autostart=\${XDEBUG_AUTOSTART}" >> ${XDEBUG_INI} \
         && echo "xdebug.remote_port=\${XDEBUG_REMOTE_PORT}" >> ${XDEBUG_INI} \
         && echo "xdebug.remote_connect_back=\${XDEBUG_REMOTE_CONNECT_BACK}" >> ${XDEBUG_INI} \
         && echo "xdebug.idekey=\${XDEBUG_IDEKEY}" >> ${XDEBUG_INI}; \
   fi

A docker-compose.yml pedig annyiban változik, hogy be kell állítani a build argumentumokat. Legalábbis a BUILD_ENV-et. Ezen kívül viszont a használt httpd image verzióját is megváltoztatom, mert időközben elkészült a 2.0. Nem kompatibilis teljesen a régivel, de ebben a példában nyugodtan át lehet írni az 1.1-es verziót 2.0-ra. Nincs különbség. Vagyis a következő lesz az eredmény:

version: "3.3"

services
:
  php
:
    build
:
      context
: .
      dockerfile
: php.Dockerfile
      args
:
        BUILD_ENV
: dev
    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:2.0
    volumes
:
      - ./public:/var/www/html/public
    environment
:
      SRV_DOCROOT
: /var/www/html/public
      SRV_PHP
: 1
  memcached
:
    image
: memcached:1.5-alpine

Ezt máris el lehet indítani

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

Majd pedig a composer is futtatható

docker-compose exec php composer update

Ha a konténer felhasználóját az alapértelmezett root-on hagytad, ezen a ponton akkor is megadhatod, hogy a composer kinek a nevében fusson:

docker-compose exec -u $(id -u):$(id -g) php composer update

Arra viszont figyelj, hogy ha véletlenül sikerült a root nevében indítani korábban a composer parancsot, létrehozta a tmp könyvtárban a cache mappát, amit a már helyes userrel nem fogsz tudni írni, ezért azt előtte törölni kell. Ez már alakul. Ha pedig mégis változtatni kell a memórialimiten, akkor azt is megteheted a változó felülírásával:

docker-compose exec -e COMPOSER_MEMORY_LIMIT=-1 php composer update

A fejlesztői image-ből indított külön konténerben

[Tartalom]

Ebben az esetben nincs szükség fejlesztés közben külön Composer image-re. A fejlesztői image már tartalmazni fogja a Composer függőségeit, viszont a Composerből gyakorlatilag tetszőleges verziót lehet használni. Minden verzió külön volume-ra töltődhet le, ha még nincs, ahol a volume-ot a verziószámmal és a felhasználói, valamint csoportazonosítóval különböztetjük meg, így a jogosultsággal sem lesz gond és bármilyen projektben újrahasznosítható. Van lehetőség arra, hogy a gazda rendszer hálózatában futtasd, így a gazdán működő SSH tunnel + hosts fájl módosítása is működik például otthoni munka esetén a munkahelyi hálózatban levő GIT és/vagy Composer szerver elérésével. Ezt egy jó kompromisszumnak gondolom a teljesen külön image és az alkalmazás konténerében futtatás között. Ehhez az alábbi fájlokat kell hozzáadni a projekthez:

  • docker-compose.composer.yml: A composer konténer extra paramétereit tartalmazza, mint a host network használatát.

  • composer.sh: Shell script, ami letölti a választott composer verzióját volume-ra, majd futtatja is.

docker-compose.composer.yml

version: '3.3'

networks
:
  default
:
    external
:
      name
: none

services
:
  php
:
    network_mode
: host

composer.sh

#!/usr/bin/env bash

cd $(dirname "$0");
WORKINGDIR=${PWD}

dockerCompose () {
    local OVERRIDE_YML=$([ -f "docker-compose.override.yml" ] && echo "docker-compose.override.yml" || echo "");
    COMPOSE_PROJECT_NAME="composer-script" docker-compose \
        -f docker-compose.yml \
        ${OVERRIDE_YML:+-f "${OVERRIDE_YML}"} \
       -f docker-compose.composer.yml \
       run --no-deps \
           -u "
${1}" \
           -w ${WORKINGDIR} \
           -e COMPOSER_HOME=${COMPOSER_HOME} \
           -e COMPOSER_MEMORY_LIMIT=2G \
           -v $HOME/.ssh/id_rsa:$HOME/id_rsa \
           -v ${WORKINGDIR}:${WORKINGDIR} \
           -v ${VOLUME}:${COMPOSER_HOME} \
           php bash -c "
${@:2}"
}

run () {
    local VERSION=1.6.5
    local VOLUME_PREFIX="composer_version"
    local USER_ID="
$(id -u)"
    local GROUP_ID="
$(id -g)"
    local VOLUME="
${VOLUME_PREFIX}_${VERSION}_${USER_ID}_${GROUP_ID}"
    local COMPOSER_HOME="
/tmp/composer"
    local LOCATION="
${COMPOSER_HOME}/composer"
    local FILENAME="
$(basename "${LOCATION}")"
    local INSTALLER_URL="
https://raw.githubusercontent.com/composer/getcomposer.org/1b137f8bf6db3e79a38a5bc45324414a6b1f9df2/web/installer"
   
    local INSTALL_COMMAND="
"
    INSTALL_COMMAND="
${INSTALL_COMMAND} mkdir -p ${COMPOSER_HOME}"
    INSTALL_COMMAND="
${INSTALL_COMMAND} && curl -L ${INSTALLER_URL} | php -- --version=${VERSION} --filename=${FILENAME} --install-dir=${COMPOSER_HOME}"
    INSTALL_COMMAND="
${INSTALL_COMMAND} && chown ${USER_ID}:${GROUP_ID} -R ${COMPOSER_HOME}"

    if [ -z "$(docker volume ls -q -f name=^${VOLUME}\$)" ]; then
        dockerCompose "
0:0" "${INSTALL_COMMAND}" 1>/dev/null
    fi;

    dockerCompose "${USER_ID}:${GROUP_ID}" "${LOCATION} ${@}"
}

run "${@:-install}"

Futtatási jogot kell adni a composer.sh-nak, majd szinte ugyanúgy használható, mint egy hagyományos verzió.

./composer.sh --version

A shell szkriptben a "run" függvényben lehet a COMPOSER_VERSION változót módosítani, amit a konténernek is át fog adni. Arról is gondoskodik, hogy minden felhasználónak minden Composer verzió külön volume-ra legyen mentve. Ha valamelyik verzió letöltése közben hiba történik, akkor szükség lehet a volume manuális törlésére, majd a szkript újra futtatására, de egyébként bármikor, bármelyik verzió újra felcsatolható bármelyik projektbe. Hiba például lehet olyan, hogy nem létezik a megadott Composer verzió, vagy csak hálózati hiba miatt nem érhető el. Hibaüzenet viszont a telepítésről nem fog megjelenni, mert a kimenet a /dev/null-ba van irányítva. A composer viszont nem lesz a keresett helyén, ezért annak a hiányára utaló hibaüzenetből sejthető, hogy a letöltés nem sikerült. Ilyenkor ideiglenesen lehet próbálkozni a kimenet átirányítás megszüntetésével. A törlendő volume neve pedig az alábbiak szerint áll össze:

${VOLUME_PREFIX}_${VERSION}_${USER_ID}_${GROUP_ID}

Ahol a VOLUME_PREFIX alapértelmezetten "composer_version". Tehát az összes composer verzió listázható az alábbi módon:

docker volume ls -q -f name=^composer_version

A "kalap" jel (^) nem fontos. Így viszont csak azokat mutatja meg, amik ténylegesen a "composer_version"-nel kezdődnek, nem csak tartalmazzák. A kimenet valami ilyesmi lesz:

composer_version_1.10.10_1000_1000
composer_version_1.6.1_1000_1000
composer_version_1.6.5_1000_1000
composer_version_1.8.8_1000_1000

Látható, hogy én is próbálkoztam két, jelenleg nem létező Composer verzióval. A composer.sh-t is elmentheted a verziókezelőbe, így továbbra is látszik, ha valamiért a Composer verzióján változtatni kellett.

A fejlesztői image-en alapuló konténerben

[Tartalom]

Abban az esetben, ha a fejlesztői image-be nem szeretnél kizárólag a Composer számára szükséges függőségeket telepíteni, készíthetsz egyedi image-et, aminek ugyanaz a módja, mint a development és production image közötti különbségek definiálásának. Örököltethetsz tehát a fejlesztői image-ből kiegészítve a Composerrel, vagy az "ARG" kulcsszó segítségével egy külön verziót készítesz. Én viszont elkerülném a túl sokféle image készítését, ha nincs különösebb igény rá.

Ráadásul, ha a Composernek saját image-e van, akkor a Docker Compose-zal készített image nevére is figyelni kell, hogy ne egyezzen meg a PHP service image-ének nevével, de mégis egyedi legyen projektenként. Ezt persze akár egy docker-compose override.yml -ben is meg lehet adni, vagy a szkript is kiokosítható szükség esetén.

Összefoglalás

[Tartalom]

A Composer használatának tehát több módja, ami nem is feltétlenül limitált azokra, amiket én a cikkben említettem. Összességében az mégis elmondható, hogy törekszünk arra, hogy ne nagyon különbözzön az alkalmazás környezete attól, ahol a fejlesztés közben a Composert futtatjuk. Ez később akár a CI/CD-re, azaz az automatizálásra is kiterjeszthető. Ennek az egyszerű igénynek a következménye minden egyéb. Az új verziókat már egyszerűbb futtatni konténerben is. Ez egy újabb ösztönző erő lehet ara, hogy próbálj frissíteni és újabb eszközöket használni, amennyiben lehetséges.

Ha hibát találtál a cikkben, kérlek oszd meg velem, vagy akár mindenkivel kommentben. Ha van jobb ötleted, javaslatod a Composer használatára, vagy éppen nem fedtem le egy problémát, arról is szívesen olvasnék.

Megosztás/Mentés