Deploy your Elixir Phoenix backend to Fly.io. Optimized for Elixir with clustering, hot upgrades, and managed PostgreSQL.
Deploying Phoenix to Fly.io
Fly.io is the premier platform for deploying Elixir applications. With first-class support for the BEAM VM, clustering, and hot code reloading, it's the ideal choice for Phoenix deployments.
Why Fly.io for Elixir?
- Native Elixir support: Optimized for the BEAM VM
- Clustering: Connect nodes across regions
- Hot upgrades: Deploy without downtime
- Managed PostgreSQL: Easy database setup
- Global deployment: Run close to your users
- Competitive pricing: Pay for what you use
Prerequisites
- Fly.io account (signup)
- Fly CLI installed
- Phoenix application ready
- Git repository
Step 1: Install Fly CLI
macOS
brew install flyctlLinux
curl -L https://fly.io/install.sh | shWindows
# PowerShell
iwr https://fly.io/install.ps1 -useb | iexVerify Installation
fly versionStep 2: Login to Fly.io
fly auth loginThis opens a browser to authenticate.
Step 3: Prepare Your Phoenix App
Release Configuration
Create backend/rel/overlays/bin/migrate:
#!/bin/sh
cd -P -- "$(dirname -- "$0")"
exec ./task_master eval TaskMaster.Release.migrateMake it executable:
chmod +x backend/rel/overlays/bin/migrateCreate backend/lib/task_master/release.ex:
defmodule TaskMaster.Release do
@moduledoc """
Used for executing DB release tasks when run in production without Mix
"""
@app :task_master
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)
end
defp load_app do
Application.load(@app)
end
endRuntime Configuration
Update backend/config/runtime.exs:
import Config
# Get environment variables
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
raise """
environment variable SECRET_KEY_BASE is missing.
You can generate one by calling: mix phx.gen.secret
"""
guardian_secret_key =
System.get_env("GUARDIAN_SECRET_KEY") ||
raise """
environment variable GUARDIAN_SECRET_KEY is missing.
"""
app_name =
System.get_env("FLY_APP_NAME") ||
raise "FLY_APP_NAME not available"
config :task_master, TaskMasterWeb.Endpoint,
server: true,
url: [host: "#{app_name}.fly.dev", port: 443],
http: [
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: String.to_integer(System.get_env("PORT") || "4000")
],
secret_key_base: secret_key_base
config :task_master, TaskMaster.Guardian,
issuer: "task_master",
secret_key: guardian_secret_key
# Database configuration
database_url =
System.get_env("DATABASE_URL") ||
raise """
environment variable DATABASE_URL is missing.
For example: ecto://USER:PASS@HOST/DATABASE
"""
config :task_master, TaskMaster.Repo,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
socket_options: ipv6_socket_options_if_present
# Configure Swoosh for emails (if using)
config :task_master, TaskMaster.Mailer,
adapter: Swoosh.Adapters.Resend,
api_key: System.get_env("RESEND_API_KEY")
# Frontend URL for CORS
config :task_master, :frontend_url, System.get_env("FRONTEND_URL", "http://localhost:5173")
# Helper for IPv6
ipv6_socket_options_if_present = if System.get_env("ECTO_IPV6"), do: [:inet6], else: []Dockerfile
Create backend/Dockerfile (if not exists):
# Find eligible builder and runner images on Docker Hub
# https://hub.docker.com/r/hexpm/elixir/tags
FROM hexpm/elixir:1.16.0-erlang-26.2.1-debian-bookworm-20231009-slim as builder
# Install build dependencies
RUN apt-get update -y && apt-get install -y build-essential git \
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
# Prepare build directory
WORKDIR /app
# Install hex + rebar
RUN mix local.hex --force && \
mix local.rebar --force
# Set build environment
ENV MIX_ENV="prod"
# Install mix dependencies
COPY mix.exs mix.lock ./
RUN mix deps.get --only $MIX_ENV
RUN mkdir config
# Copy compile-time config files
COPY config/config.exs config/${MIX_ENV}.exs config/
RUN mix deps.compile
COPY priv priv
COPY lib lib
COPY assets assets
# Compile assets
RUN mix assets.deploy
# Compile the release
RUN mix compile
# Changes to config/runtime.exs don't require recompiling
COPY config/runtime.exs config/
COPY rel rel
RUN mix release
# Start a new build stage
FROM debian:bookworm-slim as runner
RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales \
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
# Set locale
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 /app
# Set runner environment
ENV MIX_ENV="prod"
# Only copy the final release
COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/task_master ./
USER nobody
CMD ["/app/bin/server"]Create backend/.dockerignore:
# This file excludes paths from the Docker build context.
.git
.gitignore
# Build artifacts from the host
/_build/
/assets/node_modules/
/deps/
/doc/
/tmp/
# IDE
.elixir_ls
.vscode
.idea
# OS
.DS_StoreStep 4: Launch on Fly.io
Initialize Fly App
From your backend directory:
cd backend
fly launchThis will:
- Detect your Phoenix app
- Suggest configuration
- Create
fly.toml - Set up secrets
When prompted:
- App name:
your-app-name(must be globally unique) - Region: Choose closest to you
- PostgreSQL: Yes (creates managed database)
- Redis: Optional (for caching/sessions)
Configure fly.toml
Edit the generated backend/fly.toml:
app = 'your-app-name'
primary_region = 'iad'
[build]
[deploy]
release_command = '/app/bin/migrate'
[env]
PHX_HOST = 'your-app-name.fly.dev'
PORT = '8080'
FRONTEND_URL = 'https://your-frontend.pages.dev'
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ['app']
[http_service.concurrency]
type = 'connections'
hard_limit = 1000
soft_limit = 1000
[[vm]]
memory = '1gb'
cpu_kind = 'shared'
cpus = 1Set Secrets
# Generate secrets
SECRET_KEY_BASE=$(mix phx.gen.secret)
GUARDIAN_SECRET_KEY=$(mix phx.gen.secret)
# Set on Fly
fly secrets set SECRET_KEY_BASE="$SECRET_KEY_BASE"
fly secrets set GUARDIAN_SECRET_KEY="$GUARDIAN_SECRET_KEY"
fly secrets set FRONTEND_URL="https://your-frontend.pages.dev"Step 5: Database Setup
If you created a PostgreSQL cluster during launch:
Attach Database
fly postgres attach --app your-app-name your-db-nameThis sets the DATABASE_URL secret automatically.
Manual Migration
If you need to run migrations manually:
fly ssh console -C "/app/bin/migrate"Database Console
Connect to your database:
fly postgres connect -a your-db-nameStep 6: Deploy
First Deploy
fly deployWatch the deployment progress. First deploy takes longer due to building.
Verify Deployment
# Check status
fly status
# View logs
fly logs
# Open app
fly openTest your API:
curl https://your-app-name.fly.dev/api/healthStep 7: Custom Domain
Add Certificate
fly certs create tcs-svc-9k2m.yourdomain.comConfigure DNS
At your DNS provider:
Type: CNAME
Name: tcs-svc-9k2m
Value: your-app-name.fly.devOr for A/AAAA records, get IPs:
fly ips listThen create:
Type: A
Name: tcs-svc-9k2m
Value: (IPv4 from fly ips list)
Type: AAAA
Name: tcs-svc-9k2m
Value: (IPv6 from fly ips list)Security Note: Use an obscure subdomain name like tcs-svc-9k2m instead of predictable names like api or backend. Attackers scan for common subdomains automatically. An unguessable name protects your backend from automated reconnaissance and reduces attack volume.
Update fly.toml
[env]
PHX_HOST = 'tcs-svc-9k2m.yourdomain.com'Redeploy:
fly deployStep 8: Continuous Deployment
GitHub Actions
Create .github/workflows/deploy.yml:
name: Deploy to Fly.io
on:
push:
branches: [main]
paths:
- 'backend/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Fly.io
uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
working-directory: backend
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}Add FLY_API_TOKEN to GitHub secrets:
fly tokens create deploy -x 999999h
# Copy token to GitHubStep 9: Monitoring
Fly Dashboard
Monitor at fly.io/dashboard:
- CPU/Memory usage
- Request metrics
- Error rates
- Logs
Fly Logs
# Real-time logs
fly logs
# Filter logs
fly logs --verboseSentry Integration
Add to mix.exs:
defp deps do
[
{:sentry, "~> 9.0"}
]
endConfigure in config/prod.exs:
config :sentry,
dsn: System.get_env("SENTRY_DSN"),
environment_name: :prod,
enable_source_code_context: true,
root_source_code_paths: [File.cwd!()]Step 10: Scaling
Vertical Scaling
Increase VM size:
[[vm]]
memory = '2gb'
cpu_kind = 'shared'
cpus = 2Then deploy:
fly deployHorizontal Scaling
Run multiple instances:
fly scale count 2Regional Scaling
Deploy to multiple regions:
fly regions add lax
fly regions add lhr
fly deployAdvanced Configuration
Clustering
Enable node clustering in runtime.exs:
# Clustering configuration
case System.get_env("FLY_APP_NAME") do
nil ->
# Local development
:ok
app_name ->
config :libcluster,
topologies: [
fly6pn: [
strategy: Cluster.Strategy.DNSPoll,
config: [
polling_interval: 5_000,
query: "#{app_name}.internal",
node_basename: app_name
]
]
]
endAdd to supervision tree in application.ex.
Health Checks
Add to fly.toml:
[[services.http_checks]]
interval = 10000
grace_period = "5s"
method = "get"
path = "/api/health"
protocol = "http"
timeout = 2000
tls_skip_verify = falseCreate health endpoint in router.ex:
get "/api/health", HealthController, :indexHot Upgrades
Enable zero-downtime deploys:
[deploy]
strategy = "rolling"Troubleshooting
App Won't Start
Check logs:
fly logsCommon issues:
- Missing secrets
- Database connection failed
- Port misconfiguration
Database Connection Failed
Verify DATABASE_URL:
fly secrets listCheck attachment:
fly postgres list
fly postgres attach --app your-app your-dbOut of Memory
Scale up:
[[vm]]
memory = '2gb'Or reduce concurrency:
[http_service.concurrency]
hard_limit = 500
soft_limit = 250Cost Optimization
Auto-Stop Machines
[http_service]
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0App stops when idle, starts on request (adds ~500ms cold start).
Volume Mounts
For file uploads, use volumes:
fly volumes create uploads --size 10Update fly.toml:
[mounts]
source = "uploads"
destination = "/app/uploads"