CodeGym /Kursy /Docker for Spring /amd64 and arm64 without confusion

amd64 and arm64 without confusion

Docker for Spring
Poziom 7, Lekcja 2
Dostępny

1. Platform in Docker: context and terms

After multi-stage and the discussion of tag/digest, it is easy to fall into a trap: it seems that if we have “fixed” the image, then everything has become completely predictable. In practice, a mixed reality quickly appears on the team: someone is on Windows, someone is on Linux, someone is on macOS with Apple Silicon. And then it suddenly turns out that one and the same image link may mean different binaries for different CPUs.

It is important to catch the right tone: this is not a “difficult DevOps topic”, but an ordinary part of engineering thinking about the environment. We will not go into multi-platform publishing or complex pipelines. We need just enough to understand why a student on a Mac M1 sees the build behave differently, and how not to confuse platform mismatch with “Spring broke”.

Let us start with the shortest formulation of the problem: your jar is almost platform-independent, but the JVM, Linux userland, and native utilities inside the base image are platform-dependent. That is why one and the same Dockerfile can behave differently on amd64 and arm64.

Platform: OS and architecture

When Docker says platform, it usually means a combination of the operating system and the CPU architecture in the os/arch format. In this course we almost always talk about Linux containers, so most often you will see linux/amd64 or linux/arm64. It is like an image passport: “which operating system and which processor the binaries inside were built for.”

The confusion for beginners starts with simple human logic: “But I have macOS! Why is it Linux here?” The answer: because Docker Desktop usually runs Linux containers inside a Linux environment (inside a VM), even if the host OS is macOS or Windows. Containers in our course are Linux containers, and that is fundamentally important.

To lock in the terms, here is a small table — no “encyclopedia”, just what a Java developer really runs into in real life:

What you see How to read it Typical situation
linux/amd64 Linux + x86_64 (usually “regular” Intel/AMD) most CI, many Linux/Windows machines, many servers
linux/arm64 Linux + ARM 64-bit Apple Silicon (M1/M2/M3), some dev machines, some servers
os.arch=amd64 JVM sees that the architecture is x86_64 running Java inside an amd64 environment
os.arch=aarch64 JVM sees ARM64 running Java inside an arm64 environment

One important nuance so there is no magic: in Java, the architecture is often called aarch64, while in Docker it is arm64. These are not two different worlds, but two different naming traditions.

2. Multi-arch images: one FROM, different variants

Now comes a moment when it is useful to imagine that one “image” in Docker Registry is not always one file. Often it is a set of variants for different platforms (multi-arch). You write something like Dockerfile with FROM some-java-runtime:25, and Docker chooses the appropriate variant for your platform automatically during build/pull.

And here an important connection with tag/digest appears. For a person, tag is convenient, but it can point to a set of variants. digest fixes an immutable reference and protects against drift over time. But even that is not enough to automatically get the same platform-specific binary layer on amd64 and arm64: inside a multi-arch image there are different variants, and which one will be selected still depends on the platform. So reference reproducibility and platform control are neighboring but different tasks.

It is useful to see this as a simple flowchart:

flowchart TD
    %% How Docker chooses an image variant for your platform
    A["Dockerfile: FROM some-java-runtime:25"] --> B{"Is there a variant for your platform?"}
    B -->|Yes| C["Docker chooses linux/amd64 or linux/arm64"]
    B -->|No| D["Error: no matching manifest ..."]
    C --> E["Build and run continue"]

The key idea: if the image does not have a variant for your architecture, you will get a problem even before the application starts. And it will look like “Docker broke” or “some nonsense with the base image”, although in reality it is simply the absence of the required platform.

3. Host, Docker Desktop, and Linux images

When you work on macOS or Windows and run Linux containers, you live in three layers of reality at once. This is a normal situation, but it causes a “cognitive bug” for beginners: it seems that the container is like a separate computer, and the Dockerfile is a magical script unrelated to your machine. In fact, the connection is very direct: the platform of your machine affects which image variant will be selected by default.

At the level of the model, it is convenient to keep this picture in mind:

flowchart LR
    %% Important idea: even on macOS/Windows you usually work with Linux containers (the arch changes)
    Host["Your laptop macOS / Windows / Linux"] --> Docker["Docker Engine / Docker Desktop"]
    Docker --> VM["Linux environment (often a VM)"]
    VM --> C1["Container: linux/arm64 or linux/amd64"]

