How to Deploy to Production and Still Be Able to Roll Back
Getting code onto a server is the easy half. The half that matters is what happens when the deploy is wrong: how fast you can get back to the previous version, and whether the database will let you.
That second part is what makes deployment genuinely hard. Application rollback is a pointer change and takes seconds. A schema migration is not reversible in the same way, so the moment a deploy includes one, "just roll back" stops being a complete answer. Everything below is organized around that constraint.
Optimize for time-to-rollback
Deploy frequency and confidence both come from the same property: how quickly you can undo. If rollback takes twenty minutes, you will deploy less often, which makes each deploy bigger and riskier.
- ›Can you roll back to the previous version in under a minute, without a rebuild? If it requires rebuilding, it's too slow to use during an incident.
- ›Are migrations a separate step from the application deploy? They must be, so you can deploy code without schema changes.
- ›Is the migration backwards-compatible with the currently running version? During any rolling deploy both versions run at once against one schema.
- ›Does your health check actually check health? One returning 200 unconditionally will happily route traffic to a process that can't reach the database.
- ›Can you deploy without deploying? Feature flags decouple shipping code from turning behaviour on, and turn many rollbacks into a config change.
The approaches
Platform-as-a-service (Vercel, Netlify, Railway, Render, Heroku, Fly)
// best for: Nearly every team below serious scale, and plenty above it.
- Vercel
- Netlify
- Railway
- Render
- Heroku
- Next.js
- Nuxt
- SvelteKit
Push to git, the platform builds and swaps traffic to the new version. The genuinely valuable property is immutable deploys: every build is retained and addressable, so rollback is repointing at a previous build — seconds, no rebuild, and it works when your CI is also broken.
Preview deployments per pull request are the other underrated feature. A reviewer clicking a working URL catches things no diff review does, and it makes the deploy path itself boring by exercising it on every PR.
The limits show up with long-running work. Most PaaS request handlers have a hard timeout, so video processing or large report generation needs a separate worker. Vercel in particular is optimized around a serverless model that suits request/response work and fights you on persistent connections.
The gotcha
Environment variables set after a build are not in that build. Frontend variables are inlined at build time, so adding one and restarting doesn't apply it — you must redeploy. This produces the exact confusion of a variable that is visibly set in the dashboard and undefined in the browser.
Containers (Docker on ECS, Cloud Run, Kubernetes)
// best for: Teams needing portability, background workers, or infrastructure control.
- Docker
- Kubernetes
- AWS
- Google Cloud
- Azure
- DigitalOcean
An image built once and promoted unchanged through staging to production is the strongest guarantee that what you tested is what runs. Rollback is redeploying a previous image tag, which is fast and precise.
Cloud Run is the sweet spot for most teams here: containers with autoscaling and scale-to-zero, without operating a cluster. Kubernetes is the right answer when you genuinely need its primitives, and a considerable tax when you don't.
The thing to get right early is health checks. Kubernetes distinguishes liveness (restart this) from readiness (don't send traffic yet), and conflating them causes restart loops during a slow dependency, turning a degraded dependency into a full outage. Readiness should check dependencies; liveness should only check that the process isn't wedged.
# Readiness gates traffic. Liveness restarts the container.
# Pointing liveness at a dependency check turns a slow database
# into a restart loop, which is worse than the original problem.
readinessProbe:
httpGet: { path: /ready, port: 8080 } # checks DB, cache, etc.
periodSeconds: 5
livenessProbe:
httpGet: { path: /alive, port: 8080 } # process responds; nothing else
periodSeconds: 15
failureThreshold: 3The gotcha
`:latest` as an image tag defeats rollback entirely — the previous version no longer has a name you can point to, and pulls are non-deterministic across nodes. Tag every image with the commit SHA and deploy that.
Serverless and edge (Lambda, Cloudflare Workers, Vercel Functions)
// best for: Spiky traffic, background jobs, and anything that benefits from running near the user.
- AWS
- Cloudflare
- Vercel
- Netlify
- Google Cloud
- Azure
You deploy a function; the platform runs as many copies as needed and charges per invocation. For genuinely bursty workloads the economics are excellent, and edge runtimes put your code within tens of milliseconds of the user globally.
Two constraints shape everything. Cold starts add latency to the first request on a new instance — small for JavaScript, significant for a JVM. And every instance is a separate process with its own connections, which is why a traffic spike exhausts your database connections unless a pooler sits in between.
Edge runtimes are not Node. Cloudflare Workers and Vercel's edge runtime lack most of the Node standard library, so many npm packages simply don't run. Check compatibility before committing an existing codebase.
The gotcha
Serverless functions freeze between invocations rather than exiting. Work started but not awaited — a fire-and-forget analytics call, an unflushed log — is suspended mid-flight and may resume, or may never complete, on the next unrelated invocation. Await everything before returning.
Common pitfalls
Migrations that run during the rolling window
During a rolling deploy, old and new code run simultaneously. A migration that drops a column the old version still selects breaks every instance that hasn't rolled yet. Migrations must be compatible with both versions — expand first, contract a release later.
Health checks that only check the process
Returning 200 from a handler that touches nothing means the platform routes traffic to instances that can't reach the database. The readiness check should verify the dependencies the app needs to serve a request.
No way to deploy without migrating
If your deploy pipeline always runs migrations, you cannot ship an application-only hotfix during an incident where the migration is the suspect. Keep them separate steps that can be run independently.
Secrets baked into the image
An API key in the Dockerfile or a committed `.env` is in every layer and every registry copy, permanently. Inject secrets at runtime. Once one is in an image, rotating the key is the only real fix.
Questions people actually ask
How do I get zero-downtime deploys?
Rolling deploys with correct readiness checks, plus backwards-compatible migrations. The platform handles the first part; the second is on you, and it's the part that actually causes downtime.
Should migrations run automatically?
As a distinct step that must succeed before new instances start, yes. Inside application startup, no — that ties migration timing to container scheduling and makes a slow migration look like a failing health check.
How fast should rollback be?
Under a minute, and it must not require a rebuild. If rolling back means waiting for CI, you'll spend the outage debugging forward instead of restoring service.