Skip to content
← Wszystkie wpisy
5 min czytania Dawid Skłodowski

Docker dla Rubiego: wysyłka pierwszego obrazu

Docker zbliża się do 1.0 i zmieni sposób, w jaki wysyłamy aplikacje Ruby. Praktyczny przewodnik po obrazach, kontenerach i Dockerfile'ach — budowa odtwarzalnego środowiska deweloperskiego i dlaczego „działa na mojej maszynie” wreszcie odchodzi.

Jest oprogramowanie nieco ponad rok stare, o którym nagle mówi cały świat operacyjny, i tym razem szum jest uzasadniony. Docker zbliża się do wydania 1.0 i rozwiązuje problem, z którym żył każdy programista Ruby: przepaść między „działa na mojej maszynie” a „działa na serwerze”. Jeśli kiedykolwiek straciłeś popołudnie przez niezgodny OpenSSL kolegi, brakującą bibliotekę systemową za natywnym gemem albo wersję Ruby subtelnie różną na produkcji, Docker celuje wprost w ciebie.

To praktyczne wprowadzenie dla rubistów: czym Docker właściwie jest, czym różni się od maszyn wirtualnych, których być może już używasz, i jak zbudować swój pierwszy obraz dla aplikacji Ruby.

Kontenery to nie maszyny wirtualne

Zwykłe pierwsze pytanie brzmi „czy to nie po prostu VM?” — a różnica jest całym sensem. Maszyna wirtualna wirtualizuje sprzęt: bootuje pełny system operacyjny gościa, z własnym jądrem, na hypervisorze. To ciężkie — gigabajty dysku, minuta na start, realny narzut RAM na każdą VM.

Kontener wirtualizuje system operacyjny. Kontenery dzielą jądro Linuksa hosta i izolują procesy przez funkcje jądra — namespaces (kontener ma własny widok drzewa procesów, sieci i systemu plików) oraz cgroups (dostaje ograniczony wycinek CPU i pamięci). Nie ma systemu gościa do zbootowania; kontener startuje w milisekundach i waży megabajty, nie gigabajty. Dostajesz większość izolacji VM za ułamek kosztu — dlatego możesz uruchomić dziesiątki kontenerów tam, gdzie uruchomiłbyś jedną–dwie VM.

(Uwaga na 2014: kontenery potrzebują jądra Linuksa, więc na Macu czy Windowsie uruchamiasz Dockera wewnątrz małej VM Linuksa przez boot2docker. Natywnego Dockera na tych platformach jeszcze nie ma — tak naprawdę rozmawiasz z hostem Linuksa przez sieć.)

Obrazy i kontenery: analogia klasa/instancja

Dwie podstawowe koncepcje czysto mapują się na coś, co zna każdy rubista. Obraz jest jak klasa: niezmienny, warstwowy szablon — migawka systemu plików plus metadane do uruchomienia. Kontener jest jak instancja: działający (lub zatrzymany) proces uruchomiony z obrazu, z cienką zapisywalną warstwą na wierzchu. Budujesz obraz raz i startujesz z niego wiele kontenerów, dokładnie jak instancjujesz wiele obiektów z jednej klasy.

Część „warstwowy” ma znaczenie. Obraz to stos warstw tylko-do-odczytu, każda będąca różnicą wobec tej poniżej, a Docker je cache’uje i współdzieli. Pobierz dwa obrazy oparte na ubuntu:14.04, a warstwa Ubuntu jest przechowana raz. Przebuduj po zmianie jednej linii, a przebudowują się tylko dotknięte warstwy. To warstwowanie czyni Dockera szybkim w budowaniu i tanim w wysyłce, a pisanie dobrego Dockerfile polega w dużej mierze na pracy z cache’em warstw, a nie przeciw niemu.

Twój pierwszy Dockerfile

Dockerfile to przepis na obraz — sekwencja instrukcji, każda produkująca warstwę. Oto działający dla aplikacji Rails:

FROM ruby:2.1

# biblioteki systemowe potrzebne twoim natywnym gemom (pg, nokogiri, ...)
RUN apt-get update && apt-get install -y \
    build-essential libpq-dev nodejs \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# skopiuj NAJPIERW tylko gemfile, potem bundle — zobacz uwagę niżej
COPY Gemfile Gemfile.lock ./
RUN bundle install --jobs 4

