SSD Nodes Learn 🎉 VPS od $5.50/mies.
Przewodniki Matt ConnorAutor: Matt Connor

Docker Compose build czy image: różnice na VPS

Wyjaśnienie różnic między parametrami build oraz image w Docker Compose. Dowiedz się, dlaczego zmiany w Dockerfile są ignorowane na VPS i jak wymusić aktualizację obrazu.

Docker Compose build a image: krótkie wyjaśnienie

W pliku Docker Compose parametr image: wskazuje obraz do pobrania z rejestru, a build: instruuje Compose, aby zbudował obraz na bieżącej maszynie z pliku Dockerfile. Ustawienie tylko image: powoduje, że Compose pobiera wskazany tag i uruchamia go. Ustawienie tylko build: sprawia, że Compose buduje obraz lokalnie, nadając mu nazwę pochodzącą od nazwy projektu i usługi. Ustawienie obu parametrów powoduje, że Compose buduje obraz lokalnie, a następnie oznacza go nazwą z image:; jest to sposób na zbudowanie obrazu i wypchnięcie go pod wybraną nazwą.

To cała różnica. Poniższe informacje dotyczą znaczenia tych parametrów w działaniu na serwerze. Zakłada się, że Docker Engine oraz wtyczka Compose są już zainstalowane; uruchamianie Docker na VPS opisuje ten proces.

Trzy pełne formy

Pobierz opublikowany tag i uruchom go. Na żadnym etapie nie jest wymagany plik Dockerfile.

services:
  web:
    image: nginx:1.27
    restart: unless-stopped
    ports:
      - "80:80"

Zbuduj obraz z pliku Dockerfile znajdującego się w bieżącym katalogu. Nie jest pobierane nic poza obrazem bazowym wskazanym w FROM.

services:
  web:
    build: .
    restart: unless-stopped
    ports:
      - "80:80"

Zbuduj obraz lokalnie i nadaj mu tag. docker compose push może następnie wysłać ten konkretny tag do rejestru.

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    image: registry.example.com/acme/web:1.4.2
    restart: unless-stopped
    ports:
      - "80:80"

context to katalog przesyłany do procesu budowania. dockerfile jest rozwiązywana względem tego kontekstu, dlatego zapis context: . z dockerfile: docker/prod.Dockerfile jest standardowy i poprawny. Uruchom docker compose images, aby wyświetlić nazwę obrazu oraz ID obrazu dla każdego kontenera usługi; jest to najszybszy sposób na zweryfikowanie, która z tych trzech form została faktycznie zastosowana.

Dlaczego docker compose up nie buduje obrazu ponownie po zmianie Dockerfile?

Ponieważ up sprawdza jedynie, czy obraz istnieje, a nie czy jest aktualny.

Gdy Compose uruchamia usługę posiadającą sekcję build:, szuka obrazu w lokalnym rejestrze. Jeśli obraz o danej nazwie już tam jest, Compose go użyje. Narzędzie nie analizuje pliku Dockerfile, nie porównuje plików źródłowych ani nie sprawdza znaczników czasu. Specyfikacja Compose definiuje tę zasadę poprzez atrybut pull_policy, a domyślne zachowanie polega na budowaniu obrazu tylko wtedy, gdy go brakuje. Obecność obrazu jest traktowana jako wystarczająca.

W efekcie użytkownik edytuje app.py, uruchamia docker compose up -d, obserwuje komunikat Compose o działającym kontenerze i serwuje stary kod. Żaden proces nie zgłosił błędu, więc nie pojawiło się ostrzeżenie. Jest to najczęstsza przyczyna zgłoszeń typu „moje zmiany nie weszły w życie” w kontekście Compose. Wskazówką jest status, który Compose wyświetla obok nazwy kontenera: kontener zastąpiony przez Compose ma status recreated lub started, natomiast kontener, który Compose pozostawił bez zmian, ma status running.

Dwie metody pozwalają to zweryfikować. docker compose images wyświetla identyfikator obrazu używanego przez każdy kontener; należy go zanotować przed wdrożeniem i porównać po nim. docker image ls posiada kolumnę CREATED, a obraz utworzony przed ostatnim commitem jest obrazem nieaktualnym, niezależnie od tego, co wyświetlił skrypt wdrożeniowy.

Które flagi wymuszają przebudowanie

  • docker compose up -d --build najpierw buduje, a następnie odtwarza każdy kontener, którego obraz uległ zmianie. Jest to flaga, której szuka większość użytkowników.
  • docker compose build web buduje jedną usługę i nie uruchamia niczego. Użyj po niej docker compose up --no-deps -d web, aby zastąpić tylko ten kontener i pozostawić resztę stosu w działaniu.
  • docker compose build --no-cache web odrzuca każdą warstwę z pamięci podręcznej i buduje od pierwszej instrukcji.
  • docker compose build --pull próbuje pobrać nowszą wersję obrazu bazowego w FROM, dzięki czemu zmienny tag, taki jak node:22, pobiera aktualną zawartość zamiast kopii pobranej w marcu.
  • docker compose up -d --force-recreate odtwarza kontenery z obrazu, którego już używają. Nigdy nie buduje. Sięganie po tę flagę, gdy zamiarem było --build, jest częstym błędem.

