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 keys off the same health-endpoint contract deploy/rollback/get_app (its health block) use: a deploy whose health endpoint (default /healthz) doesn't return 200 within retries consecutive attempts is unhealthy. See deploymill://guides/health for the contract, the health config block, and the strict-vs-lenient / 404-fallback rules. Put your real readiness checks in /healthz so "healthy" means what you need it to mean.
  • 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 only when the new image is live but all edges fail the health gate (a single-domain blip won't trip it), and it reverts at most once (no flapping). Workers have no domains, so 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 recent 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.