← Back to Agent Rule

Zero-Downtime Blue-Green Deployments for AI Agent Services ✓ VERIFIED

2026-08-17 · 16 min read · Docker · sun-port · PostgreSQL · Git · Hermes · im-bot

A web server can restart in the blink of an eye and nobody notices. An AI agent service cannot — it holds state: a Hermes session mid-task, an im-bot room with three agents negotiating, a cron job halfway through publishing, a PostgreSQL transaction that just wrote three of its four rows. Deploy the new version with a plain docker compose up --force-recreate and you tear all of that down in front of your users. Blue-green deployment is the answer: run two identical stacks side by side, switch traffic between them with sun-port, and roll back in one command if the new one misbehaves. This tutorial builds that pipeline end to end.

Every command in this article was run against the live agent infrastructure documented on this site — two Docker Compose stacks, a sun-port route table, PostgreSQL migrations, and a Git-tagged release — and the ✓ VERIFIED badge means actual execution, not copy-paste from a README.

1. Why Agents Make Deploys Hard

A stateless API scales by adding instances and redeploys by swapping them. An agent service has four properties that break that model:

Blue-green deployment addresses all four by never tearing down the running system. You stand up the new version beside the old one, validate it, then flip a switch. The old version stays warm until you're sure.

2. The Blue-Green Model

Two environments, one database, one entry point:

The discipline that makes this work is that green must be deployable before blue is touched, and the database must accept both versions simultaneously for the duration of the switch window.

3. Prerequisites

$ docker --version
Docker version 27.3.1, build ce12230
$ docker compose version
Docker Compose version v2.29.7
$ psql --version
psql (PostgreSQL) 16.4
$ git --version
git version 2.46.0

You also need a running sun-port (see our reverse-proxy tutorial) and a PostgreSQL instance reachable from both stacks. If you haven't set up the agent itself yet, start with the Ubuntu walkthrough.

4. Containerize the Agent

The stack only works if a version is a self-contained image you can start, stop, and roll back. A minimal Dockerfile for a Hermes or im-bot service pins the runtime and bakes in the code at build time:

# Dockerfile
FROM node:20-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
CMD ["node", "src/index.js"]

Build the green image and tag it with the Git release tag so every deployed artifact is traceable to a commit:

$ git tag v1.4.0 && git push origin v1.4.0
$ docker build -t agent-service:v1.4.0 .
$ docker tag agent-service:v1.4.0 agent-service:green
Why a floating green tag? Your Compose file references green, so you never edit Compose to deploy — you just re-tag the freshly built image. Rollback is equally simple: re-point green at the last good v1.3.0 and switch traffic back. Tags are your deploy history.

5. Two Compose Stacks, One Database

Deploy both environments with Docker Compose, differing only in name, port, and image tag:

# docker-compose.yml — both stacks defined in one file
services:
  agent-blue:
    image: agent-service:v1.3.0
    container_name: agent-blue
    restart: unless-stopped
    environment:
      DATABASE_URL: postgres://agent:${DB_PASSWORD}@postgres:5432/agentdb
      RELEASE_COLOR: blue
    ports:
      - "127.0.0.1:8001:8001"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8001/health"]
      interval: 10s
      timeout: 5s
      retries: 5

  agent-green:
    image: agent-service:green
    container_name: agent-green
    restart: unless-stopped
    environment:
      DATABASE_URL: postgres://agent:${DB_PASSWORD}@postgres:5432/agentdb
      RELEASE_COLOR: green
    ports:
      - "127.0.0.1:8002:8002"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8002/health"]
      interval: 10s
      timeout: 5s
      retries: 5

  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: agentdb
      POSTGRES_USER: agent
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Three things to notice. Ports bind to 127.0.0.1 — neither stack is publicly reachable; only sun-port is. One shared postgres service means both colors read and write the same data, so switching is transparent to state. And RELEASE_COLOR is passed into the app so its own logs and health page can report which color it is — you'll use that in a moment.

$ docker compose up -d
$ docker compose ps
NAME          STATUS
agent-blue    Up (healthy)
agent-green   Up (healthy)
postgres      Up (healthy)

6. Route Traffic with sun-port

sun-port is where the switch actually happens. Define an upstream pointing at blue, then flip it to green when you're ready — a reload, not a redeploy:

# sun-port config
upstream agent_backend {
  server 127.0.0.1:8001;   # blue
}

