Java Development on an Apple Sillicon (M1, M2, M3, ARM64)

Last Updated:  March 18, 2024 | Published: March 25, 2022

It's been almost a year since I've bought the MacBook Pro M1 (arm64 processor) for my daily Java development as a freelance consultant. I had my first contact with the Apple M1 when one of my course students raised an issue that the build doesn't pass on Apple's new flagship laptop. I was first shocked to encounter hardware incompatibilities in 2021. To solve those problems (not my main intent, but I told myself so) and experience if that processor is really that fast, I decided to buy the MacBook Pro.

This article will share my initial pitfalls when working with the Apple M1 and a collection of valuable tricks and workarounds for developing and testing Java applications.

Note: Throughout this article, both arm64 or aarch64 refer to the Apple M1 chip. I'll refer to the traditional Intel/AMD processors as x64  (you may find the following synonyms: x86_64, amd64).

Installing Java Development Tools

The first thing we install as Java developers on a new machine is a JDK. While searching for a arm64 compatible JDK back in 2021, the Azul Zulu JDK build popped up first.

There's no difference in the installation process of a JDK compared to an x64 Mac. Once we've downloaded and installed a compatible JDK build, we have the baseline for our Java development for our Apple M1:

Apart from the Azul Zulu JDK, many other JDK vendors have followed and now offer an arm64 build:

The brew formula for openjdk is also installing a compatible Apple M1 JDK build. For managing different Java versions, sdkman or jEnv is an excellent option.

For those lucky developers that have the chance to work and experiment with the GraalVM, the support for Apple M1 has also landed.

With Apple's Rosetta 2 emulation, we can also try to work with an x64 JDK build. However, we may run into issues and/or won't get the expected performance.

What's next is Docker. Installing a recent Docker for Mac version comes with Apple M1 support. In the early days, there were some minor issues. But having worked with it for almost a year now, many have been continuously fixed.

All other development tools that we use on a daily basis either provide an arm64 build or the emulated x64 version works fine: IntelliJ IDEA, Visual Studio Code, Eclipse, Slack, Notion, Docker for Mac, Spotify, Firefox, Microsoft Teams, Postman.

Rule one: Whenever there's a native arm64 build of any development tool for the M1, go for it. Only try to get the x64 build running if there's no arm64 build available (yet).

To further verify the compatibility and optimization for various other tools for Apple's M1, checkout out isapplesiliconready.

Integration Tests with Docker and Testcontainers

When it comes to running our integration tests that use Testcontainers, the first thing we stumble over with our M1 machine are potential test failures because of missing arm64 platform support.

Docker images are built for specific processor architectures. We can identify the available platforms by looking at the OS/Arch column on Docker Hub for a particular Docker image:

Docker Hub Image Platform Example

The example above is a screenshot of the official Keycloak Docker image. It only comes with support for x64 machines.

If we try to run this Docker image on our arm64 Apple M1 machine, we get the following warning:

While we get past this warning and Docker tries to run an image for a different platform, it internally uses emulation to try to get the x64 container up and running. However, this emulation for x64 support for arm64 is a best effort.

Images for a different platform may or may not work on our Apple M1. There's no guarantee that the container will start and function as expected.

The Keycloak image, for example, unfortunately, crashes during the bootstrap phase:

When running our test suite for the first time on an Apple M1, we have to identify the Docker containers that fail to start.

If the image itself doesn't provide arm64 support, we can look for an alternative. For the most commonly used Docker images, these are potential alternatives:

  • PostgreSQL: The standard images come with arm64 support, no changes required
  • Webdriver images: Standalone arm64 support comes with seleniarm images
  • Keycloak: arm64 build for the jboss variant starting with version 17.0.1. There are also community multi-architecture builds (richardjkendall or mihaibob) available
  • LocalStack: Starting with version 0.13, LocalStack publishes multi-architecture Docker images
  • MySQL: Starting with version 8, the oracle version provides an arm64 build. Another alternative is the MariaDB image
  • MongoDB: The standard images come with arm64 support, no changes required
  • Kafka: No arm64 build for the bitnami image, but the wurstmeister build supports arm64

Dynamically Replace Docker Images For ARM64 Support

