Dockerize a Phoenix Application

Khimananda Oli 9 min read Programming and Languages
Dockerize a Phoenix Application

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.

Builder Stage (Erlang/Elixir)Install Hex, Rebar, System DepsCompile Dependencies (Cached Layer)Build Assets (esbuild/tailwind)Generate MIX_ENV=prod ReleaseCOPY ONLY RELEASERuntime Stage (Alpine/Slim)Minimal OS + Runtime Libs OnlyNon-root User & PermissionsEntrypoint: bin/phoenix startFinal Size: ~200MB
Multi-stage build flow isolating heavy compilation from the lean production runtime when you Dockerize a Phoenix Application

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_URL via System.fetch_env! in runtime.exs. Never embed credentials in the release artifact.
  • Secret Key Base: Use System.fetch_env!("SECRET_KEY_BASE"). Generate one with mix phx.gen.secret and inject it via your orchestrator's secret management system.
  • Host and Port: Configure the endpoint URL dynamically based on PHX_HOST and PORT environment variables to support reverse proxies and load balancers.
  • IPv6 Support: Set ECTO_IPV6=true if 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.

Container StartENV VariablesMounted SecretsConfig ProvidersBEAM Release BootLoad sys.configExecute runtime.exsStart ApplicationsRunning Phoenix AppEndpoint Listening on $PORTRepo Connected to $DATABASE_URLTelemetry & Health Checks Active
Runtime configuration loading sequence ensuring environment variables override compile-time defaults in containerized Phoenix

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.

CriteriaDocker / ContainersTraditional (Bare Metal / VM)
ReproducibilityIdentical artifact across dev, staging, prodDrift risk from manual package updates
Startup TimeSeconds (pre-compiled release)Minutes (full system boot + service init)
Dependency IsolationComplete; no host conflictsShared system libs; version collisions possible
Resource OverheadLow (~5-10MB base overhead)Higher (full OS + systemd services)
Debugging ComplexityRequires container tooling knowledgeDirect SSH access; familiar Linux tools
Compliance AuditingImmutable artifacts simplify evidence collectionRequires 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.

Defense-in-Depth for Phoenix ContainersImage Layer• Pinned Base Versions• Non-root User (nobody)• No Dev Tools in Runtime• Automated Vuln ScanningRuntime Layer• Read-only Root FS• Dropped Linux Capabilities• Resource Limits (CPU/RAM)• Secrets via Env/VaultNetwork Layer• Restricted Egress Policies• TLS Termination at Ingress• Internal Service Mesh mTLS• Rate Limiting & WAFObservability & Compliance LayerStructured Logging (JSON)OpenTelemetry TracingAudit Trail AutomationHealth Check EndpointsSBOM GenerationSOC 2 Evidence Mapping
Layered security model covering image, runtime, network, and observability when you Dockerize a Phoenix Application

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.

Frequently Asked Questions

Use the official hexpm/elixir-alpine image for minimal size and security. It includes Erlang, Elixir, and musl libc, reducing attack surface and final image size significantly compared to Debian-based alternatives for production Phoenix deployments.

Never bake secrets into images. Inject them at runtime via environment variables or mounted secret files. Use Docker secrets, Kubernetes secrets, or external vaults like HashiCorp Vault to keep credentials out of version control and image layers.

Slow builds usually result from poor layer caching. Order your Dockerfile to install dependencies before copying application code. Separate mix deps.get and compilation steps to leverage cache invalidation only when dependency files actually change.

Yes, if using server-side rendering only. Skip Node entirely by removing esbuild or Tailwind CSS build steps. Precompile assets locally or in CI, then copy static outputs into the final stage to eliminate runtime Node dependencies completely.

Port 4000.

Execute mix ecto.migrate as an entrypoint command or init container before starting the web server. Ensure the database is reachable and credentials are injected via environment variables. Avoid running migrations during image build time to prevent stateful artifacts.

Multi-stage builds are strongly recommended. They separate build-time dependencies like compilers and Node tools from the slim runtime image, producing smaller, more secure containers with only production artifacts and necessary system libraries included.

Use mix release to generate self-contained OTP releases. Configure rel/config.exs or config/runtime.exs for environment-specific settings. The release binary runs without Mix or Elixir installed, enabling minimal runtime images and faster cold starts.

Use /api/health or a custom Plug returning 200 OK. Configure Docker HEALTHCHECK or orchestrator probes to hit this lightweight endpoint. Avoid checking heavy database queries in health checks to prevent false negatives during transient load spikes.

Under 100MB typically.

Distillery is deprecated as of 2026. Native Elixir releases have been stable since Elixir 1.9 and offer better integration, simpler configuration, and active maintenance. Migrate existing Distillery configs to native releases for long-term support and compatibility.

Terminate SSL at the reverse proxy or load balancer, not inside the container. Run Phoenix on HTTP internally and let Nginx, Caddy, or cloud LBs manage certificates. This simplifies container config and enables efficient TLS offloading.

Missing runtime configuration is the most common cause. Ensure PHX_HOST, PHX_PORT, SECRET_KEY_BASE, and DATABASE_URL are set. Verify config/runtime.exs reads these correctly and that the release was built with MIX_ENV=prod.

Enable IPv6 only if your infrastructure requires it. Phoenix binds to IPv4 by default. Set inet6_backend: true in endpoint config and ensure Docker networking supports dual-stack. Test thoroughly, as some libraries still lack full IPv6 parity.

Inspect failed layers using docker build --progress=plain. Check mix.lock consistency, verify Elixir/Erlang version compatibility, and validate that all system dependencies like libstdc++ are installed in the correct build stage before compilation begins.