F7.4 · deep dive 3 · the deploy lifecycle & failure modes
One command, then the machine
A deploy is one command and then a sequence the machine runs on its own.
fly deploy builds the F7.1 image and rolls it out: the [deploy]
stanza’s strategy = "rolling" replaces machines one at a time so the service stays
answered, and the [experimental] auto_rollback = true reverts to the
prior release if the new one fails its health checks. After that the machine has a life of its own — it is
started, stopped, or suspended — and one
failure mode dominates: the F7.3 boot coupling surfaces here, so a database that is unreachable or unmigrated makes the
release exit 1, and Fly suspends the machine after ten restarts. Recovery then forks on one
question: is the fault already fixed, or is the fix in the image or config?
We follow one fly deploy end to end: the rolling release and
auto_rollback; the release_command open decision the
[deploy] stanza leaves to the Operator; the started/stopped/suspended
machine states; the suspend-after-ten crash loop; and the recovery fork between a fly machine restart
and a fresh fly deploy — read through fly status,
fly releases, and fly machine status.
The rolling deploy, and the migration it does not run
One fly deploy does two things in order. First it builds the image — the F7.1
Dockerfile, ending at mix release portal with an
ENTRYPOINT ["bin/portal", "start"]. Then it ships that image to the machines under the
[deploy] stanza’s strategy = "rolling": machines are replaced
one at a time rather than all at once, so while one machine takes the new release the others keep answering, and the
service is never fully down during the roll. If the new release fails its health checks, the
[experimental] stanza’s auto_rollback = true reverts to the
prior release, so a bad deploy does not leave the app stuck on a version that cannot pass its
GET /health check.
The chapter’s one open decision lives in this same [deploy] stanza: there is
no release_command. The Dockerfile records the intent in a comment —
“the deploy runs bin/portal eval "Portal.Release.migrate()" (release-native migration,
F6.8.2-D5) BEFORE this start” — but the intent is not wired. So the first bring-up migrated
manually, through a fly proxy 15432:5432 onto the writable primary
(the F7.3.2 path). Wiring release_command = "/app/bin/portal eval \"Portal.Release.migrate()\""
into [deploy] would make Fly run the migration in a temporary VM before the rolling
release on every deploy — the recommended path. Both paths are real; which one ships is the Operator’s call.
Step through the deploy below: select a phase to read what fly deploy does at that point,
and how the two paths — with and without a wired release_command — differ.
The rolling deploy · what each phase does
A rolling fly deploy replaces machines one at a time and self-heals a bad
release through auto_rollback; whether it also auto-migrates first is the one decision the
[deploy] stanza leaves open.
What the machine does after
Once a release is rolled, the machine has a lifecycle of its own. A Fly machine is in one of three states.
started is running and serving; the [[services]]
stanza’s min_machines_running = 1 — paired with
auto_stop_machines = false — keeps at least one machine started so the cluster and the
Presence counts stay warm and a node is not reaped mid-session. stopped is
scaled to zero on purpose; with auto_start_machines = true a stopped machine starts again on
the next request. suspended is the failure state: a machine that crash-looped
past its restart limit is suspended by Fly so it stops consuming restarts — it does not come back on its own.
Select a state below to read what it means, how a machine reaches it, and how it leaves.
Machine states · started / stopped / suspended
A machine is started, stopped, or
suspended: min_machines_running = 1 holds one started, scale-to-zero
and auto-start move it through stopped, and a crash loop past ten restarts is what suspends it.
The crash loop, and the recovery fork
The dominant failure mode is not new to this dive — it is the F7.3 boot coupling surfacing at the machine
level. Portal.Engine.init/1 reads the event log synchronously at startup, so the release
cannot finish booting until the database is both reachable and migrated. An unreachable or unmigrated database makes
init/1 raise, the supervisor reports a failed child, the release exits with
exit 1, and the machine restarts. After ten such restarts Fly gives up: the machine log carries
machine has reached its max restart count of 10, and Fly suspendeds the
machine (gotcha 4). The machine is now stopped consuming restarts, but it is also not serving — it does not
recover on its own.
Recovery forks on a single question: is the underlying fault already fixed, or is the fix in the image or
config? If the fault is already fixed — the database is now up and migrated — then
fly machine restart <id> -a echo-portal un-suspends the machine: the same image boots,
init/1 reads the log cleanly this time, and the machine returns to
started. No rebuild is needed because nothing in the image or config was wrong. But if the fix
lives in the image or config — the infra/inetrc COPY, or a
socket_options: [:inet6] consume that the running release does not carry — then a restart
boots the same broken image and loops again. That case needs a fresh fly deploy: a
fly deploy rebuilds the image and rolls it, shipping both the inetrc and the socket-options fix
in one new release.
Either way, you read the machine’s state before you choose. fly status shows the
app’s machines and the current release; fly releases shows the release history and each
one’s status; fly machine status <id> shows one machine’s state and its last
exit. Pick the recovery path below and read the exact command it names.
# the crash loop, as it appears in the machine log (F7.3 boot coupling, gotcha 4) # Portal.Engine.init/1 reads the event log synchronously; DB unreachable-or-unmigrated: failed to start child Portal.Engine # init/1 raised exit 1 # the release exits; the machine restarts # ... after ten restarts: machine has reached its max restart count of 10 # Fly suspends the machine # read state, then recover: fly status # the app's machines + the current release fly releases # the release history + each one's status fly machine status <id> # one machine's state + its last exit
The recovery fork · restart or redeploy?
The fork is one question: a fault already fixed needs only
fly machine restart on the same image; a fix that lives in the image or config needs a fresh
fly deploy to rebuild and roll it.
restart — same image
fly machine restart <id> -a echo-portal un-suspends a machine whose fault is already fixed
(the database is up and migrated); the same release boots and init/1 reads the log cleanly.
redeploy — new image
fly deploy rebuilds and rolls a new release when the fix is in the image or config — the
inetrc COPY or the socket_options: [:inet6] consume ship only in a new image.
That closes F7.4: the image (F7.1), the runtime config (F7.2), and the migrated database (F7.3) ship as
one rolling, self-healing fly deploy, and the crash this lifecycle surfaces traces back to
F7.3.3 · the boot-time DB coupling. Back to
the module hub; then F7.5 verifies the
composed result is live and gives the day-2 runbook.
References
What fly deploy does and the rolling strategy it runs, the machine states and their
restarts, the [deploy] stanza where release_command and strategy live, and the
read command that shows the app’s machines and current release.
Sources
- Fly.io — Deploy an app —
fly deploy, the rolling strategy, andauto_rollbackon a failed release. - Fly.io — Machines — the
started/stopped/suspendedmachine states and the restart behaviour behind the crash loop. - Fly.io — fly.toml configuration reference — the
[deploy]section whererelease_commandandstrategylive. - Fly.io — flyctl status —
fly status, the read command for the app’s machines and current release.
Related in this course
- F7.4.1 · The distilled fly.toml — the stanzas this lifecycle reads, walked one by one.
- F7.4.2 · Secrets & env provisioning — the secrets a missing one of which crash-loops this same boot.
- F7.4 · fly.toml & the deploy mechanics — the module hub this dive closes.
- F7.3.3 · The boot-time DB coupling — the synchronous
init/1read that is the crash this lifecycle surfaces. - F7 · Deploy to Fly — the chapter overview and the deploy ladder.