Whenever we find ourselves looking for an alternative to the default Testcontainers Docker image, we need to override the default image. If we're working on a project on our own or know that the entire team is using an arm64 processor, we can hardcode the arm64 compatible Docker image. Otherwise, we can make the image substitution more dynamic and only replace the Docker image when running on the specific architecture.

Let's use the Selenium Webdriver container as an example for a dynamic override:

Based on the system property os.arch we decided which image to use for our BrowserWebDriverContainer.

As an alternative to this workaround, we can use Testcontainers Cloud. With Testcontainers Cloud, we run the backing containers for our integration tests in the cloud. There's no change required for our test. The containers will just run on someone else machine. Since its private beta, I had the chance to demo this amazing tool and am convinced that this will drive the productivity (e.g., faster builds) for testing with Testcontainers even further.

As a last resort, we can also disable specific tests if they won't run at all on our M1 processor. Based on the os.arch Java system property we can detect the processor architecture and  use a JUnit Jupiter annotation to disable tests for our M1:

We can use this annotation on top of a test class or for a particular test. While this is not an optimal solution, at least it helps the Apple M1 developers on our team to see a green build.

These test disabling annotations should be temporary. We must check if there's a solution available (e.g. a compatible Docker image) from time to time. The final build on our CI pipeline must ensure to run all tests (usually a x64 machine).

Docker Client JNA Failure with Testcontainers

Another pitfall we may fall into before we can actually run our Java integration test with Testcontainers is the following error:

The underlying issue is a JNA (Java Native Access) dependency incompatibility with Apple's M1 chip. Testcontainers transitively depends on this library. Starting with JNA 5.7.0, this issue is resolved.

To fix this problem for our projects, we can either upgrade our Testcontainers version (preferred) or manually override the JNA version. Starting with Testcontainers 1.15.3, Tesctonainers transitively depends on a JNA version (5.7.0) that is working for the Apple M1.

Suppose we still see the same test failure even after bumping the Testcontainers version. In that case, the chances are high that another dependency also transitively depends on JNA and overrides the version.

In this situation, we can either exclude the JNA dependency from any other dependency that includes it or use the dependencyManagement section to force the resolution of a specific version of it:

To locate dependencies that transitively include JNA, run mvn dependency:tree and then search for JNA in the result.

Local Development Environment with Docker Compose

Many projects use a docker-compose.yml for running the required infrastructure when locally developing or testing a Java application.

Depending on the amount and variety of required infrastructure (e.g., messaging queues, databases, caches, etc.), tweaking the docker-compose.yml to work for both x64 and arm64 may not be an option.

The same Docker image arm64 availability issue as for testing applies here. Sometimes the community Docker builds only support one platform and not both.

As a possible workaround, we can introduce a second docker-compose-arm64.yml file to support team members working with an M1 (or any other arm64 machine).

I'm using this concept for the Testing Spring Boot Application Masterclass, where the Amazon SQS and Keycloak image needs to be replaced with an arm64 compatible image.

This adds some small maintenance effort as the team has to keep the two Docker compose files in sync. However, as soon as the availability and adoption of arm64 images is improving, the dedicated arm64 Docker compose file can be deleted.

We can then pass the underlying compose file to our docker-compose start command:

The downside of this additional compose file is the duplicated maintenance. We have to ensure both our composes files (x64 and arm64) are in sync. Similar to the workarounds for making our Testcontainers integration tests pass, this should also be a temporary solution that we reassess from time to time.

Java Development: Spring Boot Netty Warning on Startup

Upon starting a Spring Boot application on an Apple M1, we may encounter an ERROR log from Netty with a big stack trace during the bootstrap phase:

While our application should still function as expected, this log is noisy and may confuse developers. As long as we don't run our Spring Boot application on an Apple M1 in production, it's also less critical if we don't fix it.

For those that want to fix this startup error message, make sure to use Netty > 4.1.68.Final and add the following dependency to our project:

After making this change, the Spring Boot application should start again with a clean bootstrap log.

Building Multiplatform Docker Images on an Apple M1

When building Docker images, the resulted image will only work on the platform we're working on. Any output of docker build -t myimage . on an Apple M1 will only work on another arm64 machine.

