F7 · the database · module 3
The database goes live
F7.2 made the database host resolve: runtime.exs consumes
ECTO_IPV6 and Postgrex connects over Fly’s IPv6 network. A reachable database is
still not a working one. On the first bring-up portal existed on
portal-postgres but carried zero tables — a
DATABASE_URL that is set is not a schema that exists. And the boot does not degrade on an
empty database: Portal.Engine.init/1 reads the event log synchronously at
startup, so a reachable-but-unmigrated database crashes the whole release on
relation "events" does not exist, and Fly suspends the machine after ten restarts. This
module closes the gap between “the connection string resolves” and “the boot’s database read
succeeds.” A built release carries no mix, so the schema is applied release-native
through Portal.Release.migrate/0.
Three dives: how a release with no mix runs its migrations through
Ecto.Migrator, idempotently; the empty-database trap and the fly
proxy path that applies a schema against the writable primary before the app can boot; and the synchronous
boot-time database coupling — observed here, its hardening named, the engine left untouched.
Reachable and migrated
The boot has one hard precondition with two parts, and missing either looks almost the same at the crash site.
Portal.Engine.init/1 folds the Portal’s state by replaying the event log, and it
does so synchronously at startup: init/1 calls
Portal.Engine.load/0, which calls the event store’s
read_stream/1 — a real SELECT … FROM events under
the connection pool’s four-second queue_timeout. If the database is unreachable the
read times out; if it is reachable but unmigrated the read hits a table that does not exist. Either way
init/1 raises, the child fails to start, the release exits, and Fly suspends the machine
after ten restarts. Select a database condition to see what the boot’s read does.
The boot’s synchronous read · select a database condition
A boot survives only when the database is both reachable (F7.2’s address-family fix) and migrated (this module). The two failures crash in the same family — a child that will not start — which is exactly why the condition has to be read, not guessed.
Triggering the migration
A built release does not carry mix — it ships the compiled BEAM files and a
bin/portal script, not the Mix archive — so mix
ecto.migrate is not a command the deploy machine has. The schema is applied release-native instead, through
Portal.Release.migrate/0, which loads :portal, starts the
repository in isolation through Ecto.Migrator.with_repo, and runs every pending
migration — without booting the full supervision tree. Three triggers reach that one entry point; the
fourth does not exist on a release. Select a trigger to see where it runs and what it invokes.
Four triggers, one entry point · select a path
Whichever trigger fires, the entry point is the same Portal.Release.migrate/0,
and it is idempotent: Ecto.Migrator consults schema_migrations and
applies only versions not yet recorded, so a re-run on every deploy is safe. Wiring a
release_command for auto-migration is the Operator’s call — F7.4 owns the
stanza.
Three deep dives
In order: the release-native migration path by symbol — Portal.Release.migrate/0,
rollback/2, and the idempotency that makes them safe to repeat; the empty-database trap
and the fly proxy procedure that applies a schema against the writable primary; and the
synchronous boot-time database coupling, documented with its hardening named as a carried item that touches the
engine and so stays unbuilt here.
mix, how Portal.Release.migrate/0 loads :portal and runs Ecto.Migrator.with_repo without the supervision tree, rollback/2, the migrate-before-start contract, and idempotency through schema_migrations.fly proxy path DATABASE_URL is not a created schema; fly proxy 15432:5432, the writable-primary check (pg_is_in_recovery() → f), the CREATE-privileged role, and the .internal-versus-.flycast host-scheme discriminator.init/1 → load/0 → read_stream/1 path, why an unreachable or empty database is a hard crash and a suspend-after-ten, the stacked precondition, and the lazy-replay hardening named as carried — below the facade, so not built here.F7.2 resolved the host
runtime.exs consumes ECTO_IPV6, so Postgrex reaches the AAAA-only database host over IPv6 and connects.
F7.3 migrates the schema
Portal.Release.migrate/0 applies both versions, so the boot’s read_stream/1 returns instead of crashing on a missing table.
Start with release-native
migration, then the empty-database trap and the proxy
path, then the boot-time coupling. This is the third rung
of F7; once the schema is live, F7.4 ships fly.toml and the
deploy mechanics that can automate this migration. It builds on
F6.08 — Auth, deployment & going live, which produced
Portal.Release.migrate/0 and the two migration files this module takes live.
References
The release-native migration mechanism, the migration DSL that defines the two tables, the
fly proxy path to a Postgres app, and the primary-versus-replica check.
Sources
- Phoenix — Deploying with releases — the
Releasemodule pattern, running migrations throughevalbecause a release carries nomix. - Ecto — Ecto.Migrator —
with_repo/2,run/4, and theschema_migrationsbookkeeping that makes a re-run idempotent. - Ecto — Ecto.Migration —
create table,add, andindex/unique_index, the DSL behind the two migration files. - Fly.io — Connecting with flyctl —
fly proxyforwarding a local port to a Postgres app over.internal. - PostgreSQL — System administration functions —
pg_is_in_recovery(), which returnsfon the writable primary andton a read-only replica.
Related in this course
- F7.3.1 · Release-native migration
- F7.3.2 · The empty-DB trap & the fly proxy path
- F7.3.3 · The boot-time DB coupling
- F7.2 · Runtime config & the IPv6 network — resolves the database host this module migrates over.
- F7 · Deploy to Fly — the chapter overview and the deploy ladder.