Work / Zero-downtime deploys

Zero-downtime deploys, several times a day

Blue-green releases with Docker, GitHub Actions and nginx, shared by every product I lead and shipping several times a day.

Role
Designed it and rolled it out across products
Period
2025 — present
Context
Five production services on three products
Stack
Docker, GitHub Actions, nginx, PostgreSQL, Redis

The problem

Before this, some services restarted in place on every release, so every deploy was a small outage, and the team waited for the evening to ship.

The goal was simple: deploys safe enough to run on a Tuesday afternoon without anyone noticing, and a bad release that never reaches users.

How a release works

Blue-green deployment on a VPSGitHub Actions builds an image and ships it to the server. nginx routes user traffic to the live slot, green, while the blue slot keeps the previous version on standby. Both slots use the same PostgreSQL database. A single worker instance runs queues and scheduled jobs.GitHub Actionsbuild · test · imageDocker imageVPSUsersHTTPSnginxupstreamgreen · v42live · health gate passedblue · v41standby · previous versionWorkerqueues · crons · singlePostgreSQLexpand / contract
Scroll →After a release: green runs the new version and takes traffic, blue keeps the previous one.
  1. Build. GitHub Actions validates the pull request, builds the Docker image and ships it to the server.
  2. Start the idle slot. The new version boots in the slot that isn't serving traffic, next to the live one.
  3. Health gate. The new slot has to pass its health check. If it doesn't, nothing switches and users never see the bad release.
  4. Switch. nginx reloads with the new slot as its upstream. Requests already in flight finish on the old one.
  5. Report. The result goes to the team's Discord.

What can't be blue-green

Running two versions side by side is the easy part. The real design work is in the pieces that must never run twice, or must work with both versions at once.

  1. Database migrations

    Migrations follow expand and contract: add first, remove only after no running version needs the old shape. During a switch, old and new code both run against the same schema.

  2. Workers and scheduled jobs

    Queue workers and cron jobs run as a single instance and are not duplicated. Queues are paused through an endpoint during the switch, so no job is picked up twice. Financial jobs live in a dedicated worker role, so there is exactly one writer.

  3. Stateful connections

    The messaging gateway holds long-lived connections that only one process can own at a time, so it never runs twice. It restarts gracefully instead of going blue-green.

    Trade-offA few seconds of reconnection on that one service, in exchange for never having two processes fight over the same connection.

Around the deploy

  • Daily PostgreSQL backups to S3, with versioning and retention rules and least-privilege credentials.
  • One health-check contract shared by the AI platform's services. A customer's disconnected WhatsApp never turns the platform status red.
  • Dependabot and pull-request validation on every main repository.

Outcome

Deploys stopped being an event. All five production services ship through the same process, several times a day, during working hours, and a failed release stops at the health gate instead of reaching users.

Deploy frequency comes from the GitHub Actions history of the five production services. Server details are withheld.