Phoenix LiveView builds rich, real-time interfaces by keeping UI state on the server and pushing diffs to the browser over a persistent WebSocket. That architecture is fantastic for users and introduces two deployment realities you must plan for: you ship an Elixir *release* (not source), and your platform must handle long-lived WebSocket connections. Here's the full path to production.
Build an Elixir release
The modern, recommended way to deploy Phoenix is mix release — it bundles your compiled app, its dependencies, and the Erlang runtime into a self-contained artifact. Generate the release config once:
mix phx.gen.releaseThis creates rel/ and a Dockerfile template tuned for Phoenix. The release is started with a generated script:
_build/prod/rel/my_app/bin/my_app startThe two secrets you must set
Phoenix needs SECRET_KEY_BASE (signs sessions, LiveView tokens) and your database URL. Generate the secret:
mix phx.gen.secret
# -> a long base64 string; set as SECRET_KEY_BASEIn config/runtime.exs (which runs at boot, not compile time), read both from the environment:
# config/runtime.exs
import Config
if config_env() == :prod do
database_url = System.fetch_env!("DATABASE_URL")
secret_key_base = System.fetch_env!("SECRET_KEY_BASE")
port = String.to_integer(System.get_env("PORT") || "4000")
config :my_app, MyApp.Repo,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
config :my_app, MyAppWeb.Endpoint,
url: [host: System.get_env("PHX_HOST") || "example.com", port: 443, scheme: "https"],
http: [ip: {0, 0, 0, 0}, port: port],
secret_key_base: secret_key_base,
server: true
endThree critical details here:
http: [ip: {0, 0, 0, 0}]binds to all interfaces so the platform proxy can reach the app.server: truetells the release to actually start the web server (it's off by default in releases).PHX_HOSTmust be your real domain — LiveView's WebSocket and CSRF checks validate against it.
That PHX_HOST / check_origin mismatch is the most common reason LiveView "connects then immediately disconnects" in production.
Ecto migrations
Releases don't include Mix, so you can't run mix ecto.migrate in production. Phoenix's release generator creates a release module for this:
# Run migrations against the release
_build/prod/rel/my_app/bin/my_app eval "MyApp.Release.migrate"Run this as a deploy step before the new release serves traffic. The generated MyApp.Release module wraps Ecto.Migrator.
The Dockerfile
mix phx.gen.release --docker produces a production-ready multi-stage Dockerfile. The shape:
# Build stage
FROM hexpm/elixir:1.17-erlang-27-debian-bookworm AS build
# ... install deps, compile assets, mix release
# Runtime stage (small, no build tools)
FROM debian:bookworm-slim
# ... copy the release, set ENV, CMD ["bin/my_app", "start"]The key parts: assets are compiled (mix assets.deploy) in the build stage, and only the self-contained release is copied to a slim runtime image. Use the generated file — it handles Elixir/Erlang version pinning correctly.
WebSockets: the deployment-critical detail
LiveView depends on a persistent WebSocket per connected user. Your platform must:
- Support WebSocket upgrade through its ingress/proxy.
- Allow long-lived connections (don't aggressively time out idle sockets).
- Ideally keep sticky behavior unnecessary — Phoenix can run stateless across nodes, but each socket lives on one node.
If WebSockets are blocked or proxied with short timeouts, LiveView silently falls back to full-page reloads and feels broken. Confirm your platform passes WebSocket upgrades.
Deploying on PandaStack
- 1Provision a managed PostgreSQL (14.x or 16.x). PandaStack injects
DATABASE_URL;runtime.exsreads it. - 2Connect the Git repo as a container app. The Phoenix-generated Dockerfile is auto-detected.
- 3Set
SECRET_KEY_BASE,PHX_HOST(your domain), andPORThandling. Confirmserver: true. - 4Run the release migrate command (
bin/my_app eval "MyApp.Release.migrate") once before traffic. - 5Add your custom domain; SSL is automatic. The Kong-based ingress passes WebSocket upgrades, which LiveView needs.
Note: scale-to-zero is a poor fit for LiveView, because scaling to zero would drop active WebSocket connections. Run LiveView apps on always-on compute.
Production checklist
- [ ]
mix releaseartifact,server: true. - [ ]
SECRET_KEY_BASEset and stable. - [ ]
PHX_HOSTset to the real domain (fixes WebSocket origin checks). - [ ] Bind
0.0.0.0, readPORT. - [ ] Migrations via the release
evalcommand. - [ ] WebSocket-capable ingress; always-on (not scale-to-zero).
- [ ]
pool_sizeunder the DB tier's connection limit across nodes.
Verifying
curl -s -o /dev/null -w '%{http_code}' https://app.example.com/
# 200Then open the app in a browser and watch the network panel for a successful websocket connection to /live/websocket. A persistent socket (status 101) means LiveView is fully working.
References
- [Phoenix deployment with releases](https://hexdocs.pm/phoenix/releases.html)
- [Phoenix runtime configuration](https://hexdocs.pm/phoenix/deployment.html)
- [Phoenix Docker deployment guide](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Release.html)
- [Phoenix LiveView docs](https://hexdocs.pm/phoenix_live_view/welcome.html)
---
LiveView needs a WebSocket-aware, always-on host plus a managed database — PandaStack's Kong ingress passes WebSocket upgrades and injects DATABASE_URL automatically. Try it free at [dashboard.pandastack.io](https://dashboard.pandastack.io).