Docs/ Deployment/ Blue-Green & Rollbacks

Blue-Green & Rollbacks

How sh0 replaces a running version without interrupting it, what the deployment history records, and what the Rollback button really does in version 1.10.2.

How a deployment replaces the running version

Every deployment starts the new version next to the one already online and only moves traffic once the new container has proved it is up. No setting is needed. For an app deployed from Git, the sequence is:

  1. Build the image from the cloned repository (pre_build and post_build hooks run around it).
  2. Start the new container while the old one keeps serving. Both mount the same volumes during the overlap.
  3. Health check of the new container (details below).
  4. pre_deploy hooks, then additional replicas if the app has more than one.
  5. Route switch: the Caddy reverse proxy is pointed at the new container.
  6. Old container stopped: it receives a stop signal, has 30 seconds to exit, then is removed.
  7. post_deploy hooks, then a last check that the new container is still running and not restarting in a loop.

Apps deployed by archive upload, Docker image or pasted Dockerfile follow the same order without the hook stages. If any step up to the route switch fails, the new container is removed and the previous version keeps serving; the log says so: A failed deploy leaves nothing running.

Health check

sh0 waits up to 300 seconds for the new container to be judged healthy:

  • The image declares a HEALTHCHECK: sh0 waits for Docker to report it healthy, and also checks that the port answers from outside the container.
  • No HEALTHCHECK: sh0 opens a TCP connection to the app's port. No HTTP path is requested.
  • Worker without a port: the container must stay running for 15 seconds without restarting.

A crash loop or an out-of-memory kill fails the deployment at once, without waiting for the 300 seconds.

What “zero downtime” does not cover

  • Connections still open on the old container when it stops (WebSocket, long downloads, streaming responses) are cut after the 30-second grace period. They are not moved to the new container.
  • During the overlap, two containers of the same app run against the same volumes. An app that cannot run twice at once (an SQLite file locked exclusively, a job that must be unique) has to tolerate those few seconds.
  • Database migrations run by a pre_deploy hook are applied while the old version still serves traffic. Write them so that the old code keeps working against the new schema.

Deployment history

Each app's Deployments tab lists its deployments, newest first, with the trigger (upload, cli-push, cli, rollback…), the commit for Git apps, the duration and the full build log. The same list is available from GET /api/v1/apps/<app-id>/deployments.

A successful deployment keeps the status running even after a newer one replaced it: the version online is the most recent running entry, not the only one.

Rollback

In the Deployments tab, every deployment with the status running carries a Rollback to this deployment button, which asks for confirmation (Confirm Rollback). The API equivalent is POST /api/v1/deployments/<deployment-id>/rollback; it requires the developer role on the app and answers 202 with the new deployment it queued. Version 1.10.2 of the CLI has no rollback command.

What a rollback deploys

Warning
Since version 1.10.3, a rollback runs the image recorded on the chosen deployment: nothing is cloned or built. Version 1.10.2 and earlier redeploy the current version instead — upgrade before relying on a rollback to undo a bad release.

Observed on version 1.10.3 with an app deployed by archive upload: version v1, then v2, then a rollback to the v1 deployment. The rollback deployment carries the image of v1, its log states that nothing was built, and the app answers v1:

Terminal
$ curl -s https://panel.example.com/api/v1/apps/<app-id>/deployments \
    -H "Authorization: Bearer $SH0_TOKEN" \
  | jq -c '.data[] | {id: .id[0:8], status, triggered_by, commit_msg, image_id: .image_id[7:19]}'
{"id":"c97f6268","status":"running","triggered_by":"rollback","commit_msg":"Rollback to deployment 0c5abcdc","image_id":"44da57c73080"}
{"id":"a36c7932","status":"running","triggered_by":"cli-push","commit_msg":"Push update","image_id":"916d35592fa5"}
{"id":"0c5abcdc","status":"running","triggered_by":"upload","commit_msg":"File upload","image_id":"44da57c73080"}

$ curl -s https://my-site.example.com
version v1

A rollback is refused with 400 when the chosen deployment did not succeed, and with 409 when its image can no longer be run: an image built by sh0 and since deleted by the cleanup described below, or, for an app deployed from a Docker image, a moving tag such as nginx:latest that the live deployment uses too. An app set to several replicas comes back with a single container.

A rollback restores the image, not the configuration: environment variables, volumes and domains stay those of today. To return to a version whose image is gone, deploy it again: push the previous archive with sh0 push, or revert the commit and push it for a Git app.

Image retention

Every 6 hours, sh0 deletes old app images and keeps the 3 most recent per app. Both values are options of sh0 serve, not dashboard settings: --cleanup-interval-hours (0 disables the cleanup) and --cleanup-keep-images.