Claude Skill

deploy

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.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download oliver-kriska-claude-elixir-phoenix-plugins_elixir-phoenix_skills_deploy-9767a82.zip · 4 KB
Part of oliver-kriska/claude-elixir-phoenix — 93 skills

Install

skills CLI npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/plugins/elixir-phoenix/skills/deploy
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart
Git 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

  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)

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: 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:

  • ${CLAUDE_SKILL_DIR}/references/docker-config.md - Multi-stage Dockerfile, best practices
  • ${CLAUDE_SKILL_DIR}/references/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.6 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."
    effort: medium
    paths:
      - "config/runtime.exs"
      - "Dockerfile"
      - "fly.toml"
      - "rel/**/*"
    ---
    
    # 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:
    
    - `${CLAUDE_SKILL_DIR}/references/docker-config.md` - Multi-stage Dockerfile, best practices
    - `${CLAUDE_SKILL_DIR}/references/flyio-config.md` - fly.toml, clustering, commands
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related