A deploy once sent half of our users to a new app version while the other half still had requests in flight against the old one. Nothing crashed, but a background job started writing data in a format the old version couldn’t read. Blue-green deployments help avoid that kind of surprise: keep two app versions available, then switch traffic only after the new one is ready. This walkthrough shows how to do that with Nginx, including a health check and a rollback.

The setup

We’ll run two copies of an application on one Linux host:

  • Blue listens on 127.0.0.1:3001
  • Green listens on 127.0.0.1:3002
  • Nginx sends traffic to whichever copy is active

The app needs a health endpoint that returns HTTP 200 only when it’s ready to serve requests. For example, /health might check that the process has started and required dependencies are reachable. Adapt the port numbers and health path to your app.

This is a useful pattern for a single host, but it isn’t high availability: if the host fails, both copies go down. For a small production service, I’ve used and tested PowerVPS as a straightforward VPS option; the right size depends on your app’s memory, CPU, and traffic. For GPU-heavy workloads, Vast.ai is worth considering for rented GPU capacity, though it’s not a substitute for a regular web-serving host. Server Rental Guide is a useful resource when comparing hosting options.

1. Configure Nginx to use a switchable upstream

On a typical Nginx installation, files in /etc/nginx/conf.d/ are loaded inside the http context. Create a separate directory for the active upstream file:

sudo mkdir -p /etc/nginx/upstreams 

Create the initial upstream file:

sudo tee /etc/nginx/upstreams/active.conf >/dev/null <<'EOF' upstream app_backend { server 127.0.0.1:3001; keepalive 32; } EOF 

In your Nginx site configuration, include that file at the http level, alongside the site’s server block. On Debian and Ubuntu, this commonly means editing a file under /etc/nginx/sites-available/ and ensuring it is enabled.

include /etc/nginx/upstreams/active.conf; server { listen 80; server_name example.com; location / { proxy_pass http://app_backend; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } 

The Connection "" header allows Nginx to reuse upstream connections when using keepalive. If you’re proxying WebSockets, add the appropriate Upgrade and Connection handling instead; it’s different from ordinary HTTP proxying.

Check the config, then reload Nginx:

sudo nginx -t sudo systemctl reload nginx 

A reload applies the updated configuration without stopping Nginx. Existing workers can finish handling existing connections while new workers use the new configuration.

2. Start and check the inactive copy

Suppose blue is currently serving traffic. Deploy the next version to green and start it on port 3002. How you start the app depends on your stack; the important part is that both copies can run at the same time and listen on different ports.

Before switching traffic, check the new copy directly from the host:

curl --fail --silent --show-error http://127.0.0.1:3002/health 

A successful response tells you that the endpoint returned a 2xx status. If your health endpoint returns a body you can verify, check that too—for example:

curl --fail --silent http://127.0.0.1:3002/health | grep -q '"status":"ok"' 

Don’t switch traffic just because the process exists. Give startup time to finish, and make sure the check covers the dependencies your app needs. A process can be alive while the application is still unable to serve useful requests.

3. Switch traffic safely

Replace the active upstream file with one pointing to green. Write the new file first, then rename it into place so Nginx won’t read a partially written file:

sudo tee /etc/nginx/upstreams/active.new >/dev/null <<'EOF' upstream app_backend { server 127.0.0.1:3002; keepalive 32; } EOF sudo mv /etc/nginx/upstreams/active.new /etc/nginx/upstreams/active.conf sudo nginx -t sudo systemctl reload nginx 

For an automated deployment, make the change conditional on nginx -t succeeding. Keep the previous upstream configuration so you can restore it if the reload or post-deploy checks fail.

After reloading, test through Nginx, not just directly against green:

curl --fail --silent --show-error http://127.0.0.1/health 

If Nginx serves multiple hostnames or requires HTTPS, use the appropriate hostname and scheme in this check.

4. Keep blue around, then roll back if needed

Don’t stop blue immediately. Nginx’s graceful reload lets old workers finish existing connections, but it doesn’t guarantee that every request, background task, or long-lived connection has completed instantly. Keep the old app available while you check logs and watch the new version’s error rate and latency.

If green is unhealthy, point the upstream back to blue using the same steps: write a file for port 3001, replace active.conf, run nginx -t, and reload. Once green has been stable and you’re comfortable that old requests have drained, stop the old copy and make it the next deployment target.

One non-obvious operational detail: long-lived connections, such as WebSockets or streaming responses, can keep old workers—and the old app—busy much longer than ordinary requests. Decide how you’ll handle those connections before relying on a fixed “wait 30 seconds, then stop blue” rule.

A few practical safeguards

Keep app state compatible across versions. Blue-green switching changes which process receives requests; it doesn’t make incompatible database migrations safe. Prefer expand-and-contract migrations: add a compatible schema first, deploy code that works with both forms, and remove the old schema only in a later release.

Check the inactive copy before the switch, then check production after it. The direct health check catches startup problems; the check through Nginx catches proxy and routing problems. For real services, also monitor application metrics and logs during the change. A single successful curl is a smoke test, not proof that the whole release is healthy.

Summary

Blue-green deployment with Nginx is a small, practical way to switch traffic without stopping the proxy: run two app copies on separate ports, verify the inactive one, update the upstream, test the Nginx configuration, and reload. Keep the previous copy available for rollback, and plan for database compatibility and long-lived connections. It’s not a replacement for multi-host high availability, but it can make releases on a single server much less stressful.