Elixir/Phoenix Deployment Reference
Quick reference for deploying Elixir/Phoenix applications.
Iron Laws — Never Violate These
- Config at runtime, not compile time — Secrets in
config.exs get baked into the release binary. Use runtime.exs with env vars so secrets are resolved at boot
- Graceful shutdown ≥ 60 seconds — Shorter timeouts kill in-flight requests and WebSocket connections mid-operation, causing data loss for users
- Health checks required — Without startup/liveness/readiness endpoints, orchestrators can't distinguish a booting node from a dead one, leading to cascading restarts
- SSL verification for database — Skipping
verify: :verify_peer allows MITM attacks between your app and database; production data traverses the connection
- No CPU limits — The BEAM scheduler assumes it owns all cores; cgroups CPU limits cause scheduler collapse where the VM thinks it has more cores than it can use, leading to latency spikes
Quick Configuration
runtime.exs (Essential)
if config_env() == :prod do
database_url = System.get_env("DATABASE_URL") || raise "DATABASE_URL is required"
secret_key_base = System.get_env("SECRET_KEY_BASE") || raise "SECRET_KEY_BASE is required"
host = System.get_env("PHX_HOST") || raise "PHX_HOST is required"
config :my_app, MyApp.Repo,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
ssl: true,
ssl_opts: [verify: :verify_peer]
config :my_app, MyAppWeb.Endpoint,
url: [host: host, port: 443, scheme: "https"],
http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")],
secret_key_base: secret_key_base,
server: true
end
Health Check Plug
def call(%{path_info: ["health", "readiness"]} = conn, _opts) do
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
{:ok, _} -> send_resp(conn, 200, ~s({"status":"ok"})) |> halt()
{:error, _} -> send_resp(conn, 503, ~s({"status":"error"})) |> halt()
end
end
Quick Decisions
Platform Choice
| Need |
Use |
| Simple, managed |
Fly.io |
| Enterprise, existing K8s |
Kubernetes |
| Custom infrastructure |
Docker + your orchestrator |
Resource Limits
| Resource |
Recommendation |
| CPU |
NO LIMITS (BEAM scheduler issues) |
| Memory |
Set limits (256Mi-512Mi typical) |
| Graceful shutdown |
≥ 60 seconds |
Deployment Checklist
Asset Pipeline Notes
Phoenix 1.8 uses esbuild + tailwind (no Node.js required):
- Config in
config/config.exs under :esbuild and :tailwind
mix assets.deploy builds for production
mix assets.setup installs binaries on first run
- Custom JS bundlers: configure in
config/config.exs
References
For detailed patterns, see:
${CLAUDE_SKILL_DIR}/references/docker-config.md - Multi-stage Dockerfile, best practices
${CLAUDE_SKILL_DIR}/references/flyio-config.md - fly.toml, clustering, commands
1---2name: deploy-283description: Elixir/Phoenix deployment patterns — Dockerfile, fly.toml, runtime.exs, mix release, rel/ overlays. Use when configuring Fly.io, Docker, CI/CD, health checks, or production migrations.4---5
6# Elixir/Phoenix Deployment Reference
7
8Quick reference for deploying Elixir/Phoenix applications.
9
10## Iron Laws — Never Violate These
11
121. **Config at runtime, not compile time** — Secrets in `config.exs` get baked into the release binary. Use `runtime.exs` with env vars so secrets are resolved at boot
132. **Graceful shutdown ≥ 60 seconds** — Shorter timeouts kill in-flight requests and WebSocket connections mid-operation, causing data loss for users
143. **Health checks required** — Without startup/liveness/readiness endpoints, orchestrators can't distinguish a booting node from a dead one, leading to cascading restarts
154. **SSL verification for database** — Skipping `verify: :verify_peer` allows MITM attacks between your app and database; production data traverses the connection
165. **No CPU limits** — The BEAM scheduler assumes it owns all cores; cgroups CPU limits cause scheduler collapse where the VM thinks it has more cores than it can use, leading to latency spikes
17
18## Quick Configuration
19
20### runtime.exs (Essential)
21
22```elixir
23if config_env() == :prod do
24 database_url = System.get_env("DATABASE_URL") || raise "DATABASE_URL is required"
25 secret_key_base = System.get_env("SECRET_KEY_BASE") || raise "SECRET_KEY_BASE is required"
26 host = System.get_env("PHX_HOST") || raise "PHX_HOST is required"
27
28 config :my_app, MyApp.Repo,
29 url: database_url,
30 pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
31 ssl: true,
32 ssl_opts: [verify: :verify_peer]
33
34 config :my_app, MyAppWeb.Endpoint,
35 url: [host: host, port: 443, scheme: "https"],
36 http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")],
37 secret_key_base: secret_key_base,
38 server: true
39end
40```
41
42### Health Check Plug
43
44```elixir
45def call(%{path_info: ["health", "readiness"]} = conn, _opts) do
46 case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
47 {:ok, _} -> send_resp(conn, 200, ~s({"status":"ok"})) |> halt()
48 {:error, _} -> send_resp(conn, 503, ~s({"status":"error"})) |> halt()
49 end
50end
51```
52
53## Quick Decisions
54
55### Platform Choice
56
57| Need | Use |
58|------|-----|
59| Simple, managed | Fly.io |
60| Enterprise, existing K8s | Kubernetes |
61| Custom infrastructure | Docker + your orchestrator |
62
63### Resource Limits
64
65| Resource | Recommendation |
66|----------|----------------|
67| CPU | **NO LIMITS** (BEAM scheduler issues) |
68| Memory | Set limits (256Mi-512Mi typical) |
69| Graceful shutdown | ≥ 60 seconds |
70
71## Deployment Checklist
72
73- [ ] All secrets from environment variables in runtime.exs
74- [ ] `server: true` in endpoint config
75- [ ] SSL verification for database connections
76- [ ] Health endpoints: /health/startup, /health/liveness, /health/readiness
77- [ ] Graceful shutdown period ≥ 60 seconds
78- [ ] No CPU limits (memory limits only)
79- [ ] Migrations in deploy process
80
81## Asset Pipeline Notes
82
83Phoenix 1.8 uses esbuild + tailwind (no Node.js required):
84
85- Config in `config/config.exs` under `:esbuild` and `:tailwind`
86- `mix assets.deploy` builds for production
87- `mix assets.setup` installs binaries on first run
88- Custom JS bundlers: configure in `config/config.exs`
89
90## References
91
92For detailed patterns, see:
93
94- `${CLAUDE_SKILL_DIR}/references/docker-config.md` - Multi-stage Dockerfile, best practices
95- `${CLAUDE_SKILL_DIR}/references/flyio-config.md` - fly.toml, clustering, commands