# Elixir and Phoenix

A Phoenix project deploys from its repository with no configuration. Nimbus Land recognises `{:phoenix, …}` in your `mix.exs`, builds the project into a self-contained OTP release, and runs that release. Nothing runs `mix`, fetches dependencies or compiles when your app starts, so instances start in seconds.

## Quick start

A stock `mix phx.new` app deploys as is. In the console, create an app from your repository with the deploy wizard. From the CLI, run this in the project directory:

```sh
nimbus deploy
```

A stock project already does what Nimbus Land needs: it listens on `PORT`, reads its configuration in `config/runtime.exs`, builds one release and bundles the Erlang runtime. See [What your app needs](#what-your-app-needs). Nimbus Land sets `PORT`, `PHX_SERVER=true`, `SECRET_KEY_BASE`, `PHX_HOST` and, when a database is attached, `DATABASE_URL`.

Check these:

- **Migrations.** There is no `mix` at run time. The deploy wizard suggests a release command for you; see [Migrations and other commands](#migrations-and-other-commands). General rules are in [release command](apps.md#release-command).
- **Configuration that changes per environment** belongs in `config/runtime.exs`, not `config/prod.exs`. See [Compile-time configuration](#compile-time-configuration).

If your repository has a `Dockerfile` at its root, that wins and the generated build is skipped. See [Dockerfile](dockerfile.md). That is also the way out for anything the generated build doesn't support. For how Nimbus Land chooses a stack, see [Apps](apps.md).

## How the build works

The build uses Elixir 1.20.4 on Erlang/OTP 29, with Node.js and npm available for your assets. In order, it:

1. runs `mix deps.get --only prod` and `mix deps.compile` from `mix.exs`, `mix.lock`, `config/config.exs` and `config/prod.exs` (plus `apps/*/mix.exs` in an umbrella),
2. copies in the rest of your source,
3. runs `npm ci` (or `npm install` without a lockfile) in `assets/` when `assets/package.json` exists,
4. runs `mix compile`, then `mix assets.deploy` when your `mix.exs` defines it,
5. runs `mix release`.

The image your app runs contains the release (with its bundled Erlang runtime, compiled code, digested assets and anything under `rel/overlays/`), a shell and a few system libraries. It contains no source code, `deps/`, Mix, Hex, Node or compiler. The build log ends the release step with `Nimbus: assembled OTP release <name>`.

### Faster rebuilds

Each app keeps its own build cache. A commit that only changes application code (`lib/`, `assets/`, `priv/`, templates, `config/runtime.exs`) reuses the dependency fetch and compile steps, and typically takes about a third of the time of a first build. A change to `mix.exs`, `mix.lock`, `config/config.exs`, `config/prod.exs` or an umbrella child's `mix.exs` rebuilds dependencies. Cache entries are reused for two weeks.

Because dependencies are resolved before the rest of the tree is copied, `mix.exs` must not read other files (a `VERSION` file, `README.md` for docs). Inline the value, or use your own `Dockerfile`.

### Compile-time configuration

The build sees the same variables your app has, except the ones stored as secrets: the variables passed with the deploy, then the app's stored plain variables. The platform generates `SECRET_KEY_BASE` and `PHX_HOST` before the first build, so an older `config/prod.exs` that calls `System.fetch_env!("SECRET_KEY_BASE")` still compiles.

- **Prefer `config/runtime.exs`.** It runs when the app starts and reads the current environment. A value read in `config/prod.exs` is compiled into the release, so changing it needs a rebuild, not just a restart.
- **A variable nobody set stays unset.** A `config/prod.exs` that reads `DATABASE_URL` at compile time fails the build when no database is attached. Attach the database (or set the variable) and deploy again.
- **Secrets never reach the build.** If `config/prod.exs` or `config/config.exs` reads a secret-shaped key (one containing `SECRET`, `TOKEN`, `PASSWORD` or `PRIVATE`, ending in `_KEY`, or starting with `KEY`), the deploy plan shows **Check this** with a note such as `config/prod.exs reads STRIPE_SECRET_KEY at compile time, but builds never see secrets. Read it in config/runtime.exs instead, or store it as a plain variable.`

## What your app needs

A stock `mix phx.new` project already does all of this. Failures here stop the build, and a release that can't start is never deployed.

- **Listen on `PORT`.** The generated `runtime.exs` does: `http: [port: String.to_integer(System.get_env("PORT") || "4000")]`.
- **Read configuration in `config/runtime.exs`.** Nimbus Land sets `PORT`, `PHX_SERVER=true`, `SECRET_KEY_BASE`, `PHX_HOST`, and `DATABASE_URL` when a database is attached, along with your own variables.
- **Build exactly one release.** `mix release` must produce one release. Several releases without `:default_release`, or an umbrella with no release configuration, fail the build with a message saying so.
- **Bundle the Erlang runtime** (the default). `include_erts: false` fails the build, because the image has no Erlang of its own.

## Migrations and other commands

Release commands, background workers and one-off commands run in the release directory (`/app`) through `/bin/sh -c`. There is no `mix`, so `mix ecto.migrate` fails with `mix: not found`. Use the release instead:

| Purpose | Command |
| --- | --- |
| Migrations, with `mix phx.gen.release` | `bin/migrate` |
| Any Elixir expression | `bin/<release> eval 'MyApp.Release.migrate()'` |
| A script of your own | `bin/<script>` from `rel/overlays/bin/` |
| A remote console | `nimbus-release remote` |

`nimbus-release` runs your release without you naming it: with no arguments it starts the app, and any arguments are passed to `bin/<release>` (`nimbus-release eval '…'`, `nimbus-release remote`).

A stock `mix phx.new` app has no `bin/migrate`, but it can still migrate. For an app with repo `MyApp.Repo`:

```sh
nimbus run -- nimbus-release eval 'Ecto.Migrator.with_repo(MyApp.Repo, &Ecto.Migrator.run(&1, :up, all: true))'
```

**The deploy wizard suggests the release command for you.** For a Phoenix repository with files under `priv/repo/migrations/`, no root `Dockerfile`, no `release:` line in a root `Procfile` and no umbrella, the plan shows **Before each release** with a migration command, which you can edit or clear under **Customize** in the **Release command** field. It picks, in order: `bin/migrate` when `rel/overlays/bin/migrate` exists; `nimbus-release eval "MyApp.Release.migrate()"` when `lib/<app>/release.ex` defines a `migrate` function; otherwise an `Ecto.Migrator` command for each repo listed under `ecto_repos` in `config/config.exs`. The suggestion applies only to an app the wizard creates.

Commands that call `mix` or read `lib/`, `deps/` or `priv/repo/*.exs` from the source tree don't work. Move them into a `rel/overlays/bin/` script or a `MyApp.Release` module.

## Hostnames and PHX_HOST

Phoenix uses `PHX_HOST` for generated URLs and for its origin check, which rejects websocket and long-poll connections from any other hostname. A wrong host leaves pages rendering but LiveView reconnecting in a loop.

**Nimbus Land sets `PHX_HOST` for you** when you haven't: your verified custom domain if there is one, otherwise `<app>.nimbusapps.ca`. A value you set yourself is never overwritten.

**Custom domains.** When you add a custom domain on the app's **Access** tab, **Also set PHX_HOST to this domain** is checked by default. It applies to the running app immediately, so do it once DNS points at Nimbus Land. If you skip it, the domain card keeps offering **Use <domain> for PHX_HOST**. See [Custom domains](custom-domains.md).

**Serving several hostnames.** Phoenix's default check accepts one host. To accept live connections on both your custom domain and the `nimbusapps.ca` address, list them and keep `PHX_HOST` as the canonical host:

```elixir
# config/runtime.exs, inside `if config_env() == :prod do`
host = System.get_env("PHX_HOST") || "example.com"

config :my_app, MyAppWeb.Endpoint,
  url: [host: host, port: 443, scheme: "https"],
  check_origin: [
    "https://#{host}",
    "https://my-app.nimbusapps.ca",
    "https://www.example.com"
  ]
```

Don't use `check_origin: false` on a public app: it turns off the protection the check gives your sockets.

## Databases and TLS

Attaching a managed Postgres database sets `DATABASE_URL`, `DATABASE_SSLMODE=prefer` and `DATABASE_CA_CERT`, the certificate of the authority that signs the database's certificate.

**A stock `config/runtime.exs` works as-is.** With the generated Repo config (`# ssl: true` left commented out), Postgrex connects without TLS. The database accepts unencrypted connections only from apps linked to it, over the platform's private network.

**To use TLS**, pass the CA to Postgrex. `ssl: true` alone verifies against the operating system's trust store, which doesn't include this CA, and fails with `Unknown CA`:

```elixir
# config/runtime.exs, inside `if config_env() == :prod do`
database_url = System.fetch_env!("DATABASE_URL")

database_ssl_opts =
  case System.get_env("DATABASE_CA_CERT") do
    nil ->
      []

    pem ->
      cacerts =
        pem
        |> :public_key.pem_decode()
        |> Enum.map(fn {:Certificate, der, _} -> der end)

      host = URI.parse(database_url).host

      [
        ssl: [
          verify: :verify_peer,
          cacerts: cacerts,
          server_name_indication: String.to_charlist(host),
          customize_hostname_check: [
            match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
          ]
        ]
      ]
  end

config :my_app, MyApp.Repo,
       [
         url: database_url,
         pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
         socket_options: maybe_ipv6
       ] ++ database_ssl_opts
```

## Clustering

Turn on **Clustering** on the app's **Resources** tab to let your instances form a runtime cluster. Nimbus Land sets `DNS_CLUSTER_QUERY`, and the release starts with a distributed node name unless your release already sets `RELEASE_DISTRIBUTION` or `RELEASE_NODE` (your own `rel/env.sh.eex` overrides the default). A stock `mix phx.new` app, which already starts `DNSCluster`, joins up as soon as the switch is on.

Set a stable `RELEASE_COOKIE` as a [secret environment variable](secret-env-vars.md) if instances from different builds must talk to each other.

## Troubleshooting

For problems that aren't specific to Elixir, such as an app that never answers, see [Apps troubleshooting](apps.md#troubleshooting).

**`mix: not found` or `could not find a Mix.Project`** in a release command, worker or one-off command. The image is an OTP release with no Mix. Use one of the commands under [Migrations and other commands](#migrations-and-other-commands).

**`** (RuntimeError) environment variable X is missing.`** at start. `config/runtime.exs` requires a variable the app doesn't have. Set it on the **Resources** tab, or attach the service that provides it and rename its variables to the names your code reads. Setting a variable applies to the running app, with no redeploy.

**`Could not check origin for Phoenix.Socket transport`**, with LiveView reconnecting in a loop. `PHX_HOST` doesn't match the hostname the browser used. Clear `PHX_HOST` so Nimbus Land fills it, set it to the exact hostname visitors use, or list every hostname in `check_origin` as shown under [Hostnames and PHX_HOST](#hostnames-and-phx-host).

**`Unknown CA` or `{:tls_alert, {:unknown_ca, …}}`** connecting to the database. Pass `DATABASE_CA_CERT` as `cacerts` as shown in [Databases and TLS](#databases-and-tls), or remove the `ssl` option.

**`mix deps.get` fails on a missing file.** Your `mix.exs` reads another file. See [Faster rebuilds](#faster-rebuilds).

**The build ran out of memory.** The failure reason names the limit. Trim the build, or build with your own [Dockerfile](dockerfile.md).
