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 IEx

Release config (mix.exs)

def releases do
  [
    my_app: [
      include_executables_for: [:unix],
      applications: [runtime_tools: :permanent],
      steps: [&Mix.Tasks.Release.init/1]
    ]
  ]
end

Configuration 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
end

Important: Never put secrets in config.exs or dev.exs. Use runtime.exs with System.fetch_env!/1 for 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 string

Docker

# --- 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 main

Fly.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 deploy

Production 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 processes

Common 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