And now an important practical conclusion for the course: even if you have macOS, you still mostly build and run Linux images, and the platform will look like linux/*. The architecture will differ: amd64 or arm64.

To feel architecture as something observable, you can take a tiny Java code sample. It does not depend on Spring, Docker, or your project at all — it just shows what the JVM sees:

public class PlatformInfo {
    public static void main(String[] args) {
        // The OS name "inside" the environment where the JVM is running (inside a container it is almost always Linux)
        System.out.println("os.name = " + System.getProperty("os.name")); // for example: os.name = Linux

        // Architecture of the JVM/environment: for ARM64 it will often be aarch64, for x86_64 — amd64
        System.out.println("os.arch = " + System.getProperty("os.arch")); // for example: os.arch = aarch64
    }
}

If you run this code inside a container, os.name will almost certainly be Linux, even if your laptop is macOS. But os.arch will already show the architecture of the container environment: aarch64 or amd64.

4. Where amd64 and arm64 show up in Java/Spring

The architecture topic usually comes to you not because you enjoy suffering, but because the modern world makes it unavoidable. In a study group there is almost always someone with Apple Silicon. In a company there is almost always CI on amd64. And sometimes one developer “built the image locally”, while another developer “could not run the same image”, and the ancient ritual begins: “clear the cache”, “restart Docker”, “reinstall everything”, “say a prayer to the container god”.

Let us ground this in our course and in our project Container-Ready Catalog Service. We have a multi-stage Dockerfile roughly like this:

# Stage 1: build the application (JDK is needed for compilation/build)
FROM some-java-jdk:25 AS builder
WORKDIR /src

# Copy the sources into the build container
COPY . .

# Build the fat-jar / bootJar (depending on the project)
RUN ./gradlew bootJar

# Stage 2: runtime image (usually lighter than JDK)
FROM some-java-runtime:25
WORKDIR /app

# Take the built jar from the builder stage
COPY --from=builder /src/build/libs/app.jar app.jar

# Start the application
ENTRYPOINT ["java", "-jar", "app.jar"]

Both FROM lines contain native binaries: JDK/JRE, libc, shell, and possibly some utilities (depending on the distribution). If you have arm64, but the image is available only for amd64, Docker will either say “I cannot”, or try to run through emulation, or throw an error when the container starts.

In practice, even before you have time to think about Spring, you may see one of three “classes” of symptoms.

5. Symptoms of platform mismatch

When the platform does not match, Docker often gives quite honest messages. The problem is that these messages look “not junior-friendly”: a lot of words, a lot of numbers, and you want to pretend that this is not about you. But this is exactly the case when it is better to read the hint than to argue with it.

The first common symptom is an error during pull or at the FROM stage: Docker cannot find an image variant for the required architecture. Then you will see a message like “no matching manifest for linux/arm64...”. This means a very simple thing: there is no image in the registry for your platform.

The second symptom is a warning at startup: Docker says the image is linux/amd64, while your machine is linux/arm64, and it is running something other than what was expected. Often this is not yet an error, but it is a red flag: either emulation will happen, or it will hurt later.

The third symptom is that the container starts and immediately crashes with something like exec format error. This looks like “the startup command broke”, but in fact it is a typical error when the binary is built for another architecture and the operating system inside the container simply cannot execute it.

And here methodically it is important: if you see such symptoms, do not immediately dig into Spring configuration and fix application.yml. First check that you are actually running the image on the correct platform. It is like trying to fix a seat belt when there are no wheels on your car: the seat belt is not the issue, but it looks important.

6. Managing the platform: --platform

When you realize that platform is a real build parameter, a very natural question appears: “Can I tell Docker directly that I want linux/amd64 or linux/arm64?” Yes. That is exactly what the --platform flag is for. But you should use it like a scalpel, not like a hammer: a scalpel has a purpose, but if you start hammering nails with it, it will be strange and possibly bloody.

The most illustrative way is to specify the platform directly in FROM. Then you say: “I want this stage to be exactly this platform.” For example:

# Explicitly fix the platform of the base image (useful for diagnostics and CI reproduction)
FROM --platform=linux/amd64 some-java-runtime:25
WORKDIR /app

# Copy the jar (in the example — an already built artifact)
COPY app.jar app.jar

# Start the application
ENTRYPOINT ["java", "-jar", "app.jar"]

This can be useful as a diagnostic step to reproduce CI behavior on amd64, even if you are locally on arm64. But it is important to remember: if your machine is arm64, then amd64 build/run may go through emulation, and it will be slower. So --platform can solve the problem “I want to build the same thing”, but it adds the cost of “it will not be as fast”.

A slightly more “operational” option is to set the platform at the build-command level. In modern Docker this is often done through buildx:

# Explicitly build the image for the specified platform (for example, to check ARM build in CI or locally)
docker buildx build --platform=linux/arm64 -t catalog-service .

The idea is simple: you build the image as linux/arm64. In real life, this helps when you want to check that your project also builds on ARM (or vice versa — to reproduce an amd64 environment). We are not going into multi-platform publishing yet, that is, we are not doing “one command that immediately builds both amd64 and arm64 and publishes them”. Right now, it is important for us to understand the selection mechanism itself.

There is also docker run --platform=..., which allows you to explicitly run an image for the selected platform, if possible. At the learning level, this is useful to distinguish “the wrong platform image” from “the image is broken in general” or “the application really does not start”.

7. Quick check: environment, image, container

Platform is one of those cases where it is better to look at the facts once than argue with feelings five times. The good news is that the check is done with short commands and a small piece of Java code, without complicated magic.

To start with, you can see what Docker considers the architecture of your environment. Often this can be seen in docker info (the output differs in different versions), and for images and containers there is inspect. For example, to see the architecture of an image, you can use template output:

# Show the OS/architecture for which this image is "built"
docker image inspect catalog-service \
  --format '{{.Os}}/{{.Architecture}}'
# example output: linux/arm64

Similarly, you can inspect a container:

# Show the platform for a specific container (important if you start it with --platform)
docker container inspect catalog-service-container \
  --format '{{.Platform}}'
# example output (may depend on version): linux/arm64

If you prefer the Java level, then inside the container (or locally) you can print the JVM architecture, as we did above. This is especially useful when you are trying to understand: “Do I really have ARM inside the container, or am I accidentally in emulation now?” In that case os.arch will give a clear signal:

public class ArchOnly {
    public static void main(String[] args) {
        // os.arch is a quick way to understand which architecture the JVM is really running on
        String arch = System.getProperty("os.arch");

        // aarch64 usually corresponds to linux/arm64, amd64 — to linux/amd64
        System.out.println("Detected arch = " + arch); // for example: Detected arch = aarch64
    }
}

The point of all these checks is not to become an expert in inspect-JSON, but to stop treating symptoms from the wrong layer. When you see that the image is linux/amd64, while the environment is linux/arm64, further diagnosis becomes calmer: now you know that this is not “random”, but a perfectly explainable mismatch.

8. Typical mistakes when working with amd64 and arm64 platforms

Mistake #1: confusing the host OS and the image platform.
Usually this sounds like: “But I have Windows, why is the container Linux?” or “I have macOS, so the container is macOS.” In our course we almost always work with Linux containers, and that is normal even on Windows/macOS. The platform in Docker is linux/…, and the architecture is what differs.

Mistake #2: ignoring Docker warnings and going off to fix Spring.
A very common scenario: the container does not start, the student opens application.yml, starts changing the port, profiles, logging — and only later notices that Docker was already saying at startup that the platforms do not match. If you see messages like no matching manifest or platform does not match, this almost always needs to be sorted out before you touch the application code.

Mistake #3: forcing --platform everywhere “just in case”.
The --platform flag is useful, but if you put it everywhere and always, you get a new type of problem: unexpected emulation, slow builds, and strange behavior differences. A good team-level strategy is either to live on the native platform (arm64 on ARM, amd64 on x86_64), or to explicitly use --platform as a diagnostic/reproduction tool, not as a permanent workaround.

Mistake #4: changing everything at once: vendor, tag/digest, and platform.
If you change the base image, the tag version, and the platform at the same time, you get “combo chaos”: it is unclear what exactly affected the result. The engineering approach here is boring, but it works: change one parameter, check, record the observation, move on.

Mistake #5: expecting that “if the jar is cross-platform, then the image is too”.
The jar really is portable between architectures, but only if there is a JVM for the required architecture. The base image contains the JVM as a native binary. Therefore containerization is not just “copy the jar”, but “bring a correct runtime environment”, and that includes the platform.

1
Zadanie
Docker for Spring, poziom 7, lekcja 2
Niedostępne
Raport platform dla obrazu bazowego
Raport platform dla obrazu bazowego
1
Zadanie
Docker for Spring, poziom 7, lekcja 2
Niedostępne
Budowanie pod natywną platformę hosta
Budowanie pod natywną platformę hosta
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION