JavaScript a Docker

Na vývoj, testování, i v produkci používáme Docker. Vyvíjíme aplikace v JavaScriptu. Podívejte se, jak si tyto dvě technologie rozumí dohromady, jak je používat efektivně, jaké jsou naše zkušenosti s Dockerem po téměř dvou letech a co z toho vylezlo dobrého.
Nálepky:
V MSD používáme Docker téměř všude. V článku vám chci přiblížit především jeho využití ve světě JavaScriptu a nejčastější triky, které využíváme.
Co je to Docker?
Docker je nástroj, který zjednodušuje práci s virtuálními kontejnery. Nejsnadnější je představit si kontejner jako virtuální stroj. Není to sice tak úplně pravda, ale jejich výhody i nevýhody jsou podobné. Na rozdíl od virtualizace, kontejner sdílí jádro se systémem.
O Dockeru se na Zdrojáku už psalo dřív:
– Lukáš Cír: Dockerizace Maven testů v CI Jenkins
– Michal Valoušek: Lokální vývoj s Dockerem nebo Vagrantem?
– Můj dřívější článek: Sběr logů z kontejnerů
Proč používáme Docker? Naše prostředí je díky němu robustnější, jednodušší na reprodukci a idempotentní. Díky Dockerfile je naše prostředí v kódu, můžeme ho verzovat v gitu a code review je snazší. Zbavili jsme se problému „Na mém stroji to funguje“.
Základní koncepty: Kontejner a Image
Základní koncepty Dockeru jsou Kontejner a Image.

Kontejner a Image (obraz)
Kontejner je obálka pro běžící proces. Metafora s kontejnery ve fyzické přepravě je podařená: přepravce se nemusí starat o vnitřek kontejneru. Vidí jenom standardní rozhraní a ihned s ním umí pracovat. Stejné výhody mají kontejnery virtuální. Díky konzistentnímu API není důležité, jaká technologie běží uvnitř. Oproti virtuálnímu stroji startuje mnohem rychleji, v řádu milisekund.
K vytvoření kontejneru slouží Image. Image je pouze statický záznam souborů (snapshot). Není možné ho spustit; slouží pouze jako počáteční stav nového kontejneru.
Image je, podobně jako cibule, strukturovaný ve vrstvách. Každá nová vrstva je jenom rozdíl (diff) oproti té předchozí. Díky tomu je snadné rozšířit jiný Image. Také to znamená, že odebrání souboru nezmenší celkovou velikost! Na druhou stranu, Image mezi sebou vrstvy sdílejí. Jestliže máme na jednom klientu deset Image a všechny rozšiřují node:6, pak je tento (base image) uložený pouze jednou.
K vytvoření Image používáme Dockerfile. Dockerfile
je textový soubor s popisem instrukcí. Během buildu si Docker vrstvy cachuje, takže kromě prvního jsou buildy rychlé.
Docker používáme především v příkazové řádce. Nejčastější příkazy jsou docker run
pro spuštění nového kontejneru z Image, a docker build
pro vytvoření Image z Dockerfile.

