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.
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.
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.
$ 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.
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
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.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)
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
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:
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.
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.
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
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:
agent-service:v1.4.0 should be reconstructable from git checkout v1.4.0. If it isn't, rollback is a guess.deploy.log or a tagged commit noting "switched blue→green v1.4.0" gives you an audit trail — the same discipline from our observability tutorial.Blue-green solves the infrastructure problem, but agents add two wrinkles worth planning for:
SELECT ... FOR UPDATE SKIP LOCKED on a scheduled_jobs table — so only the active color takes the lease.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.
0.0.0.0. A green stack bound to all interfaces is reachable by the public internet, bypassing sun-port's TLS entirely. Bind 127.0.0.1.green still points at the old image, you "deploy" nothing and wonder why the bug persists.sun-port -s reload is graceful; restarting sun-port drops connections. Always reload.db:"up" and the right color on the target before touching traffic.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.