CodeGym /Kursy /Docker for Spring /Czytelny Dockerfile

Czytelny Dockerfile

Docker for Spring
Poziom 4, Lekcja 4
Dostępny

1. Co to jest czytelny Dockerfile

„Czytelny Dockerfile" brzmi jak zdanie ze świata dorosłych ludzi, którzy stawiają przecinki w README i nie nazywają zmiennych x2final_final2. Ale sens jest tu bardzo praktyczny: Dockerfile powinien być taki, żebyś za miesiąc otworzył go i zrozumiał, co się dzieje, bez wzywania szamana od kontenerów i bez ponownego czytania całego internetu.

Czytelność tu — nie piękno dla piękna. To sposób na utrzymanie kontroli nad trzema rzeczami: gdzie bierzemy środowisko Java, gdzie kładziemy artefakt (bootJar) i jak uruchamiamy proces wewnątrz kontenera. Gdy tylko Dockerfile staje się w stylu „chyba działa, ale nie wiadomo dlaczego", tracisz główną zaletę podejścia inżynierskiego: możliwość przewidywalnego zmieniania zachowania uruchomienia, nie psując wszystkiego wokół.

Naiwny Dockerfile już rozwiązywał jedno zadanie: wziąć Java runtime, położyć jar do obrazu i jakoś go uruchomić. Ale osobno już zobaczyliśmy, że tego za mało: build-side i run-side trzeba rozdzielić, ENTRYPOINT powinien uruchamiać Javę bezpośrednio, a zmienne parametry nie mogą być zapiekane przez każde przebudowanie. Teraz pozostaje zebrać to wszystko w jeden stabilny wariant Dockerfile, żeby optymalizacja warstw i cache szła potem na stabilnym uruchomieniu, a nie zamiast niego.

2. Szkielet Dockerfile: pięć bloków

Naiwny wariant zazwyczaj wygląda bardzo krótko: obraz bazowy, COPY jar, java -jar. Do pierwszego uruchomienia wystarczy. Czytelny wariant różni się nie liczbą linii, lecz tym, że każda linia utrwala osobną umowę: gdzie mieszka aplikacja, jak trafia do obrazu, jakie wartości domyślne są już w nim zadane i jaki proces Docker uważa za główny.

Końcowy Dockerfile dnia

 
  # 1) Baza: Java 25 runtime (tylko do uruchamiania, bez budowania)
FROM eclipse-temurin:25-jre-jammy

# 2) Katalog roboczy aplikacji wewnątrz kontenera
WORKDIR /opt/app

# 3) Wkładamy do obrazu gotowy artefakt budowania (Spring Boot bootJar)
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar

# 4) Domyślne wartości do uruchomienia (można nadpisać przez docker run -e ...)
ENV SPRING_PROFILES_ACTIVE=standalone
ENV SERVER_PORT=8080

# 5) Dokumentujemy port, którego słucha aplikacja wewnątrz kontenera
EXPOSE 8080

# 6) Startujemy Javę jako główny proces kontenera (exec-form)
ENTRYPOINT ["java", "-jar", "app.jar"]
 

To ten wariant, gdzie wszystkie umowy wreszcie są zebrane w jednym miejscu. Tu świadomie nie dodajemy jeszcze CMD z tym samym portem czy profilem: komenda uruchomienia już jest stabilna przez ENTRYPOINT, a wartości domyślne żyją w ENV i mogą być nadpisane w konkretnym uruchomieniu.

Od góry do dołu ten Dockerfile czyta się tak:

flowchart TD
    A["FROM (baza: Java runtime)"] --> B["WORKDIR (katalog roboczy)"]
    B --> C["ARG + COPY (przeniesienie bootJar)"]
    C --> D["ENV + EXPOSE (wartości domyślne i dokumentacja portu)"]
    D --> E["ENTRYPOINT (exec-form uruchomienie java)"]
