Elixir on Miren
Deploying a Phoenix app to Miren takes two commands, run from your project root:
miren init
miren deploy
There's no Dockerfile to write. Miren reads your mix.exs, builds a Mix
release, and runs it on a slim image.
Along the way it takes care of the parts that usually need hand-wiring:
- Assets get built, including every app in an umbrella, and the Phoenix endpoint starts on its own.
SECRET_KEY_BASEis generated and stored for you bymiren init.- Migrations from Phoenix's release helpers run before each deploy goes live, and a failed one stops the rollout.
- Clustering with DNSCluster works out of the box, and replicas only connect to others on the same version, so a rollout never mixes old and new nodes.
- Versions of Elixir and Erlang/OTP come from your
.tool-versionsormise.toml.
Before you deploy, set PHX_HOST to your public hostname and add a database. The next
section covers both.
Ask your AI coding agent to "set up this Phoenix app on Miren" after installing the Miren agent skills. It wires up the database addon and secrets and deploys, using this page as its reference.
Before your first deploy
A Phoenix app's config/runtime.exs refuses to boot without DATABASE_URL and
SECRET_KEY_BASE. miren init takes care of SECRET_KEY_BASE. The database and your
hostname are up to you.
A web app scales to zero, so a deploy can report success without ever starting an instance. A missing value only shows up when the first request boots one and it crashes.
Add a database
The simplest way to get DATABASE_URL is a managed Postgres addon.
Declare it in .miren/app.toml, and Miren provisions it and injects the connection
string (plus PG* variables):
[addons.miren-postgresql]
variant = "small"
Set your hostname
Phoenix uses PHX_HOST to build URLs and to check LiveView's websocket origin:
miren env set -e PHX_HOST=my_app.example.com
Add -s for anything secret, so it's masked in output and logs.
Environment variables at a glance
| Variable | Required | Notes |
|---|---|---|
DATABASE_URL | Yes | Injected by the miren-postgresql addon |
SECRET_KEY_BASE | Yes | Generated by miren init, or mix phx.gen.secret |
PHX_HOST | For LiveView | Your public route hostname |
PHX_SERVER | Set by Miren | Starts the endpoint in the release |
PORT | No | Injected by Miren |
POOL_SIZE | No | DB pool size, defaults to 10 |
DNS_CLUSTER_QUERY | For DNSCluster apps | Set to web.app.miren by miren init |
RELEASE_COOKIE | No | Set only to override the cookie Mix bundles in the release |
Miren also scans your code and config for System.fetch_env!/1 and
System.get_env/1 and reports the variables it finds, so you can spot one you haven't set. See
App Configuration: Environment Variables.
Private dependencies
Git dependencies in mix.exs, including private repositories, fetch with a netrc.
Store one with a token that can read them and mount it:
[[build.secrets]]
id = "netrc"
ref = "github/netrc"
file = "~/.netrc"
SSH URLs like git@github.com:acme/lib.git work as they are. See Private Dependencies for the full
walkthrough.
Migrations
For a Phoenix app with Ecto, run mix phx.gen.release locally and commit what it
generates. With rel/overlays/bin/migrate in place, Miren runs it once against each new
release before the rollout. If it fails, the deploy doesn't go live.
To run migrations your own way, declare a task named migrate in .miren/app.toml.
That replaces the automatic one, even with a manual trigger. Any other task with
trigger = "deploy" replaces it too, so a migration hook you already have under another
name doesn't run twice:
[tasks.migrate]
command = '/app/bin/my_app eval "MyApp.Release.migrate()"'
trigger = "deploy"
You can also run the helper by hand:
miren app run -a my_app -- /app/bin/migrate
See Tasks for more on deploy tasks.
Apps using ecto_sqlite3 don't get the automatic migration. Deploy tasks and
miren app run don't mount your service's disks, so they can't reach the database the
miren-sqlite addon provides. Run migrations in the service itself, before it starts
serving requests.
Choosing Elixir and Erlang versions
Miren uses the first of these it finds:
[build] versionin.miren/app.toml- The
elixiranderlangentries in.tool-versionsormise.toml - Elixir 1.19 on Erlang/OTP 28
Write versions the way your version manager does: 1.18, 1.18.4, or
1.18.4-otp-27. Miren supports Elixir 1.16 through 1.20 and builds on the latest
patch of the minor you name, so 1.18.4 builds on 1.18.5. The build output shows the
exact versions it used.
[build]
version = "1.18-otp-26"
For an exact pairing outside that set, use a full
hexpm/elixir tag on Debian bookworm, such
as 1.18.4-erlang-27.3.4.18-debian-bookworm-20260918-slim.
Umbrella apps
Add a releases: block to your umbrella's mix.exs so Miren knows which release to
build. Each app under apps/ with its own assets gets them built, with nothing extra to
configure. A single app without a releases: block gets the default release, named
after the app.
Clustering
Phoenix apps are stateless by default, so replicas run side by side behind Miren's
load balancer with no setup. If your app uses DNSCluster, miren init points it at
web.app.miren and replicas find each other on their own. During a rollout, old and
new versions stay in separate clusters even when both are running.
Replicas share the cookie Mix bundles in the release, so there's no RELEASE_COOKIE to
manage. A cookie you've already configured keeps working, and setting RELEASE_NODE
or RELEASE_DISTRIBUTION yourself overrides Miren's node naming.
Anyone who can pull your release image can read its cookie, and with network access to
the Erlang distribution ports, they can run code on your nodes. Restrict image and
network access, or set RELEASE_COOKIE as a sensitive environment variable if image
readers shouldn't have clustering credentials.
Metrics
Phoenix's generated telemetry module defines metrics but needs an exporter to
expose them. Add
telemetry_metrics_prometheus
as a production dependency, and add the exporter to your application supervisor:
{TelemetryMetricsPrometheus, metrics: MyAppWeb.Telemetry.metrics(), port: 9568}
It serves /metrics on port 9568. Keep your application's HTTP port and add
the exporter as a TCP port so it does not become Phoenix's PORT. For an app
using port 3000, configure .miren/app.toml as follows; preserve your existing
HTTP port if it differs:
[[services.web.ports]]
port = 3000
name = "http"
type = "http"
[[services.web.ports]]
port = 9568
name = "metrics"
type = "tcp"
[services.web.metrics]
enabled = true
port = 9568
path = "/metrics"
interval = "30s"
The TCP port type does not change the scraper's HTTP requests to /metrics.
The exporter must listen on an interface reachable from the private overlay,
not only loopback. Your cluster also needs a managed metrics remote-write
destination. See Observability.
Apps that aren't Phoenix
Plain Elixir apps deploy the same way. Read the port from the PORT environment
variable and listen on 0.0.0.0. Phoenix's generated runtime.exs already does both.
Custom builds
If you need build steps Miren doesn't run, add a Dockerfile.miren and Miren uses it
instead. See Using Dockerfile.miren.
Under the hood
Miren builds with MIX_ENV=prod on the hexpm/elixir
image: dependencies first (cached until mix.exs, mix.lock, or config/ changes),
then your code, then assets, then mix release. The builder has build-essential and
Git for native and Git dependencies. The final image is debian:bookworm-slim with just
the release at /app, and the web service runs /app/bin/<release> start.
Agent quick reference
- Detection:
mix.exsin the project - Version:
[build] version, else.tool-versions/mise.toml, else Elixir 1.19 / OTP 28; forms like1.18,1.18.4-otp-27, or a full bookworm hexpm tag - Build:
deps.get,deps.compile,compile,assets.deploy(if aliased),release, all withMIX_ENV=prod - Release: the
releases:entry inmix.exs, else the OTP app name; umbrellas needreleases: - Umbrella Phoenix: npm dependencies and
assets.deployaliases underapps/*are handled automatically before release - Start command:
/app/bin/<release> start(version-scoped, IP-named for DNSCluster apps);PHX_SERVER=trueis set for Phoenix - Secrets:
SECRET_KEY_BASEgenerated bymiren init; the release bundles its clustering cookie; setPHX_HOSTto the route host for LiveView - Database:
[addons.miren-postgresql]injectsDATABASE_URL(andPG*) - Migrations: automatic deploy task when Phoenix's
rel/overlays/bin/migrateexists, except forecto_sqlite3; explicit migration/deploy tasks take precedence; SQLite migrations must run where the service disk is mounted - Metrics: add a Prometheus exporter and configure
[services.web.metrics] - Dockerfile: not needed; add
Dockerfile.mirenonly for custom builds - Private deps:
[[build.secrets]]; netrc viafile = "~/.netrc"; see Private Dependencies
Next steps
- Addons: managed Postgres and other backing services
- App Configuration: customize
.miren/app.toml - Deployment: how deploys build and activate