Illustrative people-free WIUNIX Academy operations workspace teaching blue and green releases, health gates and reversible proxy routing

WIUNIX ACADEMY

Docker Compose blue/green: health gates, proxy switch and rollback

Rehearse two stateless releases in separate Compose projects. Gate candidate health, verify the proxy path, switch a checked Nginx route and restore the old route without claiming zero downtime or data rollback.

Intermediate to advanced45 min readUpdated
Make this guide yours

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

Arch Linux (rolling)Reviewed against official documentation ·

USE YOUR OWN VALUES

Configure this tutorial

Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.

Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.

Set values for my environment5 editable values

THE OUTCOME

What will you have at the end?

Rehearse two stateless releases in separate Compose projects. Gate candidate health, verify the proxy path, switch a checked Nginx route and restore the old route without claiming zero downtime or data rollback.

Supported operating systems and versions

Existing Docker CE/Compose plugin paths for Ubuntu 26.10/26.04/24.04, Debian 13/12 and RHEL/Rocky/AlmaLinux 10/9 plus distribution Arch/Manjaro paths were documentation-reviewed on 2026-11-05. A local rootful Engine 28+ is required. No installation/container execution/all-distro qualification is claimed; channel and host-policy differences are explicit.

Before you begin

  • Disposable Linux VM with authorized Bash/sudo, an existing local rootful daemon and capacity for both release slots plus the gateway.
  • Docker Engine 28+, docker compose plugin with --wait/--wait-timeout, curl, iproute/iproute2 and coreutils.
  • Fresh lab path/project prefix, three unused loopback ports and no application data or secrets in the fixture.
  • Authorized access to the Docker Official Image digest; registry failure is not permission to remove the pin or use an unreviewed mirror.

Healthy is a gate, not a traffic switch

Compose can wait for a container healthcheck, but that does not switch a reverse proxy or prove a release works for users. This disposable lab runs two stateless demo releases in two Compose projects, each with its own bridge network and loopback test port. A proxy in the blue project joins both networks and has a third loopback port. We explicitly switch its route to green and then restore blue.

Blue and green serve different tiny static markers using the same pinned Nginx runtime. This teaches release separation, health gates and routing evidence, not an application-image build pipeline. There is no database, migration, shared mutable state, real user traffic, TLS termination or production release. Keep both slots alive through the rollback window.

This is not a zero-downtime guarantee. Reloaded Nginx workers may coexist with draining old workers; long-lived connections, background jobs, cache state and schema compatibility need separate application design. A local green probe does not certify those behaviors.

Choose unused names, ports and an exclusive lab directory

Open the public-values form and apply one new lowercase project prefix, a new absolute lab directory and three distinct unprivileged TCP ports. Derived project names end in -blue and -green. All published addresses stay fixed to 127.0.0.1; this guide intentionally provides no public bind-address field.

Values remain in this tab for the selected tutorial/language/environment. Pending or invalid edits disable copying until Apply or Reset. Enter no registry credential or application secret. The checks below reject existing project containers/networks and the lab directory; a port can still be claimed by another process after inspection, so a later bind error is a stop, not permission to kill that process.

01

LET’S BUILD

Identify the existing rootful engine and Compose plugin

Use a disposable Linux VM with an existing approved local rootful Docker Engine 28+ and a Compose plugin with --wait and --wait-timeout support. The helper below fixes the local Unix socket and removes context/host environment overrides. Keep this Bash session for later panels. Docker daemon access is administrative access; this is not a rootless, Docker Desktop, remote-daemon or Podman compatibility path.

For the rolling path reviewed on 2026-11-05, verify the distribution’s docker and docker-compose packages. Current Arch packaging also installs the CLI plugin; still check the docker compose launcher rather than assuming behavior from a filename. Manjaro’s package/branch may differ: do not import Arch packages or switch its branch/daemon for the exercise. Missing tools belong to approved full-upgrade maintenance, not a partial in-incident installation.

bash

Disposable Linux lab VM; same authorized Bash session

