Deployment History & Rollback

Every deploy is logged. Every deploy is reversible. The history layer (lib/history/) records who deployed what to where, when, and what changed; the rollback path puts the previous version back without you needing to remember image tags.

What gets recorded

For every ./portoser deploy ..., Portoser writes a record containing:

  • Deployment ID (e.g. deploy-20260315-143022-a3f2) — sortable timestamp + short hash
  • Service(s) deployed
  • Target machine(s)
  • Deployment type (docker, local, native)
  • Image tag / commit SHA / version before and after
  • Health check outcome (passed, failed-and-self-healed, failed-hard)
  • Duration
  • User (whoever invoked the CLI or web UI)

Records live in ~/.portoser/history/ as JSON, indexed by timestamp.

Inspecting history

./portoser history list                              # last 50 deploys
./portoser history list reports-api                  # filter by service
./portoser history list --json-output                # for scripting

./portoser history show deploy-20260315-143022-a3f2  # full record for one deploy
./portoser history compare ID1 ID2                   # diff between two deploys
./portoser history stats                              # success rate, mean duration, recent failures
./portoser history stats reports-api 30              # last 30 days for one service

The web UI's deployment history page renders the same data with timeline scrubbing and click-through to per-deploy detail.

Rolling back

./portoser history preview deploy-20260315-143022-a3f2
# Shows what would change if you rolled back to this deploy:
#   - Image tag changes
#   - Compose file diff
#   - Affected dependents

./portoser history rollback deploy-20260315-143022-a3f2
# Walks the change in reverse — re-deploys the previous version

The rollback path is the same code as deploy; it just targets the old image / commit / config. So all the safety from a normal deploy applies: dependency check, health verification, self-healing if something flaps. If the rollback itself fails health, Portoser stops and surfaces the diagnostic — it doesn't double back further automatically.

--force skips the dependency check, useful when the dependent services are already broken and you just need the rollback to land.

What "rolling back" actually does, by deployment type

docker — image tag swap

The previous image tag is in the history record. Rollback re-pulls that tag, brings up the old container, and waits for health. The current container is stopped first; if rollback health fails, Portoser will log it but won't auto-revert-the-revert.

local — git ref swap + redeploy

Rollback checks out the previous commit (history records the SHA), runs uv sync, restarts the process. Your service repo needs to actually contain the previous version — no commit, no rollback target.

native — config rewind

For systemd or launchd services, rollback regenerates the previous unit / plist content and reloads the service manager. This is the trickiest type — anything done outside the unit file (a manual systemctl edit override, a launchctl-loaded plist that was modified by hand) is invisible to rollback.

Cleanup

History grows. The cleanup subcommand trims it:

./portoser history cleanup            # keep defaults: last 200 records or 90 days, whichever is more
./portoser history cleanup 100 30     # keep last 100 records or 30 days

Records older than the policy are deleted from ~/.portoser/history/. The rollback target window shrinks accordingly — you can't rollback to a record that no longer exists.

What history is not

  • Not a backup system. It records what was deployed, not the data your services manage. Database backups are a separate concern; see your database's tooling.
  • Not version-controlled config. If you change registry.yml and commit it, that's git. History records what got applied, not what was edited.
  • Not blue-green. There's no "two versions running side by side" mode. Deploys replace, rollbacks replace. If you want gradual rollouts, you wrap that at the application layer.

Where this earns its keep

  • A deploy fails health, the self-healing loop tries its patterns, none stick. You ./portoser history rollback <previous-id> and you're back in 30 seconds.
  • A change in service A breaks dependent service B. ./portoser history compare shows the diff, you rollback, then think about whether you wanted that change at all.
  • A team member's deploy is causing trouble overnight. You ./portoser history list --json-output | jq '.[].user' to see who, then rollback.

Next