Skip to content

Blue Green Deploy

Griffen Fargo edited this page Jul 10, 2026 · 2 revisions

Blue-Green Deploy

Since v0.20.0.

Blue-green deploy stands the new version of a stack up alongside the current one, waits until it passes health checks, swaps the reverse proxy to point at it, then drains and stops the old version. If the new version never becomes healthy, the old version is left untouched and the new one is torn down — so a failed deploy never takes traffic down with it.

Quick Reference

strut my-stack deploy --env prod --blue-green          # run this deploy blue-green
strut my-stack deploy --env prod --standard            # force in-place (overrides config)
strut my-stack deploy --env prod --blue-green --dry-run  # preview the 9-step plan
strut my-stack rollback --env prod                     # flip active color back

Enable it as the default for a project by setting DEPLOY_MODE=blue-green in strut.conf.

How It Works

Each blue-green deploy runs under a dedicated Docker Compose project named <stack>-<env>-<color>, where color alternates bluegreen. The two colors live side by side in separate compose projects, so they can run concurrently without container-name or volume-claim collisions.

Flow:

1. Pre-flight               — docker / compose plugin present
2. Pre-deploy validation    — strut validate + pre_deploy hook (opt-out: --skip-validation)
3. Registry auth            — shared with standard deploy
4. Start <new color>        — pull images, up -d under <stack>-<env>-<new>
5. Health gate              — poll the new color's own containers until healthy or timeout
6. Swap proxy               — BLUE_GREEN_PROXY_HOOK or built-in reload
7. Drain <old color>        — wait BLUE_GREEN_DRAIN seconds
8. Stop <old color>         — compose stop (containers kept, not removed — see below)
9. Mark active              — write stacks/<stack>/.bluegreen

If step 5 fails, strut tears down the new color (down --remove-orphans --volumes), emits a deploy.failed event, and exits non-zero. The old color is never touched, so traffic keeps flowing on the last known-good version.

Step 8 deliberately uses stop, not down: down removes the containers, severing the container↔image binding a later rollback depends on — a subsequent up -d would then recreate from whatever the compose file's tag currently resolves to (possibly a since-moved mutable tag), not what was actually running. stop leaves the containers (and volumes) in place so rollback's up -d just restarts them on their original image.

Health gate details: a single healthy snapshot isn't trustworthy — up -d returns as soon as a container reaches "running", which can be a moment before a crash-looping entrypoint actually exits. The gate requires two consecutive healthy polls (3s apart) before trusting a deploy, and additionally checks each container's Docker RestartCount: any nonzero count resets the gate, since that's a persistent signal that survives even if the container happens to be back in a "running" state between polls when the gate checks it.

State File

stacks/<stack>/.bluegreen is a tiny INI-style file used to pick which color comes next:

active_color=blue
active_project=my-stack-prod-blue
updated_at=2026-04-21T14:30:00Z
  • First deploy (no file): strut deploys to blue, then writes the file.
  • Subsequent deploys: strut reads the active color and deploys to the opposite slot.
  • Rollback: strut flips active_color back and brings the drained project up.

You normally don't edit this file. If you do need to reset after out-of-band recovery, delete it and the next blue-green deploy will start fresh from blue.

Configuration

Set in strut.conf:

# Deploy mode: standard (default, in-place) or blue-green
DEPLOY_MODE=blue-green

# How long (seconds) to wait for the new color's health checks (default: 30)
BLUE_GREEN_HEALTH_TIMEOUT=30

# How long (seconds) to let the old color drain traffic before stopping (default: 60)
BLUE_GREEN_DRAIN=60

# Optional path to a shell file that defines bluegreen_proxy_swap()
# BLUE_GREEN_PROXY_HOOK=/path/to/hooks/bluegreen_proxy_swap.sh

CLI flags always win over DEPLOY_MODE:

Flag Effect
--blue-green Run this deploy blue-green even if DEPLOY_MODE=standard
--standard Run this deploy in-place even if DEPLOY_MODE=blue-green

Compose File Requirements

Blue-green runs two compose projects in parallel. Your docker-compose.yml has to be compatible:

  • Do not set container_name: on any service — two copies of the same stack would collide on the fixed name.
  • Do not bind the same host port twice from the same service. Only one color's proxy should win the public port at a time.
  • Volume names are isolated per compose project automatically — no change needed.
  • Networks defined in the compose file are also per-project; cross-project discovery goes through Docker's default bridge by IP or by the swap hook wiring upstream names.

The templates shipped in templates/ under v0.20.0 and later are blue-green-ready. If you scaffolded an older stack, audit for container_name: and fixed port bindings before enabling DEPLOY_MODE=blue-green.

Proxy Swap Hook

The swap step is the moment the new color starts receiving traffic. How that happens depends on how your stack's reverse proxy is deployed:

  • Single-project embedded proxy (the default scaffold): the nginx/caddy container is part of the stack's compose file. The built-in fallback reloads the new color's proxy container, which only works if the compose file is built for blue-green (e.g., upstream config that resolves a service name both colors provide).
  • External router (a shared nginx/caddy/traefik or cloud LB): you own the swap. Provide a hook.

