Guides · Deployment
Phoenix hosting: deploy an Elixir release
What a Phoenix 1.8 release needs to start and accept LiveView connections, how to run migrations, and the Dockerfile Staxa builds.
By Jonathan Pitter ·
The short answer
Deploy Phoenix as a release (mix release), not with mix phx.server. A release is a self-contained directory with your compiled app and the Erlang runtime, and it needs four environment variables to start properly:
| Variable | Why |
|---|---|
PHX_SERVER=true | mix phx.new's runtime.exs starts the HTTP endpoint only when it's set. Without it, the release boots and listens on nothing. |
PHX_HOST | Your domain. Phoenix builds URLs from it, and LiveView only accepts WebSocket connections whose origin matches it. |
SECRET_KEY_BASE | Signs and encrypts cookies and sessions. The release refuses to start without it. |
DATABASE_URL | The Ecto connection, read by runtime.exs. |
PORT sets the listening port, defaulting to 4000. We tested everything below on Phoenix 1.8 with Elixir 1.17 and 1.19 and PostgreSQL 16: the release built and running as a non-root user, a LiveView WebSocket accepted for the right origin, and migrations run from the release.
PHX_SERVER and PHX_HOST
config/runtime.exs in a new project contains:
if System.get_env("PHX_SERVER") do
config :demo, DemoWeb.Endpoint, server: true
end
and, for production:
host = System.get_env("PHX_HOST") || "example.com"
config :demo, DemoWeb.Endpoint,
url: [host: host, port: 443, scheme: "https"],
The url host is what LiveView's origin check compares the browser's Origin header against. With PHX_HOST unset, it's example.com, and every LiveView page renders once and then fails to connect, reloading in a loop. We measured the WebSocket upgrade with the origin https://web-acme.staxa.app: 101 Switching Protocols with PHX_HOST=web-acme.staxa.app, 403 with PHX_HOST=0.0.0.0. Set it to the domain users visit, and if you serve the app on more than one, list them in check_origin in config/prod.exs.
url: [port: 443, scheme: "https"] is right behind a proxy that terminates TLS. The generated prod.exs also sets force_ssl: [rewrite_on: [:x_forwarded_proto]], which trusts the proxy's X-Forwarded-Proto header, so proxied HTTPS requests aren't redirected.
SECRET_KEY_BASE
mix phx.gen.secret
Set the output as SECRET_KEY_BASE. Keep it stable between deploys; changing it signs everyone out.
The database
runtime.exs reads DATABASE_URL and raises at boot if it's missing. A URL with query options works as it is; we connected with postgres://user:pass@host:5432/db?sslmode=disable. Set POOL_SIZE to change the connection pool from its default of 10.
Migrations from a release
A release doesn't include Mix, so mix ecto.migrate isn't available. Generate the release helpers once:
mix phx.gen.release
That adds lib/demo/release.ex and two scripts, rel/overlays/bin/migrate and rel/overlays/bin/server, which end up in the release's bin/. Run bin/migrate before the new version takes traffic; in our test it applied the migration from inside the running release's image. With one instance, the simplest place is an entrypoint that migrates and then starts the server:
#!/bin/sh
set -e
bin/migrate
exec bin/server
mix phx.gen.release --docker also writes a Debian-based Dockerfile that does the build for you, if you want to start from that.
Deploying Phoenix on Staxa
If your repository has a Dockerfile, Staxa builds it; the Phoenix and PostgreSQL demo has one with an entrypoint that migrates and seeds. If not, Staxa detects Phoenix from mix.exs, reads your app's name from it (app: :demo), and generates this Dockerfile (port 4000, Elixir 1.19):
FROM elixir:1.19-alpine AS build
WORKDIR /app
RUN apk add --no-cache build-base git npm
ENV MIX_ENV=prod
RUN mix local.hex --force && mix local.rebar --force
COPY mix.exs mix.lock ./
RUN mix deps.get --only prod && mix deps.compile
COPY . .
RUN mix compile
RUN if [ -f assets/package.json ]; then npm ci --prefix assets; fi
RUN if [ -d assets ]; then mix assets.deploy; fi
RUN mix release
# The same image as the build: a release links against its OpenSSL and musl,
# and the elixir image moves to newer Alpine releases on its own schedule.
FROM elixir:1.19-alpine
WORKDIR /app
RUN adduser -D -u 1001 app
COPY --from=build --chown=app /app/_build/prod/rel/demo .
USER app
ENV LANG=C.UTF-8
# mix phx.new starts the endpoint only when PHX_SERVER is set.
ENV PHX_SERVER=true
ENV PORT=4000
EXPOSE 4000
CMD ["bin/demo", "start"]
What this means for your app:
PHX_SERVERandPORTare handled. SetSECRET_KEY_BASEandPHX_HOSTin the service's environment.PHX_HOSTis<service>-<tenant>.staxa.app, or your custom domain.DATABASE_URLis set for you when the tenant has a database, withDB_HOST,DB_PORT,DB_NAME,DB_USERandDB_PASSWORD.- Assets are built with the esbuild and Tailwind versions in
config/config.exs, andnpm ciruns only ifassets/package.jsonexists (new Phoenix 1.7 and 1.8 apps have none). - It doesn't run migrations. After
mix phx.gen.release, the release containsbin/migrate; use your own Dockerfile with the entrypoint above to run it on each deploy. - The runtime stage is the Elixir image. A release's crypto library is linked against the build image's OpenSSL, and a mismatch stops it loading. We hit exactly that copying a release built on the current
elixir:1.17-alpineintoalpine:3.20:Unable to load crypto library ... EVP_MD_CTX_get_size_ex: symbol not found. If you write your own two-stage Dockerfile, build and run on the same distribution release.
Common problems
The release starts but nothing listens
PHX_SERVER isn't set. Set it to true, or start with bin/server (from mix phx.gen.release), which sets it for you.
LiveView pages reload in a loop
The WebSocket is being refused by the origin check. Set PHX_HOST to the domain in the browser's address bar.
environment variable DATABASE_URL is missing
runtime.exs requires it in production. Attach a database, or remove the Repo from your supervision tree if the app has none.
Unable to load crypto library
The runtime image's OpenSSL is a different version from the one the release was built against. Build and run on the same base image.
Frequently asked questions
Do I need Elixir installed on the server?
No. A release includes the Erlang runtime (ERTS) and your compiled code. The runtime image needs only the system libraries it links against.
Can I cluster nodes?
Yes, with libcluster and a discovery strategy for your platform (DNS or the Kubernetes API). A single node is enough for most apps; LiveView and PubSub work within one node with no extra setup.
Which Elixir version?
At least the one mix.exs names: our project, generated on Elixir 1.17, says elixir: "~> 1.17". Pick that version or newer in your host.