Deploying a Phoenix App to Production: What Actually Works

Gigalixir Engineering
April 9, 2026
5 min read
Pattern

A Phoenix app deploys to Gigalixir with a git push. Clustering, TLS, and Remote Observer work on the first deploy without additional configuration.

First-time Phoenix production deploys have a reputation for complexity. Most of that complexity comes from platforms that were not built for Elixir. Gigalixir handles BEAM-specific requirements like distributed clustering, long-lived GenServer state, and persistent WebSocket connections. No workarounds required.

Elixir Releases Are the Right Choice for Production. Mix Mode Is for Prototyping.

Gigalixir supports both Elixir Releases and Mix mode. The choice affects which buildpacks run and how runtime configuration works.

Releases (recommended for production): Add config/releases.exs to the project. Gigalixir auto-detects this file and runs the releases buildpack. Releases produce a self-contained binary. Runtime environment variables are read at startup, not at compile time.

Mix mode: Used when no config/releases.exs is present. Simpler to start, but the app runs with Mix in production. Not recommended for high-traffic production workloads.

One important edge case: if config/runtime.exs is present but config/releases.exs is not, Gigalixir cannot auto-detect the mode. In that case, buildpacks must be specified manually in a .buildpacks file at the project root.

# .buildpacks — example for a releases app using runtime.exs
https://github.com/gigalixir/gigalixir-buildpack-elixir.git
https://github.com/gigalixir/gigalixir-buildpack-phoenix-static.git
https://github.com/gigalixir/gigalixir-buildpack-releases.git

Runtime Config Belongs in config/runtime.exs, Not config/prod.exs

Compile-time config does not belong in production. Secrets and connection strings go into config/runtime.exs so they are read from environment variables at startup.

# config/runtime.exs
import Config
 
if config_env() == :prod do
  database_url =
    System.get_env("DATABASE_URL") ||
      raise "DATABASE_URL is not set"
 
  config :my_app, MyApp.Repo,
    url: database_url,
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
 
  secret_key_base =
    System.get_env("SECRET_KEY_BASE") ||
      raise "SECRET_KEY_BASE is not set"
 
  config :my_app, MyAppWeb.Endpoint,
    http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")],
    secret_key_base: secret_key_base
end

Set environment variables via the Gigalixir CLI before the first deploy:

gigalixir config:set SECRET_KEY_BASE=$(mix phx.gen.secret)
gigalixir config:set DATABASE_URL=postgresql://...

A First Deploy Takes Four Commands After the CLI Is Installed

Install the CLI, create an account, and connect the git remote:

pip3 install gigalixir
gigalixir signup
gigalixir login
gigalixir apps:create

Create a free-tier PostgreSQL database:

gigalixir pg:create --free
gigalixir pg

Run migrations. On Gigalixir, migrations run as internal processes, not as new paid instances:

gigalixir ps:migrate

Deploy:

git push gigalixir main

Gigalixir builds the app in a container derived from heroku/heroku, runs the configured buildpacks in order, and rolls the new version in with zero downtime. The previous version keeps serving traffic until the new version passes its health check.

Clustering, TLS, Observer, and Zero-Downtime Deploys Work Without Configuration

Several things that require manual setup on other platforms work automatically on Gigalixir.

TLS: Let's Encrypt certificates are provisioned and renewed automatically for any custom domain.

Clustering: libcluster is configured automatically. Nodes in the same app find each other. Phoenix PubSub and distributed GenServers work across replicas without Redis or any additional service.

Remote Observer: Connect a GUI Observer to a live production node to inspect process state, memory usage, and message queues in real time. No additional setup needed.

Remote Console: Open an IEx session directly on a running production node:

gigalixir account:ssh_keys:add ~/.ssh/id_rsa.pub  # Standard Tier only
gigalixir ps:remote_console

Gigalixir sees this pattern regularly in support tickets: engineers are surprised that Remote Console connects to the actual running node rather than spinning up a separate container. That distinction matters for inspecting live process state.

No forced restarts: Unlike Heroku, Gigalixir does not restart apps on a daily cycle. GenServer state, ETS tables, and Cachex data persist across deploys as long as the app remains running.

The Procfile Controls How the Release Boots

A Procfile at the project root controls how the app starts. For a standard Phoenix release:

# Procfile
web: _build/prod/rel/my_app/bin/my_app start

The app must bind to 0.0.0.0 on the port provided in the PORT environment variable. Gigalixir sets PORT automatically. The runtime config example above already handles this.

Free Tier Is for Prototyping. Standard Tier Starts at $25/Month.

The Free Tier is appropriate for development and prototyping. For production traffic, Standard Tier is the right choice.

Free Tier limits:

  • One replica, max size 0.5 (500MB memory)
  • Free database capped at 10,000 rows and 2 connections
  • No SSH access, no database backups
  • App scales to zero replicas after 30 days without a deploy (warning email sent at 23 days)

Standard Tier starts at $0.05 per MB per month per replica, prorated to the second. A size 0.5 replica running a full month costs $25. Scaling up for a traffic spike and back down charges only for the hours the larger replica ran.

A Production-Ready Phoenix Deployment Is a git Push

The full path from a local Phoenix app to a running production deployment on Gigalixir is: configure runtime environment variables, add a Procfile, push to the gigalixir git remote. Clustering, TLS, zero-downtime deploys, and Remote Observer work without additional configuration.

For a complete walkthrough including asset pipeline setup and custom domain configuration, see the Gigalixir Getting Started Guide. Replica and database pricing is at gigalixir.com/pricing.

Featured Blog

Explore our latest technical post-mortems, database optimization guides, and platform features designed to simplify your production workflow.

High-Performance Hosting for Modern Apps.

Stop fighting generic cloud infrastructure.

Whether you’re working with Elixir, Python, or Node.js, get the specialized support and performance your application deserves.