Skip to main content

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_BASE is generated and stored for you by miren 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-versions or mise.toml.

Before you deploy, set PHX_HOST to your public hostname and add a database. The next section covers both.

Let your agent do this

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.

Set these up before the app serves traffic

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​

VariableRequiredNotes
DATABASE_URLYesInjected by the miren-postgresql addon
SECRET_KEY_BASEYesGenerated by miren init, or mix phx.gen.secret
PHX_HOSTFor LiveViewYour public route hostname
PHX_SERVERSet by MirenStarts the endpoint in the release
PORTNoInjected by Miren
POOL_SIZENoDB pool size, defaults to 10
DNS_CLUSTER_QUERYFor DNSCluster appsSet to web.app.miren by miren init
RELEASE_COOKIENoSet 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.

SQLite apps migrate on startup

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:

  1. [build] version in .miren/app.toml
  2. The elixir and erlang entries in .tool-versions or mise.toml
  3. 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.

The bundled cookie is readable from the image

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.exs in the project
  • Version: [build] version, else .tool-versions / mise.toml, else Elixir 1.19 / OTP 28; forms like 1.18, 1.18.4-otp-27, or a full bookworm hexpm tag
  • Build: deps.get, deps.compile, compile, assets.deploy (if aliased), release, all with MIX_ENV=prod
  • Release: the releases: entry in mix.exs, else the OTP app name; umbrellas need releases:
  • Umbrella Phoenix: npm dependencies and assets.deploy aliases under apps/* are handled automatically before release
  • Start command: /app/bin/<release> start (version-scoped, IP-named for DNSCluster apps); PHX_SERVER=true is set for Phoenix
  • Secrets: SECRET_KEY_BASE generated by miren init; the release bundles its clustering cookie; set PHX_HOST to the route host for LiveView
  • Database: [addons.miren-postgresql] injects DATABASE_URL (and PG*)
  • Migrations: automatic deploy task when Phoenix's rel/overlays/bin/migrate exists, except for ecto_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.miren only for custom builds
  • Private deps: [[build.secrets]]; netrc via file = "~/.netrc"; see Private Dependencies

Next steps​