Nejčastější příkazy CLI Dockeru
Dockerfile
FROM node:6
MAINTAINER Pavel Vanecek <email@pavelvanecek.cz>
WORKDIR /app
COPY ./package.json /tmp/package.json
RUN cd /tmp &&\
npm install &&\
cp -a /tmp/node_modules /app/ &&\
rm /tmp/package.json
COPY . .
RUN npm test
Takto vypadá typický Dockerfile pro aplikaci napsanou v JavaScriptu. Každá instrukce vytvoří ve výsledném Image jednu vrstvu.
Instrukce FROM node:6
označuje základní Image, ke kterému budeme přidávat vrstvy. Tag za dvojtečkou označuje konkrétní verzi. Je možné ho vynechat, potom Docker použije :latest
. Doporučuji tag vždy specifikovat! Mysleli jsme si, že nám latest stačí a naše buildy se nerozbijí. Pletli jsme se.
Následují instrukce COPY package.json
a RUN npm install
, které do Image zkopírují soubor a spustí příkaz. Pamatujete na cache vrstev? Docker při kopírování souborů kontroluje, jestli se nezměnil obsah. Kvůli tomu by se při změně kteréhokoliv souboru znovu instalovaly všechny balíčky. Díky tomuto triku se ale budou instalovat jenom při změně package.json
.
Tento způsob není optimální. Nová instalace běží při každé změně package.json, byť jen v "description"
. Je možné použít volume a nechat npm, ať nestahuje balíčky znovu. Je také možné použít npm shrinkwrap nebo yarn. Zjistili jsme ale, že je výhodné nechat občas aplikaci nainstalovat od nuly; některé buildy sice běží déle než by bylo nutné, ale dříve zjistíme případné nedostatky.
Spouštění testů je součástí všech image. Na jednu stranu tak balíme do produkčního image i testy a testovací frameworky, na druhou jsme si jistí, že všechny naše image jsou otestované.
Tento příklad je nejmenší vhodný pro aplikaci. Nejspíš vás budou zajímat i volumes, logování, otevření portů, entrypoint nebo CMD, labely, uživatel bez root práv, instalace dalších balíčků, nebo healthcheck.
.dockerignore
node_modules
Složka node_modules má někde uvnitř pravděpodobně nativní kompilované balíčky a uvnitř kontejneru nemusí fungovat. Je lepší ji pomocí .dockerignore vynechat a dovolit npm install
nainstalovat vše znovu. Navíc, Docker pro Mac (a možná i na Windows) kopíruje soubory zoufale pomalu.
Environment variables
const OUTPUT_FILENAME = process.env.OUTPUT_FILENAME || './output.txt' const PORT = process.env.PORT || 8080
Vynechali jsme všechny konfigurační soubory a proměnné předáváme do našich aplikací pomocí environment variables. Docker a nástroje kolem něj si s tímto způsobem dobře rozumí a není potřeba řešit, kde že to vlastně zase chybí config.json
.
Docker compose
Až budete hotovi s vaším Dockerfile
, bude vše aplikace připravená fungovat uvnitř kontejneru. Aplikace nicméně málokdy běží sama o sobě; závisí na dalších službách. I ty běží v kontejneru? Samozřejmě.
Stejně tak samozřejmě je jednoduché spustit jeden kontejner, ale obsluhovat jich několik pomocí docker run
a pamatovat si k tomu desítky přepínačů je otrava.

Práce s mnoha kontejnery může být zábava
Docker nabízí naštěstí i další nástroj: Docker compose. Slouží pro orchestraci jednoho i více kontejnerů na jednom stroji. Konfiguraci čte ze souboru docker-compose.yml
. Ten může vypadat třeba takto:
version: '2'
services:
mysql:
image: mysql:5.7
restart: always
volumes:
- "./.data/db:/var/lib/mysql"
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: wordpress
web:
depends_on:
- mysql
build:
context: .
dockerfile: run.Dockerfile
ports:
- 80:80
restart: always
Tento soubor při spuštění pomocí docker-compose up
nastartuje dva kontejnery: jeden s databází, druhý s webovou aplikací. V tuto chvíli už není poznat, jaký jazyk nebo technologie uvnitř kontejneru vlastně běží.
Docker compose si umí chybějící image stáhnout, případně podle instrukcí sestavit sám. Data v databázi jsou díky volumes:
uložená na filesystému mimo Docker a přežijí tak i smazání kontejneru.
Vzpomínáte si na environment variables? Toto je jeden ze způsobů, jak je aplikaci předat. Tento příklad není ale úplně vhodný: určitě nechcete heslo k databázi nechávat v git repozitáři! Nám se osvědčil Ansible Vault. V gitu jsou hesla uložená v zašifrovaném souboru a samotný docker-compose.yml
pak necháme Ansible vyrobit ze šablony.
Pomocí depends_on
můžete explicitně vyjádřit závislost mezi kontejnery. Docker compose je pak spouští ve správném pořadí. Jenom si dejte pozor, že Docker kontejnery spouští, ale nekontroluje, jestli je aplikace uvnitř připravená! To může zabrát pár vteřin, nebo minut. Zajistit, aby aplikace nezkolabovala, si musíte pohlídat sami.
Kontejnery jsou společně na privátní síti a nejsou zvenčí přístupné. To je sice fajn u databáze, ale webový server chcete mít vidět i odjinud. Pomocí ports
můžete některé porty vystrčit ven.
Díky restart:always už nepotřebujeme žádné další process managery. Zbavili jsme se forever, pm2, i init skriptů. Docker udrží kontejnery naživu, i po restartu hostu.
K čemu je to všechno dobré
Docker od rána do večera. Docker na triku, Docker v CI, soubory navíc a učení se nových nástrojů. K čemu nám to vlastně pomohlo?

