ShieldThemes Web Development
+1 (415) 555-0142 Get a quote →
← Journal/DevOps

Zero-downtime deploys for a monolith: blue-green or rolling

You do not need microservices to ship without a maintenance page. How we choose between blue-green and rolling deploys for a single application.

Leo Tanaka
Leo Tanaka
Head of DevOps · Jan 13, 2025 · 6 min read
Zero-downtime deploys for a monolith: blue-green or rolling

Most of the applications we inherit are monoliths: one Laravel, Django or Rails codebase, one database, and a deploy script someone wrote in a hurry three years ago. The deploy works, mostly, but it takes the site down for thirty to ninety seconds while dependencies install and migrations run. For a brochure site that is tolerable. For a store doing a few hundred orders a day, or a SaaS product with customers in four time zones, it is not. The good news is that you do not need to split the codebase into services to deploy without downtime. You need a release strategy, disciplined migrations and a health check you actually trust.

What zero downtime really requires

Zero downtime is not a tool; it is three properties holding at the same time. First, there must always be at least one healthy copy of the application serving traffic. Second, the old and new versions must be able to run side by side for a few minutes without corrupting each other's data. Third, you must be able to go back to the previous version faster than you can debug the new one.

The first property is about infrastructure. The second is about code and schema discipline, and it is where most teams get hurt. The third is about process: a rollback that requires a meeting is not a rollback.

Blue-green: two environments, one switch

In a blue-green setup you run two identical production environments. Blue serves traffic; green receives the new release, warms up, passes its checks, and then the load balancer or DNS layer switches over. If something breaks, you switch back.

We reach for blue-green when:

  • The application has expensive warm-up, such as large caches, compiled templates or a JVM that needs a minute to settle.
  • The team wants a full smoke test against production data before any customer sees the release.
  • Releases are infrequent but high-stakes, for example a quarterly platform upgrade.

The cost is obvious: you are paying for double capacity during the switch, and on some platforms permanently. On a pair of mid-size cloud instances that might be an extra 150 to 300 dollars a month, which is usually cheaper than one bad outage. The less obvious cost is state. Anything stored on local disk, such as uploads, sessions or cache files, must live somewhere shared, or the switch will log everyone out and lose files. Moving sessions to Redis and uploads to object storage is typically the first ticket in a blue-green project.

Rolling deploys: replace instances one at a time

A rolling deploy updates a pool of instances gradually. With four application servers behind a load balancer, you take one out of rotation, deploy, run the health check, put it back, and move to the next. Container orchestrators and most managed platforms do this by default.

Rolling deploys are cheaper because you only need one spare slot of capacity, and they suit teams that ship many small releases a day. The trade-off is that for several minutes both versions are serving real users simultaneously. A customer can load a page from version 41 and submit a form to version 42. If the form fields changed, you need to handle both shapes.

The deploy strategy is the easy decision. The hard part is making two versions of your code tolerate the same database for ten minutes.

The migration rule that makes both work

Whichever strategy you pick, database changes must be backward compatible with the version currently running. We use an expand and contract pattern, split across at least two releases:

  1. Expand. Add the new column, table or index. Make it nullable or give it a default. The old code ignores it.
  2. Migrate. Ship code that writes to both old and new structures and reads from the new one with a fallback. Backfill existing rows in batches, not in one transaction.
  3. Contract. Once every running instance uses the new structure and the backfill is verified, drop the old column in a later release.

Renaming a column in one migration is the classic outage. So is adding a non-nullable column without a default to a table with ten million rows, which can lock writes for minutes on some database engines. Large index builds should use the online or concurrent option your database provides.

A migration step in the pipeline might look like this:

php artisan migrate --force --isolated
php artisan queue:restart
curl -fsS https://green.internal/healthz && ./switch-traffic.sh green

The isolated flag ensures only one instance runs migrations even if several start at once, and restarting queue workers makes sure background jobs pick up the new code rather than processing with stale classes.

Health checks, draining and rollback

A health endpoint that returns 200 as long as the web server is up is worse than useless, because it gives false confidence. A useful check confirms the application booted, can reach the database and cache, and has the expected release identifier. Keep it fast, under 200 milliseconds, and do not let it call third-party APIs, or a payment provider's slow afternoon will fail your deploy.

Connection draining matters too. When an instance leaves rotation, the load balancer should stop sending new requests but let in-flight ones finish, typically with a 30 to 60 second timeout. Without draining, users uploading a file or completing checkout at the wrong moment see an error.

For rollback, we keep the previous release artifact on disk or in the registry and make reverting a single command. Because migrations are backward compatible, rolling back code does not require rolling back the schema. We test the rollback path on staging at least once per quarter; a rollback that has never been exercised usually fails when you need it.

Picking one for your team

Our default for small teams on two to six servers is a rolling deploy with expand and contract migrations, because it is cheap and fits frequent releases. We move a client to blue-green when warm-up is slow, when compliance requires a verified pre-production check against live infrastructure, or when the business simply cannot tolerate mixed versions, such as during a pricing change. Either way, the pipeline that runs it belongs in version control; our CI/CD pipeline work usually starts by moving the tribal deploy script into a reviewed, repeatable job, and containerized teams can get the same behavior from Docker and Kubernetes rollout settings.

A realistic target for a mature monolith is a deploy under ten minutes from merge, no user-visible errors during the switch, and a rollback under two minutes. Teams that get there tend to deploy more often, which makes each release smaller and safer, which makes the next deploy easier still.

Ship your next release without the maintenance page

If your deploys still involve a banner, a late-night window or crossed fingers, we can help. Tell us about your stack and release cadence and we will send a fixed-price plan within a day. Start the conversation here.

Leo Tanaka
WRITTEN BY
Leo Tanaka
Leo runs our hosting and infrastructure practice — CI/CD, cloud cost, observability and the on-call rotation behind every care plan.
All articles by Leo Tanaka →
Want this on your project?
Get a fixed-price quote from a senior lead within 24 hours.
Request a quote →

Keep reading

How we shipped a support agent that resolves 62% of tickets
AI · 5 min
How we shipped a support agent that resolves 62% of tickets
What to learn in the two weeks before a website redesign
Design · 5 min
What to learn in the two weeks before a website redesign
Migrating to Shopify Plus without losing a single ranking
Shopify · 5 min
Migrating to Shopify Plus without losing a single ranking