We boot every update before you can install it

The industry default for updating a self-hosted app is a cron job that pulls :latest and restarts the container. It works right up until the night it does not -- upstream ships a build with a breaking migration, a renamed environment variable, a base image that no longer starts, and you find out because your app is down and the tag that broke it is the same tag that worked yesterday. The uncomfortable truth underneath: an update is whatever upstream shipped. Not a patch you reviewed. Not a change you chose. Whatever they shipped, whenever they shipped it. We think the only honest response to that is to trust upstream the way you trust any dependency -- verify it, mechanically, before it gets anywhere near your app.
Pinned to the build that actually ran
For bring-your-own apps, the platform does not deploy the tag you pasted. It deploys the exact build behind it -- the content digest -- that was verified when your app first booted. A tag like myapp:latest is a pointer, and upstream can silently repoint it at any moment; a digest is the build itself, immutable by construction. Pinning the digest means the thing that was tested is, byte for byte, the thing that deploys. Redeploy your app next month and you get the build that ran, not whatever the tag has drifted to since.
This is the foundation everything else stands on. Once "your app's version" means an exact build rather than a floating name, updating stops being an accident that happens on restart and becomes a discrete, testable event -- something with a before, an after, and a decision in between.
The nightly sandbox boot
Pinning alone would leave you frozen in time, so a nightly job does the watching for you. Each night, the platform re-resolves the tag you originally pasted. Most nights it resolves to the same digest and there is nothing to do. When upstream has published a new build, that build is booted in a sandbox -- with your app's proven configuration, the one that is actually running -- before anyone is told an update exists.
That last clause is the point. The sandbox does not boot the image with default settings and call it compatible. It boots the new build the way your app runs: your environment shape, your ports, your volumes. A build that starts fine with upstream's defaults and falls over with a real configuration is exactly the failure mode this catches.
Only builds that pass are offered as updates. If you have opted in to auto-updates -- a per-app choice, not a platform-wide one -- a passing build is applied for you. Either way, the sequence is the same: resolve, boot, verify, and only then act.
A failing build is recorded, never offered
When the sandbox boot fails, the build is recorded as failed and never offered. Your app stays on its working version. There is no notification urging you to update to something we watched crash, and no 3 a.m. page because an auto-pull swallowed upstream's bad day whole. From your side, a broken upstream release simply does not happen -- the platform absorbed it in a sandbox, wrote it down, and moved on. Upstream ships a fixed build eventually; the nightly job resolves it, boots it, and offers that one instead.
This inverts the usual deal. With :latest and a cron job, you are upstream's canary: every release tests itself in your production. With a sandbox gate, the canary is a disposable boot on our infrastructure, and your app only ever sees releases that already survived it.
Rollback is a digest, not a hope
A sandbox boot is strong evidence, not a guarantee -- an update can pass the boot and still disagree with your app's data. So every applied update keeps a rollback reference to the previous exact build. Not the previous tag, which upstream may have already repointed at the very build that just failed, but the previous digest -- the working version, addressable forever.
If an applied update fails to start, the previous version is restored automatically. Not a support ticket, not a runbook -- the platform notices the failed start and puts back the build that was running before. The worst case of accepting an update is a brief interruption and a return to exactly where you were, which is a very different worst case from the industry's.
Catalog apps: watched by digest, applied by you
Catalog apps get the same discipline with one difference in posture. Updates are detected by watching the real image digest -- not version strings, which lie freely; a latest tag can ship five different builds in a week without a version number moving anywhere. When the digest behind a catalog app's image changes, that is an update, and it is surfaced as one.
Applying it is always your decision. Auto-update is off by default, per app, because an update is still whatever upstream shipped, and for the app holding your data the person who decides when to take that should be you. When you do apply one, the same rollback reference is kept, and the same automatic restore covers a failed start.
The through-line for both kinds of app is the same. Trust upstream -- you chose their software. Verify mechanically -- because trust is not a deployment strategy, and "it is probably fine" is not a rollback plan.
Try it
None of this needs configuring -- digest pinning, the nightly sandbox checks and automatic rollback are how updates work here, and auto-apply is one switch on each app for the ones you want hands-off. Deploy something on a capacity subscription and let upstream have its bad days without you. Apps deploy in minutes, with HTTPS, backups, SFTP and a browser terminal included.