The Setup
We had seven Java microservices running in Docker on an M-series Mac. They kept crashing. Services would start, run for a few minutes, then cascade-fail one after another. Tests failed with cryptic “Connection reset” errors. Restart storms became the norm.
We blamed Spring Boot for the slow startups. We blamed the application code for the connection resets. We blamed Apple Silicon for running hot.
It was none of those things. It was infrastructure the whole time — and it came down to one line in a Dockerfile.
The Root Cause: A Single Base Image
Here’s the line that was silently destroying our dev environment:
FROM alpine/java:21-jre
This image is amd64-only. There’s no ARM64 variant. On an Apple Silicon Mac, that means every single JVM instruction gets routed through QEMU emulation — a translation layer that converts x86 instructions to ARM on the fly. The overhead is enormous: 2–3x more CPU usage, inflated memory consumption, and significantly slower startup times.
Now multiply that by seven services, all competing for 8 GB of RAM inside Docker Desktop. Under emulation, memory pressure builds fast. The OOM killer starts picking off containers. Services restart, consume more resources during startup, trigger more kills. That’s the restart storm.
The Fix: One Line, Massive Impact
The solution was a drop-in base image replacement:
FROM eclipse-temurin:21-jre-alpine
Eclipse Temurin publishes multi-architecture images. On Apple Silicon, Docker automatically pulls the ARM64 variant and runs it natively — no emulation layer, no translation overhead. Same Alpine base, same JRE version, same fc-cache setup for font rendering. Functionally identical, architecturally correct.
The Results
The improvement was immediate and dramatic:
| Metric | Before (amd64 emulated) | After (ARM64 native) |
|---|---|---|
| Startup CPU per service | 179% | 68% |
| Idle CPU per service | 5% | 0.3% |
| Total memory (7 services) | 5.8 GB | 4.5 GB |
| Service restarts | Every few minutes | Zero |
The cluster went from barely functional to completely stable. No more restart storms, no more connection resets, no more mysterious slowness.
Three Lessons from Debugging This
1. Always check your base image architecture
Run this before trusting any Docker image on Apple Silicon:
docker inspect --format='{{.Architecture}}' image:tag
If it reports amd64 and you’re running on ARM hardware, you’re paying an emulation tax on every CPU instruction. For short-lived build steps this might be acceptable. For long-running services, it’s a performance disaster.
2. Docker Compose memory limits don’t apply retroactively
We had mem_limit configured in our docker-compose.yml, but docker stats showed the limits weren’t being enforced. The reason: those containers were created before the limits were added. Docker Compose doesn’t update resource constraints on existing containers — you need to force recreation:
docker compose up --force-recreate
This is an easy trap to fall into. You add limits, see them in your config, assume they’re active, and move on. Always verify with docker stats after changing resource constraints.
3. Health checks against authenticated endpoints fail silently
We had health checks configured like this:
wget -q -O /dev/null http://localhost:8000/api/ping
The problem: /api/ping requires authentication. The endpoint returns HTTP 401, and wget exits with code 1 for any non-200 response. Docker interprets exit code 1 as unhealthy. The service is running perfectly fine, but Docker thinks it’s down and starts restarting it — adding fuel to the restart storm.
The fix is to use a genuinely public endpoint:
wget -q -O /dev/null http://localhost:8000/actuator/health
Spring Boot’s Actuator health endpoint returns 200 without authentication by default, making it ideal for container health checks.
The Bigger Takeaway
The scariest part of this issue is that it “worked” for months. The symptoms were plausible enough to blame on other things — Spring Boot’s known startup overhead, occasional network hiccups in microservice communication, Apple Silicon being a relatively new platform for dev tooling. Each explanation was reasonable on its own. Together, they masked an infrastructure problem that had a one-line fix.
When your Docker containers on Apple Silicon feel sluggish, don’t start optimizing application code. Start by checking docker inspect on your base images. The emulation tax is real, it’s significant, and it’s completely avoidable.