Browse docs All docs
Docs / Rollback reference
Guidedeploymill://guides/rollback

Rollback

DeployMill supports image-swap rollback: after a bad deploy, swap the running container back to the image of a previous deploy in seconds, with no rebuild required.

How it works

When rollback: true is set in .deploymill/project.json and reconcile_project has been run:

  1. Reconcile wires up a container registry on the app (using credentials configured on the server).
  2. It enables rollback recording on the application.
  3. The next deploy builds the image as normal, then pushes it to the registry and creates a rollback record (with a rollbackId).
  4. list_deployments exposes each deploy's rollbackId, and flags the one the workload is currently serving with live: true.
  5. rollback swaps the running image to whichever rollbackId you pass. The platform pulls the image from the registry and restarts the container.

Knowing what's live (live: true)

list_deployments marks at most one record live: true: the deploy whose image the workload is actually serving right now (its image is the live spec and the rollout has converged + is healthy). This is what you'd be rolling back from. It's derived from the live cluster, not record order, so it stays correct in the cases where "newest = live" misleads:

  • After a previous rollback, the served image is an older deploy, so live: true lands on an earlier entry, not the top of the list.
  • During an in-flight deploy, the new record isn't live until it converges. Its status walks building (image building) → deploying (built, but the container swap + health gate haven't converged, so the old image is still serving) → done. Until it reaches done, no record is marked live. That's the point: the build finishing is not the new code going live. Wait for status: "done" (or live: true) before trusting the new version is serving.

If nothing is deployed yet (or the live image can't be read), no entry is marked. Use live/status: "done" to confirm what's serving before and after a rollback, and don't roll back to the entry that's already live.

Enabling rollback

  1. Set "rollback": true in .deploymill/project.json.
  2. Commit + push.
  3. Run reconcile_project with the app's applicationId and repoUrl. Reconcile flips the rollback toggle and configures the container registry.
  4. Run deploy. This is the first deploy whose image will be available to roll back to. Deploys made before enabling rollback have no captured image.

Requires the container registry to be configured on the DeployMill server. If it's not, reconcile fails with a clear error.

Performing a rollback

1. list_deployments({ applicationId })       → the entry with live:true is what's serving now; pick a different healthy (status:"done") deploy to revert to and copy its rollbackId
2. rollback({ applicationId, rollbackId })   → image swap completes in ~seconds

list_deployments returns the most-recent records only (a bounded window, default 20). On an app with deep history, page into older deploys with offset (the response's hasMore/nextOffset tell you when and where) if the target deploy isn't in the first window.

The swap is non-destructive to the registry: the current image stays available, so you can roll forward again (rollback to the deploy you just rolled away from).

What rollback does NOT do

  • Doesn't roll back database migrations. If the deploy you're reverting from added a column or table, the image you're rolling back to will likely throw at startup. Treat rollback as for code-only changes; schema changes need a forward fix or a manual alembic downgrade / node-pg-migrate down.
  • Doesn't roll back env vars. set_env_vars is not history-tracked. If a bad deploy went out with a wrong env var, fix the env var first (set_env_vars again), otherwise the rolled-back code will hit the same bad value.
  • Doesn't roll back data writes. Reverting code that corrupted data only restores the code. The data stays corrupted.
  • Doesn't roll back mounts or domain changes. Those are app-level config, not per-deploy.

When to use rollback

  • ✅ A code change broke a route or threw an exception at startup. Roll back, fix in a PR, redeploy.
  • ✅ A perf regression you can't immediately diagnose. Roll back to buy time.
  • ✅ A misconfigured runtime behavior (wrong feature flag default, wrong static file).
  • ⚠️ A schema migration broke things. Rollback alone may leave the schema ahead of the code. Usually you need a forward fix + redeploy, or a manual downgrade first.
  • ❌ Data corruption. Restore from backup, not from a rollback record.

Automatic rollback (self-healing)

Set "rollback": "auto" (instead of true) in .deploymill/project.json, commit, reconcile. This enables rollback recording exactly like true, and arms post-deploy self-healing: when a deploy builds and swaps a new image but the health gate comes back unhealthy, deploy automatically reverts to a known-good image, with no second tool call from you.

  • The health gate is the trigger. Auto-rollback identifies the ready, non-terminating Service endpoint that belongs to the new deployment when the compute backend can do so, and probes the configured path through the public edge. A target release that never gains an endpoint still rolls back within the configured bound. A 200 mixed only with transient gateway or network failures during a confirmed old-pod drain counts as edge convergence; no 200 or an application error still fails. See deploymill://guides/health for the full contract.
  • You don't pass anything to deploy; the intent is persisted by reconcile_project and deploy acts on it. reconcile's plan.rollback.auto shows whether it's armed.
  • **Reverts to the last healthy deploy, not just the previous one.** deploy records each deploy's health verdict; auto-rollback walks back to the most recent earlier deploy that was recorded healthy (falling back to the most recent earlier image when nothing has a recorded result). So stacking two broken deploys won't land you on the second-broken one.
  • deploy's response carries an autoRollback object and an autoRollbackNote:
    • { attempted: true, toLastHealthy: true, recovered: true, … } → reverted to the last recorded-healthy image and it recovered. Fix forward in a PR.
    • { attempted: true, recovered: false, … } → reverted but it's still unhealthy (degraded). Something broader is wrong (DB, dependency, or the target image is also bad). Investigate with get_logs / get_app (its health block).
    • { attempted: false, reason: "no_rollback_point" } → there was no earlier captured image to revert to (the first deploy after enabling rollback). The bad image is still live; fix forward.
  • It triggers when the new workload never gets a ready endpoint or its bounded edge-health check fails. Backends that cannot identify release instances retain the all-edges-failed fallback. It reverts at most once. Workers have no HTTP health target, so edge-triggered auto-rollback never fires for them.
  • Orchestrator-level gate (recommended). Declaring a health block also wires an orchestrator-level health gate (a stack-independent TCP port-readiness probe), so the platform won't cut over to, or complete the rollout on, a new container whose port never starts listening, and self-heals (restarts/holds) at the orchestrator layer before our edge probe even runs. The strict HTTP /healthz check stays at the edge probe and is what trips auto-rollback. See deploymill://guides/health.

Disabling rollback

Set "rollback": false in .deploymill/project.json, commit, reconcile. The next deploy won't push to the registry; existing rollback records remain queryable but new ones won't accumulate. Disabling also disarms auto-rollback.

What NOT to do

  • Don't rollback past a migration. Check what shipped in the gap; if a migration ran, forward-fix instead.

Troubleshooting

  • list_deployments shows no rollbackId on a deploy with status: "error" → that deploy failed before it pushed an image, so there is nothing to roll back to. Read get_logs on its deploymentId to see why, and pick an older status: "done" deploy as the revert target.
  • A deploy you canceled isn't in list_deployments at all → that's by design, not a lost record. Failed deploys are kept as history (status: "error", null rollbackId), but canceled ones are filtered out entirely — they're not counted in total either, so don't page looking for one. cancel_deploy's own return value is the confirmation that the deploy was aborted, and a canceled deploy is never a rollback target. (get_logs and get_timeline do still see canceled records.)
  • list_deployments shows no rollbackId on recent successful deploys → rollback was never enabled, OR the deploy ran before reconcile turned rollback on. Confirm with a dryRun: true reconcile to see the current toggle state.
  • rollback fails with auth error → registry credentials expired or were rotated on the server. The operator needs to refresh them.
  • Rolled-back container won't start → likely a schema mismatch. The image is older than the current DB schema. Forward-fix or manual downgrade.