
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Default Elixir Docker images often exceed 1GB because they bundle the full Erlang/OTP toolchain, build dependencies, and source code that your production runtime never needs. To effectively shrink Elixir Docker images, you must separate the build environment from the runtime environment using multi-stage builds and minimal base images like Alpine or slim Debian variants. This guide walks through the exact Dockerfile patterns, release configurations, and dependency pruning techniques I use to get production Elixir containers under 100MB without breaking OTP functionality.
How do you configure a multi-stage Dockerfile to shrink Elixir Docker images?
The single most impactful technique to reduce Docker image size with multi-stage builds is discarding the entire build toolchain after compilation. Elixir requires Erlang, C compilers, and header files during dependency compilation (especially for NIFs like bcrypt or crypto libraries), but none of these are needed once the BEAM bytecode is generated. A common mistake is using a single-stage Dockerfile where the final image retains gigabytes of build artifacts.
Defining the builder stage correctly
Your builder stage should use the official hexpm/elixir image matching your exact OTP and Elixir versions. Pinning versions prevents silent breakage when upstream updates. Always set MIX_ENV=prod before fetching dependencies to avoid pulling test or dev-only packages.
# Builder Stage
FROM hexpm/elixir:1.17.3-erlang-27.1-debian-bookworm-20240904 AS builder
ENV MIX_ENV=prod
WORKDIR /app
# Install build dependencies for NIFs
RUN apt-get update -y && \
apt-get install -y build-essential git && \
rm -rf /var/lib/apt/lists/*
# Cache dependency layer
COPY mix.exs mix.lock ./
RUN mix local.hex --force && \
mix local.rebar --force && \
mix deps.get --only prod && \
mix deps.compile
# Copy source and build release
COPY . .
RUN mix compile && \
mix release Constructing the minimal runtime stage
The runtime stage uses hexpm/elixir:1.17.3-erlang-27.1-alpine-3.20.3 or a plain alpine:3.20 if you copy the Erlang runtime manually. Alpine reduces the base OS footprint from ~120MB (Debian slim) to ~8MB. However, Alpine uses musl libc instead of glibc, which can break some NIFs. Test thoroughly before committing to Alpine for production workloads handling cryptography or database drivers.
# Runtime Stage
FROM hexpm/elixir:1.17.3-erlang-27.1-alpine-3.20.3 AS runtime
RUN apk add --no-cache libstdc++ openssl ncurses-libs
WORKDIR /app
COPY --from=builder /app/_build/prod/rel/my_app ./
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
ENV PHX_SERVER=true
EXPOSE 4000
CMD ["bin/my_app", "start"] This pattern consistently produces images between 65MB and 85MB for typical Phoenix applications. If your app has heavy native dependencies, consider debian:bookworm-slim as a safer middle ground at ~110MB final size.
What runtime dependencies does an Elixir release actually need in production?
A frequent cause of bloated images is copying unnecessary system libraries. An Elixir release is self-contained regarding Erlang/OTP and Elixir stdlib, but it still depends on specific shared libraries at the OS level. Understanding this boundary is critical when you shrink Elixir Docker images aggressively.
- libstdc++: Required by the BEAM VM itself and most NIFs. Missing this causes immediate segfaults on startup.
- openssl / libssl: Needed for TLS connections (database, HTTP clients, certificate verification). Alpine splits this into
opensslandlibcrypto. - ncurses-libs: Required for the Erlang shell and observer tools. Even if you never open a shell in production, the release boot script may reference it.
- ca-certificates: Essential for outbound HTTPS. Without it, HTTPoison or Req calls to external APIs fail with certificate verification errors.
- tzdata: Needed only if your application handles timezones explicitly. Many apps can skip this and rely on UTC-only operations.
Everything else — gcc, make, python, git, hex, rebar, mix, erlang-dev headers — belongs exclusively in the builder stage. I have audited production images where teams accidentally copied /usr/local/lib/erlang from the builder, adding 300MB of redundant OTP libraries that the release already bundles internally.
How does Alpine Linux compare to Debian slim for Elixir production containers?
Choosing between Alpine and Debian slim is the most consequential decision when you shrink Elixir Docker images. Both work, but they optimize for different constraints. I maintain production fleets using both bases depending on the application's dependency profile.
| Criteria | Alpine 3.20 | Debian Bookworm Slim |
|---|---|---|
| Base image size | ~8 MB | ~80 MB |
| Typical final Elixir release | 65–85 MB | 100–130 MB |
| C library | musl libc | glibc |
| NIF compatibility | Problematic for some crypto/DB drivers | Near-universal compatibility |
| Security patch cadence | Fast, community-driven | Enterprise-backed, predictable |
| Debugging tooling availability | Limited (busybox-based) | Full apt ecosystem available |
| Best for | Pure Elixir apps, edge deployments | Apps with complex NIFs, compliance environments |
In practice, I default to Alpine for internal services and API gateways where I control all dependencies. For customer-facing fintech applications in Nepal that integrate with local payment gateways requiring specific OpenSSL versions or legacy C libraries, I use Debian slim to avoid musl-related debugging sessions at 2 AM. The 40MB size difference is irrelevant compared to the operational risk of subtle runtime failures.
Which Mix release configuration options reduce final image size?
The Dockerfile alone does not determine image size. Your mix.exs release configuration controls what gets packaged into the tarball that ultimately lands in the runtime container. Misconfigured releases include documentation, source maps, test fixtures, and development tooling that bloat the artifact before Docker even sees it.
- Disable included_executables for unused tools: Set
include_executables_for: [:unix]if you never run Windows. This removes .bat and .ps1 wrappers. - Strip debug information: Add
strip_beams: truein your release config. This removes debug chunks from .beam files, typically saving 15–25% of release size with zero runtime impact. - Exclude unnecessary applications: In
releases: [my_app: [applications: [runtime_tools: :optional]]], mark optional apps that aren't needed in production. Observer, debugger, and etop are common candidates. - Use overlays selectively: Overlays copy additional files into the release. Audit every overlay entry; teams frequently copy entire config directories including dev.exs and test.exs into production releases.
- Enable relup generation only when needed: Hot code upgrade support adds metadata and duplicate module versions. Disable
relup_pathunless you actively perform hot upgrades in production.
# config/releases.exs
import Config
config :my_app, MyApp.Release,
strip_beams: true,
include_executables_for: [:unix],
applications: [
runtime_tools: :optional,
observer: :none,
debugger: :none
] After applying these settings, rebuild and measure. Use du -sh _build/prod/rel/my_app before Dockerization to isolate release bloat from base image bloat. A well-configured Phoenix 1.7 release typically compresses to 25–35MB before being layered onto the Alpine base.
How do you verify and benchmark Elixir Docker image size improvements?
You cannot optimize what you do not measure. After implementing multi-stage builds and release tuning, validate the results systematically rather than trusting docker images output alone, which shows virtual size including cached layers.
Use dive (github.com/wagoodman/dive) to inspect individual layer sizes. This reveals whether you accidentally copied source code into the runtime stage or left package manager caches uncleaned. Integrate size checks into your CI pipeline alongside functional tests; I treat image size regression as a failing build condition for projects where deployment velocity matters. For teams managing Kubernetes resource limits, smaller images directly translate to faster pod scheduling and reduced node pressure during rolling updates.
Optimizing Elixir Containers for Production Reliability
When you successfully shrink Elixir Docker images using multi-stage builds, Alpine bases, and disciplined release configuration, you gain more than storage savings. Smaller images reduce attack surface, accelerate CI feedback loops, and lower egress costs in cloud environments. However, size must never compromise observability or debuggability. Ensure your structured logging pipeline works identically in slim containers, and retain enough tooling in the runtime stage to execute bin/my_app rpc commands for production diagnostics. If your team needs hands-on guidance optimizing Elixir deployments or auditing existing container pipelines, reach out to discuss your infrastructure.