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:
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. 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
mixat run time. The deploy wizard suggests a release command for you; see Migrations and other commands. General rules are in release command. - Configuration that changes per environment belongs in
config/runtime.exs, notconfig/prod.exs. See Compile-time configuration.
If your repository has a Dockerfile at its root, that wins and the generated build is skipped. See Dockerfile. That is also the way out for anything the generated build doesn't support. For how Nimbus Land chooses a stack, see Apps.
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:
- runs
mix deps.get --only prodandmix deps.compilefrommix.exs,mix.lock,config/config.exsandconfig/prod.exs(plusapps/*/mix.exsin an umbrella), - copies in the rest of your source,
- runs
npm ci(ornpm installwithout a lockfile) inassets/whenassets/package.jsonexists, - runs
mix compile, thenmix assets.deploywhen yourmix.exsdefines it, - 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 inconfig/prod.exsis compiled into the release, so changing it needs a rebuild, not just a restart. - A variable nobody set stays unset. A
config/prod.exsthat readsDATABASE_URLat 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.exsorconfig/config.exsreads a secret-shaped key (one containingSECRET,TOKEN,PASSWORDorPRIVATE, ending in_KEY, or starting withKEY), the deploy plan shows Check this with a note such asconfig/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 generatedruntime.exsdoes:http: [port: String.to_integer(System.get_env("PORT") || "4000")]. - Read configuration in
config/runtime.exs. Nimbus Land setsPORT,PHX_SERVER=true,SECRET_KEY_BASE,PHX_HOST, andDATABASE_URLwhen a database is attached, along with your own variables. - Build exactly one release.
mix releasemust 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: falsefails 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:
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.
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:
# 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:
# 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 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.
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.
` (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.
Unknown CA or {:tls_alert, {:unknown_ca, …}} connecting to the database. Pass DATABASE_CA_CERT as cacerts as shown in 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.
The build ran out of memory. The failure reason names the limit. Trim the build, or build with your own Dockerfile.
Also available as Markdown.