Anatomy of a release, step by step
What actually happens between clicking deploy and your new release serving traffic. A walk through every step symflip runs, and the one line that divides the safe half from the live half.
You click deploy, a stream of log lines scrolls past, and a minute later your new version is live with zero downtime. This post is about what happens in that minute. Not a marketing version of it, the actual ordered list of steps symflip runs on your server, and why they're in that order.
The short version: symflip does all the risky work while your old release keeps serving every request, and only goes live once the new release is built and proven. There is a single step where "not live" becomes "live," and everything before it is safe to fail. Once you can see that dividing line, the whole design makes sense.
Where a release lives
Before the steps, the layout. Everything symflip does happens inside one directory on your server that you point it at once. Inside it:
your-app/
current -> releases/20260803T140200Z the live release (a symlink)
releases/
20260803T101500Z/ the previous release, still on disk
20260803T140200Z/ the new release
shared/
.env your secrets, one copy, reused every deploy
storage/ uploads and other data that must outlive a release
repo/ a cached git checkout, so fetches are fast
Two things are worth noticing here. Each deploy gets its own timestamped
release directory, so a new release never overwrites the running one. And
anything that has to survive across deploys, your environment file and your
persistent storage, lives in shared/ outside every release and gets linked in.
That separation is what makes both zero-downtime go-live and instant rollback
possible.
The steps, in order
Here is the full pipeline. Steps that depend on a command you configured (build, migrate, reload, and the two hooks) are skipped cleanly if you leave that command blank, so a simple app runs a shorter list.
1. Ensure directory structure |
2. Checkout release |
3. Write environment file | the SAFE half:
4. Link shared files | old release still serving,
5. Install dependencies & build | any failure here is a no-op
6. Run migrations |
7. Pre-activate hook |
------------------------------- < the atomic swap
8. Activate release |
9. Reload service | the LIVE half:
10. Post-deploy hook | new release is now serving
11. Health check |
12. Purge old releases |
Let's walk through it.
1. Ensure directory structure. symflip makes sure releases/, shared/, and
the rest exist. On a first deploy this creates them; after that it's a quick
check.
2. Checkout release. Your chosen commit is checked out into a fresh
releases/<timestamp>/ directory, using the cached repo/ so it doesn't re-clone
your whole history every time. The timestamp is precise to the microsecond and
forced to always move forward, so two deploys can never land in the same directory.
3. Write environment file. Your secrets are rendered into shared/.env with
locked-down permissions. They were sealed in the database until this moment; if
you want the detail on how that sealing works, that's the subject of
a whole separate post.
4. Link shared files. The new release gets its .env and storage
symlinked to the copies in shared/. This is why your uploads and your
environment survive every deploy and every rollback: they never actually live
inside a release.
5. Install dependencies and build. Your build command runs inside the new release directory: install packages, compile assets, whatever your app needs. This happens beside the live release, not on top of it, so a broken build costs you nothing but a red log line.
6. Run migrations. If you configured a migrate command, it runs here, before go-live. Running it now, while the old code is still serving, is deliberate and it has one important implication: for additive schema changes it's completely safe, but rollback later will revert your code and not your schema. That trade-off has its own post too, because it's the one thing worth understanding before you ship a migration.
7. Pre-activate hook. Your last chance to run something before the switch: warm a cache, ping an external service, whatever you need. It's the final step that runs while nothing is live yet, so if it fails, the deploy aborts and your site never noticed.
Step 8: the line everything pivots on
Everything above ran with your old release still serving every single request. If any of it failed, the deploy stopped and nothing changed. Your users saw an error in your dashboard, not on your site.
Activation is the moment that changes. symflip creates a new symlink pointing at
the new release, then renames it over current in a single operation:
create current.tmp -> releases/20260803T140200Z
rename current.tmp -> current (one atomic rename(2))
Because a rename of a symlink is atomic at the operating-system level, there is no
instant where current points at nothing or at a half-built directory. A request
arriving one microsecond before the rename is served by the old release; one
microsecond after, by the new one. That single system call is the entire
zero-downtime guarantee. No connection draining, no load-balancer dance, just a
pointer that flips cleanly.
This is also the line that defines rollback. Rolling back is nothing more than this same step aimed at the previous release directory, which is still sitting on disk. No rebuild, no re-checkout, one rename. That's why it's basically instant.
After the swap: confirm it's healthy
9. Reload service. Your reload command runs so your app picks up the new release. Because you're now live, a failure here is treated as a real post-activation problem, not a quiet abort.
10. Post-deploy hook. Anything you want to run once the new version is live: notify a channel, clear a CDN, kick off a background job.
11. Health check. symflip checks that the new release is actually well. If you've turned on automatic rollback and this check fails, symflip flips the symlink straight back to the previous release on its own. Worth repeating the honest caveat: that automatic rollback restores your code, never a schema change that already ran at step 6.
12. Purge old releases. Finally, symflip cleans up ancient release
directories to save disk, but it always keeps at least the last two and never
deletes the one current points at. Keeping the previous release is not
housekeeping you can lose; it's the thing that makes one-click rollback possible,
so symflip guards it.
Why the order is the whole point
Read the list once more with the dividing line in mind. Fetch, build, migrate, and your pre-activate hook all happen while the old release serves traffic, so every way a deploy can go wrong before step 8 is a non-event: the switch simply doesn't happen and your live site is untouched. Only after the new release is fully built and staged does symflip make it live, in one atomic instant, with the previous release kept warm behind it in case you want it back.
That's the anatomy of a symflip release. Nothing exotic, just a careful ordering where the dangerous work is always reversible and the irreversible step is a single, instant flip. Once you've seen it, "zero-downtime deployment" stops sounding like magic and starts sounding like the obvious way to do it.
Try it free. Connect your first server in minutes.