deploy
Elixir/Phoenix deployment patterns — Dockerfile, fly.toml, runtime.exs,
Install
npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/targets/amp/skills/deploy
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart
git clone https://github.com/oliver-kriska/claude-elixir-phoenix.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole oliver-kriska/claude-elixir-phoenix collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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.exsget baked into the release binary. Useruntime.exswith 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_peerallows 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
- Guard optional service credentials —
runtime.exsruns whenever a release boots, includingeval-based migration commands. Only require S3, Redis, and similar credentials when that integration is enabled
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
Guard Optional Services
Keep core boot secrets such as DATABASE_URL and SECRET_KEY_BASE required.
Gate credentials for optional integrations behind the same feature switch that
enables the integration:
s3_config =
if System.get_env("STORAGE_BACKEND") == "s3" do
[
access_key_id:
System.get_env("S3_ACCESS_KEY") ||
raise("S3_ACCESS_KEY is required when STORAGE_BACKEND=s3"),
secret_access_key:
System.get_env("S3_SECRET_KEY") ||
raise("S3_SECRET_KEY is required when STORAGE_BACKEND=s3")
]
else
[]
end
config :my_app, :s3_config, s3_config
This lets release tasks that do not use S3 start without S3 credentials while still failing fast when S3 is selected.
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
- All secrets from environment variables in runtime.exs
- Optional service credentials required only when their integration is enabled
-
server: truein endpoint config - SSL verification for database connections
- Health endpoints: /health/startup, /health/liveness, /health/readiness
- Graceful shutdown period ≥ 60 seconds
- No CPU limits (memory limits only)
- Migrations in deploy process
Asset Pipeline Notes
Phoenix 1.8 uses esbuild + tailwind (no Node.js required):
- Config in
config/config.exsunder:esbuildand:tailwind mix assets.deploybuilds for productionmix assets.setupinstalls binaries on first run- Custom JS bundlers: configure in
config/config.exs
References
For detailed patterns, see:
references/docker-config.md- Multi-stage Dockerfile, best practicesreferences/flyio-config.md- fly.toml, clustering, commands
Files (claude-elixir-phoenix)
-
references
-
docker-config.md 2.9 KB
# Docker Configuration Reference ## Multi-Stage Build ```dockerfile # Dockerfile ARG ELIXIR_VERSION=1.18.0 ARG OTP_VERSION=27.0 ARG DEBIAN_VERSION=bookworm-20240130-slim ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" # Build stage FROM ${BUILDER_IMAGE} as builder RUN apt-get update -y && apt-get install -y build-essential git \ && apt-get clean && rm -f /var/lib/apt/lists/*_* WORKDIR /app RUN mix local.hex --force && \ mix local.rebar --force ENV MIX_ENV=prod # Dependencies COPY mix.exs mix.lock ./ RUN mix deps.get --only $MIX_ENV RUN mkdir config # Config COPY config/config.exs config/${MIX_ENV}.exs config/ RUN mix deps.compile # Assets (esbuild + tailwind, configured in config/config.exs) COPY priv priv COPY assets assets RUN mix assets.deploy # Application COPY lib lib RUN mix compile # Release COPY config/runtime.exs config/ COPY rel rel RUN mix release # Runner stage FROM ${RUNNER_IMAGE} RUN apt-get update -y && \ apt-get install -y libstdc++6 openssl libncurses6 locales ca-certificates \ && apt-get clean && rm -f /var/lib/apt/lists/*_* RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en ENV LC_ALL=en_US.UTF-8 WORKDIR /app RUN chown nobody:nogroup /app USER nobody:nogroup COPY --from=builder --chown=nobody:nogroup /app/_build/prod/rel/my_app ./ ENV HOME=/app HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD curl -f http://localhost:4000/health/liveness || exit 1 CMD ["bin/my_app", "start"] ``` ## Release Configuration ### mix.exs ```elixir def project do [ releases: [ my_app: [ strip_beams: [keep: ["Docs"]], include_erts: true, config_providers: [], steps: [:assemble, :tar] ] ] ] end ``` ### rel/env.sh.eex ```bash #!/bin/sh # Clustering setup export RELEASE_DISTRIBUTION=name export RELEASE_NODE="${FLY_APP_NAME}@${FLY_PRIVATE_IP:-127.0.0.1}" # For Fly.io IPv6 export ERL_AFLAGS="-proto_dist inet6_tcp" ``` ### rel/vm.args.eex ``` ## Performance tuning +sbwt very_short +swt very_low ## Increase process limit +P 1000000 ## Enable dirty schedulers +SDio 16 ## Disable kernel poll on containers (can cause issues) # +K true ``` ## Migration in Production ```elixir # rel/overlays/bin/migrate #!/bin/sh cd -P -- "$(dirname -- "$0")" exec ./my_app eval MyApp.Release.migrate # lib/my_app/release.ex defmodule MyApp.Release do @app :my_app def migrate do load_app() for repo <- repos() do {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) end end def rollback(repo, version) do load_app() {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) end defp repos, do: Application.fetch_env!(@app, :ecto_repos) defp load_app, do: Application.load(@app) end ``` -
flyio-config.md 1.6 KB
# Fly.io Configuration Reference ## fly.toml ```toml app = "my-app" primary_region = "iad" [build] [deploy] release_command = "/app/bin/migrate" strategy = "rolling" [env] PHX_HOST = "my-app.fly.dev" PORT = "8080" ECTO_IPV6 = "true" [http_service] internal_port = 8080 force_https = true auto_stop_machines = true auto_start_machines = true min_machines_running = 1 [http_service.concurrency] type = "connections" hard_limit = 1000 soft_limit = 800 [[vm]] cpu_kind = "shared" cpus = 1 memory_mb = 512 [processes] app = "/app/bin/server" ``` ## Commands ```bash # Create app fly launch # Set secrets fly secrets set SECRET_KEY_BASE=$(mix phx.gen.secret) fly secrets set DATABASE_URL="postgres://..." # Create Postgres fly postgres create --name my-app-db fly postgres attach my-app-db # Deploy fly deploy # SSH into running instance fly ssh console --pty -C "/app/bin/my_app remote" # View logs fly logs # Scale fly scale count 3 fly scale vm shared-cpu-2x ``` ## Clustering on Fly.io ```elixir # config/runtime.exs if System.get_env("FLY_APP_NAME") do config :libcluster, topologies: [ fly6pn: [ strategy: Cluster.Strategy.DNSPoll, config: [ polling_interval: 5_000, query: System.get_env("FLY_APP_NAME") <> ".internal", node_basename: System.get_env("FLY_APP_NAME") ] ] ] end ``` ## rel/env.sh.eex for Fly.io ```bash #!/bin/sh # Clustering setup export RELEASE_DISTRIBUTION=name export RELEASE_NODE="${FLY_APP_NAME}@${FLY_PRIVATE_IP:-127.0.0.1}" # For Fly.io IPv6 export ERL_AFLAGS="-proto_dist inet6_tcp" ```
-
-
SKILL.md 4.5 KB
--- name: deploy description: 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. --- # Elixir/Phoenix Deployment Reference Quick reference for deploying Elixir/Phoenix applications. ## Iron Laws — Never Violate These 1. **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 2. **Graceful shutdown ≥ 60 seconds** — Shorter timeouts kill in-flight requests and WebSocket connections mid-operation, causing data loss for users 3. **Health checks required** — Without startup/liveness/readiness endpoints, orchestrators can't distinguish a booting node from a dead one, leading to cascading restarts 4. **SSL verification for database** — Skipping `verify: :verify_peer` allows MITM attacks between your app and database; production data traverses the connection 5. **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 6. **Guard optional service credentials** — `runtime.exs` runs whenever a release boots, including `eval`-based migration commands. Only require S3, Redis, and similar credentials when that integration is enabled ## Quick Configuration ### runtime.exs (Essential) ```elixir 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 ``` ### Guard Optional Services Keep core boot secrets such as `DATABASE_URL` and `SECRET_KEY_BASE` required. Gate credentials for optional integrations behind the same feature switch that enables the integration: ```elixir s3_config = if System.get_env("STORAGE_BACKEND") == "s3" do [ access_key_id: System.get_env("S3_ACCESS_KEY") || raise("S3_ACCESS_KEY is required when STORAGE_BACKEND=s3"), secret_access_key: System.get_env("S3_SECRET_KEY") || raise("S3_SECRET_KEY is required when STORAGE_BACKEND=s3") ] else [] end config :my_app, :s3_config, s3_config ``` This lets release tasks that do not use S3 start without S3 credentials while still failing fast when S3 is selected. ### Health Check Plug ```elixir 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 - [ ] All secrets from environment variables in runtime.exs - [ ] Optional service credentials required only when their integration is enabled - [ ] `server: true` in endpoint config - [ ] SSL verification for database connections - [ ] Health endpoints: /health/startup, /health/liveness, /health/readiness - [ ] Graceful shutdown period ≥ 60 seconds - [ ] No CPU limits (memory limits only) - [ ] Migrations in deploy process ## 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: - `references/docker-config.md` - Multi-stage Dockerfile, best practices - `references/flyio-config.md` - fly.toml, clustering, commands
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.