cat /etc/os-release
pacman -Q docker docker-compose curl iproute2 coreutils
docker_lab() { sudo env -u DOCKER_HOST -u DOCKER_CONTEXT docker --host unix:///var/run/docker.sock "$@"; }
docker_lab version --format '{{.Server.Version}}'
docker_lab info --format '{{.OperatingSystem}} {{json .SecurityOptions}}'
docker_lab compose version
compose_help=$(docker_lab compose up --help) || exit 1
printf '%s\n' "$compose_help" | grep -E -- '--wait([[:space:]]|$)' || exit 1
printf '%s\n' "$compose_help" | grep -F -- '--wait-timeout' || exit 1
engine_version=$(docker_lab version --format '{{.Server.Version}}') || exit 1
engine_major=${engine_version%%.*}
case "$engine_major" in ''|*[!0-9]*) exit 1;; esac
test "$engine_major" -ge 28 || exit 1
How do I know it worked?

The selected package channel, local rootful daemon, Engine 28+ and required plugin options are confirmed. Stop for an unsupported/different installation.

02

LET’S BUILD

Guard ownership and create only fresh lab paths

The lower-case project-name check is stricter than the generic form identifier. Inspect all existing containers and networks carrying either derived Compose project label, including stopped containers. A failed Docker query is an error, not an empty result. Resolve name collisions without removing another workload.

Create a new root-owned directory with public demo subdirectories. Never point this at existing application files: later SELinux private relabeling is restricted to these fresh paths. Check the three listening ports; the existing daemon may still allocate a port concurrently. No existing container or listener is stopped.

bash

Disposable Linux lab VM; same authorized Bash session

prefix='academybg'
case "$prefix" in ''|[!a-z0-9]*|*[!a-z0-9_-]*) printf 'Use a lowercase Compose prefix\n' >&2; exit 1;; esac
for project in "$prefix-blue" "$prefix-green"; do
  containers=$(docker_lab ps -aq --filter "label=com.docker.compose.project=$project") || exit 1
  networks=$(docker_lab network ls -q --filter "label=com.docker.compose.project=$project") || exit 1
  test -z "$containers$networks" || exit 1
done
listeners=$(ss -H -ltn '( sport = :18081 or sport = :18082 or sport = :18080 )') || exit 1
test -z "$listeners" || exit 1
sudo test ! -e '/srv/academy-bluegreen-demo' || exit 1
sudo test ! -L '/srv/academy-bluegreen-demo' || exit 1
sudo mkdir -m 0755 -- '/srv/academy-bluegreen-demo' || exit 1
sudo mkdir -m 0755 -- '/srv/academy-bluegreen-demo/blue' '/srv/academy-bluegreen-demo/green' '/srv/academy-bluegreen-demo/proxy' || exit 1
sudo mkdir -m 0755 -- '/srv/academy-bluegreen-demo/proxy/versions' || exit 1
How do I know it worked?

Both project names and the three ports were unused at inspection; only the fresh lab directory/subdirectories were created.

03

LET’S BUILD

Reserve demo markers and the four configuration files

The demo markers are deliberate static release artifacts. Create them only inside the fresh directory, then reserve the four configuration files without overwriting anything. Paste each following file panel into its exact path with sudoedit. If reservation stops partway, inspect the named empty files before retrying; do not remove the collision guard.

These files contain no secrets. Keep application data, databases and existing reverse-proxy configuration out of this fixture. The two route templates are created before the proxy mount is privately relabeled; later hard-link/rename selection preserves their existing inode labels instead of creating unlabeled replacement content.

bash

Disposable Linux lab VM; same authorized Bash session