Blok Dockerfile Za co odpowiada Co mamy w naszym Dockerfile
FROM Jaka Java i jakie środowisko bazowe trafią do kontenera obraz runtime z Java 25
WORKDIR Gdzie mieszka aplikacja wewnątrz obrazu /opt/app
ARG + COPY Jak bootJar trafia do obrazu pod stabilną nazwą build/libs/*.jar → app.jar
ENV + EXPOSE Jakie wartości domyślne widzi kontener i jaki wewnętrzny port jest oczekiwany SPRING_PROFILES_ACTIVE, SERVER_PORT, EXPOSE 8080
ENTRYPOINT Jak uruchamiany jest główny proces kontenera
["java","-jar","app.jar"]

CMD trzymamy w głowie jako osobny wzorzec dla wartości domyślnych argumentów aplikacji. Ale w tym Dockerfile jest on świadomie niepotrzebny: te same startowe wartości już żyją w ENV, a drugie źródło wartości domyślnych tylko wprowadza zamieszanie.

Baza i katalog roboczy

FROM i WORKDIR są tu potrzebne nie dla zbędnej teorii, lecz żeby Dockerfile od razu czytał się od góry do dołu bez chaosu ścieżek. Od razu widzimy, na jakiej Javie żyje serwis i gdzie wewnątrz obrazu jest jego „dom".

Dzięki temu app.jar nie błąka się po przypadkowych katalogach, a wszystkie kolejne instrukcje działają względem jednego zrozumiałego katalogu. Rzecz mała, ale właśnie na takich drobiazgach plik przestaje być kruchy.

Artefakt budowania

Świadomie kopiujemy nie cały projekt, lecz wynik bootJar. ARG JAR_FILE zostawia buildowi mały margines, jeśli trzeba wskazać jar jawnie, a COPY ${JAR_FILE} app.jar utrwala wewnątrz obrazu stabilną nazwę pliku.

To jest wygodne zarówno do czytania, jak i do uruchamiania: ENTRYPOINT już nie zależy od nazwy wersjonowanego artefaktu z build/libs/, a Dockerfile nie zamienia się w listę przypadkowych plików, które trafiły do obrazu „na wszelki wypadek".

Wartości domyślne i port

ENV zadaje rozsądne wartości domyślne: profil standalone i wewnętrzny port 8080. To nie są „na zawsze przykute" wartości, lecz startowe umowy, które można pobić przez docker run -e lub argumenty aplikacji.

EXPOSE obok jest potrzebny nie do publikacji portu na zewnątrz, lecz żeby od razu było widać, jaki port serwis oczekuje wewnątrz kontenera. Zewnętrzny dostęp nadal konfiguruje się wyłącznie przez docker run -p.

Uruchomienie procesu

ENTRYPOINT ["java", "-jar", "app.jar"] utrwala to co najważniejsze: Docker uruchamia Javę bezpośrednio, bez pośrednika shellowego, i właśnie JVM staje się głównym procesem kontenera. Dzięki temu stop/restart zachowują się przewidywalnie, a Spring dostaje normalną szansę na poprawne zakończenie.

CMD pamiętamy jako działający wzorzec dla domyślnych argumentów aplikacji, gdy naprawdę wygodnie je trzymać osobno. Ale w tym Dockerfile świadomie go nie używamy dla portu i profilu: duplikowanie tych samych wartości zarówno w ENV, jak i w CMD — to tylko mnożenie zamieszania.

3. Budowanie i uruchamianie bez edytowania Dockerfile

Teraz warto sprawdzić sam princip w praktyce: jeden i ten sam obraz uruchamiany w kilku trybach, a Dockerfile przy tym pozostaje ten sam.

Najpierw, jak poprzednio, budujemy jar i obraz:

 
  # Budujemy executable jar (Spring Boot bootJar)
./gradlew bootJar

# Budujemy Docker image z bieżącego katalogu (Docker build context = .)
docker build -t docker-java-catalog-service .
 

Podstawowe uruchomienie „jak jest", z wartościami domyślnymi z Dockerfile:

 
  docker run --rm -p 8080:8080 docker-java-catalog-service
 

Jeśli wszystko dobrze, w logach zobaczysz oznaki startu i port. Logi u Spring Boot bywają długie, ale teraz ważne jest, żebyś zobaczył sensowne kawałki: linię o aktywnym profilu i linię o tym, że serwer podniósł się na potrzebnym porcie. Sformułowanie może się różnić, ale idea jest jedna.

Teraz uruchamiamy ten sam obraz na innym porcie przez zmienne env:

 
  docker run --rm -p 9090:9090 \
  -e SERVER_PORT=9090 \
  -e SPRING_PROFILES_ACTIVE=standalone \
  docker-java-catalog-service
 

Tu ważne jest, żeby nie pomylić dwóch światów. -p 9090:9090 — to o sieci Dockera (host:container). SERVER_PORT=9090 — to o tym, na jakim porcie sama aplikacja wewnątrz kontenera będzie słuchać HTTP. Jeśli zmieniłeś jedno i zapomniałeś o drugim, dostaniesz klasyczne „kontener żyje, ale serwis się nie otwiera".

Ten sam efekt można osiągnąć przez argumenty aplikacji: przy exec-form ENTRYPOINT wszystko, co dopiszesz po nazwie image, trafi do Spring Boot jako app args.

 
  docker run --rm -p 9090:9090 \
  docker-java-catalog-service \
  --server.port=9090 \
  --spring.profiles.active=standalone
 

A oto osobny kanał dla opcji JVM — JAVA_TOOL_OPTIONS. Jego sens polega na tym, że to sposób na dodanie parametrów JVM, bez zamieniania ENTRYPOINT w shell-form i bez przyklejania tam „sklejonego ciągu".

Na przykład ograniczymy heap — czysto jako demonstrację kanału, bez fanatyzmu:

 
  docker run --rm -p 8080:8080 \
  -e JAVA_TOOL_OPTIONS="-Xms256m -Xmx256m" \
  docker-java-catalog-service
 

Zwróć uwagę na przyjemną rzecz: Dockerfile się nie zmienia. Nie przebudowujemy obrazu dla „inny port" lub „mniej pamięci". Po prostu uruchamiamy ten sam artefakt w różnych trybach, i właśnie po to w ogóle wymyślono normalny model ENTRYPOINT + zewnętrzna konfiguracja.

Żeby nie trzymać wszystkiego w głowie, można posługiwać się małą pamięcią:

Co chcesz zmienić Najprostszy kanał dziś Przykład
Port aplikacji env var lub app args -e SERVER_PORT=9090 lub --server.port=9090
Aktywny profil env var lub app args -e SPRING_PROFILES_ACTIVE=standalone lub --spring.profiles.active=standalone
Parametry JVM JAVA_TOOL_OPTIONS -e JAVA_TOOL_OPTIONS="-Xmx256m"

Ta tabelka nie jest po to, żeby zakuwać, lecz żeby szybko odróżniać kanał Dockera od kanału JVM i kanału Spring Boot.

4. Typowe błędy przy pracy z Dockerfile

Błąd nr 1: Dockerfile nie widzi jar, choć na pewno jest.
Najczęściej przyczyna jest banalna: jar nie trafił do build context. Niekiedy studenci zbyt agresywnie skonfigurowali .dockerignore i wykluczyli build/ w całości, a potem dziwią się, że COPY build/libs/*.jar nie działa. Leczy się to nie mistyką, lecz sprawdzeniem: gdzie jar leży, co reale jest wysyłane do build context i jakie reguły ignorowania zadziałały.

Błąd nr 2: EXPOSE 8080 napisano, ale serwis się nie otwiera.
EXPOSE nie publikuje portu na zewnątrz. Tylko dokumentuje oczekiwany port wewnątrz kontenera. Zewnętrzny dostęp pojawia się wyłącznie przez docker run -p hostPort:containerPort. Jeśli pomylić te role, można bezustannie zmieniać Dockerfile, choć problem leży w komendzie uruchomienia.

Błąd nr 3: przejście na shell-form dla „ładnej linii".
Niekiedy chce się napisać ENTRYPOINT java -jar app.jar, bo tak wygląda „jak w terminalu". Ale to shell-form, i ciągnie za sobą powłokę jako pośredni proces. W efekcie pogarszasz dostarczanie sygnałów do procesu Java i dostajesz mniej stabilne zachowanie zatrzymania kontenera. Dla serwisu Java podstawowy wariant powinien być nudny i prawidłowy: ENTRYPOINT ["java", "-jar", "app.jar"].

Błąd nr 4: mieszanie ARG z ENV, i wszystko zaczyna wydawać się nieprzewidywalne.
ARG żyje tylko podczas budowania. Jeśli spróbujesz przez ARG „zadać port" lub „włączyć profil", dostaniesz niespodziankę: kontener tego nie dostanie. Dla parametrów runtime potrzebny jest ENV jako wartość domyślna obrazu lub parametry docker run jako nadpisanie. Gdy tylko rozdzielisz build-time i runtime w głowie, połowa zamieszania znika.

Błąd nr 5: niespójność portu w trzech miejscach (i potem boli).
Student zmienia -p 9090:8080, potem ustawia SERVER_PORT=9090, potem dodaje --server.port=7070, a na górze jeszcze chce położyć ten sam port w CMD. Pomaga tu prosta reguła: w jednym uruchomieniu staraj się zadawać parametr jednym kanałem i zachowuj zgodność między portem aplikacji a mapowaniem portów kontenera. Inaczej będziesz diagnozował nie Dockera, lecz własną kombinację parametrów.

1
Zadanie
Docker for Spring, poziom 4, lekcja 4
Niedostępne
Czytelny Dockerfile i weryfikacja przez image inspect
Czytelny Dockerfile i weryfikacja przez image inspect
1
Zadanie
Docker for Spring, poziom 4, lekcja 4
Niedostępne
Przepisz hałaśliwy Dockerfile na schludną wersję
Przepisz hałaśliwy Dockerfile na schludną wersję
1
Ankieta/quiz
Podstawy Dockerfile, poziom 4, lekcja 4
Niedostępny
Podstawy Dockerfile
Instrukcje i uruchamianie kontenerów
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION