CodeGym /Kursy /Docker for Spring /Projekt Gradle: częstotliwość zmian

Projekt Gradle: częstotliwość zmian

Docker for Spring
Poziom 5, Lekcja 3
Dostępny

1. Częstotliwość zmian i cache Dockera

Teraz, gdy zrozumiałe jest, że COPY trzeba budować od stabilnego do zmiennego, pojawia się praktyczne pytanie: co dokładnie w projekcie Gradle uznać za stabilne, a co mieszka w strefie codziennych zmian. Projekty Java/Gradle mają swoją cechę: „ciężkość" budowania często siedzi nie w Twoim kodzie, lecz w zależnościach i krokach budowania. Dlatego rozsądne podzielenie plików według częstotliwości zmian — to nie estetyka, lecz realna oszczędność minut życia, a niekiedy i komórek nerwowych.

Wyobraź sobie, że cache Dockera — to jak lista kontrolna na wejściu do klubu: jeśli za każdym razem zmieniasz paszport, za każdym razem Cię sprawdzają. Nasze zadanie — sprawić, żeby „dane paszportowe" (wrapper + pliki build) zmieniały się rzadko i leżały we wczesnych krokach, a „dzisiejsza garderoba" (src/) zmieniała się ile razy w ciągu dnia, ale dotykała tylko późnych kroków.

Tu ważne jest, żeby nie pomylić: nie próbujemy osiągnąć „0 sekund przebudowania" ani nie robimy z Dockerfile zawodów w mikro-optymalizacjach. Chcemy przewidywalnego zachowania: zmieniłem kod — przebudowała się tylko ta część, która logicznie powinna się przebudować.

2. Mapa repozytorium Gradle/Spring Boot

