Deployment PHP (Nette) aplikace pomocí Dockeru a Tutum

Update: tento článek je mírně zastaralý, Tutum je přejmenován na Docker Cloud a má nové grafické rozhraní.

Pokud máte aplikaci psanou v PHP, je možné její deployment řešit několika způsoby. Například můžete rozběhnout prostředí ručně připojením na server přes SSH, použít automatizovaný nástroj typu Puppet nebo Chef, použít některý z dostupných PAAS. Pokud ale chcete mít vše ve svých rukách a zároveň si chcete práci maximálně zjednodušit (například u nás je pro běh aplikace vyžadován NGINX, PHP, RabbitMQ, Elasticsearch, Redis, Nodejs, Phantomjs a databáze Percona), nabízí se použití Dockeru. To s sebou nese řadu výhod:

  • Spuštění celého prostředí je možné "jedním příkazem"
  • Služby jsou izolované, paralelně mi tedy může běžet například databáze v různých verzích
  • Konfiguraci lze snadno přenést na testovací nebo produkční server, vždy mám jistotu že je vše stejné
  • Proces je možné automatizovat

Je však třeba počítat i s nevýhodami tohoto řešení. Některé Docker image nejsou úplně optimálně vytvořené a jsou tak zbytečně velké, což v kombinaci s pomalým připojením k internetu může trochu zdržovat. Je také třeba počítat, že použití Dockeru o něco zvýší využití procesoru a paměti.

Předpoklady

Tento článek je rozdělený na několik částí - nejprve spustíme webserver s PHP aplikací lokálně, následně totéž spustíme na serveru a nastavíme automatický deployment při každé změně kódu. Nakonec nastíním možnosti další využití a škálování nasazené aplikace.

Pokud si budete chtít spustit kontejnery lokálně, uude třeba nainstalovat Docker a Docker Compose a git. K sdílení kódu bude použit GitHub, bude tedy třeba mít účet na GitHubu (pokud chcete deployovat vlastní kód). Dále předpokládám server s operačním systémem Linux (Ubuntu, Debian, CentOS nebo Fedora).

Spuštění prostředí lokálně

Pokud vás zajímá samotný deployment na produkci a nachcete si hrát s Dockerem lokálně, tuto kapitolu klidně přeskočte.

Prvním krokem je spustit Nette Quickstart lokálně. Tato aplikace pro svůj běh vyžaduje webserver s PHP a databázi MySQL. Předpokládám že máte nainstalovaný Docker, pokud ne, postupujte podle oficiálního návodu. Dalším nástrojem, který budeme potřebovat je Docker Compose, který umožňuje spuštění a propojení několika kontejnerů pomocí jediného konfiguračního souboru. Ověříme, že oba nástroje máme v pořádku nainstalované:

bash-3.2$ docker -v  
Docker version 1.7.1, build 786b29d  
bash-3.2$ docker-compose -v  
docker-compose version: 1.3.0  
CPython version: 2.7.9  
OpenSSL version: OpenSSL 1.0.1j 15 Oct 2014  

Stažení zdrojových kódů aplikace

Prvním krokem je stažení zdrojových kódů aplikace. Já jsem si vytvořil fork quickstartu, do kterého následně přidám nové soubory. Můžete ale zkusit libovolnou PHP aplikaci.

V terminálu se přepneme do požadované složky a pomocí gitu zdrojové soubory naší aplikace stáhneme z GitHubu (můžete stáhnout můj fork).

bash-3.2$ git clone git@github.com:ludekvesely/tutorial-quickstart.git  
Cloning into 'tutorial-quickstart'...  
remote: Counting objects: 236, done.  
remote: Total 236 (delta 0), reused 0 (delta 0), pack-reused 236  
Receiving objects: 100% (236/236), 349.68 KiB | 0 bytes/s, done.  
Resolving deltas: 100% (82/82), done.  
Checking connectivity... done.  
bash-3.2$ cd tutorial-quickstart/  
bash-3.2$ ls -l  
total 104  
drwxr-xr-x   8 ludekvesely  staff    272 Dec 29 02:16 app  
-rw-r--r--   1 ludekvesely  staff    829 Dec 29 02:16 composer.json
-rw-r--r--   1 ludekvesely  staff  39125 Dec 29 02:16 composer.lock
-rw-r--r--   1 ludekvesely  staff   7233 Dec 29 02:16 database.sql
drwxr-xr-x   3 ludekvesely  staff    102 Dec 29 02:16 log  
drwxr-xr-x   3 ludekvesely  staff    102 Dec 29 02:16 temp  
drwxr-xr-x   4 ludekvesely  staff    136 Dec 29 02:16 tests  
drwxr-xr-x  11 ludekvesely  staff    374 Dec 29 02:16 www  

