1. Платформа в Docker: контекст і терміни
Після multi-stage і розмови про tag/digest легко потрапити в пастку: здається, що якщо ми «зафіксували» образ, то все стало повністю передбачуваним. На практиці в команді швидко виникає змішана реальність: хтось працює на Windows, хтось — на Linux, хтось — на macOS з Apple Silicon. І раптом з’ясовується, що одне й те саме посилання на image може означати різні бінарники під різні CPU.
Важливо одразу вловити правильну інтонацію: це не «складна DevOps-тема», а звичайна частина інженерного мислення про середовище. Ми не будемо занурюватися в multi-platform-публікації або складні пайплайни. Нам потрібно рівно стільки, щоб розуміти, чому в студента на Mac M1 збирання поводиться інакше, і як не сплутати platform mismatch із «зламався Spring».
Почнімо з найкоротшого формулювання проблеми: ваш jar майже платформонезалежний, а от JVM, Linux userland і нативні утиліти всередині base image — платформозалежні. Тому один і той самий Dockerfile може по-різному поводитися на amd64 і arm64.
Platform: ОС і архітектура
Коли Docker говорить platform, він зазвичай має на увазі поєднання операційної системи і архітектури CPU у форматі os/arch. У межах курсу ми майже завжди говоримо про Linux-контейнери, тому найчастіше ви будете бачити linux/amd64 або linux/arm64. Це схоже на паспорт образу: «під яку операційну систему і який процесор зібрано бінарники всередині».
У новачків плутанина починається з дуже простої людської логіки: «Але ж у мене macOS! Чому тут Linux?» Відповідь така: Docker Desktop зазвичай запускає Linux-контейнери всередині Linux-оточення, тобто у VM, навіть якщо хостова ОС — macOS або Windows. У нашому курсі контейнери — саме Linux-контейнери, і це принципово важливо.
Щоб закріпити терміни, ось невелика таблиця — без «енциклопедії», лише те, що справді трапляється в житті Java-розробника:
| Що ви бачите | Як читати | Типова ситуація |
|---|---|---|
| linux/amd64 | Linux + x86_64 (найчастіше «звичайні» Intel/AMD) | більшість CI, багато машин на Linux і Windows, багато серверів |
| linux/arm64 | Linux + ARM 64-bit | Apple Silicon (M1/M2/M3), деякі dev-машини, частина серверів |
| os.arch=amd64 | JVM бачить, що архітектура x86_64 | запуск Java всередині amd64-середовища |
| os.arch=aarch64 | JVM бачить ARM64 | запуск Java всередині arm64-середовища |
Є ще один важливий нюанс, щоб не було магії: у Java архітектуру часто називають aarch64, а в Docker — arm64. Це не два різні світи, а дві різні традиції найменування.
2. Multi-arch образи: один FROM, різні варіанти
Тут важливо уявити, що один «образ» у Docker-реєстрі — це не завжди один файл. Часто це набір варіантів під різні платформи (multi-arch). Ви пишете в Dockerfile щось на кшталт FROM some-java-runtime:25, а Docker під час збирання або завантаження автоматично обирає відповідний варіант під вашу платформу.
І тут з’являється важливий зв’язок із tag/digest. tag зручний для людини, але він може вказувати на набір варіантів. digest фіксує незмінне посилання і захищає від дрейфу з часом. Але цього все одно недостатньо, щоб автоматично отримати один і той самий бінарний шар, зібраний під конкретну платформу, на amd64 і arm64: у multi-arch-образі всередині живуть різні варіанти, і який із них буде обрано, залежить ще й від платформи. Тому відтворюваність посилання й контроль платформи — близькі, але різні завдання.
Корисно побачити це як просту блок-схему:
flowchart TD
%% Як Docker обирає варіант образу під вашу платформу
A["Dockerfile: FROM some-java-runtime:25"] --> B{"Є варіант під вашу platform?"}
B -->|Так| C["Docker обирає linux/amd64 або linux/arm64"]
B -->|Ні| D["Помилка: no matching manifest ..."]
C --> E["Збирання та запуск тривають далі"]
Ключова думка така: якщо в образу немає варіанта під вашу архітектуру, проблема з’явиться ще до запуску застосунку. І виглядатиме це як «Docker зламався» або «щось не так із базовим образом», хоча насправді все набагато простіше: потрібної платформи просто немає.
3. Host, Docker Desktop і Linux-образи
Коли ви працюєте на macOS або Windows і запускаєте Linux-контейнери, одночасно співіснують три шари реальності. Це нормально, але в початківців це викликає «когнітивний збій»: здається, що контейнер — це окремий комп’ютер, а Dockerfile — магічний сценарій, не пов’язаний із вашою машиною. Насправді зв’язок дуже прямий: платформа вашої машини впливає на те, який варіант образу буде обрано за замовчуванням.
На рівні моделі зручно тримати таку картину:
flowchart LR
%% Важлива думка: навіть на macOS/Windows ви зазвичай працюєте з Linux-контейнерами (змінюється arch)
Host["Ваш ноутбук macOS / Windows / Linux"] --> Docker["Docker Engine / Docker Desktop"]
Docker --> VM["Linux-оточення (часто VM)"]
VM --> C1["Контейнер: linux/arm64 або linux/amd64"]
І тепер важливий практичний висновок для курсу: навіть якщо у вас macOS, ви все одно здебільшого збираєте й запускаєте Linux-образи, і платформа матиме вигляд linux/*. Відрізнятиметься архітектура: amd64 або arm64.
Щоб відчути архітектуру як щось спостережуване, можна взяти крихітний Java-код. Він не залежить від Spring, від Docker і взагалі від вашого проєкту — він просто показує, що бачить JVM:
public class PlatformInfo {
public static void main(String[] args) {
// Назва ОС "всередині" того середовища, де запущено JVM (у контейнері майже завжди це Linux)
System.out.println("Назва ОС = " + System.getProperty("os.name")); // наприклад: Назва ОС = Linux
// Архітектура JVM/середовища: для ARM64 часто буде aarch64, для x86_64 — amd64
System.out.println("Архітектура = " + System.getProperty("os.arch")); // наприклад: Архітектура = aarch64
}
}
Якщо запустити цей код усередині контейнера, os.name майже напевно буде Linux, навіть якщо ваш ноутбук — macOS. А от os.arch уже покаже архітектуру контейнерного середовища: aarch64 або amd64.
4. Де спливають amd64 і arm64 у Java/Spring
Тема архітектури найчастіше приходить до вас не тому, що ви любите страждати, а тому, що сучасний світ робить це неминучим. У навчальній групі майже завжди знайдеться людина з Apple Silicon. У компанії майже завжди є CI на amd64. А іноді розробник «зібрав образ у себе», а інший розробник «не зміг запустити той самий образ», і починається давній обряд: «почисти кеш», «перезапусти Docker», «перевстанови все», «прочитай молитву до контейнерного бога».
Повернімося до нашого курсу й до проєкту Container-Ready Catalog Service. У нас є multi-stage Dockerfile приблизно такого вигляду:
# Етап 1: збирання застосунку (JDK потрібен для компіляції та збирання)
FROM some-java-jdk:25 AS builder
WORKDIR /src
# Копіюємо вихідний код у контейнер збирання
COPY . .
# Збираємо fat-jar / bootJar (залежно від проєкту)
RUN ./gradlew bootJar
# Етап 2: образ рантайму (зазвичай легший за JDK)
FROM some-java-runtime:25
WORKDIR /app
# Забираємо зібраний jar із builder-етапу
COPY --from=builder /src/build/libs/app.jar app.jar
# Запускаємо застосунок
ENTRYPOINT ["java", "-jar", "app.jar"]
В обох FROM усередині містяться нативні бінарники: JDK/JRE, libc, shell, можливо, якісь утиліти — залежно від дистрибутива. Якщо у вас arm64, а образ доступний лише під amd64, Docker або скаже «не можу», або спробує запустити його через емуляцію, або видасть помилку вже на старті контейнера.
На практиці, ще до того, як ви встигнете подумати про Spring, можете побачити один із трьох «класів» симптомів.
5. Симптоми platform mismatch
Коли платформа не збіглася, Docker часто дає доволі чесні повідомлення. Проблема в тому, що вони виглядають не надто дружніми для новачка: багато слів, багато цифр, і хочеться вдавати, що це не про вас. Але саме в таких ситуаціях краще прочитати підказку, ніж сперечатися з нею.
Перший частий симптом — помилка під час pull або на етапі FROM: Docker не може знайти варіант образу під потрібну архітектуру. Тоді ви побачите повідомлення на кшталт «no matching manifest for linux/arm64…». Це означає дуже просту річ: у реєстрі немає образу під вашу платформу.
Другий симптом — попередження під час запуску: Docker говорить, що образ linux/amd64, а ваша машина linux/arm64, і він запускає не зовсім те, що очікується. Часто це ще не помилка, але вже червоний прапорець: або буде емуляція, або згодом стане боляче.
Третій симптом — контейнер стартує і одразу падає з чимось на кшталт exec format error. Це виглядає як «зламалася команда запуску», але насправді це типова помилка, коли бінарник зібрано під іншу архітектуру, і операційна система всередині контейнера просто не може його виконати.
І тут важливо діяти методично: якщо ви бачите такі симптоми, не потрібно одразу копати конфігурацію Spring і виправляти application.yml. Спочатку перевірте, чи взагалі запускаєте образ під правильною платформою. Це як намагатися лагодити ремінь безпеки, коли в машини немає коліс: ремінь тут ні до чого, але виглядає важливим.
6. Керування платформою: --platform
Коли ви розумієте, що платформа — це реальний параметр збирання, виникає дуже природне запитання: «А можна сказати Dockerʼу прямо, що я хочу linux/amd64 або linux/arm64?» Можна. Для цього і існує прапорець --platform. Але користуватися ним треба як скальпелем, а не як молотком: у скальпеля є сенс, але якщо почати ним забивати цвяхи — буде дивно і, можливо, з кров’ю.
Найнаочніший спосіб — вказати платформу прямо у FROM. Тоді ви говорите: «Я хочу, щоб цей stage був саме такої платформи». Наприклад:
# Явно фіксуємо платформу базового образу (корисно для діагностики та відтворення CI)
FROM --platform=linux/amd64 some-java-runtime:25
WORKDIR /app
# Копіюємо jar (у прикладі — вже готовий артефакт)
COPY app.jar app.jar
# Запускаємо застосунок
ENTRYPOINT ["java", "-jar", "app.jar"]
Це може бути корисно як діагностичний крок, щоб відтворити поведінку CI на amd64, навіть якщо локально ви працюєте на arm64. Але важливо пам’ятати: якщо ваша машина arm64, то збирання або запуск amd64 може піти через емуляцію, а отже — стати повільнішим. Тобто --platform може розв’язати проблему «хочу зібрати те саме», але додасть ціну: «це буде не так швидко».
Трохи більш операційний варіант — задати платформу на рівні build-команди. У сучасному Docker це часто робиться через buildx:
# Явно збираємо образ під задану платформу (наприклад, щоб перевірити ARM-збирання на CI або локально)
docker buildx build --platform=linux/arm64 -t catalog-service .
Сенс простий: ви збираєте образ як linux/arm64. У реальному житті це допомагає, коли ви хочете перевірити, що ваш проєкт збирається і під ARM, або, навпаки, відтворити amd64-оточення. Ми поки не переходимо до multi-platform-публікації, тобто не робимо «одну команду, яка одразу збирає і amd64, і arm64, а потім публікує». Зараз нам важливо зрозуміти сам механізм вибору.
Є ще й docker run --platform=..., який дозволяє явно запустити образ під вибраною платформою, якщо це можливо. На рівні навчання це корисно, щоб відрізнити «образ не тієї платформи» від «образ узагалі битий» або «застосунок справді не стартує».
7. Швидка перевірка: середовище, образ, контейнер
Платформа — це той випадок, коли краще один раз подивитися на факти, ніж п’ять разів сперечатися з відчуттями. Гарна новина: перевірка робиться короткими командами і маленьким фрагментом Java-коду, без складної магії.
Для початку можна подивитися, що Docker вважає архітектурою вашого середовища. Часто це видно в docker info (у різних версіях вивід відрізняється), а для образів і контейнерів є inspect. Наприклад, щоб побачити архітектуру образу, можна використати шаблонний вивід:
# Показуємо ОС/архітектуру, під яку "зібрано" цей образ
docker image inspect catalog-service \
--format '{{.Os}}/{{.Architecture}}'
# приклад виводу: linux/arm64
Аналогічно можна подивитися контейнер:
# Показуємо platform уже у конкретного контейнера (важливо, якщо ви запускаєте з --platform)
docker container inspect catalog-service-container \
--format '{{.Platform}}'
# приклад виводу (може залежати від версії): linux/arm64
Якщо вам ближчий рівень Java, то всередині контейнера або локально можна вивести архітектуру JVM, як ми робили вище. Це особливо корисно, коли ви намагаєтеся зрозуміти: «У мене всередині контейнера справді ARM, чи я зараз випадково в емуляції?» У такому разі os.arch дасть зрозумілий сигнал:
public class ArchOnly {
public static void main(String[] args) {
// os.arch — швидкий спосіб зрозуміти, під яку архітектуру реально запущено JVM
String arch = System.getProperty("os.arch");
// aarch64 зазвичай відповідає linux/arm64, amd64 — linux/amd64
System.out.println("Виявлена архітектура = " + arch); // наприклад: Виявлена архітектура = aarch64
}
}
Сенс усіх цих перевірок не в тому, щоб стати експертом з inspect-JSON, а в тому, щоб перестати лікувати симптоми не того шару. Коли ви бачите, що образ linux/amd64, а середовище linux/arm64, подальша діагностика стає спокійнішою: ви вже знаєте, що це не «рандом», а цілком пояснювана невідповідність.
8. Типові помилки під час роботи з платформами amd64 і arm64
Помилка № 1: плутати host OS і platform образу.
Зазвичай це звучить так: «Але ж у мене Windows, чому контейнер Linux?» або «У мене macOS, отже контейнер macOS». У нашому курсі ми майже завжди працюємо з Linux-контейнерами, і це нормально навіть на Windows/macOS. Платформа в Docker — це linux/…, а відрізнятиметься архітектура.
Помилка № 2: ігнорувати попередження Docker і йти виправляти Spring.
Дуже частий сценарій: контейнер не запускається, студент відкриває application.yml, починає змінювати порт, профілі, логування — і лише потім помічає, що Docker ще на старті говорив про невідповідність платформи. Якщо ви бачите повідомлення у стилі no matching manifest або platform does not match, це майже завжди треба розібрати до того, як ви торкаєтеся коду застосунку.
Помилка № 3: примусово прописувати --platform «про всяк випадок».
Прапорець --platform корисний, але якщо ставити його всюди й завжди, ви отримаєте новий тип проблем: неочікувану емуляцію, повільні збирання і дивні відмінності в поведінці. Хороша стратегія на рівні команди — або жити на нативній платформі (arm64 на ARM, amd64 на x86_64), або явно використовувати --platform як діагностичний чи відтворювальний інструмент, а не як постійну тимчасову латочку.
Помилка № 4: змінювати одразу все: vendor, tag/digest і platform.
Якщо одночасно змінити базовий образ, версію тега і ще й платформу, ви отримаєте «комбо-хаос»: незрозуміло, що саме вплинуло на результат. Інженерний підхід тут нудний, але робочий: змінюємо один параметр, перевіряємо, фіксуємо спостереження, рухаємося далі.
Помилка № 5: очікувати, що «раз jar кросплатформений, то й образ теж».
jar справді переноситься між архітектурами, але лише за наявності JVM під потрібну архітектуру. Base image містить JVM як нативний бінарник. Тому контейнеризація — це не просто «скопіювати jar», а «привезти коректне середовище запуску», яке включає платформу.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