Decyzję można również przenieść do pliku. Zgodnie ze specyfikacją Compose, pull_policy: build oznacza, że Compose buduje obraz i przebudowuje go, jeśli już istnieje. Każde up wiąże się wtedy z kosztem budowania, co jest pożądane na laptopie, ale rzadko na serwerze.

services:
  web:
    build: .
    image: registry.example.com/acme/web:dev
    pull_policy: build

Warto znać jeszcze jedną interakcję. docker compose pull próbuje pobrać obrazy również dla usług posiadających sekcję build, a jeśli pobieranie się nie powiedzie, informuje, że obraz musi zostać zbudowany. Użyj --ignore-buildable, aby pominąć te usługi bez wyświetlania komunikatów.

Wpływ pamięci podręcznej budowania na czas wdrożenia

Każda instrukcja w pliku Dockerfile tworzy warstwę, a mechanizm budowania ponownie wykorzystuje zbuforowaną warstwę, gdy instrukcja oraz jej dane wejściowe pozostają niezmienione. W przypadku COPY danymi wejściowymi jest zawartość kopiowanych plików. Gdy jedna warstwa nie zostanie znaleziona w pamięci podręcznej, każda kolejna warstwa jest budowana od nowa, ponieważ każda z nich opiera się na systemie plików wygenerowanym przez poprzednią.

Ta jedna zasada decyduje o tym, czy wdrożenie trwa sekundy, czy minuty. Należy układać instrukcje w pliku Dockerfile od tych, które zmieniają się rzadko, do tych, które zmieniają się przy każdym commicie.

FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]

npm ci znajduje się powyżej COPY . ., więc edycja pliku źródłowego sprawia, że warstwa instalacji pozostaje w pamięci podręcznej, a proces budowania wznawia się od kroku kopiowania. Zamiana tych dwóch linii miejscami spowoduje, że zmiana nawet jednego znaku wymusi ponowną instalację wszystkich zależności, ponieważ COPY . . unieważnia warstwę, na której zbudowane jest npm ci. Ten sam schemat dotyczy pip install -r requirements.txt oraz go mod download.

--no-cache jest właściwym narzędziem, gdy istnieje podejrzenie, że nieaktualna warstwa maskuje poprawkę. Jest to jednak złe ustawienie domyślne, ponieważ niweluje korzyści z ponownego wykorzystania warstw, które ma zapewniać odpowiednia kolejność w pliku Dockerfile.

Jedną z rzeczy ustawianych w obrazie, którą Compose może nadpisać, jest CMD z pliku Dockerfile – to właśnie ona jest domyślnie uruchamiana przez obraz, a klucz command: w definicji usługi zastępuje ją. Interakcja między command a entrypoint ma tutaj kluczowe znaczenie, ponieważ nadpisanie w Compose może sprawić, że świeżo zbudowany obraz będzie zachowywał się dokładnie tak samo, jak stary.

Kontekst budowania i .dockerignore

context: . oznacza, że Compose pakuje ten katalog i wysyła go do budowniczego przed wykonaniem pierwszej instrukcji. Wszystko, co znajduje się wewnątrz, zostaje wysłane, włącznie z .git oraz wszelkimi katalogami danych przechowywanymi obok kodu źródłowego. Jeśli proces budowania zatrzymuje się na etapie przesyłania kontekstu w niezmienionym projekcie, oznacza to, że kontekst jest zbyt duży.

Plik .dockerignore umieszczony w katalogu głównym kontekstu wyklucza ścieżki z tego transferu. Składnia jest zbliżona do .gitignore.

.git
node_modules
*.log
data/
.env

Przynosi to dwie korzyści. Transfer staje się mniejszy, więc każde budowanie rozpoczyna się szybciej. Ponadto COPY . . nie może już skopiować .env do obrazu, skąd każdy, kto pobierze ten obraz, mógłby go odczytać.

Przyczyną powolnego budowania, która narasta z czasem, jest bind mount. Wolumen nazwany znajduje się poza katalogiem projektu, ale bind mount, taki jak ./data:/var/lib/postgresql/data, znajduje się wewnątrz kontekstu budowania, więc budowanie staje się wolniejsze z każdym tygodniem w miarę wzrostu bazy danych. Jedna linia w .dockerignore rozwiązuje ten problem. Bind mounts a wolumeny nazwane omawia szersze aspekty tego zagadnienia.

Argumenty budowania niosą ze sobą mniejsze ryzyko tego samego typu. Wartości przekazywane przez args: są widoczne w historii obrazu dla każdego, kto posiada ten obraz, dlatego należy umieszczać tam numery wersji, a nigdy tokeny. Pliki Env i sekrety w Compose wyjaśnia, gdzie w takim razie powinny znajdować się dane uwierzytelniające.

Czy budować na VPS, czy budować gdzie indziej i pobierać gotowe obrazy?

Budowanie na serwerze obsługującym ruch jest domyślnym rozwiązaniem, ponieważ stanowi najkrótszą ścieżkę: git pull, a następnie docker compose up -d --build. Jest to dopuszczalne na małym serwerze, od którego nikt jeszcze nie zależy. Przestaje to być jednak dobre rozwiązanie z dwóch powodów, które można zmierzyć, oraz jednego, który ujawnia się tylko w sytuacjach awaryjnych.

Pamięć. Proces budowania uruchamia kompilatory i bundlery obok działającej aplikacji, a są to najbardziej zasobożerne elementy większości stosów technologicznych. Na VPS z 1 GB pamięci RAM bundler JavaScript lub kompilacja Rust rutynowo stają się największym procesem w systemie. Gdy jądro systemu wyczerpie pamięć, zabija największy proces: albo budowanie kończy się błędem Killed i kodem wyjścia 137, albo zabijana jest baza danych, co powoduje awarię serwisu w trakcie wdrażania. dmesg -T | grep -i oom wyświetla komunikat o zabiciu procesu wraz z jego nazwą, dzięki czemu można ustalić przyczynę, zamiast zgadywać.

Dysk. Każde budowanie pozostawia warstwy, a narzędzie budujące przechowuje własną pamięć podręczną oddzielnie od obrazów. docker system df pokazuje oba te elementy, przy czym wiersz pamięci podręcznej budowania stale rośnie. Można odzyskać miejsce za pomocą docker image prune dla nieużywanych obrazów oraz docker builder prune dla warstw pamięci podręcznej. Pełny dysk zatrzymuje nie tylko proces budowania. Baza danych również przestaje zapisywać dane, a taka awaria kosztuje znacznie więcej niż powolne wdrażanie.

Powtarzalność. Obraz zbudowany na serwerze istnieje tylko na tym serwerze. Wycofanie zmian (rollback) wymaga pobrania starego commita i ponownego budowania, a takie budowanie nie gwarantuje uzyskania identycznego rezultatu, ponieważ tag bazowy mógł ulec zmianie, podobnie jak repozytoria pakietów. Budowanie w innym miejscu i wypychanie tagu sprawia, że wycofanie zmian staje się prostą edycją: należy wskazać image: na poprzedni tag i uruchomić docker compose up -d.

Najbardziej stabilne rozwiązanie jest przejrzyste. Ciągła integracja (CI) wykonuje budowanie i wypycha registry.example.com/acme/web:<git-sha>, a plik Compose na serwerze VPS zawiera image: bez żadnego klucza build:. Wdrożenie składa się wtedy z dwóch poleceń, które niemal nie zużywają pamięci.

docker compose pull
docker compose up -d

Należy raz uruchomić docker login registry.example.com na serwerze, aby Compose mógł od tego momentu pobierać prywatne tagi.

Warto zachować sekcję budowania na potrzeby środowiska programistycznego zamiast ją usuwać, umieszczając ją w pliku o własnej nazwie.

# compose.dev.yaml
services:
  web:
    build:
      context: .
    pull_policy: build
docker compose -f compose.yaml -f compose.dev.yaml up -d --build

Plik ten należy nazwać compose.dev.yaml, a nie compose.override.yaml. Compose automatycznie ładuje plik nadpisujący (override), jeśli jest obecny, więc przypadkowo skopiowany na serwer plik nadpisujący mógłby po cichu uruchomić proces budowania bezpośrednio na serwerze. Łączenie kilku plików Compose wyjaśnia, w jaki sposób mechanizm łączenia rozstrzyga wartości poszczególnych kluczy.

Pułapka architektury przy budowaniu na innej maszynie

Obraz zawiera informację o architekturze procesora, dla której został zbudowany. Jeśli obraz zostanie zbudowany na laptopie z Apple Silicon, wypchnięty do rejestru, a następnie pobrany na serwer VPS z architekturą x86_64, Docker wyświetli ostrzeżenie, że żądana platforma obrazu nie jest zgodna z wykrytą platformą hosta. Proces zakończy się błędem exec format error, który sugeruje uszkodzenie pliku binarnego, choć w rzeczywistości nim nie jest. Należy jawnie określić platformę docelową podczas budowania:

