CLI Reference
rollback
Returns every role on every host to a previously deployed version.
What this does not restore
rollback only re-runs the app deploy path — it starts app containers from an older image, on every role and host. It never touches databases, migrations, volumes, or dependency containers (db, redis, ...): those live under a separate dependency command and separate config, and nothing about a rollback reaches them. If a deploy went bad because of a migration, rolling back brings the old code but not the old schema. It also runs with today's secrets and environment, not whatever was active on the deploy you're rolling back to — you get the old image with the current configuration, not a snapshot of both.
Synopsis
odysseus rollback [VERSION] [--list] [--config FILE] [-v]
With VERSION, that becomes the target, checked against what every host actually has. Without it, odysseus picks the previous version for you — see Choosing a target for exactly what "previous" means, because it is not always the version you last deployed.
Unlike deploy, rollback needs no git repository and no checkout on the machine you run it from — just deploy.yml and SSH access to the hosts. Worth knowing if you're reaching for whatever machine is closest, mid-incident.
Not sure what to roll back to? Run --list first.
odysseus rollback --list shows, per host, what has been deployed, what images remain, and what is currently serving. It only reads — no image is pulled, no container is started or stopped, nothing is written anywhere. It is the safe first move when a deploy has just gone wrong and you are not yet sure what "back" should mean. --list ignores a VERSION argument if you also pass one; the two are mutually exclusive in practice, not just in intent.
Choosing a target
With no VERSION
odysseus considers every version any host's deploy log has ever recorded for this service — newest deploy first, across all hosts combined — and picks the newest one that every host currently has an image for and none is currently serving. That last condition matters: the previous version is not "whatever ran before this one," it is the newest candidate that isn't already running anywhere, so a fleet that already partially rolled back doesn't get told to roll back to where it already is. It also means a partial rollback has a trap: see If a rollback fails partway below.
If no host has a deploy log at all for this service, there is no history to rank by. odysseus falls back to the first host's image list, newest-first, excluding latest — a moving pointer, not a version — and warns that the ordering is approximate, because image creation time is build time, not deploy time, and images can reach a host out of order.
Only versions deployed by odysseus 0.4.2 or later have a deploy-log entry at all; earlier deploys were tagged :latest, so no image identifies them individually and the automatic fallback above skips them.
With an explicit VERSION
The version you name is trusted as the target outright — odysseus infers nothing about ordering, and it doesn't have to come from a logged history at all. It still has to be present as an image on every host. If any host lacks it, the rollback refuses before touching a single host, naming which hosts are missing it. latest is only excluded from the automatic pick above; naming it explicitly (odysseus rollback latest) is checked like any other version and runs if it passes.
If no host has a deploy log, an explicit VERSION still works; you just get a plain warning that there's no record of what's been deployed here, rather than the "approximate ordering" warning above — naming the version explicitly sidesteps the ranking that warning is about.
Where candidates come from
Every candidate comes from a deploy log, one plain-text file per service that each host keeps of what has been deployed to it. It's why doctor checks the deploy log's location as one of its five checks: if that check warns, rollback isn't broken, but it will quietly offer fewer versions than it could, because history written before the log moved isn't being read.
Hosts can disagree
Candidates for an unversioned rollback are pooled from every host's log, so a version only one host ever recorded is still in the running. But the target that's actually chosen still has to pass on every host — present as an image everywhere, not currently serving anywhere — so a host with a thinner history (one that missed a deploy, or was rebuilt) can rule out a version the rest of the fleet would happily accept.
--list doesn't hide this: it prints one section per host, each with its own rows, so a host that missed a deploy visibly has fewer rows and a different "Serving" line than the rest. If you name a version explicitly and it's missing on even one host, the refusal names exactly which hosts don't have it — nothing is changed on any host when that happens.
If a rollback fails partway
A rollback works through every role and host in order, the same way deploy does, and stopping partway is not undone: hosts already rolled back stay rolled back, the host that failed is left as it was when the failure happened, and every host after it in the list is never touched. Nothing makes the whole operation atomic.
Re-running odysseus rollback with no VERSION after a partial failure has a trap: the target version is now serving on the hosts that succeeded, so the automatic pick's own "not currently serving anywhere" rule (above) skips it and offers something older instead. To finish the job on the hosts that didn't get rolled back, name the target VERSION explicitly — that check doesn't apply to an explicit target.
What a version can tell you
--list's Ref and Deployer columns depend entirely on how each version was deployed:
- Deployed from a clean git repository, with no
--image— the proper way — odysseus tags the image with the commit SHA (that's the Version column) and records the branch it was on, plus who ran the deploy, in the host's log. - Deployed with
--image TAG, on odysseus 0.8.0 or later, still records who ran it — that never depended on a commit — but records no branch or commit, because an arbitrary tag identifies neither. Its Ref column reads-. Before 0.8.0, an--imagedeploy recorded neither the deployer nor a ref, so older log entries show-in both columns — that's not a bug, just history from before the fix.
The sharper cost shows up if a tag gets reused: the version is the tag, so two deploys of the same --image TAG leave two log entries odysseus cannot distinguish. --list shows one row per version, most-recently-deployed, so the earlier deploy's identity is simply gone — there is no way to ask "which one of these was actually running last Tuesday." A commit-tagged version can't collide this way; the tag is the commit, and the commit changes every time the code does.
Output
--list
Odysseus Rollback
Service: myapp
▸ app1.example.com
Serving: f00dcafe1234
Version Deployed Ref Deployer Image
──────────── ──────────────────── ────── ────────────────── ───────
a1b2c3d4e5f6 2026-08-14T09:02:11Z main ops@example.com present
f00dcafe1234 2026-08-10T13:44:02Z - thomas@example.com present
v1.0.0 2026-08-01T10:00:00Z - - missing
▸ app2.example.com
Serving: f00dcafe1234
(no deploy history on this host)
"Serving" prints the bare version label a host is actually running — no image name in front of it. a1b2c3d4e5f6 was a git-commit deploy from branch main; f00dcafe1234 was an --image deploy that still recorded a deployer (0.8.0+); v1.0.0 predates that fix, so both Ref and Deployer are unknown, and its image has since been pruned from this host.
app2.example.com has no logged history at all — perhaps it was rebuilt — so its section shows only the "Serving" line: an unversioned rollback still has to find a version present on it too, or refuse.
odysseus rollback (no VERSION, web role shown)
Here a1b2c3d4e5f6 is the automatic pick: newer than f00dcafe1234, present everywhere, and not currently serving anywhere (f00dcafe1234 is what's serving, per the --list output above).
Odysseus Rollback
01 ✓ Checking what every host can run
Service: myapp
Rolling back to: myapp:a1b2c3d4e5f6
Commit: main
02 > Rolling back service
02 ✓ Starting Caddy
02 ✓ Starting container
02 ✓ Health check
02 ✓ Caddy route update
02 ✓ Draining old container
✓ Rollback complete in 34.2s
The Commit line prints the branch odysseus recorded for that version (see What a version can tell you) — it only appears when the target has one. Every role on every host shares step 02, streamed one sub-step at a time; a fleet with more roles and hosts repeats these same sub-step lines once per role@host, in order, not all at once. A jobs role skips the two Caddy lines, since only web roles sit behind the proxy.
A rollback re-runs the deploy path on every role and host — starting a container from the image the pre-flight already confirmed is present, waiting for health checks, updating routing — so it takes roughly as long as a normal deploy, minus build and transfer time. Nothing here pulls an image: that presence check is exactly what makes a pull unnecessary.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Rollback completed on every role and host |
| 1 | No valid target could be determined, a host was missing the target image, or a host failed mid-rollback |