Parę miesięcy temu zaproponowałem by Composer dostarczany był w obrazach zawierających jedynie plik wykonywalny. Wczoraj mój pull request został zmerdżowany, więc już można używać tego typu obrazów podczas budowania swoich własnych, co jest najprostszą metodą instalacji Composera 😁

Wyjaśnienie

Gdy istnieje potrzeba dołączenia Composera do budowanego obrazu Dockera, można to zrobić na kilka sposobów. Jednak wcześniej, chcąc skorzystać z podejścia COPY --from=composer, trzeba było pobrać obraz Composera ważący ~190MB, tylko po to, by z niego przekopiować jeden plik o wadze 2.5MB. Oczywistym jest, że nie było to optymalne, nawet jeśli weźmiemy pod uwagę build cache Dockera.

Po moich zmianach, każdy dockerowy obraz Composera będzie miał swój lekki odpowiednik zawierający jedynie plik wykonywalny. Są jednak 3 różnice, na które należy zwrócić uwagę:

  • należy używać obrazów composer/composer (szczegóły poniżej)
  • do tagu należy dodać sufiks -bin
  • plik wykonywalny zlokalizowany jest w głównym katalogu (/composer), czyli gdzie indziej niż w pełnych obrazach (/usr/bin/composer)

Na przykład, aby zainstalować najnowszą wersję Composera z gałęzi v2, wystarczy zrobić:

FROM php:8-alpine

COPY --from=composer/composer:2-bin /composer /usr/bin/composer

Kiedy tego używać (a kiedy nie)

Obrazy z plikiem wykonywalnym są przydatne w trakcie budowania własnych obrazów opartych o PHP, gdy istnieje potrzeba dostarczenia ich wraz z Composerem. Zamiast instalować go programowo, można użyć dockerowego obrazu i wyekstrahować z niego pojedynczy, gotowy do użytku plik.

W dość oczywisty sposób wynika z tego, że obrazy binary-only nie nadają się do uruchamiania czegokolwiek z ich użyciem (w sposób bezpośredni). Uruchomienie docker run -it --rm composer/composer:2-bin <cokolwiek> nie zadziała, ponieważ obrazy te nie zawierają w sobie niczego poza plikiem wykonywalnym Composera — nie ma w nich ani środowiska uruchomieniowego PHP, ani powłoki (shell).

Szczegóły implementacyjne

We wspomnianym Pull Requeście zauważyć można dwa rodzaje zmian:

  • pliki Dockerfile zostały rozwinięte o tzw. build targety, by możliwe było budowanie obu wersji obrazów
  • definicje GitHub Actions zostały zmodyfikowane tak, by każdy pipeline budował zarówno pełne, jak i lekkie wersje obrazów (dodatkowo są tagowane w dedykowany sposób)

Ta pierwsza zmiana jest bardziej istotna, zatem przyjrzyjmy się jej na przykładzie zmian dla gałęzi 2.4:

diff --git a/2.4/Dockerfile b/2.4/Dockerfile
index c0b4ca7..866be1c 100644
--- a/2.4/Dockerfile
+++ b/2.4/Dockerfile
@@ -1,4 +1,4 @@
-FROM php:8-alpine
+FROM php:8-alpine AS binary-with-runtime
 
 RUN set -eux ; \
   apk add --no-cache --virtual .composer-rundeps \
@@ -89,3 +89,10 @@ WORKDIR /app
 ENTRYPOINT ["/docker-entrypoint.sh"]
 
 CMD ["composer"]
+
+FROM scratch AS standalone-binary
+
+COPY --from=binary-with-runtime /usr/bin/composer /composer
+
+# This is defined as last target to be backward compatible with build without explicit --target option
+FROM binary-with-runtime AS default

Na samym początku pliku, do już istniejącej definicji budowania pełnego obrazu, dodany został target binary-with-runtime. Jest on później wykorzystywany na dwa sposoby: do budowania pełnego obrazu (docker build --target binary-with-runtime) oraz jako baza do budowania wersji binary-only (COPY --from=binary-with-runtime /usr/bin/composer /composer). Ten drugi proces nazywamy multi-stage buildem i jest to metoda polegająca na dzieleniu procesu budowania na mniejsze, dedykowane etapy, między którymi mogą istnieć relacje (dziedziczenie warstw, przenoszenie pojedynczych plików). Takie podejście jest przydatne szczególnie w kontekście optymalizacji rozmiaru wynikowych obrazów, ponieważ m.in. można w ten sposób łatwo pominąć pliki tymczasowe.

Ostatnia linijka jest ciekawa — zapewnia wsteczną kompatybilność poprzez zdefiniowanie aliasu default dla targetu binary-with-runtime. Ponieważ jest to ostatni target w pliku, zostanie on użyty automatycznie, gdy opcja --target nie zostanie zdefiniowana w komendzie wyzwalającej build.

Podsumowanie

To niewielka, ale ważna i pomocna zmiana, dzięki której będzie możliwe zoptymalizowanie wielu istniejących procesów budowania obrazów. Wypróbuj to w swoim procesie i daj znać co myślisz 🙂