Gdy patrzysz na korzeń repozytorium, wydaje się on jednym wielkim folderem: „no tam przecież jest cały projekt". Ale cache Dockera tak nie myśli. Dla niego ważne co innego: „do tej warstwy kopiuję właśnie te pliki". I jeśli nie masz w głowie mapy projektu, niemal nieuchronnie spłyniesz do COPY . . i potem będziesz heroicznie cierpieć (i pisać na czacie: „dlaczego Docker nie cache'uje???").

Utrwalmy minimalną mapę, na której będziemy się opierać w naszym docker-java-catalog-service:

 
  docker-java-catalog-service/
|-- gradlew
|-- gradle/
|-- build.gradle.kts
|-- settings.gradle.kts
|-- src/
\-- build/   # generowane przez Gradle, w Git zazwyczaj nie commitowane
 

W tym obrazku już jest ukryta logika dzisiejszego wykładu. gradlew i folder gradle/ — to „silnik", który pozwala uruchamiać Gradle jednakowo u wszystkich. build.gradle.kts i settings.gradle.kts — „przepis", według którego Gradle buduje projekt i decyduje, jakie zależności podciągnąć. src/ — to, co zmieniasz najczęściej. A build/ — wynik, który w ogóle nie powinien być danymi wejściowymi dla budowania: to „spaliny", a nie „surowiec".

3. Gradle Wrapper

W tym miejscu zazwyczaj następuje typowy dialog początkującego programisty z rzeczywistością: „A po co mi ten gradlew, przecież mogę po prostu gradle bootJar". Możesz… dopóki jesteś jeden w projekcie, masz jedną maszynę i nie zmieniałeś wersji Gradle pół roku. Jak tylko pojawia się drugi człowiek (albo przeinstalowałeś system), zaczyna się loteria: ktoś ma Gradle 9.4, ktoś 8.7, a ktoś w ogóle „nie zainstalowany".

Gradle Wrapper — to właśnie rozwiązanie tej loterii. Utrwala wersję Gradle dla projektu i sprawia, że uruchomienie budowania jest reprodukowalne. Dla Dockera jest to szczególnie ważne: wewnątrz kontenera nie chcemy „zgadywać", jaki Gradle tam jest. Chcemy przynieść do obrazu dokładnie to, co potrzebne do budowania.

Mini-mapa wrappera wygląda tak:

 
  .
|-- gradlew
\-- gradle/
    \-- wrapper/
        |-- gradle-wrapper.properties
        \-- gradle-wrapper.jar
 

Jeśli uprościć do jednego zdania, gradlew — to „przycisk uruchamiający Gradle", a gradle/wrapper/* — „wbudowana instrukcja, który dokładnie Gradle pobrać i uruchomić". I to wszystko zmienia się rzadko. Znaczy, z punktu widzenia cache Dockera to idealni kandydaci na wczesne COPY.

Jest jeszcze jeden praktyczny niuans, który pojawia się przynajmniej u części osób: gradlew musi być wykonywalny (chmod +x). Na Linuksie i macOS zazwyczaj „po prostu działa", a na Windows niekiedy wyskakuje niespodziewane permission denied. To nie powód nienawidzić Dockera, lecz powód pamiętać, że docker build najczęściej wykonuje się w kontekście Linuksa.

4. Pliki build i zależności

Po wrapperze drugą płytą oporową projektu są pliki build. W Kotlin DSL to zazwyczaj build.gradle.kts i settings.gradle.kts. Mieszka w nich wszystko, co Gradle uważa za opis budowania: pluginy, zależności, wersja projektu, nazwa projektu, struktura modułów. Nie zmieniasz ich co godzinę, ale i „raz w roku" też nie zawsze: dodałeś bibliotekę, zaktualizowałeś wersję, podłączyłeś plugin — i plik już się zmienił.

Z punktu widzenia cache Dockera pliki build — to strefa średniej stabilności. Trzeba je kopiować wcześniej niż src/, bo zmiana kodu biznesowego nie powinna psuć cache tam, gdzie umownie podciągasz zależności. Ale jeśli reale zmieniasz zależności — tak, to uczciwy powód inwalidacji części cache i przebudowania ciężkich warstw. Tu Docker zachowuje się prawidłowo, i nie ma sensu z nim spierać.

Przykładowy fragment build.gradle.kts w naszym serwisie może wyglądać tak:

 
  dependencies {
    // Warstwa web (kontrolery, obsługa HTTP). Zmieniasz zależności tu — zmienia się dependency graph.
    implementation("org.springframework.boot:spring-boot-starter-webmvc")

    // Endpointy /actuator do monitorowania i health aplikacji.
    implementation("org.springframework.boot:spring-boot-starter-actuator")

    // JPA + transakcje. To zazwyczaj "ciężka" warstwa, ciągnąca paczkę zależności.
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")

    // Sterownik bazy danych potrzebny tylko w runtime (w kodzie zazwyczaj nie importuje się bezpośrednio).
    runtimeOnly("org.postgresql:postgresql")
}
 

Tu ważne jest, żeby zobaczyć logikę: zmieniłeś ten blok — znaczy projekt stał się inny z punktu widzenia grafu zależności. Normalne, że cache Dockera w tym miejscu „powie": okej, przebuduję warstwę, gdzie Gradle coś rozwiązuje, pobiera i przebudowuje. Ale jeśli nie zmieniałeś zależności, a po prostu poprawiłeś CatalogItemService, Docker powinien mieć szansę, żeby nie dotykać tej warstwy. Właśnie po to rozdzielamy pliki na grupy.

Jeszcze jeden często spotykany plik — gradle.properties. Jeśli jest i leżą w nim ustawienia budowania (wersje, flagi), to sensu należy do tej samej kategorii co build.gradle.kts: jest częścią „przepisu" budowania, więc logicznie trzyma się go w tej samej grupie.

5. Źródła: src/

Jeśli wrapper i pliki build — to fundament, to src/ — Twoja codzienna budowla. Tu mieszkasz: piszesz klasy Java, zmieniasz kontrolery, poprawiasz serwisy, dodajesz DTO, koregujesz application.yml, zmieniasz migracje SQL i tak dalej. I tak, będzie się to często zmieniać. Niekiedy — dziesiątki razy dziennie, szczególnie gdy się uczysz.

Dlatego src/ prawie zawsze powinien trafiać do Dockerfile później niż wrapper i pliki build. To nie „optymalizacja dla optymalizacji", lecz sposób na zachowanie uczciwy cache dla bardziej stabilnych warstw. Zmiana src/ powinna inwalidować warstwę COPY src ... i wszystko, co poniżej w Dockerfile (np. krok budowania bootJar). To normalne. Nienormalne jest to, gdy zmiana src/ inwaliduje jeszcze kroki, które w ogóle nie mają związku ze źródłami, np. przygotowanie zależności czy kopiowanie wrappera.

Jeśli uprościć: src/ — to jak biurko programisty, a wrapper + pliki build — to jak szuflada z narzędziami i instrukcją do nich. Możesz przestawiać papiery na biurku ile chcesz, ale nie powinno to zmuszać Cię za każdym razem do kupowania nowego młotka.

6. build/ i generowane artefakty

W tym miejscu często chce się zrobić „prostą" rzecz: „Zbuduję bootJar lokalnie, a potem po prostu skopiuję build/ do obrazu". Na wczesnym etapie to reale działa, i tak już robiliśmy, gdy chcieliśmy szybko zobaczyć, że „kontener startuje". Ale jeśli mówimy o budowaniu przyjaznym cache i stopniowym rozwoju Dockerfile, ważne jest, żeby nie zamieniać folderu build/ w część danych wejściowych dla docker build tylko dlatego, że tak krótcej pisać.

Folder build/ — to wynik pracy Gradle. Wewnątrz leżą skompilowane klasy, zasoby, raporty, wyniki testów, niekiedy pliki tymczasowe, i tak, Twój jar też. Ten folder z natury jest „hałaśliwy": zmienia się nawet wtedy, gdy zmieniłeś drobiazg. A jeszcze łatwo gromadzi w nim śmietnik z poprzednich buildów. Jeśli Docker zobaczy, że do build context przyleciał build/, będzie traktować to jako część wejścia. I dalej zaczyna się historia „dlaczego mój cache jest zawsze chybiony".

Dlatego reguła jest bardzo prosta i bardzo użyteczna: build/ najczęściej powinien być wykluczony z build context przez .dockerignore, a Dockerfile powinien pracować ze źródłami i przepisami budowania, a nie z tym, co Gradle już „naśmiecił" na Twój system plików.

Mini-przykład .dockerignore dla projektu Java/Gradle:

 
  # Wyjście buildowe Gradle: "hałaśliwy" output, nie powinien trafiać do build context
build
.gradle

# Pliki IDE: zmieniają się same z siebie i psują cache "na równej ziemi"
.idea
*.iml
out

# Zazwyczaj nie są potrzebne do budowania JAR wewnątrz obrazu (jeśli nie używasz ich w skrypcie build)
# requests
# scripts
 

Nawet jeśli nic więcej nie zrobisz, samo to już zauważalnie zmniejszy liczbę przypadkowych przyczyn, dla których cache Dockera staje się bezużyteczny.

7. Lokalne cache i ślady IDE

Istnieje szczególna kategoria plików, które mieszkają obok projektu, ale w ogóle nie są częścią „źródeł aplikacji". To lokalne cache Gradle (.gradle/), ustawienia IDE (.idea/, *.iml), foldery pomocnicze (out/) i inne ślady Twojej maszyny. Często zmieniają się niepostrzeżenie dla Ciebie: IDE coś zapisało, Gradle zaktualizował cache, przełączyłeś gałąź.

Jeśli takie pliki trafią do build context i używasz szerokiego COPY . ., Docker będzie zmuszony reagować na ich zmiany tak, jakbyś zmienił ważną część projektu. A Ty będziesz siedzieć i myśleć: „Przecież nic nie zmieniałem, czemu cache jest zepsuty?". Spoiler: nie zmieniałeś kodu biznesowego, ale zmieniały się pliki w kontekście budowania.

Dlatego .dockerignore działa tu jak filtr zdrowego rozsądku: jawnie mówimy Dockerowi „to nie dotyczy budowania obrazu, nie ciągnij tego do build context". I tak, .dockerignore nie zastępuje kolejności COPY, ale drastycznie obniża poziom przypadkowego szumu, szczególnie u początkujących.

Nawiasem mówiąc, w repozytorium dydaktycznym możemy mieć requests/ i scripts/ do smoke-testów. To użyteczne dla programisty, ale dla budowania jar wewnątrz obrazu zazwyczaj nie są potrzebne. Jeśli kopiujesz cały projekt w całości, zmiany w pliku requests/catalog.http też będą psuć cache Dockera po prostu dlatego, że zmienił się plik w kontekście. Dlatego kolejny naturalny krok — kopiować do wnętrza obrazu tylko to, co reale potrzebne do budowania.

8. Tabela częstotliwości zmian

Po wszystkich wyjaśnieniach przydatne jest utrwalenie tego nie jako zestaw emocji („wydaje mi się, że to jest stabilne"), lecz jako mapa inżynierska. Poniżej — właśnie ta tabela, którą wygodnie trzymać w głowie, gdy piszesz COPY w Dockerfile.

Grupa plików Przykłady Jak często się zmienia Co to znaczy dla cache Dockera Gdzie zazwyczaj w Dockerfile
Gradle Wrapper
gradlew
gradle/wrapper/*
rzadko doskonały kandydat na wczesny COPY i długo żyjący cache na samym początku (po WORKDIR)
Opis budowania i zależności
build.gradle.kts
settings.gradle.kts
gradle.properties
niekiedy powinno inwalidować cache, gdy reale zmieniasz zależności (to normalne) po wrapperze, ale przed src/
Źródła aplikacji
src/main/java
src/main/resources
często będzie często psuć cache dolnych kroków, i jest to oczekiwane bliżej końca
Testy (jeśli są)
src/test/*
niekiedy/często jeśli w obrazie uruchamiasz testy, to jeszcze jedna „gorąca" warstwa zależy od strategii, ale zazwyczaj osobno
Wyjście buildowe (generowane)
build/
cały czas psuje cache „szumem", zazwyczaj szkodliwe jako wejście do obrazu zazwyczaj nie kopiujemy
Lokalny śmietnik
.gradle/
.idea/
out/
cały czas i nieprzewidywalnie tworzy przypadkowe cache miss wykluczamy przez .dockerignore

Zwróć uwagę na ważną myśl: cache Dockera — to nie „sprawić, żeby wszystko było cached". Cache Dockera — to „cache'ować to, co logicznie cache'ować, i przebudowywać to, co reale się zmieniło". Jeśli dodałeś nową zależność, przebudowanie — to właściwa cena. A jeśli poprawiłeś jedną metodę w serwisie, to ponowne pobieranie połowy internetu brzmi już jak kpina — i leczymy to strukturą Dockerfile.

9. Grupujemy COPY w Dockerfile

Teraz ważne jest dla nas nie dopisanie Dockerfile do ostatniej linii, lecz przetłumaczenie mapy repozytorium na kolejność COPY. Na poziomie struktury plików logika zazwyczaj wygląda tak: najpierw wrapper, potem pliki build, potem src.

Mini-fragment, który odzwierciedla właśnie ideę grupowania:

 
  WORKDIR /app

# 1) Najrzadsze zmiany: wrapper (daje reprodukowalną wersję Gradle)
COPY gradlew ./
COPY gradle/ ./gradle/

# 2) "Przepis" budowania: zmienia się rzadko/niekiedy, ale krytyczne dla zależności
COPY build.gradle.kts settings.gradle.kts ./

# 3) Najczęstsze zmiany: źródła aplikacji
COPY src/ ./src/
 

Tego fragmentu już wystarczy, żeby zobaczyć właściwą granicę inwalidacji. src/ powinien przychodzić po plikach build, żeby zwykła poprawka kodu nie budziła górnej części budowania. A jeśli zmienia się build.gradle.kts, granica podniesie się wyżej — i to właśnie uczciwa cena zmiany grafu zależności.

Z tej mapy już niemal wychodzi cały Dockerfile: powyżej zostają stabilne wejścia budowania, niżej schodzi gorąca strefa src/, a ciężki bootJar powinien stać po tej granicy.

10. Typowe błędy przy pracy z cache Dockera

Błąd nr 1: postrzeganie całego projektu jako jednego niepodzielnego folderu i pisanie COPY . . na początku.
Taki Dockerfile jest przyjemnie krótki, ale dosłownie prosi Dockera, żeby traktował „każde kichnięcie" jako powód do przebudowania: poprawiłeś plik .java, zaktualizowało się .idea, poprawiłeś README.md, IDE stworzyło nowy plik — i już przebudowuje się ciężka część. To jest szczególnie bolesne w projekcie Gradle, gdzie budowanie i zależności kosztują drogo.

Błąd nr 2: ciągnięcie build/ do build context i zdziwienie, że cache nigdy nie trafia.
build/ — to wyjście, zmienia się często i hałaśliwie. Gdy uczestniczy jako wejście, mówisz Dockerowi: „Uznaj, że projekt zmienia się zawsze". Po tym cache staje się loterią, a przebudowanie — wieczne. Prawidłowa droga — wykluczyć build/ w .dockerignore i kopiować tylko źródła i pliki budowania.

Błąd nr 3: zapomnienie o Gradle Wrapper i próba „po prostu zainstalowania Gradle w obrazie".
Niekiedy chce się zrobić RUN apt-get install gradle lub „wziąć Gradle z obrazu". Ale wtedy tracisz reprodukowalność i zaczynasz zależeć od tego, co znalazło się w obrazie bazowym i managerze pakietów. Wrapper jest stworzony właśnie po to, żeby tego nie było. Dla projektu dydaktycznego jest to szczególnie ważne: chcemy, żeby polecenia działały jednakowo u wszystkich studentów.

Błąd nr 4: niezrozumienie, że pliki build i źródła — to różne „klasy zmian".
Jeśli kopiujesz pliki build i src/ jednym krokiem, mieszasz „rzadkie zmiany" (np. aktualizacja zależności) z „częstymi zmianami" (poprawka kodu). W efekcie rzadkie części nie mają normalnej szansy na zcache'owanie. Rozdzielenie COPY według sensu — to jedna z najprostszych i najbardziej efektywnych rzeczy, które można zrobić w Dockerfile.

Błąd nr 5: zostawienie w build context .gradle/ i .idea/, a potem łapanie przypadkowych cache miss „na równej ziemi".
Te foldery łatwo zmieniają się same z siebie. Możesz nawet nie dotykać kodu, ale IDE zaktualizuje pliki, Gradle zaktualizuje cache, i Docker zobaczy zmiany. W efekcie tracisz zaufanie do budowania („jest nieprzewidywalne"). Zazwyczaj leczy się to dwiema rzeczami: dobrym .dockerignore i węższym zestawem plików w COPY.

1
Zadanie
Docker for Spring, poziom 5, lekcja 3
Niedostępne
Czysty build context dla projektu Gradle
Czysty build context dla projektu Gradle
1
Zadanie
Docker for Spring, poziom 5, lekcja 3
Niedostępne
Grupy plików według częstotliwości zmian
Grupy plików według częstotliwości zmian
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION