Core workflow
Versions & rollback
Promote only healthy deployments and recover binaries together with their environment state.
Versioned builds
Every successful build - whether via phelix build, phelix rebuild, or a zero-downtime deploy - creates a numbered version (v1, v2, v3, ...) rather than overwriting. Versions store both the binary and its paired encrypted env snapshot, so rollback always restores a known-good binary + env pair - never binary-only.
The build-and-deploy ordering guarantee
Versions use a two-phase commit to ensure safety:
- Build succeedsThe version is created on disk (builds/vN/binary, env/vN.enc) and recorded in versions.json with is_current set to false.
- Deploy succeedsThe instance starts and the health check passes: PromoteVersion flips is_current to true and updates the current symlink.
- Deploy failsThe version exists on disk for inspection or retry, but is_current stays false and the current symlink is never moved. The active running instance is untouched.
Version metadata
Each version row in versions.json carries an additive build_report object with the metrics captured during that build:
{ "version": 12, "built_at": "2026-08-26T03:20:00Z", "size_bytes": 15518924, "git_commit": "8f31c2a...", "is_current": false, "build_report": { "language": "go", "compiler": "go", "compiler_version": "1.27", "started_at": "2026-08-26T03:19:29Z", "ended_at": "2026-08-26T03:20:00Z", "duration_ms": 31200, "cache": { "status": "cold", "source": "go-build-cache" }, "artifact": { "type": "binary", "size_bytes": 15518924, "platform": "linux/amd64" } }}The field is optional and additive: versions recorded by older Phelix releases (and Docker-image versions) simply lack it. Missing report metadata only makes that version unavailable for regression comparisons - rollback, rollback listing, status, version loading, retention/pruning, and the current symlink logic all keep working unchanged. A malformed optional build_report payload degrades gracefully to "no report" instead of breaking the version index.
Rollback
phelix rollback reverts to a previous versioned build and automatically chooses the right strategy: classic stop-and-start for plain builds, or zero-downtime for apps deployed with blue-green/rolling.
| Flag | Default | Purpose |
|---|---|---|
| --to | - | Target: v3, 3, or a tag name (bypasses the interactive picker) |
| --list | false | List all retained versions with metadata (non-interactive) |
| --dry-run | false | Preview the rollback plan without changing application, process, proxy, or deployment state |
| --reason | - | Record why the rollback was performed (stored in rollback history; max 500 characters) |
| --verify | - | Observe rollback stability for a Go duration (e.g. 30s, 1m, 2m30s) after the rollback completes |
phelix rollback myapp # interactive picker (TTY), else previous versionphelix rollback myapp --to v2 # roll back to a specific version (no picker, no prompt)phelix rollback myapp --to 3 # version number without 'v' prefix also worksphelix rollback myapp --to hotfix-auth-bug # roll back by tag namephelix rollback myapp --to v3 --dry-run # preview only - no changesThe --to flag accepts either a version ID (v3, 3) or a unique tag name. If a tag matches exactly one version, it resolves automatically. If a tag matches zero or more than one version, an error is returned - use a version ID to disambiguate.
The interactive picker
In a TTY, phelix rollback <App> without --to opens an interactive picker instead of silently choosing the previous version. It lists valid rollback targets newest to oldest (the current version and versions whose binary is missing are excluded), shows built-time metadata per entry, and after you press Enter displays the rollback preview and asks for confirmation before executing.
Rollback 'myapp' Current: v12 Select version to rollback to: ❯ v11 hotfix-auth 2 min ago v10 release-2.4.0 1 hour ago v9 stable yesterday v8 - 3 days ago ↑/↓ select Enter continue Esc cancelMoving the cursor updates a details footer for the focused version, rendered from stored build metadata only - no process is started and no network check runs while navigating. Values that are not stored show -:
v11├── Tag: hotfix-auth├── Commit: 8f31c2a├── Built: 2 min ago├── Binary: 14.8 MB├── Health: -└── Deploy: blue-greenEsc / Ctrl-C cancels cleanly (Rollback cancelled. is printed, nothing is deployed, and it is not reported as a failure). Explicit mode (--to v7) resolves and executes the requested target exactly - no picker, no confirmation prompt - keeping scripts and automation deterministic. In non-interactive environments (CI, redirected stdin) the picker is skipped and rollback falls back to the previous-version default, so automation never hangs waiting for input.
Preview with --dry-run
--dry-run previews exactly what a rollback would do - target resolution, strategy, traffic transition, health checks, and the step-by-step plan - without making any changes. Nothing is started, stopped, switched, promoted, or written: no process, proxy, versions.json, deploy.json, current symlink, or rollback.log mutation, and no rollback audit entry is recorded. It is safe to run any number of times, including while the app serves traffic.
→ Rollback Preview Application myapp Current v12 Target v7 Built 2026-08-31 14:22:10 Commit 8f31c2a Deploy Mode blue-green Health Check Tier 1 (/health, 2xx required) Environment v7 snapshot available Changes: Version v12 → v7 Binary 15.2 MB → 14.8 MB Environment v7 snapshot available Traffic: Public :3000 Current green Target blue Rollback Plan: 1. Ensure the proxy daemon is running 2. Load the v7 artifact (.../builds/v7/binary) 3. Restore the v7 environment snapshot (env/v7.enc) 4. Start the new instance on the inactive slot blue (internal port assigned at startup) 5. Run health checks against the new instance 6. Switch proxy traffic on public port 3000 from slot green to slot blue 7. Promote v7 as current (versions.json + current symlink) 8. Drain and stop the old slot green instance (grace 30s) ✓ No changes will be made.The preview is a plan and validation preview, not a guarantee: it reads the same metadata the real rollback resolves, validates that the target artifact exists, and reports warnings - but it does not start instances, perform live health checks, or predict ports that are only assigned at startup. For classic apps the preview shows the stop-and-start plan and an explicit Downtime: Expected yes line; for rolling apps it lists one replacement step per replica, in the order the real rollback replaces them.
The same target resolution as a real rollback applies, so --dry-run fails on an invalid or missing target with the same error codes a real rollback would return - with nothing modified. Warnings (a missing env snapshot, a large rollback distance, a deploy lock held by another operation) are shown in a Warnings: section without blocking the preview.
Rollback reasons
The --reason text becomes part of the rollback history/audit record, so phelix rollback history explains why each rollback happened:
phelix rollback myapp --to v7 --reason "Login endpoint returning 500"The reason is optional; --dry-run displays it in the plan but persists nothing. Explicitly supplied-but-empty reasons are rejected (rollback reason cannot be empty), whitespace is collapsed, and the text is capped at 500 characters. It is serialized through encoding/json - never concatenated - so it cannot corrupt the structured history or inject log lines. In the interactive picker the reason is requested after the target version is chosen (Enter skips it); an explicit --reason never re-prompts.
Verification windows
A rollback should not count as successful merely because the process started. With --verify <duration>, Phelix observes the application for the requested period after the rollback reaches its committed, traffic-serving state (target loaded, environment restored, instance healthy, proxy switched, deployment state persisted) and fails if it does not remain healthy:
phelix rollback myapp --to v7 --verify 30s→ Verifying rollback stability... 5s ✓ healthy 10s ✓ healthy ...✓ Rollback remained healthy for 30s- Verification observes the instances actually serving traffic - the active blue-green slot, all rolling replicas, or the classic process - never a drained slot.
- It reuses the existing tiered health checks and the per-app health configuration; the duration is the observation window, not a request timeout, and it is parsed as a real Go duration (
30is rejected - use30s). - Verification failure is distinct from rollback execution failure. The CLI prints
Rollback execution: SUCCESS / Verification: FAILEDand exits 23 (ROLLBACK_VERIFY_FAILED), while an execution failure exits 22 (ROLLBACK_FAILED). History records the execution asSUCCESSwith a verification block (passed/failed/cancelled). Ctrl+Cduring the window cancels the observation (history:cancelled); the completed rollback stays active. Phelix never rolls forward or backward on its own - recovery is your call.- Omitting
--verifypreserves the existing rollback behavior exactly; no extra delay is added.--dry-run --verify 30sshows the planned window and performs nothing.
Automatic rollback
Add --auto-rollback to phelix rebuild and a deploy-phase failure restores the previous known-good version automatically - the same path a manual rollback takes, so locks, health checks, proxy switching, state reconciliation, and history are shared, not duplicated:
phelix rebuild myapp --blue-green --auto-rollbackphelix rebuild myapp --replicas 4 --auto-rollback✗ v13 failed health checks→ Automatic rollback enabled → automatic rollback: restoring v12 ✓ v12 started, healthy and serving traffic✓ Deployment rolled back automatically- Failure boundaries. Build/compile failures never trigger a rollback (no new version was recorded, nothing was displaced). Deployment-phase failures do: instance start failure, failed health checks, proxy switch failure, and a partially-completed rolling rollout.
- Canary/progressive. A regression during a rollout is handled by the rollout itself: the stable version is restored to 100% of traffic before the command returns (
CANARY_REGRESSION), so there is normally nothing left for--auto-rollbackto do - it reports "kept serving" instead of faking a rollback. It still covers the corner cases where the rollout's own restore fails. - Blue-green. A failed candidate is killed before the traffic switch, so the previous version usually kept serving the whole time - Phelix reports that ("kept serving") instead of faking a rollback. No history entry is written for a rollback that did not happen.
- Rolling. If the rollout failed after some replicas switched to the new version, recovery redeploys the known-good version over the fleet one replica at a time, preserving availability. If it failed at the first replica, the old fleet is still intact and nothing is redeployed.
- Classic. The known-good binary is copied back and restarted (brief downtime is inherent to classic - Phelix does not claim zero downtime).
- Last known good is authoritative. The restore target is the version
versions.jsoncurrently promotes (a failed deploy never promotes itself), restricted to versions whose binary still exists - never "current minus one". With no known-good version, Phelix says so instead of fabricating a target. - History. Automatic recoveries appear in
phelix rollback historywith theSOURCEcolumn set toautomaticand a reason derived from the deployment failure ("Deployment v13 failed: ..."). Manual rollbacks showmanual. - Recovery can fail too. If the previous version cannot be restored safely, Phelix prints the degraded state explicitly and exits 24 (
AUTO_ROLLBACK_FAILED) - it never claims success. A recovered deployment still exits 21 (the deployment itself failed; the output and history say recovery succeeded). The rollback is triggered exactly once per failed deployment and cannot recurse: recovery failures are terminal, never new rollback triggers.
Rollback history
phelix rollback history <AppName> shows the recorded rollback outcomes for one application, newest first. Every rollback attempt that reaches execution is recorded - successes and failures - at the moment the rollback transaction completes, together with the deployment mode actually used for that rollback (classic, blue-green, rolling). The mode is a historical fact: later re-deploys of the app never rewrite old records. FROM/TO are the versions of that transition, not the app's current version.
phelix rollback history myappphelix rollback history myapp --limit 50Rollback History - myapp TIME FROM TO STATUS MODE REASON2026-09-06 14:20:31 v12 v7 SUCCESS blue-green Login endpoint returning 5002026-09-06 13:11:02 v13 v12 SUCCESS rolling API regression2026-09-02 09:13:12 v9 v8 FAILED rolling Database migration issue2026-08-28 18:42:09 v8 v6 SUCCESS classic -| Flag | Default | Purpose |
|---|---|---|
| --limit | 20 | Maximum number of entries to show (must be a positive number) |
- An app with no rollbacks shows
No rollback history found.- a normal state, not an error. --dry-runpreviews never record history; cancelling the interactive picker (Esc) never records history either, and is not a failure.- Malformed legacy lines in the history file are skipped with a short warning; they are never silently rewritten.
REASONshows the recorded--reason, or-for reason-free and pre-reason records (old history entries load unchanged and are never rewritten to add an empty reason).- When
--verifywas requested, each record also carries a verification block on disk (statuspassed/failed/cancelled). A failed window renders asVERIFY_FAILEDwhileSTATUSstaysSUCCESS- execution outcome and verification outcome are kept separate so history always tells the truth about what happened.
History is stored per app as JSON Lines at ~/.phelix/apps/<AppName>/rollback_history.jsonl.
How rollback works
Rollback is split into two phases: resolve + plan, then execute. The CLI first resolves the target version and deployment strategy and validates the target artifact (shared by both the preview and the real path); a real rollback then executes the plan, --dry-run renders it and exits.
Zero-downtime path (blue-green / rolling apps):
- ResolveResolveVersionOrTag resolves the --to argument to a concrete version ID.
- LocateExistingVersionSource resolves the binary and env paths for the target version.
- StartA new instance starts on the inactive slot (blue or green).
- VerifyTiered health checks verify the instance is healthy.
- SwitchThe proxy atomically switches traffic to the new instance.
- DrainThe old instance is gracefully drained and stopped.
- PromoteThe current symlink and versions.json are updated.
Classic path (apps built without --blue-green / --replicas): resolve the target version, stop the current instance, copy the versioned binary to the app's expected location, start the app, and promote the version (is_current set to true, current symlink updated). If the start fails, the version exists on disk but is_current stays false - you can retry without a broken "current" pointer.
Rollback safety
- Dry-run preview:
--dry-runshows the full plan with zero mutations - verify before you commit. - Concurrent protection: a deploy lock prevents rollback from racing with another deploy or rollback on the same app.
- Versioned env: binary and env are paired per version; rollback always restores both.
- Audit log: every rollback attempt (success or failure) is recorded in
~/.phelix/apps/<AppName>/rollback.log.
Inspecting versions
phelix rollback myapp --list| Column | Meaning |
|---|---|
| Version | vN label |
| Tag | Optional label (e.g. hotfix-auth-bug), if provided via --tag |
| Commit | Git commit hash (if available at build time) |
| Built | Build timestamp (RFC 3339) |
| Size | Binary size on disk |
| Current | Whether this version is actively serving traffic |
| Prune soon | Whether this version would be removed after the next build (retention) |
Retention
Old versions are automatically pruned after each successful build, keeping the last 5 versions by default. The currently active version is never pruned, even if it falls outside the retention window. The retention count is configurable per plan tier.
| Plan | Retained versions |
|---|---|
| Free | 3 |
| Default | 5 |
| Pro | 10 |
| Enterprise | Unlimited |