Back to Blog
Tutorial11 min read2026-07-04

How to Deploy a Phoenix (Elixir) App to the Cloud

Deploy Phoenix to the cloud with Elixir releases, a multi-stage Docker build, runtime config, Ecto migrations, and the clustering and LiveView considerations that production demands.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Phoenix deployments are a bit different from the Node/Python world because of the BEAM virtual machine and Elixir's releases — self-contained packages that bundle your app, its dependencies, and a trimmed Erlang runtime. Done right, the result is rock-solid and self-healing. Here's the production path.

Elixir releases: the foundation

Since Elixir 1.9, mix release builds a self-contained release with its own boot scripts and a stripped ERTS (Erlang runtime). You don't need Elixir or Mix installed on the production host — just the release artifact. Phoenix generates the release config for you:

mix phx.gen.release --docker

This creates a Dockerfile, a rel/ directory, and a bin/migrate helper. It's the recommended starting point.

A multi-stage Dockerfile

The generated Dockerfile follows this shape — build with the full toolchain, run with a slim base:

FROM hexpm/elixir:1.17-erlang-27-debian-bookworm AS build
ENV MIX_ENV=prod
WORKDIR /app
RUN mix local.hex --force && mix local.rebar --force
COPY mix.exs mix.lock ./
RUN mix deps.get --only prod && mix deps.compile
COPY assets assets
COPY priv priv
COPY lib lib
RUN mix assets.deploy
RUN mix release

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y libstdc++6 openssl libncurses6 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build /app/_build/prod/rel/my_app ./
ENV PHX_SERVER=true
EXPOSE 4000
CMD ["bin/my_app", "start"]

mix assets.deploy runs esbuild/tailwind and digests static assets. PHX_SERVER=true tells the release to actually start the web server (a common gotcha — without it, the release boots but doesn't serve HTTP).

Runtime configuration

Releases evaluate config/runtime.exs when the app boots, not at build time — so this is where you read environment variables:

# config/runtime.exs
import Config

if config_env() == :prod do
  database_url = System.fetch_env!("DATABASE_URL")
  config :my_app, MyApp.Repo,
    url: database_url,
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")

  secret_key_base = System.fetch_env!("SECRET_KEY_BASE")
  config :my_app, MyAppWeb.Endpoint,
    http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")],
    secret_key_base: secret_key_base,
    server: true
end

Generate SECRET_KEY_BASE once with mix phx.gen.secret and store it as a secret. Binding the endpoint IP to {0,0,0,0} is required so the container accepts external traffic.

Ecto migrations

Releases don't include Mix, so you can't run mix ecto.migrate in production. The generated bin/migrate script handles it:

bin/my_app eval "MyApp.Release.migrate"

Run this as a deploy step before the new release serves traffic.

LiveView and clustering considerations

Phoenix LiveView holds stateful WebSocket connections. Two things matter in production:

  • Sticky sessions or shared PubSub — by default Phoenix.PubSub is process-local. For multi-node deployments, use a distributed PubSub adapter so LiveView updates propagate across nodes.
  • WebSocket support at the ingress — make sure your load balancer/ingress allows long-lived WebSocket upgrades. (Kong, used by PandaStack, supports this.)

For most single-region apps you can start with a single node and scale later; the BEAM handles enormous concurrency per node.

Deploying on PandaStack

  1. 1Create a PostgreSQL database — DATABASE_URL is injected automatically.
  2. 2Connect your repo as a container app. The phx.gen.release --docker Dockerfile is detected and built via rootless BuildKit.
  3. 3Set SECRET_KEY_BASE, PHX_SERVER=true, and PHX_HOST (your domain) in the dashboard.
  4. 4Add the bin/my_app eval "MyApp.Release.migrate" migration command and push.

Kong ingress handles the WebSocket upgrades LiveView needs, and you get automatic SSL, live logs, and rollbacks.

ConcernSetting
Packagingmix release
Web serverPHX_SERVER=true / server: true
Configconfig/runtime.exs
Migrationsbin/app eval "...Release.migrate"
SecretSECRET_KEY_BASE

Common pitfalls

  • Forgetting PHX_SERVER=true — the release boots but serves nothing.
  • Reading env vars in config/prod.exs — that runs at build time; use runtime.exs.
  • Trying mix ecto.migrate in prod — Mix isn't in the release; use the eval helper.
  • Ingress dropping WebSockets — breaks LiveView; confirm upgrade support.

References

  • Phoenix deployment with releases: https://hexdocs.pm/phoenix/releases.html
  • Mix release docs: https://hexdocs.pm/mix/Mix.Tasks.Release.html
  • Elixir runtime configuration: https://hexdocs.pm/elixir/config-and-releases.html
  • Phoenix LiveView deployment notes: https://hexdocs.pm/phoenix_live_view/deployments.html
  • Ecto migrations: https://hexdocs.pm/ecto_sql/Ecto.Migration.html

---

PandaStack's free tier includes container apps with WebSocket-capable Kong ingress and a managed PostgreSQL database — ideal for Phoenix and LiveView. Push your release and it runs: https://dashboard.pandastack.io

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also