Skip to content

flowcanon/deploy

Repository files navigation

Flow Deploy

CI Release License: MIT Coverage: 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: app
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 10s
      timeout: 5s
      retries: 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

Label Behavior
deploy.role=app Rolled during deploy. Health-checked. Rolled back on failure.
deploy.role=accessory Never touched during deploy.
(no label) Ignored entirely.

Configuration Labels

All configuration is via Docker labels on your services:

Label Default Description
deploy.role β€” app or accessory (required)
deploy.order 100 Deploy order. Lower goes first.
deploy.drain 30 Seconds to wait for graceful shutdown
deploy.healthcheck.timeout 120 Seconds to wait for healthy
deploy.healthcheck.poll 2 Seconds between health polls
deploy.healthcheck.skip false Skip 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.com
  user: deploy
  port: 22
  dir: /srv/myapp

services:
  web:
    labels:
      deploy.role: app
  worker:
    labels:
      deploy.role: app
      deploy.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