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

Dockeryzacja aplikacji Rails pod development i CI

Docker dojrzał z ciekawostki w domyślny sposób na spójne środowisko. Przewodnik po dockeryzacji aplikacji Rails pod development i CI — docker-compose dla całego stosu, dyscyplina cache'u warstw i gdzie Docker pomaga, a gdzie dodaje tarcie.

Kilka lat temu Docker był obiecującą ciekawostką; dziś jest dojrzały, a docker-compose uczynił uruchamianie całego stosu aplikacji — aplikacji, bazy, cache, workerów w tle — jedną komendą. Dla zespołu Rails największym zyskiem nie jest już „wysyłaj kontenery na produkcję” (choć możesz); to spójność: każdy programista i twoje CI uruchamiają dokładnie to samo środowisko, a „działa na mojej maszynie” przestaje być zdaniem, które ktokolwiek wypowiada. Oto jak rozsądnie zdockeryzować aplikację Rails pod development i CI — i uczciwa uwaga o tym, gdzie zarabia na swoje, a gdzie dodaje tarcie.

Dockerfile, z dyscypliną cache’u

Obraz zaczyna się od bazy Ruby i instaluje biblioteki systemowe oraz gemy. Najważniejszą rzeczą — tą samą lekcją z wczesnych dni Dockera, wciąż tą, którą ludzie mylą — jest skopiowanie Gemfile’i i bundle przed skopiowaniem reszty kodu, by edycja kodu aplikacji nie unieważniała (wolnej) warstwy instalacji gemów:

FROM ruby:2.5

RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# warstwa bundle — przebudowywana tylko, gdy Gemfile się zmieni
COPY Gemfile Gemfile.lock ./
RUN bundle install --jobs 4

# teraz kod
COPY . .

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

Ułóż to na odwrót, a każda zmiana kodu reinstaluje każdy gem — dwuminutowe czekanie, które sprawia, że ludzie porzucają Dockera. Z poprawnym warstwowaniem gemy reinstalują się tylko, gdy Gemfile faktycznie się zmieni.

docker-compose: cały stos w jednej komendzie

To czyni Dockera naprawdę przyjemnym w 2018. Aplikacja Rails to nie jeden proces — to aplikacja, baza, zwykle Redis, często worker w tle. docker-compose deklaruje je wszystkie, połączone, w jednym pliku:

# docker-compose.yml
version: "3"
services:
  web:
    build: .
    command: bundle exec rails server -b 0.0.0.0
    volumes:
      - .:/app                 # żywy kod, bez przebudowy przy edycji
      - bundle:/usr/local/bundle
    ports:
      - "3000:3000"
    depends_on: [db, redis]
    environment:
      DATABASE_URL: postgres://postgres@db/app_development
      REDIS_URL: redis://redis:6379
  db:
    image: postgres:10
  redis:
    image: redis:4
  worker:
    build: .
    command: bundle exec sidekiq
    depends_on: [db, redis]

volumes:
  bundle:
docker-compose up      # cały stos — web, db, redis, worker — startuje razem

Nowy programista klonuje repo i uruchamia jedną komendę, by dostać dokładnie ten stos, który mają wszyscy — właściwe Ruby, właściwą wersję Postgresa, właściwy Redis, bez README „najpierw zainstaluj te czternaście rzeczy”. Dwa praktyczne dotknięcia czynią doświadczenie deweloperskie dobrym: zamontuj kod jako wolumen (.:/app), by edycje były żywe bez przebudowy obrazu, i zamontuj nazwany wolumen dla bundle, by gemy przetrwały restarty kontenera zamiast się reinstalować. Usługi dosięgają się po nazwie (aplikacja łączy się z db i redis jako hostnamami), co jest siecią compose obsługującą okablowanie za ciebie.

Docker w CI: to samo środowisko, od końca do końca

Ten sam obraz i ustawienie compose wypłacają drugą dywidendę w CI. Zamiast szytego na miarę środowiska twojego dostawcy CI — konkretnego Ruby, konkretnego Postgresa, skonfigurowanego po ich myśli — uruchamiasz testy wewnątrz tych samych kontenerów, w których rozwijasz. Config CI buduje obraz (albo pobiera cache’owany) i uruchamia zestaw wobec tego samego Postgresa i Redisa:

# CI: zbuduj, potem uruchom zestaw w kontenerze wobec prawdziwych usług db/redis
test:
  - docker-compose build
  - docker-compose run web bundle exec rails db:create db:schema:load
  - docker-compose run web bundle exec rspec

Wygraną jest to, że „przechodzi w CI” i „przechodzi na mojej maszynie” wreszcie znaczą to samo, bo są tym samym środowiskiem — eliminując całą klasę zagadek „zielone lokalnie, czerwone w CI” spowodowanych dryfem środowiska. (Paruje się to naturalnie z pracą nad testowaniem równoległym: uruchom zestaw przez kontenery/węzły w ten sam sposób.) Dla szybkości buildu cache’uj warstwy obrazu między przebiegami CI i rozważ build wieloetapowy, by finalny obraz niósł tylko to, czego potrzebuje.

Gdzie Docker pomaga, a gdzie dodaje tarcie

Uczciwa ocena, bo Docker nie jest darmowy:

  • Naprawdę pomaga: onboarding (jedna komenda do działającego stosu), spójność środowiska (dev = CI, opcjonalnie = produkcja), uruchamianie wielu projektów z konfliktowymi wersjami zależności obok siebie i dopasowanie OS oraz wersji bibliotek produkcji dokładnie. Dla zespołu większego niż jeden albo projektu z kilkoma usługami to realne, codzienne wygrane.
  • Dodaje tarcie: jest krzywa uczenia, buildy i zużycie dysku są cięższe, a na macOS w 2018 wydajność systemu plików zamontowanych wolumenów jest zauważalnie wolna (Docker działa tam w VM), co może sprawić, że aplikacja Rails z zamontowanym wolumenem czuje się ospała — realny, dobrze znany punkt bólu. Dla solowego programisty na prostej aplikacji natywne ustawienie rbenv + lokalny Postgres może wciąż być szybsze i prostsze na co dzień.

Osąd: wartość Dockera rośnie z rozmiarem zespołu, liczbą usług i tym, jak bardzo dryf środowiska cię ugryzł. Dla aplikacji wielousługowej i zespołu jest wart tarcia; dla jednoosobowego, jednousługowego projektu zważ to uczciwie, zamiast przyjmować dla mody.

Werdykt

Docker dojrzał w domyślny sposób na spójne, odtwarzalne środowisko, a dla zespołu Rails jego największym zyskiem jest spójność, nie deployment: docker-compose up daje każdemu programiście i twojemu CI ten sam pełny stos — aplikację, bazę, cache, workery — z jednego pliku i jednej komendy, kończąc „działa na mojej maszynie” na dobre. Zrób cache’owanie warstw Dockerfile dobrze (Gemfile przed kodem), zamontuj kod i bundle jako wolumeny dla szybkiej pętli dev i uruchamiaj CI w tych samych kontenerach, by zielone znaczyło zielone wszędzie. Zważ realne tarcie — krzywą uczenia i wydajność wolumenów na macOS — wobec rozmiaru twojego zespołu i stosu. Dla większości zespołów w 2018 handel jest wyraźnie wart, a odtwarzalność, którą kupuje, to fundament, na którym zbudowane jest wszystko inne — równoległe CI, dopasowanie produkcji, bezbolesny onboarding.