sudo sh -c 'set -euC
printf "BLUE\n" > "$1/blue/release"
printf "BLUE_READY\n" > "$1/blue/healthz"
printf "GREEN\n" > "$1/green/release"
printf "GREEN_READY\n" > "$1/green/healthz"
: > "$1/blue.yml"
: > "$1/green.yml"
: > "$1/proxy/versions/blue.route"
: > "$1/proxy/versions/green.route"
chmod 0644 "$1"/blue/* "$1"/green/* "$1/blue.yml" "$1/green.yml" "$1"/proxy/versions/*
' sh '/srv/academy-bluegreen-demo' || exit 1
sudoedit '/srv/academy-bluegreen-demo/blue.yml' '/srv/academy-bluegreen-demo/green.yml' '/srv/academy-bluegreen-demo/proxy/versions/blue.route' '/srv/academy-bluegreen-demo/proxy/versions/green.route'
How do I know it worked?

Fresh release markers and four reserved configuration files are ready for the following contents.

Blue project: old release plus the shared entry point

Blue and the gateway belong to one project, but only the gateway joins the external green network. The project-specific blue-app alias prevents the proxy from confusing identical service names across networks. The gateway healthcheck proves only that Nginx can serve its own readiness location; the blue dependency is a startup gate, not continuous routing control.

Each bind directory has one container owner and uses a private Z label when SELinux is active. No directory is shared between blue/green containers. Do not apply Z to system directories or existing content. Docker port publishing can affect host firewall rules even with loopback bindings; use the approved disposable daemon and keep its manager configuration intact.

yaml · /srv/academy-bluegreen-demo/blue.yml

File contents; not a shell command

services:
  blue:
    image: nginx:stable-alpine@sha256:0e52bd054126cdc040b1276c0af347aeb02a41b9cfe305d6094767ac30e97949
    restart: "no"
    ports:
      - "127.0.0.1:18081:80"
    volumes:
      - ./blue:/usr/share/nginx/html:ro,Z
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1/healthz | grep -qx BLUE_READY"]
      interval: 2s
      timeout: 2s
      retries: 10
    networks:
      default:
        aliases: [blue-app]
  gateway:
    image: nginx:stable-alpine@sha256:0e52bd054126cdc040b1276c0af347aeb02a41b9cfe305d6094767ac30e97949
    restart: "no"
    ports:
      - "127.0.0.1:18080:80"
    volumes:
      - ./proxy:/etc/nginx/conf.d:ro,Z
    depends_on:
      blue:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1/gateway-ready | grep -qx PROXY_READY"]
      interval: 2s
      timeout: 2s
      retries: 10
    networks: [default, green]
networks:
  default: {}
  green:
    external: true
    name: academybg-green_default

Green project: candidate release on its own network

Green has a separate Compose project, default bridge network, content directory and loopback port. Its healthcheck reads the same small local readiness endpoint, not the public gateway route. There is no cross-project depends_on: the later explicit gate checks green from both the host and the gateway network.

The first create operation will prepare this project and its network without starting green. That lets the blue gateway attach to the external network while blue still serves traffic. No database initialization or traffic switch occurs during create.

yaml · /srv/academy-bluegreen-demo/green.yml

File contents; not a shell command

services:
  green:
    image: nginx:stable-alpine@sha256:0e52bd054126cdc040b1276c0af347aeb02a41b9cfe305d6094767ac30e97949
    restart: "no"
    ports:
      - "127.0.0.1:18082:80"
    volumes:
      - ./green:/usr/share/nginx/html:ro,Z
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1/healthz | grep -qx GREEN_READY"]
      interval: 2s
      timeout: 2s
      retries: 10
    networks:
      default:
        aliases: [green-app]
networks:
  default: {}

Known-good route to blue

The active file initially selects this template. The route marker header identifies the selected proxy configuration, while /release identifies the backend response. Neither is a business transaction. Static upstream names resolve when Nginx loads configuration; changing a container IP later can require another controlled reload.

nginx · /srv/academy-bluegreen-demo/proxy/versions/blue.route

File contents; not a shell command

upstream demo_release {
    server blue-app:80;
}
server {
    listen 80;
    location = /gateway-ready {
        default_type text/plain;
        return 200 "PROXY_READY\n";
    }
    location / {
        proxy_pass http://demo_release;
        proxy_set_header Host $host;
        proxy_connect_timeout 2s;
        proxy_read_timeout 5s;
        add_header X-Demo-Route blue always;
    }
}

Candidate route to green

Only the upstream and visible demo route marker differ. The templates live under versions with a .route extension, outside Nginx’s top-level *.conf include. Do not create a second server file in the included directory or leave an editor backup ending in .conf.

nginx · /srv/academy-bluegreen-demo/proxy/versions/green.route

File contents; not a shell command

upstream demo_release {
    server green-app:80;
}
server {
    listen 80;
    location = /gateway-ready {
        default_type text/plain;
        return 200 "PROXY_READY\n";
    }
    location / {
        proxy_pass http://demo_release;
        proxy_set_header Host $host;
        proxy_connect_timeout 2s;
        proxy_read_timeout 5s;
        add_header X-Demo-Route green always;
    }
}
Explore all tutorials ↗