Deployment & Configuration
Phoenix deployment — releases, environment config, Docker, Heroku, and production setup
Phoenix applications are typically deployed as self-contained releases that include the Erlang VM, compiled bytecode, and all assets.
Releases
# Build a release
mix deps.get --only prod
MIX_ENV=prod mix compile
MIX_ENV=prod mix assets.deploy
MIX_ENV=prod mix release
# Run the release
_build/prod/rel/my_app/bin/my_app start
_build/prod/rel/my_app/bin/my_app stop
_build/prod/rel/my_app/bin/my_app restart
_build/prod/rel/my_app/bin/my_app remote # Connect remote shell
_build/prod/rel/my_app/bin/my_app eval "MyApp.hello()"
_build/prod/rel/my_app/bin/my_app daemon # Start as daemon
_build/prod/rel/my_app/bin/my_app daemon_iex # Start as daemon with IExRelease config (mix.exs)
def releases do
[
my_app: [
include_executables_for: [:unix],
applications: [runtime_tools: :permanent],
steps: [&Mix.Tasks.Release.init/1]
]
]
endConfiguration layers
Phoenix uses three config layers, evaluated in order:
| File | When evaluated | Use for |
|---|---|---|
config/config.exs |
Compile time | Static defaults |
config/#{env}.exs |
Compile time | Environment-specific defaults |
config/runtime.exs |
Runtime (release starts) | Environment variables, secrets |
config/config.exs
import Config
config :my_app,
ecto_repos: [MyApp.Repo]
config :my_app, MyAppWeb.Endpoint,
url: [host: "localhost"],
render_errors: [view: MyAppWeb.ErrorHTML, accepts: ~w(html json)]
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
import_config "#{config_env()}.exs"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")
phx_host = System.get_env("PHX_HOST") || "localhost"
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: phx_host, port: 443, scheme: "https"],
http: [ip: {0, 0, 0, 0}, port: port],
secret_key_base: secret_key_base,
server: true
endImportant: Never put secrets in
config.exsordev.exs. Useruntime.exswithSystem.fetch_env!/1for production secrets.
Environment variables
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | Ecto database URL |
SECRET_KEY_BASE |
Yes | Signing/encryption key |
PHX_HOST |
No | Public hostname (default: localhost) |
PORT |
No | HTTP port (default: 4000) |
POOL_SIZE |
No | DB pool size (default: 10) |
Generate a secret key base:
mix phx.gen.secret
# => long random stringDocker
# --- Build stage ---
FROM hexpm/elixir:1.16.2-erlang-26.2.3-alpine-3.19.1 AS build
RUN apk add --no-cache build-base git python3
WORKDIR /app
RUN mix local.hex --force && \
mix local.rebar --force
ENV MIX_ENV=prod
COPY mix.exs mix.lock ./
RUN mix deps.get --only prod
RUN mkdir config
COPY config/config.exs config/
COPY config/prod.exs config/
RUN mix deps.compile
COPY lib lib
COPY priv priv
COPY assets assets
RUN mix assets.deploy
RUN mix release
# --- Run stage ---
FROM alpine:3.19 AS app
RUN apk add --no-cache libstdc++ openssl ncurses-libs
COPY --from=build /app/_build/prod/rel/my_app /app
ENV HOME=/app
ENV PHX_SERVER=true
CMD ["/app/bin/my_app", "start"]Heroku
# Create app
heroku create my-app --stack heroku-22
# Add buildpacks
heroku buildpacks:set https://github.com/HashNuke/heroku-buildpack-elixir
heroku buildpacks:add https://github.com/gj/heroku-buildpack-phoenix-static
# Set environment
heroku config:set SECRET_KEY_BASE=$(mix phx.gen.secret)
heroku config:set POOL_SIZE=18
# Deploy
git push heroku mainFly.io
# Initialize
fly launch
# Set secrets
fly secrets set SECRET_KEY_BASE=$(mix phx.gen.secret)
fly secrets set DATABASE_URL=ecto://user:pass@host/db
# Deploy
fly deployProduction checklist
| Item | Details |
|---|---|
| Secrets | SECRET_KEY_BASE via env var, not config file |
| Database | DATABASE_URL via env var; set pool_size |
| Assets | Run mix assets.deploy before release |
| HTTPS | Configure endpoint URL with scheme: "https" |
| Logging | Configure logger format for production |
| Health checks | Add /health endpoint or use Plug.Static check |
| Clustering | Use libcluster for multi-node |
| SSL | Use reverse proxy (nginx, caddy) or Bandit with TLS |
Observer & debugging in production
# Connect to a running release
_build/prod/rel/my_app/bin/my_app remote
# Inside IEx remote shell
iex> :observer.start() # Visual process tree (if display available)
iex> MyApp.Repo.aggregate(Post, :count) # Run queries
iex> Process.list() |> length() # Count processesCommon production endpoints
# lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
# Serve static files from "priv/static"
plug Plug.Static,
at: "/", from: :my_app, gzip: false,
only: ~w(assets fonts images favicon.ico robots.txt)
# Code reloading disabled in production
if code_reloading? do
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :my_app
end
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.Session,
store: :cookie,
key: "_my_app_key",
signing_salt: "your_signing_salt"
plug Plug.Head
plug MyAppWeb.Router
end