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 --dockerThis 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
endGenerate 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
- 1Create a PostgreSQL database —
DATABASE_URLis injected automatically. - 2Connect your repo as a container app. The
phx.gen.release --dockerDockerfile is detected and built via rootless BuildKit. - 3Set
SECRET_KEY_BASE,PHX_SERVER=true, andPHX_HOST(your domain) in the dashboard. - 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.
| Concern | Setting |
|---|---|
| Packaging | mix release |
| Web server | PHX_SERVER=true / server: true |
| Config | config/runtime.exs |
| Migrations | bin/app eval "...Release.migrate" |
| Secret | SECRET_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; useruntime.exs. - Trying
mix ecto.migratein 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