Zdrojové kódy máme stažené a nyní bude třeba vytvořit kontejner, ve kterém aplikace poběží.

Vytvořejí kontejneru s Apache a PHP

Kontejner je spuštěná image, my tedy nejprve musíme vytvořit image, kterou budeme moci spustit. Při vytváření Docker image je dobré se nejprve podívat, zda už nějaký vhodný neexistuje. Začít vyhledávat je možné na adrese hub.docker.com. My použijeme image s připraveným Apache a PHP, který spravuje společnost tutum.co (využijeme ji pak pro deploy), můžete jej najít na docker hubu a jeho zdrojové kódy na githubu.

V rootu aplikace vytvoříme soubor Dockerfile. Jeho obsah bude vypadat následovně:

FROM tutum/apache-php

RUN apt-get update && apt-get install -yq git php5-sqlite && \  
    rm -rf /var/lib/apt/lists/* && rm -fr /app

COPY . /app

RUN chmod 777 log temp && \  
    composer install && \
    rm -rf composer* Dockerfile .git && \
    sed -i "s/DocumentRoot \/var\/www\/html/DocumentRoot \/var\/www\/html\/www/g" /etc/apache2/sites-available/000-default.conf

ENV ALLOW_OVERRIDE true  

Co to ten Dockerfile je, k čemu a jak se použije? Je to vpodstatě seznam příkazů, který se provede před spuštěním webserveru. Můžete si to představit jako když máte server s čistou instalací a toto je posloupnost kroků, které se musí provést před spuštěním aplikace. Určitě vás napadne, jestli není zbytečné a pomalé pokaždé provádět apt-get install. Každý příkaz v Dockerfile se provede a uloži jako další vrstva vytvářeného obrazu. Před vykonáním každého kroku Docker zkontroluje, zda je opravdu nutné ho provést - například před kopírováním souboru si porovná jeho hash s cachovaným hashem z minulosti a krok provede až pokud se liší, v opačném případě pouze použije vrstvu, kterou již v minulosti vytvořil.

Projděme si jednotlivé části Dockerfile. Zápis FROM tutum/apache-php značí, z kterého image je Dockerfile odvozený. Vpodstatě to znamená, že tento Dockerfile končí tam, kde předchozí skončil - podívat se na něj můžete na GitHubu.

Následující řádky začínající RUN apt-get update && apt-get install -yq git php5... nainstalují nástroje potřebné k rozběhnutí aplikace. Nette vyžaduje php5-sqlite, nakonec smažeme nepotřebné soubory. Za RUN může přijít jakýkoliv příkaz, který bude spuštěn v bashi.

Příkaz COPY . /app nakopíruje kód aplikace do image, . značí aktuální adresář (tedy složky app, temp, log, www...), je možné uvést adresář nebo konkrétní soubor, /app značí kam se soubory nakopírují.

Následně řádky začínající RUN chmod 777 log temp... nastaví potřebná práva nakopírovaným adresářům, odstraní nepotřebné soubory (adresář .git nemá na produkci co dělat) a pomocí sedu provede změny konfigurace Apache.

Posledním krokem ENV ALLOW_OVERRIDE true je nastavení proměnné ALLOW_OVERRIDE na true. Ta je pak v kontejneru vždy dostupná - můžeme k ní přistupovat jako k jakékoliv jiné proměnné, například ji vypsat příkazem echo $ALLOW_OVERRIDE. Po spuštění kontejneru je ale spuštěn tento soubor, kde se na základě této proměnné povolí mod rewrite.

Dockerfile a zdrojové kódy apikace máme připraveny, můžeme tedy provést build. Ve rootu aplikace spustíme docker build -t nette-quickstart .. Parametr -t značí, jak si výsledný image pojmenujeme (jaký bude mít tag). Důležitá je tečka na konci, kdy se build provede v aktuálním adresáři, kde je také očekáván Dockerfile.

bash-3.2$ docker build -t nette-quickstart .  
Sending build context to Docker daemon 523.3 kB  
Sending build context to Docker daemon  
Step 0 : FROM tutum/apache-php  
 ---> cdced04212b6
Step 1 : RUN apt-get update && apt-get install -yq php5-sqlite &&     rm -rf /var/lib/apt/lists/* && rm -fr /app  
 ---> Running in dfce886a3ceb
Ign http://archive.ubuntu.com trusty InRelease  
Get:1 http://archive.ubuntu.com trusty-updates InRelease [64.4 kB]  
Get:2 http://archive.ubuntu.com trusty-security InRelease [64.4 kB]

...

Processing triggers for libapache2-mod-php5 (5.5.9+dfsg-1ubuntu4.14) ...  
 ---> 0af4e3fa2efe
Removing intermediate container dfce886a3ceb  
Step 2 : COPY . /app  
 ---> 3f09c2f8c42c
Removing intermediate container fc3e65c222c1  
Step 3 : RUN chmod 777 log temp &&     composer install &&     rm -rf composer* Dockerfile .git &&     sed -i "s/DocumentRoot \/var\/www\/html/DocumentRoot \/var\/www\/html\/www/g" /etc/apache2/sites-available/000-default.conf  
 ---> Running in 22809fbf0bb1
Warning: This development build of composer is over 60 days old. It is recommended to update it by running "/usr/local/bin/composer self-update" to get the latest version.  
Loading composer repositories with package information  
Installing dependencies (including require-dev) from lock file  
Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. Run update to update them.  
  - Installing dg/adminer-custom (v1.6.1)
    Downloading: 100%

...

  - Installing nette/tester (v1.4.0)
    Downloading: 100%

nette/utils suggests installing ext-intl (for script transliteration in Strings::webalize() and toAscii())  
Generating autoload files  
 ---> e026f4842c1c
Removing intermediate container 22809fbf0bb1  
Step 4 : ENV ALLOW_OVERRIDE true  
 ---> Running in f59173c8afee
 ---> 8e15d5de7d02
Removing intermediate container f59173c8afee  
Successfully built 8e15d5de7d02  

Ve výstupu můžete vidět, že pro každý příkaz v Dockerfile je následně proveden jeden krok, odtud například Step 4 : ENV ALLOW_OVERRIDE true v logu. Pokud build spustíme znovu, pod každým krokem uvidíme ---> Using cache, což znamená, že Docker nezjistil změnu a pokračuje dalším krokem. Jakmile ale upravíme jediný soubor, v kroku COPY . /app zjistí pomocí kontrolního součtu změnu a tento a následující kroky už jsou provedeny. Z toho vyplývá, že věci, které se často nemění (instalace nástrojů, nastavení proměnných) je dobré mít před věcmi, které se mění často (např. nakopírování zdrojových kódů aplikace).

Image máme připravený, zbývá ho jen spustit. To provedeme příkazem docker run --rm -p 8012:80 nette-quickstart. Po zadání příkazu se spustí kontejner a uvolní svůj port 80, ukončit jej můžeme zkratkou ctrl + c. Parametr --rm značí, že bude po ukončení kontejner automaticky odebrán a můžeme jej tak spustit znovu. Parametr -p definuje které porty kontejneru mají být zveřejněny (zde port 80, na kterém běží Apache) a pod jakým portem mají být zveřejněny (zde 8012, pod kterým bude webová stránka dostupná v prohlížeči). Pokud bysme jej nezadali, zvolí se náhodný volný port.

Spuštěný kontejner lze dobře prohlédnout pomocí nástroje Kitematic, pokud jej nemáte nainstalovaný, zkuste můj návod.

Zde lze prohlížet log (který je i v terminálu), náhled webové stránky a nastavení kontejneru. A co zde není zajímavého - server error v náhledu webové stránky! Jak ale zjistit v čem je problém, jak se dostat do kontejneru? Ideální by bylo, kdyby byly chyby vidět přímo v logu, samotné logování by ale vydalo na samostatný článek, nyní si ukážeme, jak ladit běžící kontejner. V horní liště má Kitematic možnost EXEC, klikneme na ni a otevře se terminál v běžícím kontejneru. Nyní můžu prohlížet a editovat soubory, spouštět příkazy... Podíváme se co Tracy zalogovala:

# cat log/exception.log
[2015-12-31 01-08-09] Nette\Database\ConnectionException: SQLSTATE[HY000] [2003] Can't connect to MySQL server on '127.0.0.1' (111) in /app/vendor/nette/database/src/Database/DriverException.php:25 caused by PDOException: SQLSTATE[HY000] [2003] Can't connect to MySQL server on '127.0.0.1' (111) in /app/vendor/nette/database/src/Database/Connection.php:70  @  http://192.168.99.100:8012/  @@  exception-2015-12-31-01-08-09-377469fd79b0ea3f7414b9d5d0198f74.html

Z logu je patrné, že se aplikace nemůže připojit k databázi, protože žádná neběží.

Spuštění MySQL databáze a propojení s webserverem

Spustit databázi můžeme dvěma způsoby - buď ji nainstalovat do existujícího kontejneru a oba procesy držet při životě nějakým správcem procesů (například supervisord), jak to tutum dělá ve svém image LAMP. Druhou variantou je spustit nový kontejner s databází a oba pak propojit. Filozofii dockeru lépe odpovídá druhá varianta. Obecně je lepší držet kontejnery co nejmenší a nejjednodušší. V jednom kontejneru by měl ideálně běžet jeden proces, lépe se pak kontejnery udržují, pokud vše dáme do jednoho kontejneru, vytrácí se pak výhody, které Docker přináší. To, jaké všechny kontejnery budeme spouštět a jak je mezi sebou propojíme nám usnadní Docker Compose, kde celou sadu kontejnerů definujeme v souboru docker-compose.yml, který bude vypadat následovně:

web:  
  build: .
  links:
    - db
  ports:
    - "80"
db:  
  image: tutum/mysql
  environment:
    STARTUP_SQL: "/tmp/database.sql"
    ON_CREATE_DB: test
    MYSQL_PASS: testpass
  volumes:
    - ./database.sql:/tmp/database.sql:ro

Tento yml soubor má dvě sekce - web a db. Dle sekce web by se měl provést build v aktuálním adresáři (tak jak jsme provedli příkazem docker build...). Zveřejní se port 80 a připojena bude k službě db. Ta je definována v další sekci a vychází z image tutum/mysql. V proměnných jsou definovány přihlašovací údaje a název databáze. Důležitá je část volumes, kde je definováno že soubor database.sql bude připojen do kontejneru jako /tmp/database.sql v režimu read-only. V proměnné STARTUP_SQL je pak definována cesta právě k tomuto souboru a použije se při inicializaci databáze.

Ještě musíme nastavit připojení k databázi v aplikaci úpravou souboru app/config/config.local.neon.

database:  
  dsn: 'mysql:host=db;port=3306;dbname=test'
  user: admin
  password: testpass
  options:
    lazy: yes

Nyní můžeme zavolat příkaz docker-compose up a oba kontejnery se spustí a propojí. V Kitematic by měly být oba kontejnery vidět a web by měl jít otevřít v prohlížeči.

Tímto jsme schopni spustit PHP aplikaci v Dockeru, připravit databázi a kontejnery propojit, vše jediným příkazem docker-compose up. Pokud vám něco uniklo, podívejte se na tento commit. Kód můžeme pushnout na github a zavřít terminál, pro deployment na server už nám bude stačit jen webový prohlížeč.

Deployment aplikace na produkci

Pro spuštění aplikace na serveru by bylo možné se na něj přes SSH připojit, nainstalovat Docker, stáhnout kód a spustit kontejnery. Existuje však služba Tutum, která toto všechno umí skrze webový prohlížeč a celý proces nasazení aplikace automatizovat a monitorovat.

Přihlaste se na adrese https://dashboard.tutum.co/accounts/login/ - můžete pomocí účtu na Docker Hubu, GitHubu nebo pomocí vlastní e-mailové adresy.

Po vytvoření účtu ho musíme propojit s GitHubem a serverem.

Propojení Tutum a serveru

Nejprve musíme propojit server s Tutum, aby bylo kam deployovat - to je možné na záložce Nodes. Můžete si přidat účet například z Amazonu a servery tak vytvářet rovnou z rozhraní Tutum. Pokud ale už máte server vlastní, zvolte možnost Bring your own node.

Přihlaste se na server přes SSH a spusťte zkopírovaný příkaz. Nainstaluje se docker a všechny nástroje nutné pro chod Tutum.

Propojení Tutum a GitHubu

Pro propojení s GitHubem klikněte na záložku Repositories, propojte s GitHubem (pokud jste se přes něj přihlásili, pravděpodobně už to nebude třeba) a pokračujte možností Create new repository. Zvolte libovolný název a popis a potvrďte.

Vytvoří se repository, do které bychom mohli pushnout image vytvořený lokálně v první kapitole. Zde je repository obdobou repozitáře na githubu - také můžu provést docker push, docker pull, mít různé tagy... My chceme ale mít vše automatizované, proto v divu Automated build from GitHub klikneme na tlačítko Edit repository.

Potvrdíme kliknutím na Save and build. Tutum vybere volný server z těch, které jsme k účtu připojili, tam provede build a v případě úspěchu pushne vytvořený image do repozitáře.

Vytvoření kontejnerů na serveru

Jakmile je build hotový, v detailu repository zvolíme Launch service. Spustí se průvodce, který umožňí službu nakonfigurovat a spustit. V kroku service configuration můžeme novou službu pojmenovat a provedeme zde jedinou změnu - zveřejníme port 80. Pokud chceme, aby se aplikace nasadila automaticky po každém úspěšném buildu, zaškrtneme ještě možnost Autoredeploy.

Krok Environment variables můžeme přeskočit a posledním nastavením před spuštěním je nastavení Volumes. Volumes slouží k persistenci dat - ukládají se mimo kontejner a přežijí tak jeho restart, což se hodí například u databází. Zde pomocí volumes zpřístupníme složku aplikace, ve které je dump databáze. Na řádek Add volume do pole Container path zadejte /app a klikněte na Add. Výsledek by měl vypadat následovně:

Potvrdíme kliknutím na Create and deploy. Nyní se na server stahuje vytvořený image a následně se spouští. Průběh můžeme sledovat pod záložkou Timeline. Po spuštění je veškerý výstup vidět na záložce Logs.

Dalším krokem je spuštění databáze - v hlavním menu zvolíme Services a následně Create service. Image MySQL je možné vyhledat na záložce Public repositories -> Search Docker hub zadáním tutum/mysql.

Zvolíme vyhledaný image a v dalším kroku je důležité službu pojmenovat db. V konfikuraci Nette aplikace totiž máme definovanou databázi právě pod db.

V dalším kroku nastavíme proměnné prostředí. Je třeba nastavit MYSQL_PASS na testpass a ON_CREATE_DB na test. Dále je třeba vložit novou proměnnou na řádku Add environment variable STARTUP_SQL s hodnotou /app/database.sql, což je cesta k souboru s dumpem databáze. Ten by aktuálně v kontejneru dostupný nebyl, zpřístupníme ho nastavením volumes v dalším kroku.

Posledním krokem před spuštěním databáze je načtení volumes definovaných v předchozí službě. V kroku Volumes tedy na řádku Add volumes from zvolíme službu nette-quickstart a potvrdíme.

Potvrdíme kliknutím na Create and deploy a počkáme až se databáze spustí. Přes menu Services se vrátíme do služby nette-quickstart. Zde nás zajímá záložka Endpoints, na které vidíme seznam všech zveřejněných portů. Zde by měl být právě jeden, který můžeme otevřít.

Po otevření bycho měli vidět spuštěnou a funkční aplikaci Nette Quickstart.

Co dál?

Prošli jsme celý proces, od kódu editovaného lokálně přes jeho zveřejnění na GitHubu až po spuštění aplikcace pomocí Docker a Tutum na serveru. Nyní můžeme provést změny v kódu, pushnout na GitHub a aplikace se automaticky aktualizuje. Pro skutečné použití na produkci by ale bylo dobré provést následující kroky:

  • Persistence dat v databázi pomocí nastavení volumes. Nyní se při restartu databáze data přemažou.
  • Aktualizace struktury databáze pomocí migrací. Jakmile totiž máme v databázi data, bylo by dobré provádět jen přírůstkové změny (aktuálně používám nextras/migrations a fungují výborně).
  • Zálohování databáze. Přístupů je více - například použít další kontejner s nainstalovaným mysqldump a nastavenými volumes from.
  • Testy. Pokud do rootu zdrojových kódů přidáte soubor pro docker compose docker-compose.test.yml, který má definovanou sekci sut - test se provede při každém buildu. Více zde.
  • Škálování. Výhodou použití kontejnerů je snadné škálování - před web stačí předřadit proxy, s jejíž pomocí můžete simulovat virtualhosty, rozdělovat zátěž.. Kontejnery pak můžou běžet na více serverech a lze tak snadno rozdělit jejich výkon.
  • HTTPS - pokud už budete používat haproxy, je nastavení https otázkou přidání dvou proměnných v konfiguraci.
  • Monitoring a logování - o tom snad v některém z dalších článků.

Alternativy orchestrace kontejnerů

Tutum je zatím zdarma, jednou ale přejde na placený model. Je možné že cenu nasadí tak vysoko, že se provoz služby velmi prodraží a tak je třeba počítat s dostupnými alternativami.

  • ClusterHQ Flocker: Super věc, při migraci kontejneru na jiný server migruje i data a lze tak v klidu například přemigrovat databázi na silnější server. Nemá webové rozhraní jako Tutum.
  • Rancher: Mají vlastní operační systém pro provoz kontejnerů a nástroj pro jejich správu. Vypadá slibně, není to ale služba, vše si člověk musí rozběhat sám.
  • Další nástroje pro orchestraci: Docker Swarm, Google Kubernetes, Apache Mesos, CoreOS Fleet. Nemám zkušenosti, mělo by ale jít o prověřené nástroje nabízející orchestraci kontejnerů.