
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Shipping Elixir applications consistently across environments requires reproducible artifacts, and the most reliable way to achieve this is to Dockerize a Phoenix Application using modern multi-stage builds. Many teams struggle with bloated images, missing environment variables at runtime, or broken asset pipelines because they treat Phoenix like a standard Node.js or Python app. This guide provides a battle-tested configuration that separates build-time dependencies from the final runtime image, ensuring your container is secure, lightweight, and ready for Kubernetes or bare-metal deployment.
Why Should You Dockerize a Phoenix Application Using Multi-Stage Builds?
When you Dockerize a Phoenix Application without multi-stage builds, you inevitably ship gigabytes of unnecessary tooling: compilers, header files, npm caches, and Mix development dependencies. In my experience auditing infrastructure for SOC 2 compliance, these oversized images are not just a storage cost issue; they expand the attack surface and slow down autoscaling events during traffic spikes. A single-stage build might produce a 1.2GB image, whereas a properly optimized multi-stage build for the same application typically lands between 150MB and 250MB.
The separation of concerns also matters for CI/CD pipeline efficiency. When your build environment is distinct from your runtime, you can cache dependency layers independently. If you change application code but not mix.exs, Docker reuses the compiled dependency layer. This cuts build times from minutes to seconds on subsequent runs. For teams in Nepal or regions with variable internet connectivity, minimizing redundant downloads through effective layer caching is often more impactful than raw bandwidth upgrades.
How Do You Write a Production-Ready Dockerfile for Phoenix?
A common mistake when engineers first Dockerize a Phoenix Application is placing the COPY . . command before installing dependencies. This invalidates the Docker cache on every commit. The correct sequence prioritizes stability: install system packages, fetch Hex deps, compile them, copy application source, build assets, and finally generate the release. Below is a verified Dockerfile structure for Phoenix 1.7+ with esbuild and Tailwind CSS.
# --- BUILDER STAGE ---
FROM hexpm/elixir:1.17.2-erlang-27.0.1-debian-bookworm-20240701 AS builder
ENV MIX_ENV=prod \
ERL_AFLAGS="-proto_dist inet_tcp"
RUN apt-get update -y && \
apt-get install -y --no-install-recommends git curl ca-certificates && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install Hex and Rebar first (rarely changes)
RUN mix local.hex --force && \
mix local.rebar --force
# Copy dependency manifests and install (cached unless mix.exs changes)
COPY mix.exs mix.lock ./
RUN mix deps.get --only $MIX_ENV && \
mix deps.compile
# Copy config and compile app
COPY config config
COPY lib lib
RUN mix compile
# Build frontend assets
COPY assets assets
COPY priv priv
RUN mix assets.deploy
# Generate the self-contained release
COPY rel rel
RUN mix release
# --- RUNTIME STAGE ---
FROM debian:bookworm-20240701-slim AS runtime
RUN apt-get update -y && \
apt-get install -y --no-install-recommends libstdc++6 openssl libncurses5 locales ca-certificates && \
sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen && \
rm -rf /var/lib/apt/lists/*
ENV LANG=en_US.UTF-8 \
LANGUAGE=en_US:en \
LC_ALL=en_US.UTF-8
WORKDIR /app
# Create non-root user for security compliance
RUN chown nobody:nobody /app
USER nobody:nobody
# Copy only the built release from builder
COPY --from=builder --chown=nobody:nobody /app/_build/$MIX_ENV/rel/my_app ./
ENV PHX_HOST=localhost \
PORT=4000 \
ECTO_IPV6=true
EXPOSE 4000
CMD ["bin/my_app", "start"] This configuration assumes you have defined a release in mix.exs under the releases function. Without an explicit release definition, mix release will fail. Note the use of nobody:nobody ownership; running containers as root violates basic security standards and will flag failures in automated compliance scans. If your application needs to write to disk, create a specific directory with proper permissions rather than relaxing the entire workdir.
What Are the Critical Runtime Configurations for Containerized Phoenix?
Once you successfully Dockerize a Phoenix Application, the next failure point is runtime configuration. Phoenix uses config/runtime.exs for environment-specific settings that must be evaluated at boot time, not compile time. Hardcoding values in config/prod.exs means they get baked into the release and cannot be overridden by container environment variables.
- Database URLs: Always parse
DATABASE_URLviaSystem.fetch_env!in runtime.exs. Never embed credentials in the release artifact. - Secret Key Base: Use
System.fetch_env!("SECRET_KEY_BASE"). Generate one withmix phx.gen.secretand inject it via your orchestrator's secret management system. - Host and Port: Configure the endpoint URL dynamically based on
PHX_HOSTandPORTenvironment variables to support reverse proxies and load balancers. - IPv6 Support: Set
ECTO_IPV6=trueif deploying to modern cloud VPCs or Kubernetes clusters where IPv6 is preferred or required.
For teams managing sensitive configurations, integrating with tools discussed in Kubernetes secrets management ensures credentials never touch the image layer. The release binary reads these at startup, keeping the artifact itself completely generic and safe to store in any registry.
How Does Docker Compare to Traditional Deployment for Phoenix?
Before you commit to containerization, understand the trade-offs. While I strongly recommend you Dockerize a Phoenix Application for any team larger than one person, traditional deployments still have valid niches. The table below reflects real-world operational differences observed across multiple production environments in 2026.
| Criteria | Docker / Containers | Traditional (Bare Metal / VM) |
|---|---|---|
| Reproducibility | Identical artifact across dev, staging, prod | Drift risk from manual package updates |
| Startup Time | Seconds (pre-compiled release) | Minutes (full system boot + service init) |
| Dependency Isolation | Complete; no host conflicts | Shared system libs; version collisions possible |
| Resource Overhead | Low (~5-10MB base overhead) | Higher (full OS + systemd services) |
| Debugging Complexity | Requires container tooling knowledge | Direct SSH access; familiar Linux tools |
| Compliance Auditing | Immutable artifacts simplify evidence collection | Requires continuous configuration monitoring |
For solo developers managing a single VPS, a direct deployment via SSH and systemd might feel simpler initially. However, the moment you need to replicate that environment for staging or onboard another developer, the lack of a defined artifact becomes technical debt. Containerization front-loads complexity to gain long-term operational predictability.
What Security Hardening Steps Are Essential for Phoenix Containers?
Security cannot be an afterthought when you Dockerize a Phoenix Application. The BEAM VM is remarkably stable, but the surrounding container environment introduces new vectors. Start with base image hygiene: pin exact versions of both the Elixir builder and the runtime OS. Floating tags like latest or even 1.17 can silently introduce breaking changes or vulnerabilities during rebuilds.
Implement read-only filesystems where possible. Phoenix releases generally do not need write access to the application directory. If your app handles uploads, mount a dedicated volume at a specific path and configure the endpoint's static file serving accordingly. Scan every built image with tools like Trivy before pushing to your registry; automate this in your CI pipeline as described in container image scanning best practices.
Network segmentation matters equally. Your Phoenix container should not have unrestricted egress. Define network policies that allow outbound connections only to your database, cache, and external API endpoints. In Kubernetes, this means explicit NetworkPolicy resources. On Docker Compose, use custom networks with restricted access. These controls limit blast radius if an attacker compromises the application layer.
Next Steps After Containerizing Your Phoenix App
Successfully building the image is just the beginning. Validate your setup locally with docker compose up before pushing to any registry. Test actual production-like conditions by setting MIX_ENV=prod and providing real environment variables through an .env file excluded from version control. Verify that migrations run correctly via docker exec or a dedicated migration job pattern, as running migrations automatically at container startup can cause race conditions in scaled deployments.
Monitor your containerized application with the same rigor as bare-metal deployments. The BEAM's introspection capabilities remain fully available inside Docker, so expose telemetry metrics via a /metrics endpoint and scrape them with Prometheus. If you need guidance on what signals matter most, review the four golden signals to establish baseline SLOs before traffic hits production.
Containerizing Phoenix removes an entire class of deployment inconsistencies, but only if done with discipline. Prioritize reproducibility, enforce security boundaries early, and treat your Dockerfile as production code subject to review and testing. If your team needs help establishing compliant, scalable Elixir infrastructure, reach out to discuss your specific requirements.