-
Notifications
You must be signed in to change notification settings - Fork 3
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.
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 backEnable it as the default for a project by setting DEPLOY_MODE=blue-green in strut.conf.
Each blue-green deploy runs under a dedicated Docker Compose project named <stack>-<env>-<color>, where color alternates blue ↔ green. 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.
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_colorback 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.
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.shCLI 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
|
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.
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.
# 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
}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.
When stacks/<stack>/.bluegreen exists, strut rollback delegates to the blue-green rollback path instead of restoring images from a snapshot. It:
- Reads the current color from the state file and computes the opposite.
- Brings the drained project back up (
up -d --remove-orphans). - Swaps the proxy back (same hook, in reverse direction).
- Stops the now-current project.
- 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 # executeIf 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.
| 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.
| 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.
| 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.
- Deployment — standard deploy path
-
Deploy Rollback — image-restore rollback (standard mode) and
rollback diff -
Lifecycle Hooks —
pre_deploy/post_deployhooks still fire in blue-green mode - Domain and SSL — proxy config shipped by scaffold templates
strut · v0.28.0 · Report an Issue
Getting Started
Core Concepts
Operations
- Deployment
- Ship and Rebuild
- GitHub Action
- Webhook Automation
- Remote Host Setup
- Provisioning
- Blue-Green Deploy
- Deploy Rollback
- Database Backups
- Secrets Management
- Stack Groups
- Lifecycle Hooks
- Notifications
- Key Rotation
- Drift Detection
- Domain and SSL
- Certificate Management
- Gateway Management
- Monitoring
- Volume Management
Advanced
- Security Posture
- VPS Audit and Migration
- Stack Validation
- Data Anonymization
- Debugging
- Local Development
Extending
Contributing