server {
  listen 443 ssl;
  server_name agent.agent-rule.com;

  ssl_certificate     /etc/letsencrypt/live/agent.agent-rule.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/agent.agent-rule.com/privkey.pem;

  location / {
    proxy_pass http://agent_backend;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

Switching is a one-line edit plus a reload:

# cut over blue -> green
upstream agent_backend {
  server 127.0.0.1:8002;   # green
}
$ sun-port -t            # validate config
configuration file is valid
$ sun-port -s reload     # hot reload, no dropped connections
reload signal sent
sun-port reloads its route table without dropping existing connections, so an in-flight request that started on blue finishes on blue — only new connections go to green. That's the graceful handoff that makes this zero-downtime rather than just "fast".

7. Migrate PostgreSQL Backward-Compatibly

The database is the one resource both colors share, so a schema change must be compatible with both versions at once. The rule is expand-contract:

  1. Expand — deploy a migration that only adds (new columns with defaults, new tables). Blue ignores them; green uses them. Run this before switching.
  2. Switch — flip sun-port to green. Green now serves traffic on the new schema.
  3. Contract — in a later release, once blue is long gone, drop the old columns.

A safe additive migration with psql:

$ psql "$DATABASE_URL" <<'SQL'
ALTER TABLE conversations
  ADD COLUMN IF NOT EXISTS model_provider TEXT NOT NULL DEFAULT 'openai';
CREATE INDEX IF NOT EXISTS idx_conversations_model
  ON conversations (model_provider);
SQL
ALTER TABLE
CREATE INDEX

ADD COLUMN IF NOT EXISTS and a default value are the two habits that make a migration runnable against either color. The moment you need to remove a column or change a type, that's a two-step dance across two releases — never do it in the same deploy that flips traffic.

Never drop or rename in the same release you switch. If green drops a column blue still writes, blue errors instantly and your "zero-downtime" deploy becomes an outage on both colors. Additive-only for the switch window; destructive changes wait for the follow-up release.

8. Health-Gate the Switch

Before you flip traffic, prove green is healthy — not just running, but actually working. A real check hits the green container directly and confirms it can reach the database and report its color:

$ curl -sf http://127.0.0.1:8002/health
{"status":"ok","color":"green","db":"up"}

$ curl -sf http://127.0.0.1:8002/health | grep -q '"db":"up"' \
    && echo "green is ready" || echo "green is NOT ready — do not switch"
green is ready

The color field is your safety net: if green ever returns "color":"blue", your floating tag got mis-pointed and you'd be about to deploy the old version as if it were new. Gate the switch on both status and db.

9. The Switch, Automated

Wrap the whole cutover in one script so it's reproducible — and so a tired 2am operator can't skip a step:

#!/usr/bin/env bash
# deploy.sh — blue-green cutover
set -euo pipefail
COLOR=${1:-green}          # which color to send traffic to
PORT=$([ "$COLOR" = green ] && echo 8002 || echo 8001)

# 1. Health-gate the target
curl -sf "http://127.0.0.1:$PORT/health" | grep -q '"db":"up"' \
  || { echo "target $COLOR is not healthy"; exit 1; }

# 2. Point sun-port at it
sed -i "s/server 127.0.0.1:800[0-9];/server 127.0.0.1:$PORT;/" \
  /etc/sun-port/sun-port.conf

# 3. Validate and reload
sun-port -t
sun-port -s reload

echo "traffic is now on $COLOR"
$ ./deploy.sh green
traffic is now on green

Rollback is the same script with the other color — and because the old stack was never torn down, it's instant:

$ ./deploy.sh blue      # green misbehaved — back to blue in one command
traffic is now on blue

10. Git Tags and Release Discipline

Blue-green is a process, and the process lives in Git. Keep it simple and linear:

$ git checkout -b release/v1.4.0
$ # ...code changes, tests...
$ git tag v1.4.0
$ git push origin v1.4.0

Three habits keep the pipeline honest:

11. Agent-Specific Concerns: Hermes and im-bot

Blue-green solves the infrastructure problem, but agents add two wrinkles worth planning for:

The cleanest pattern: keep blue fully live during the switch window (minutes to an hour), let in-flight work drain naturally, then docker compose stop agent-blue once it's quiet. Zero dropped work, zero downtime.

12. Pitfalls

13. Key Takeaways

Blue-green deployment turns the scariest part of running agent infrastructure — "is the new version going to take everything down?" — into a reversible, one-command operation. For a fleet that orchestrates Hermes agents, im-bot rooms, Docker workloads, and PostgreSQL state through sun-port, that's the difference between deploying on a Friday and deploying with confidence on a Friday.