Today I published FlyDeploy, an Elixir package providing mix fly_deploy.hot for hot code upgrades of your Elixir and Phoenix applications. This allows you to deploy changes to your running apps without breaking websocket connections or restarting processes. It’s particularly useful for stateful apps and Phoenix LiveView UIs.
Let’s see it in action:

FlyDeploy implements the same approach of Erlang’s release handler, where processes running changed code are suspended, code_change/3(optional callback to migrate state) is invoked, and processes are resumed. This allows any OTP primitive, ie GenServers, Phoenix LiveViews, etc, to upgrade instantly and maintain (or migrate) state. For Phoenix LiveView apps, FlyDeploy automatically dispatches messages to affected LiveViews to re-render, so end-users get the latest UI for your app in realtime without state resets or hard refreshes.
While limited compared to true OTP releases, FlyDeploy strikes a balance where you can hot upgrade most day-to-day code changes, and fallback to fly deploy whenever you change deps, supervision tree structure, or elixir versions.
You also get the best of both worlds. If you introduce a bad upgrade, you’re always a cold fly deploy away from a new baseline to continue upgrading from.
How it works
Under the hood, this library simply leans on existing fly primitives – the fly cli, the machines API, and tigris object storage. mix fly_deploy.hot goes like this:
We first run fly deploy –-build-only –-push to build a new container image and push it to the app private registry. This allows the upgrade to share the same Dockerfile “buildserver” setup as your regular deploys, but instead of deploying to your app machines, we skip the deploy.
Next, we fly machine run a “temporary” machine with the newly built container image, except we replace the entrypoint with sleep inf so starting the machine does not actually boot the app. And then we eval into the Elixir release to run some library code inside the temporary machine:
fly machines run –-image #{image_ref} --entrypoint sleep inf --shell --command "/app/bin/yourapp eval 'FlyDeploy.orchestrate(app: :your_app, image_ref: \"#{image_ref}\")'"
--shell opens a shell on the machine and auto --rm’s it when the shell exits. The trick is we eval into the build’s mix release (that we prevented from starting with sleep inf) and the FlyDeploy.orchestrate/1 tars all the current beam files on disk and uploads them to tigris. Next, we call the Fly machine API’s /exec endpoint and tell all the running app machines “hey you have a new hot upgrade at this image_ref”. We do this by rpc’ing to the running Elixir release:
command =
"/app/bin/#{binary_name} rpc \"FlyDeploy.hot_upgrade(\\\"#{tarball_url}\\\", :#{app})\""
url = "https://api.machines.dev/apps/#{app}/machines/#{machine_id}/exec"
Req.post(url, json: %{cmd: command, timeout: 30})
FlyDeploy.hot_upgrade/2 runs on the currently deployed app machines, fetches the tar’d beam files from tigris, then uses some Erlang stdlib functions to detect changed code, suspend affected processes, reload their code, run their code_change callbacks, then resume them.
The only other trick is making sure we reapply hot upgrades on cold restarts since the container resets the upgraded beam files (fly apps restart, VM crashes, host crashes, etc), which is done by 1 line in your application.ex:
# In your Application.start/2
def start(_type, _args) do
# Check for and apply any pending hot upgrades
:ok = FlyDeploy.startup_reapply_current(:my_app)
# Start your supervision tree
children = [...]
Supervisor.start_link(children, strategy: :one_for_one)
end
And that’s it!
You can try it out today by following the Quickstart which takes only a couple steps.
Happy deploying!
–Chris