Update & upgrade
dagron migrates its own schema at engine startup and upgrades in place: back up, swap the binary or image, start. This page covers the order to roll a deployment, what a failed migration actually does, and how to go backwards.
Versioning policy
dagron follows Semantic Versioning and keeps a
changelog in the
Keep a Changelog format. One rule matters more than the rest:
pre-1.0, a minor version bump means breaking changes. Treat 0.5.x → 0.6.0
the way you would treat a major-version upgrade elsewhere.
Release notes call out breaking changes explicitly and note when a version ships schema migrations. Reading the changelog is step 2 of the upgrade procedure below, not optional homework: it tells you whether the release migrates the database, and whether any API or configuration contract moved.
Never skip a changelog read on a minor bump. Until 1.0, patch releases (0.5.1 → 0.5.2)
are the only ones guaranteed non-breaking.
How schema migrations work
Four facts decide every procedure on this page.
- Migrations are embedded in the binary and run automatically at startup. The engine applies them
(via sqlx) before the scheduler serves anything. There is no
dagron migratesubcommand — the only way to migrate is to start an engine. Starting the new binary is the migration step. - They are forward-only. There are no down migrations, so “rolling back a schema change” is not a thing. Rollback means restoring the pre-upgrade backup and starting the old binary.
- A failed migration is fail-fast, not half-applied. Each migration runs in one transaction; a failure rolls the transaction back, the engine exits non-zero before serving, and the database is left exactly where it was. There is no partial state to clean up.
- Older binaries tolerate a newer database. Migrations are additive, and the migration ledger is applied with ignore-missing semantics, so an engine that does not ship version N tolerates finding N already applied. That is what makes a rolling upgrade survivable if you have to stop halfway — it is not a licence to run mixed versions permanently, since older code simply ignores newer columns.
One deliberate asymmetry: dagron-api does not run migrations. It only creates its own
tables with CREATE TABLE IF NOT EXISTS, so it starts happily against any database — including one
restored from a partial dump, where it recreates its tables empty. Always back up and restore the whole
database, never a table subset.
Upgrade procedure — single node
The stack upgrades in place: stop, swap the image or binary, start. In order:
- Back up. Always, before every upgrade — it is the only way backwards (see Rollback).
- Read the changelog. Note any migration or breaking change listed for the version.
- Stop ingestion. Pause schedules and stop whatever feeds the queue or submits runs.
- Let running work drain — or accept reclaim: task leases expire (default 30 s) and work is re-dispatched on restart, so tasks must be idempotent.
- Stop the engine and swap the binary or image.
- Start the engine. It applies any pending migrations at startup, inside a transaction, before serving.
- Verify. Exit code 0,
GET /healthzon the ops API answers, and the migration ledger shows the expected version (panel below). - Upgrade
dagron-api, then the frontend, then resume ingestion.
# Postgres — nightly, plus before every upgrade $ pg_dump -U dagron -d workflow -Fc -f "workflow-$(date -u +%Y%m%dT%H%M%SZ).dump" # SQLite — safe while the daemon is running (WAL) $ sqlite3 workflow.db ".backup 'workflow-backup.db'"
Never copy a running SQLite workflow.db file by itself — the -wal/-shm
sidecars carry committed data. Use .backup as above, or stop the daemon and copy all three files.
$ psql -U dagron -d workflow -tAc \
"SELECT count(*)||' migrations, max='||max(version) FROM _sqlx_migrations;"Compare the count and max version against a healthy install of the same release. If the engine exited 0
and /healthz answers, the migrations applied — a failed migration never leaves the engine
running.
Upgrade procedure — multi-node and Helm
Multi-node engines (Postgres backend) coordinate only through the database — there is no RPC between
nodes. That is why a rolling restart does not lose work: a stopped node’s task leases expire
(default 30 s) and any surviving scheduler reclaims and re-dispatches the orphaned tasks. Singleton
duties (cron, GC, DB-backed schedules) run on one leader; if the leader restarts, another node takes over
when the leadership lease expires (LEADER_LEASE_SECS, default 30). See
Scaling & HA for the model.
The order is the same as single-node, with one addition that matters:
- Back up, read the changelog, stop ingestion, drain or accept reclaim (steps 1–4 above).
- Upgrade ONE engine first. It applies the migrations at startup.
- Verify it — exit code 0,
GET /healthz, ledger check — before touching the rest. - Roll the remaining engines, then
dagron-api, then the frontend. - Resume ingestion.
Why one engine first: migrations are additive and applied under a transaction, but two engines starting simultaneously against an unmigrated database both try to apply the same set. One wins; the other may error and exit. Roll one, confirm, then the rest. Mixed versions during the roll are expected and safe for the duration of the roll — the schema is a superset and coordination is through database rows.
Helm
The chart pins matching image tags for every component, so moving the chart version moves the whole stack together:
$ helm upgrade dagron oci://registry-1.docker.io/mancube/dagron \
--version <new-version>Kubernetes rolls the engine pods; the first replacement engine to start applies the migrations, and the lease/reclaim behaviour above covers tasks in flight. The same caveat applies: tasks must be idempotent, because a reclaimed task may be re-dispatched.
Compose
The quickstart compose file pins every image to DAGRON_VERSION. Upgrading is setting the new
version, pulling, and re-upping — after the same backup and changelog steps.
Rollback
Migrations are forward-only — there are no down migrations. Rollback = restore the pre-upgrade database backup and start the old binary. Everything written after that backup — runs, task history, schedule state — is discarded with it. This is why step 1 of every upgrade is a backup.
Two cases, in order of likelihood:
- The migration never applied (the new engine failed at startup). The transaction rolled back and the schema is still at the previous version — just start the previous version’s binary. Nothing to restore, no partial state to clean up.
- The migration applied and you still need to go back. Restore the pre-upgrade backup, then start the old binary against it. An old binary against a newer database usually starts fine (ignore-missing, additive migrations), which can buy you time mid-roll — but it is a stopgap for finishing or unwinding a roll, not a supported steady state.
After any restore, runs that were running at backup time come back with expired leases; the
reconcile loop reclaims and re-dispatches them. If a run must not re-execute, cancel it before starting the
engine.
When an upgrade fails
The migration errors partway
The engine exits non-zero and the transaction rolls back, so the database stays at the previous version. Then:
- Read the error — it names the failing statement.
- If it is data-dependent (a constraint an existing row violates), fix the data and restart the engine. The migration re-runs from the top.
- If the migration itself is wrong, run the previous version’s binary — the schema is still where it was — and report it.
- Restore the pre-upgrade backup only if 1–3 leave you stuck; there is no partial state to clean up.
“previously applied but has been modified”
Startup fails with an error of this shape:
Error: migration 40 was previously applied but has been modified
The binary’s copy of that migration differs from the checksum recorded when it was applied. The
engine exits 1 and does not serve — the database is untouched, so there is no urgency to
“fix” the DB. Fix the binary. Real causes, in order of likelihood:
| Cause | Fix |
|---|---|
| edited migration file | Someone edited a released migration. Revert the edit and ship a new migration instead — never edit an applied one. |
| mismatched builds | Two builds from different commits (a fork, a patched vendor image). Deploy the binary whose migrations match, or roll forward with a build that supersedes the change. |
| tampered ledger row | The recorded checksum itself was altered. Restore from backup. |
Editing _sqlx_migrations to match the new checksum makes the error go away and leaves the
schema silently not matching what the code expects. Only do it when you have confirmed the two migration
bodies are semantically identical — and write down that you did.
Startup mentions an unknown or newer migration
Expected and tolerated: the ledger runs with ignore-missing semantics, so an engine that finds migration rows it does not ship (a newer release’s, or another edition’s) starts normally. If you see this as an error rather than a startup, you are on a build predating that fix — upgrade.
Where releases come from
Every release is tagged v*.*.* and published three ways.
Prebuilt binaries — GitHub Releases
GitHub Releases carries prebuilt archives
of the dagron binary (default build: SQLite + ops API) plus a single SHA256SUMS
file covering the whole set, so a download can be verified without trusting the transport. Linux builds are
statically linked against musl, so they run on any distro.
| Target | Archive |
|---|---|
| x86_64-unknown-linux-musl | tar.gz |
| aarch64-unknown-linux-musl | tar.gz |
| aarch64-apple-darwin | tar.gz |
| x86_64-pc-windows-msvc | zip |
Only the root dagron binary is published as an archive — it is the one a person runs
directly. dagron-api, the GitOps worker and the MCP server are services and ship as images.
Platforms not listed (e.g. Intel macOS) can build from source with Cargo from the
repository.
Container images — Docker Hub
Multi-arch (amd64/arm64) images publish under mancube/ on Docker Hub:
mancube/dagron-engine, -api, -frontend, -gitops, and
-mcp. A stable release vX.Y.Z tags each image :X.Y.Z,
:X.Y, and :latest. Pin the full version tag in production so upgrades happen when
you run the procedure above, not when a pull happens to land.
Helm chart — OCI
oci://registry-1.docker.io/mancube/dagron
The chart version matches the release version, and the image tags inside the chart are pinned to the
same version — one helm upgrade --version moves everything in step.