The Art of Deployment: A Tale of ARM, AMD, and GLIBC
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
- Cross-platform Docker builds are tricky on Apple Silicon
- Always match builder and runtime base images
-
Use
Application.app_dir/2in releases, never relative paths - Plan for persistent storage from day one
- Caddy is magical for automatic HTTPS
The app is live. The journey continues.