docker buildx build --platform linux/amd64 \
  -t registry.example.com/acme/web:1.4.2 --push .

Ten sam problem występuje w odwrotnej sytuacji, jeśli laptop posiada architekturę x86, a uruchamiasz serwer VPS z architekturą ARM zamiast x86. Zlecenie budowania w systemie CI na architekturze, na której odbywa się wdrożenie, eliminuje ten problem.

Co sprawdzić po wdrożeniu

  • docker compose images wyświetla obraz i tag dla każdego uruchomionego kontenera. Zmieniony identyfikator obrazu stanowi dowód, że nowa kompilacja jest w użyciu.
  • docker compose config wyświetla scalony plik po podstawieniu zmiennych, co pozwala odczytać ostateczną nazwę obrazu, której użyje Compose, przed uruchomieniem czegokolwiek.
  • docker compose logs -f web przez pierwsze pół minuty po podmianie. Kontener, który uruchamia się i kończy działanie, wpada w pętlę restartów zamiast pozostać włączonym, a pętla ta jest cicha, dopóki nie zostanie sprawdzona.
  • docker image ls pokazuje kolumnę CREATED. Obraz starszy niż ostatni commit oznacza, że nie został przebudowany.

Jeśli nadal przygotowujesz plik, dla którego wykonywane są te testy, podstawy pliku Compose na VPS omawiają kluczowe elementy, a ściąga z poleceń Compose zawiera listę pozostałych podpoleceń.

FAQ

Czy mogę używać build i image w tej samej usłudze?

Tak, jest to standardowa konfiguracja w przypadku projektów budowanych samodzielnie. Compose buduje obraz na podstawie sekcji build: i oznacza go wartością image:. Ten znacznik jest wysyłany przez docker compose push do rejestru i pobierany przez inne maszyny. Bez klucza image: Compose nadal przeprowadza budowanie, ale nadaje obrazowi nazwę pochodzącą od projektu i usługi, ostrzegając jednocześnie, że brak tego atrybutu uniemożliwia wypchnięcie obrazu do rejestru.

Dlaczego docker compose up nie uwzględnia zmian w moim Dockerfile?

Ponieważ up sprawdza jedynie, czy obraz o danej nazwie istnieje. Jeśli obraz jest obecny, Compose uruchamia go bez porównywania z plikiem Dockerfile lub plikami źródłowymi. Należy wykonać docker compose up -d --build lub uruchomić docker compose build web, a następnie docker compose up --no-deps -d web, aby zastąpić pojedynczą usługę. Ustawienie pull_policy: build dla usługi wymusza ponowne budowanie przy każdym up, co jest przydatne w środowisku programistycznym.

Jaka jest różnica między --build a --force-recreate?

--build buduje obraz ponownie, a następnie odtwarza kontenery, których obraz uległ zmianie. --force-recreate odtwarza kontenery z obrazu, który już posiadają, więc nie uwzględnia zmian w kodzie. Jeśli zmiana dotyczy kodu źródłowego lub pliku Dockerfile, należy użyć flagi --build. --force-recreate służy do resetowania samego kontenera, na przykład w celu wyczyszczenia jego warstwy zapisu przy zachowaniu tego samego obrazu.

Czy powinienem budować obrazy Docker na serwerze VPS czy gdzie indziej?

Zaleca się budowanie w innym miejscu i pobieranie gotowego obrazu, gdy serwer obsługuje ruch. Proces budowania konkuruje z aplikacją o pamięć RAM, a na małym serwerze VPS może to doprowadzić do zabicia przez jądro systemu największego procesu, którym może być baza danych lub sam proces budowania. Budowanie pozostawia również na dysku pamięć podręczną, która nie jest automatycznie usuwana. Budowanie na serwerze jest dopuszczalne w małych projektach bez użytkowników, a późniejsza migracja jest prosta, jeśli sekcja build: zostanie umieszczona w pliku Compose przeznaczonym wyłącznie do prac rozwojowych.

Jak zapobiec zapełnianiu dysku przez pamięć podręczną budowania Docker?

Uruchom docker system df, aby sprawdzić, ile miejsca zajmują obrazy oraz pamięć podręczna budowania. docker builder prune usuwa warstwy z pamięci podręcznej, a docker image prune usuwa nieużywane obrazy pozostałe po wcześniejszych budowaniach. Dodanie -a do któregokolwiek z tych poleceń jest bardziej agresywne i wymusza rozpoczęcie kolejnego budowania od zera. Nie należy planować automatycznego uruchamiania docker system prune -af --volumes na serwerze, ponieważ --volumes usuwa każdy wolumen, który nie jest aktualnie używany przez żaden kontener, a zatrzymany w celach konserwacyjnych stos może przechowywać bazę danych właśnie w takim wolumenie.