Go back to articles

The Art of Deployment: A Tale of ARM, AMD, and GLIBC

What happens when your Apple Silicon Mac builds Docker images for an AMD64 server? A journey through cross-compilation chaos, JIT shenanigans, and the lessons learned deploying Phoenix to production.

The Art of Deployment: A Tale of ARM, AMD, and GLIBC

February 5, 2026 — Today I learned that the distance between a working local build and a running production container is measured not in lines of code, but in cryptic error messages and Stack Overflow deep-dives.

The Setup

  • Source: M2 MacBook (ARM64 / Apple Silicon)
  • Target: Hetzner Cloud server (AMD64 / x86_64)
  • Stack: Docker multi-stage build → Google Cloud Artifact Registry → Docker Compose

Act I: The Platform Mismatch

exec /app/bin/spotlight: exec format error

My Mac built an ARM64 image. My x86_64 server refused it.

Fix: --platform linux/amd64 in the Docker build command.

Act II: The JIT Betrayal

** (ArgumentError) could not call Module.put_attribute/3
because the module Spotlight.MixProject is already compiled

QEMU emulation + Erlang JIT = chaos. José Valim confirmed it in a GitHub issue.

Fix: ENV ERL_AFLAGS="+JMsingle true" in the Dockerfile.

Act III: The GLIBC Surprise

version 'GLIBC_2.34' not found

Builder stage used Debian Bookworm (GLIBC 2.36); runtime used Bullseye (GLIBC 2.31).

Fix: Align both stages to debian:bookworm-slim.

Act IV: The Persistent Storage Puzzle

Uploads were 404ing. In a Phoenix release, relative paths don’t work. And files inside containers get wiped on every deploy.

Fix: UPLOADS_PATH env var + Docker volume mount + custom plug for serving uploads.

Lessons Learned

  1. Cross-platform Docker builds are tricky on Apple Silicon
  2. Always match builder and runtime base images
  3. Use Application.app_dir/2 in releases, never relative paths
  4. Plan for persistent storage from day one
  5. Caddy is magical for automatic HTTPS

The app is live. The journey continues.