# teraz skopiuj resztę aplikacji
COPY . .

EXPOSE 3000
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]

Najważniejsza sztuczka jest już tutaj: skopiuj Gemfile i Gemfile.lock i uruchom bundle install przed skopiowaniem reszty kodu. Ponieważ każda instrukcja to cache’owana warstwa, a cache warstwy unieważnia się, gdy zmienią się jej dane wejściowe, taka kolejność oznacza, że edycja kodu aplikacji nie psuje warstwy bundle. Gemy reinstalują się tylko wtedy, gdy Gemfile faktycznie się zmieni — zamieniając dwuminutowe przebudowanie w dwusekundowe. Pomyl kolejność (skopiuj wszystko, potem bundle), a reinstalujesz każdy gem przy każdej zmianie kodu — to pierwsza rzecz, która sprawia, że ludzie rezygnują z Dockera.

Druga subtelność: zwiąż serwer Rails z 0.0.0.0, nie domyślnym localhost. Wewnątrz kontenera localhost to własna pętla zwrotna kontenera; związanie ze wszystkimi interfejsami jest tym, co pozwala hostowi dosięgnąć opublikowanego portu.

Buduj, uruchamiaj, iteruj

docker build -t myapp .
docker run -p 3000:3000 myapp        # opublikuj kontenerowy :3000 na hostowy :3000

-p 3000:3000 mapuje port hosta na port kontenera — most między odizolowaną siecią kontenera a twoją maszyną. Wejdź na localhost:3000 (albo IP VM boot2docker), a twoja aplikacja jest serwowana z wnętrza kontenera.

W developmencie nie chcesz przebudowywać obrazu przy każdej edycji. Zamontuj swoje źródło jako wolumen, by działający kontener widział twoje żywe pliki, a kontener staje się stabilnym środowiskiem owiniętym wokół kodu, który wciąż edytujesz:

docker run -p 3000:3000 -v $(pwd):/app myapp

Teraz wersja Ruby, biblioteki systemowe, zbundlowane gemy — wszystko poniżej twojego kodu — jest ustalone przez obraz, podczas gdy sam kod jest tym, co na dysku. To wygrana środowiska deweloperskiego: nowy członek zespołu uruchamia dwie komendy i ma dokładnie ten stos, który mają wszyscy, bez README „najpierw zainstaluj te czternaście rzeczy”.

Wiele usług: bazy danych i fig

Prawdziwe aplikacje to nie jeden proces. Masz Postgresa, może Redisa, może robotnika w tle — a filozofia kontenerów to jeden problem na kontener, połączone ze sobą. W 2014 wczesną odpowiedzią na „połącz te kontenery i uruchom jako jednostkę” jest fig (narzędzie, które wkrótce stanie się docker-compose), deklarujący twój stos w małym pliku YAML:

# fig.yml
web:
  build: .
  ports:
    - "3000:3000"
  links:
    - db
  volumes:
    - .:/app
db:
  image: postgres:9.3

fig up buduje i startuje całość; links czynią kontener db osiągalnym z web po nazwie. To kształt nadchodzących rzeczy — opisanie środowiska wielousługowego jako kodu, odtwarzalnego na dowolnej maszynie — i warto adoptować je już teraz, nawet gdy narzędzia są młode. (To w istocie to, co robi stos w stylu docker-lamp: kontener webowy połączony ze swoją bazą, całe środowisko ujęte w jednym pliku, który możesz zacommitować.)

Dokąd to zmierza

Jest wcześnie — narzędzia są surowe, historia na Macu to VM-w-VM, a dobre praktyki wciąż wykuwają się publicznie. Ale rdzenna idea jest wyraźnie słuszna, a trajektoria oczywista. Odtwarzalny obraz, który budujesz na development, jest tym samym artefaktem, który wyślesz na produkcję, co zwija przepaść między tymi dwoma środowiskami, będącą przyczyną dekady niespodzianek w dniu deployu. „Działa na mojej maszynie” przestaje być wzruszeniem ramion, a staje się gwarancją, bo maszyna podróżuje z aplikacją.

Zbuduj jeden obraz dla realnego projektu w tym miesiącu. Dockerfile będzie krótki, lekcja o cache’u warstw uratuje cię od razu, a ty zyskasz przewagę startową w tym, co szybko staje się domyślnym sposobem pakowania i wysyłki oprogramowania serwerowego.