Where downtime actually comes from
Downtime during a deploy rarely comes from one big failure. It usually comes from a few short windows that add up.
- Files are replaced while requests read them, so a request loads half old and half new code
- Dependencies are reinstalled in place, so the app briefly runs without its libraries
- The process restarts and nothing listens on the port for a few seconds while it boots
- A migration locks a large table, and every query waits behind the lock
- The new release is broken, and the only fix is another full deploy
Each strategy below closes one or more of these windows. None of them closes all of them alone, which is why real setups combine two or three.
Strategy one, atomic releases with a symlink switch
Atomic releases are the foundation for any app on a single server. Each deploy goes into a new folder, gets its dependencies and build output there, and only then becomes live through a symlink switch. A rename on the same file system is atomic, so there is no moment where current points at a half built folder.
REL=/srv/app/releases/$(date +%Y%m%d-%H%M%S) git clone --depth 1 "$REPO" "$REL" cd "$REL" && ./build.sh ln -sfn "$REL" /srv/app/current.new mv -T /srv/app/current.new /srv/app/current
This removes the first two windows completely. It also gives you instant rollback: keep the last five releases and point the symlink back. For PHP with PHP-FPM, reload FPM or clear OPcache after the switch so workers stop serving cached old files. For long running processes in Node.js, Python, Ruby, Go, Java or .NET, the switch alone is not enough, because the process still runs the old code until it restarts. That is where the next strategies come in.
Strategy two, graceful reloads and health checks
Many application servers can replace their workers without dropping connections. Gunicorn reloads on HUP, Puma has phased restarts, PM2 has reload in cluster mode, and PHP-FPM reloads gracefully. Old workers finish their requests while new ones start. Combine this with a health check that runs before traffic moves.
curl -fsS --retry 10 --retry-delay 1 http://127.0.0.1:8081/healthz || exit 1
A useful health endpoint checks more than "the process is up". It opens a database connection, reads from the cache and returns 200 only when the app can serve a real request. If the check fails, the deploy stops and the old release keeps serving. This single check prevents most broken releases from ever reaching users.
Strategy three, blue green on one server or many
Blue green runs two complete copies of the app. Blue serves traffic, green gets the new release, passes its health check, and then the proxy switches. Blue stays running for a while, so rollback is just switching back. On a single server, blue and green can be two process instances on two ports.
upstream app {
server 127.0.0.1:8081; # green, new release
# server 127.0.0.1:8080; # blue, previous release
}Change the active line, run nginx -t and nginx -s reload, and new connections go to green while blue finishes its open requests. Across several servers, the same idea moves to the load balancer. Blue green needs double the memory during a deploy, and both versions must work with the same database, which leads straight to migrations.
Strategy four, rolling deploys across servers
With several app servers behind a load balancer, a rolling deploy updates them one or a few at a time. Each server is taken out of the pool, updated, health checked and put back, then the next one follows. Capacity drops slightly during the deploy, but there is never a moment without healthy servers.
- Drain connections before updating a server, so long requests can finish
- Update in batches small enough that the remaining servers carry the load
- Stop the rollout automatically when a server fails its health check
- Expect old and new versions to serve traffic at the same time for minutes
That last point is the price of rolling deploys. Your API responses, sessions, queues and cache formats must be readable by both versions. If a new release changes a cached object shape, version the cache key.
Database migrations without downtime
Most real outages during deploys come from the database, not from the app servers. In every strategy above, old and new code run against the same database for some time. The rule that makes this safe is expand and contract.
- Expand: add new tables, nullable columns and new indexes in a release that still works with the old code
- Migrate: backfill data in batches, write to both the old and new columns if needed
- Contract: remove the old column or constraint in a later release, once no running code uses it
Some operations lock tables and must be handled carefully. On PostgreSQL, create indexes concurrently and add NOT NULL constraints through a validated check constraint first. On MySQL, prefer online DDL and check that the operation supports ALGORITHM=INPLACE, or use a tool built for online schema changes on very large tables. Always run migrations before the traffic switch, not after, and take a backup before any migration that drops data.
Queues, workers and long running jobs
Background workers often get forgotten. If workers keep running the old code after a deploy, they may write data in an old format or call methods that no longer exist. Restart workers on every deploy with a graceful stop, so jobs in progress finish first. Make job payloads backward compatible for one release, because jobs queued by the old version will be processed by the new one.
Choosing between zero downtime deployment strategies
The right choice depends on how many servers you run and how much complexity you accept.
| Setup | Recommended strategy | Rollback |
|---|---|---|
| One server, PHP or static | Atomic releases and FPM reload | Symlink back |
| One server, long running app | Atomic releases plus graceful reload or two ports | Symlink back and restart |
| One server, strict uptime needs | Blue green on two ports behind nginx | Switch upstream back |
| Several servers | Rolling deploy behind a load balancer | Roll back server by server |
Whatever you pick, expand and contract migrations and a real health check are not optional. They are what make the other strategies safe.
How Deployment applies these strategies
Deployment builds atomic releases into every deploy by default. Each push goes into a new release folder on your own server, runs your editable deploy script, passes a health check, and only then switches the symlink. Queue workers and daemons restart on every deploy, PHP-FPM reloads gracefully, the last releases stay on disk for one click rollback, and failed deploys stop before they touch live traffic. You keep full control of migration steps in the deploy script, with logs streamed to the panel. Read the zero downtime deployment page for the full mechanics, see how pushes trigger releases on the deploy from git page, or paste a public repository into the demo on the home page to see the deploy script Deployment generates for it.