Skip to content

Repository files navigation

Flow Deploy

CIReleaseLicense: MITCoverage: 90%+Python

Minimal, opinionated rolling deploys for Docker Compose + Traefik stacks.

Replaces Kamal's useful subset — rolling deploys, health checks, automatic failure recovery — without its baggage.

Install

curl -fsSL https://deploy.flowcanon.com/install | sh

This installs the flow-deploy binary to ~/.local/bin. Start a new login shell if it's not in your PATH.

Quick Start

1. Label your services in docker-compose.yml:

services:
web:
image: ghcr.io/myorg/myapp:${DEPLOY_TAG:-latest}labels:
deploy.role: apphealthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]interval: 10stimeout: 5sretries: 5

Every deploy.role=app service must have a healthcheck defined in compose (Dockerfile HEALTHCHECK is not detected). To opt out, set deploy.healthcheck.skip=true. Services without a deploy.role label are ignored.

2. Deploy:

flow-deploy deploy --tag abc123f
[12:34:56] ── deploy ──────────────────────────────
[12:34:56] tag: abc123f
[12:34:58] ▸ web
[12:34:58] pulling ghcr.io/myorg/myapp:abc123f...
[12:35:02] pulled (3.8s)
[12:35:02] starting new container...
[12:35:05] waiting for health check (timeout: 120s)...
[12:35:08] healthy (6.2s)
[12:35:08] draining old container (a1b2c3d, 30s timeout)...
[12:35:11] ✓ web deployed (16.1s)
[12:35:11] ── complete (16.1s) ─────────────────────

That's it. If the health check fails, the old container keeps serving traffic and the deploy exits 1.

How It Works

For each deploy.role=app service, in order:

  1. Pull the new image
  2. Scale to 2 — start a new container alongside the old one
  3. Health check — poll the new container until healthy or timeout
  4. Cutover — if healthy, gracefully drain the old container and scale back to 1
  5. Abort — if unhealthy, remove the new container. Old container is untouched.

Service Roles

LabelBehavior
deploy.role=appRolled during deploy. Health-checked. Rolled back on failure.
deploy.role=accessoryNever touched during deploy.
(no label)Ignored entirely.

Configuration Labels

All configuration is via Docker labels on your services:

LabelDefaultDescription
deploy.roleapp or accessory (required)
deploy.order100Deploy order. Lower goes first.
deploy.drain30Seconds to wait for graceful shutdown
deploy.healthcheck.timeout120Seconds to wait for healthy
deploy.healthcheck.poll2Seconds between health polls
deploy.healthcheck.skipfalseSkip healthcheck validation and waiting

Host Discovery

For CI/CD orchestration across multiple hosts, declare topology in your compose file:

x-deploy:
host: app-1.example.comuser: deployport: 22dir: /srv/myappservices:
web:
labels:
deploy.role: appworker:
labels:
deploy.role: appdeploy.host: worker-1.example.com # override per-service

Resolution order: GitHub Actions variable > per-service label > x-deploy default.

See GitHub Actions Setup for CI/CD integration.

CLI Commands

flow-deploy deploy [--tag TAG] [--service NAME] [--dry-run]
flow-deploy status
flow-deploy exec SERVICE COMMAND...
flow-deploy logs SERVICE [-f] [-n LINES]
flow-deploy upgrade

Compose Command Resolution

The tool resolves the compose command in this order:

  1. COMPOSE_COMMAND environment variable
  2. script/prod (if executable)
  3. docker compose

Links

Releases

Packages

Contributors

Languages