Ne úplně šťastný commit
Commit jako tento by mohl rozbít aplikaci, aniž by si toho někdo všimnul. Díky cache nainstalovaných balíčků zůstane React nainstalovaný pořád, dokud někdo nezavolá npm prune
; a to se ani na localhostu, ani na dalších serverech nedělo často. Tato chyba by se teoreticky mohla projevit až po mnoha měsících, v nejhorší možnou chvíli. Dockerfile a build od nuly by tuto chybu objevil ihned.
My ji mimochodem našli během Code review. Děláte code review? Měli byste.

Razantně jednodušší instrukce pro nastavení nového stroje
Ukázka souboru README.md
před a po použití docker-compose. Instalace prostředí na úplně novém stroji se zkrátila z celého dne (bez Dockeru) na několik hodin (jenom s pomocí Dockeru) na několik minut (Docker compose).
Díky Dockeru jsou naše aplikace, build i deployment jednodušší, rychlejší, odolnější a robustnější.
Docker není ale vhodný vždy. Osobně například preferuju vývoj na mém stroji přímo, bez kontejneru: verze node.js střídám pomocí nvm. Je to nicméně možné. Podívejte se na přednášku Davida Blurtona, kde ukazuje celé nastavení včetně vývoje přímo uvnitř kontejneru:
Docker, podobně jako většina nástrojů, má i svoje nevýhody. Zde jsou některé články, které z velké části popisují i naše zkušenosti a problémy s Dockerem:
- The HFT Guy: Docker in Production: A History of Failure
- Sysadmin 4 lyfe: Docker in Production: A retort
- Simon Hørup_ Eskildsen: Why Docker is Not Yet Succeeding Widely in Production
- David P. Pollak: Docker Not Ready for Prime Time
- Sysadmin 4 lyfe: Thou shalt not run a database inside a container
To je vše, přátelé
Používáte Docker? Budu rád, když se podělíte s vlastními zkušenostmi.
Nepoužíváte? Vyzkoušejte!
Kedysi som tiez spustal vsetky projekty z lokalneho terminalu (db, frontend & webpack, backend nodejs, mikroservicy). Ale ked uz som musel pri starte projektu otvarat cez 5 terminalov a potom este kolegom vysvetlovat co si maju ako a kde nainstalovat tak som nahodil docker. Zo zaciatku tomu treba venovat nejaky cas na instalaciu a konfiguraciu, ale teraz uz iba napisem: „docker-compose start“ a hotovo. Vsetky sluzby sa spustia v spravnom poradi a nikto nemusi nic riesit. Zdrojove kody su namapovane z lokalneho disku do dockeru, takze sa vyvija normalne v lokalnom IDE.
Myslim ze ak projekt uz prerastie istu hranicu je docker uz nevyhnutnost.
Proc nepouzivate Docker/rkt pro vyvoj? Neni lepsi mit nahozene cele prostredi pres docker-compose up jednim prikazem?
Jinak z nvm jsem presel na tj/n, je to pro mne podstatne jednodussi pristup.