Set BLUE_GREEN_PROXY_HOOK to a shell file that defines:

# bluegreen_proxy_swap <stack> <old_project> <new_project> <env_file>
bluegreen_proxy_swap() {
  local stack="$1" old_project="$2" new_project="$3" env_file="$4"
  # …rewrite upstream config to point at <new_project> containers and reload…
}

The file is sourced (not exec'd), so it can read exported strut vars and call any strut helper (log, run_cmd, ssh_exec, etc.). Return 0 for success; a non-zero return aborts the deploy before the drain step.

Example: shared nginx on the VPS

# hooks/bluegreen_proxy_swap.sh
bluegreen_proxy_swap() {
  local stack="$1" old_project="$2" new_project="$3" env_file="$4"
  # shellcheck disable=SC1090
  set -a; source "$env_file"; set +a

  # Point the shared upstream at the new color's container IP.
  local new_ip
  new_ip=$(docker inspect -f '{{.NetworkSettings.Networks.strut_shared.IPAddress}}' \
            "${new_project}-api-1")
  [ -n "$new_ip" ] || { echo "could not resolve $new_project IP" >&2; return 1; }

  # Template nginx upstream and reload.
  sed -i.bak "s/server .*:8080;/server ${new_ip}:8080;/" /etc/nginx/conf.d/my-stack.conf
  nginx -t && nginx -s reload
}

Example: Caddy with admin API

bluegreen_proxy_swap() {
  local stack="$1" old_project="$2" new_project="$3" env_file="$4"
  local new_ip
  new_ip=$(docker inspect -f '{{.NetworkSettings.Networks.strut_shared.IPAddress}}' \
            "${new_project}-api-1")
  curl -s -X PATCH "http://localhost:2019/config/apps/http/servers/srv0/routes/0/handle/0/upstreams/0/dial" \
       -H "Content-Type: application/json" \
       -d "\"${new_ip}:8080\""
}

The hook is invoked synchronously — treat it like any other critical deploy step.

Rollback

When stacks/<stack>/.bluegreen exists, strut rollback delegates to the blue-green rollback path instead of restoring images from a snapshot. It:

  1. Reads the current color from the state file and computes the opposite.
  2. Brings the drained project back up (up -d --remove-orphans).
  3. Swaps the proxy back (same hook, in reverse direction).
  4. Stops the now-current project.
  5. Rewrites the state file with the flipped color.

This is faster than a standard rollback because the drained project's volumes and images are still present — no pull, no image restore. It's also an honest rollback: the .bluegreen flip is atomic, so there's no window where both colors think they're live.

strut my-stack rollback --env prod --dry-run   # preview the flip
strut my-stack rollback --env prod             # execute

If you need the classic image-restore rollback (for a deploy that pre-dates the state file), delete stacks/<stack>/.bluegreen and re-run; strut will fall through to the snapshot path.

Tuning Timeouts

Setting When to raise When to lower
BLUE_GREEN_HEALTH_TIMEOUT App boot includes JIT warm-up, migrations, or async init Services are fast-booting and you want faster failure feedback
BLUE_GREEN_DRAIN Long-lived HTTP requests (video streaming, file uploads) or stateful websockets Short request tails (APIs, batch workers)

The drain window is a plain sleep. If your proxy supports graceful upstream removal (connection draining, proxy_next_upstream), the drain only needs to cover in-flight requests — 30–60 s is usually enough.

Failure Modes

Symptom What happened Next step
green never became healthy Health probe never reported green within the timeout Check strut <stack> logs api --env prod on the green project; bump BLUE_GREEN_HEALTH_TIMEOUT if legitimate slow boot
Hook … did not define bluegreen_proxy_swap() BLUE_GREEN_PROXY_HOOK points to a file that's missing the function Define bluegreen_proxy_swap in that file
BLUE_GREEN_PROXY_HOOK points to missing file Config pointer is stale Fix the path in strut.conf
Both colors end up stopped Rollback flipped while the drained project's containers were already gone Delete .bluegreen, redeploy with --standard to restore a known baseline

A failed blue-green deploy does not snapshot to the rollback log — nothing was actually swapped. Only successful blue-green deploys write state.

Comparison

Standard Blue-Green
Downtime window compose down → pull → up (seconds) Zero (swap is atomic)
Disk usage at peak 1 copy 2 copies (both colors briefly)
Rollback cost Pull + restart (snapshot) Proxy flip (state file)
Rollback speed Seconds–minutes Seconds
Compose file changes None No container_name:, no host-port collisions
Proxy integration None (in-place restart) Hook or built-in reload

Blue-green is the right default for user-facing prod stacks. Standard is fine for background workers, cron runners, or anything without a traffic contract.

Related

Clone this wiki locally