While this is completely obvious, it took me a 2 hour debug session to be aware of it. I was building a new feature for a Spring Boot application (Stratospheric) and wanted to push the resulting Docker image directly to the ECR (Elastic Container Registry) from my local machine. While we have a running CI/CD in place, I wanted to short-circuit this process and save time. After pushing the image to the container registry, I encountered weird error logs, and the ECS (Elastic Container Service) task was unable to start.

That's because the underlying EC2 instance is an x64 machine. Even the obvious things sometimes take it the hard way to understand them.

Whenever we find ourselves in a similar situation, where we want to create a Docker image for a different processor architecture, we have two options:

  • Build the target Docker image on a CI/CD server running on an x64 machine (or any architecture)
  • Use Docker's buildx, a CLI plugin, to get access to all features of the Moby BuildKit  (e.g., multiplatform images)

We won't go further into the first option, as a CI/CD workflow (e.g., GitHub actions) should be already in place for (hopefully) most projects.

The second option is more interesting to us as this will allow us to build locally arm64 and x64 compatible Docker images.

As a prerequisite for this to work, we have to ensure our Docker base image (FROM part of the Dockerfile) provides a variant for all architectures we want to build the final Docker image for. We can verify this by visiting Docker Hub and the tags section of our Docker base image.

Taking eclipse-temurin (OpenJDK build of the Adoptium project, former AdoptOpenJDK) as a Docker base image example, we can see that this image supports various architectures:

Eclipse Temurin Docker Image Multi Platform Example
Once our Dockerfile use a base image with support for multiple architectures, we can use the Docker buildx command to build the image for multiple platforms:

In the example above, we build both an arm64 and x64 image for our Java Spring Boot application:

We can also directly push the result into a container registry with:

Using this technique, we now can build new Docker images locally and deploy them to run on x64 machines in the cloud. This gives us great flexibility for the Java development on our Apple M1.

Building ARM64 Images on GitHub Actions

If we're in the lucky position to develop and publish our own Docker images, we may want to adopt arm64 builds to increase the overall adoption rate.

We're using a custom ActiveMQ Docker image for the Stratospheric project for testing and local development purposes. We were all using a classic Intel processor during the inception phase of this project and when creating this Docker image. Things changed with my MacBook Pro purchase, and therefore, we started looking into multiplatform support.

The ActiveMQ message broker runs on the JVM. Hence any ingredient of our Dockerfile must also be available for an arm64 architecture. For our example, we were using fabric8/java-alpine-openjdk11-jre as the base image. Unfortunately, this image only supports linux/amd64 (i.e. x64) and hence we had to replace it with a JRE base image that supports all platforms we want to build our image for.

We went for eclipse-temurin:11-jre-focal as the base Docker image as it supports multiple architectures:

Given the multiplatform support from the base image, we can now locally build an arm64 variant of the Docker image (using buildx of the last section) and push it to a Docker registry.

As an alternative, we can automate this process on GitHub Actions and build a multiplatform image on each commit. The Docker team provides ready-to-use actions to build and push a multiplatform Docker image.

For our custom ActiveMQ image, we adjusted our GitHub Actions as the following:

These five steps automate everything from checking out the source code, building a multiplatform image for arm64 and x64 and then finally pushing this image to Docker Hub.

What's left for us is to configure a username and access token for Docker Hub. We store them as GitHub secrets for our repository.

Given the GitHub automation, we deploy new versions of our Docker image to DockerHub on every push to our main branch:

Multiple Platform Docker Build Example using GitHub Actions

 

Summary: Java Development on an Apple M1

Having worked with an Apple M1 primarily for Java development for almost a year now, I don't regret the choice. It took me some time to resolve the initial hurdles and get everything running. The mentioned strategies in this article solved all my testing and developing concerns for Java applications on the M1.

The good news is that with every passing day, more tools adopt arm64 as Apple continues to release new flagship products running on arm64 processors. It's only a matter of time until the last Java development tool works perfectly fine on an Apple M1.

The performance of the laptop (Apple MacBook Pro M1 2021) is excellent, and a huge productivity boost as my Java builds, and tests run faster compared to an x64 Mac. I still keep my old Ubuntu desktop PC around as there are occasionally tasks where I need a good old x64 machine.

Let me know about your tips & tricks for Java development on an Apple M1 in the comments.

Joyful testing,

Philip

>