Skip to content

Deployment

Griffen Fargo edited this page Aug 1, 2026 · 11 revisions

Deployment

Deploy and manage Docker stacks on VPS infrastructure. strut supports dry-run previews, service profiles, blue-green releases, and one-command rollback.

Quick Deploy

strut my-stack deploy --env prod --dry-run   # Preview
strut my-stack deploy --env prod             # Deploy

There is one deploy command, and it goes wherever the stack lives. If the stack maps to a VPS, deploy runs the full pipeline on that host over SSH: sync the repo → run migrations → pull images → restart services → verify health → roll back if unhealthy. If the stack has no VPS, the same command deploys against your local Docker daemon.

You never pick the target by choosing a command name — the stack's topology already knows it. See Multi-Host Topology for how a stack resolves to a host.

Where a Deploy Lands

Situation What deploy does
Stack maps to a VPS Full pipeline on that VPS, over SSH
Stack has no VPS mapping Local Docker daemon
You're already on the VPS Local Docker daemon (no SSH round trip)
--local passed Local Docker daemon, always
--require-remote passed Fails rather than ever going local

Related commands:

Command What It Does
release Alias for deploy --require-remote — see below
update Sync the repo on the VPS only, restart nothing
stop Stop containers

release is an alias

release used to be the separate verb for "deploy to the VPS", while deploy always meant "deploy right here". That split was the single most common source of mistakes with strut: the two read like alternatives, but release was actually the orchestrator and deploy was the primitive it SSHed in to run.

Now that deploy resolves its own target, release is exactly deploy --require-remote, and it remains as a permanently-supported alias. Existing scripts, runbooks, and CI jobs keep working unchanged — there is no deprecation warning and no removal planned. New docs and examples use deploy.

--require-remote

Use this anywhere a local deploy could never be what you meant — CI runners, automation, agent tooling:

strut my-stack deploy --env prod --require-remote

Without it, a stack whose VPS_HOST fails to resolve falls back to the local Docker daemon and exits 0 — on a CI runner that's a successful-looking no-op that deployed nothing. With it, that case is a hard error. The GitHub Action and the MCP Server both pass it automatically.

Controlling the Pipeline

Skip individual steps when you don't need them:

strut my-stack deploy --env prod --no-sync      # Don't git-sync; deploy the code already on the host
strut my-stack deploy --env prod --no-migrate   # Skip the migration steps
strut my-stack deploy --env prod --no-rollback  # Don't auto-roll-back on health failure
strut my-stack deploy --env prod --backup-first # Back up databases before deploying
strut my-stack deploy --env prod --strict       # Halt the deploy if a migration fails

--no-sync is the one to reach for when you've changed an env var or image tag and want to restart the stack without shipping new code.

To deploy against your local Docker daemon even though the stack maps to a VPS:

strut my-stack deploy --env prod --local

(--force-local is the older spelling and still works.)

Manual Deploy Steps

If you want to drive the steps yourself:

strut my-stack update --env prod              # Sync strut repo on VPS
strut my-stack deploy --env prod --no-sync    # Deploy the code now on the host
strut my-stack health --env prod --json       # Verify

Preview Changes vs VPS (diff)

strut <stack> diff shows semantic changes between local state and the VPS: which env vars would change and which compose image tags would move. Exit 1 when changes exist (useful as a CI gate), 0 when identical.

strut my-stack diff --env prod
strut my-stack diff --env prod --json

# Typical CI gate: block PR merge when prod has pending changes
strut my-stack diff --env prod || echo "pending changes"

Since v0.13.0. Complements drift (VPS → local) and deploy --dry-run (command-level preview).

Blue-Green Deploys (since v0.20.0)

For zero-downtime releases, enable blue-green mode. strut stands the new version up alongside the old one under a <stack>-<env>-<color> compose project, health-gates it, swaps the reverse proxy, and drains the old color.

strut my-stack deploy --env prod --blue-green
strut my-stack deploy --env prod --standard      # force in-place, overrides config

Default for a project: set DEPLOY_MODE=blue-green in strut.conf. Rollback uses a fast state-file flip instead of an image restore. See Blue-Green Deploy for proxy-hook examples, compose-file requirements, and failure modes.

Deploy Health Gate (since v0.42.0)

After docker compose up -d, strut polls container health before printing the success banner. A crash-looping or unhealthy stack now fails the deploy instead of silently succeeding.

How it works: The deploy polls containers every 3 seconds, checking that all are running and passing Docker health checks (or have no healthcheck). Three consecutive healthy polls are required. On timeout or restart detection, the deploy fails with reason=health_gate_failed, fires the on_health_fail hook, and sends a deploy.failed notification.

Environment variables:

Variable Default Description
DEPLOY_HEALTH_TIMEOUT 60 Seconds to wait for containers to become healthy
DEPLOY_SKIP_HEALTH_GATE false Set to true or 1 to skip the gate entirely

CLI flag:

strut my-stack deploy --env prod --skip-health-gate   # skip for one-shot/migration stacks

When to skip: Stacks with intentionally short-lived containers (one-shot migrations, seed scripts) that exit after up -d. These would fail the gate because no containers remain running.

Performance: Most healthy stacks pass in ~9s (3 polls × 3s), much faster than the old fixed 60s wait.

Service Profiles

strut my-stack deploy --env prod                          # core (default)
strut my-stack deploy --env prod --services messaging     # + messaging
strut my-stack deploy --env prod --services full          # everything

Profiles map to Docker Compose profiles defined in your docker-compose.yml.

Stopping Containers

strut my-stack stop --env prod                # Stop containers
strut my-stack stop --env prod --volumes      # Stop + remove volumes
strut my-stack stop --env prod --dry-run      # Preview what would stop

Dry Run

All destructive operations support --dry-run:

strut my-stack deploy --env prod --dry-run
strut my-stack stop --env prod --dry-run
strut my-stack backup postgres --env prod --dry-run
strut my-stack rollback --env prod --dry-run
strut my-stack domain example.com admin@example.com --env prod --dry-run

This shows the execution plan without making any changes.

First-Time VPS Setup

  1. Provision the host (if needed):
    strut harbor provision                     # Run setup script
  2. Bootstrap strut on the VPS:
    strut my-stack remote:init --env prod      # Clone repo, install strut
  3. Create env file from template:
    strut my-stack init-secrets --env prod     # Generate secrets
    nano .prod.env                              # Fill in remaining values
  4. Push secrets to VPS:
    strut my-stack secrets push --env prod
  5. Deploy:
    strut my-stack deploy --env prod

Post-Deploy Verification

strut my-stack health --env prod --json       # Health checks
strut my-stack status --env prod              # Container status
strut my-stack logs my-service --since 30m --env prod  # Recent logs

Pre-Deploy Validation

Deploy automatically validates config before pulling images. If validation fails, the deploy aborts before making any changes.

# [2/7] Pre-deploy validation...
# ✓ strut.conf: valid
# ✓ services.conf: valid (3 services, 2 databases)
# ✓ docker-compose.yml: syntax valid
# ✓ secrets: no issues detected
# ✓ Pre-deploy validation passed

Checks run automatically:

  1. strut validate — config schema validation + secret scanning (since v0.9.0)
  2. docker compose config — compose file syntax
  3. Custom lifecycle hooks (see Lifecycle Hooks)

The secret scanner flags known credential patterns (GitHub PATs, AWS access keys, sk-* API keys, Slack webhooks) and weak/placeholder values in PASSWORD/SECRET/TOKEN/KEY vars. It also errors if an env file is tracked by git.

Skip for emergencies:

strut my-stack deploy --env prod --skip-validation

Configure in strut.conf:

PRE_DEPLOY_VALIDATE=true    # Run config validation (default: true)
PRE_DEPLOY_HOOKS=true       # Run custom hooks (default: true)

Deploy Concurrency Locks

Since v0.13.0, deploy acquires a local lock under ~/.strut/locks/<stack>-<env>.lock.d/ (and a remote lock under <deploy_dir>/.strut-locks/ when VPS_HOST is set) so two concurrent deploys can't race.

strut my-stack lock status --env prod
strut my-stack lock release --env prod --force    # break a stuck lock
strut my-stack deploy --env prod --force-unlock   # break + deploy
strut my-stack deploy --env prod --no-lock        # advanced: skip entirely

Locks auto-release on process exit via the unified EXIT trap. Locks older than STRUT_LOCK_STALE_SECONDS (default 600s) are detected as stale by lock status.

Lifecycle Hooks

Each stack can drop executable scripts into stacks/<stack>/hooks/:

Hook Fires Abort on non-zero?
pre_deploy.sh before deploy yes
post_deploy.sh after deploy no (warn)
pre_backup.sh before backup yes
post_backup.sh after backup no (warn)
on_health_fail.sh after health check fails no (warn)
on_drift_detected.sh when drift is found no (warn)

Snake_case and dash forms both work (e.g. pre-deploy.sh). See Lifecycle Hooks for event payload env vars and examples.

Rollback

If a deploy goes wrong, roll back to the previous container images:

strut my-stack rollback --env prod              # Restore previous deploy
strut my-stack rollback --env prod --list       # List available snapshots
strut my-stack rollback --env prod --dry-run    # Preview rollback

Snapshots are saved automatically before each deploy. See Deploy Rollback for details.

Migrations

Run database migrations as part of deployment:

strut my-stack migrate postgres --status --env prod   # Check pending
strut my-stack migrate postgres --up --env prod       # Apply
strut my-stack migrate neo4j --down 1 --env prod      # Rollback one

Best Practices

  1. Always --dry-run first for destructive commands
  2. Backup before major changes: strut my-stack backup all --env prod
  3. Use --no-sync when you're changing config, not code — it skips the git sync and just restarts
  4. Keep services.conf up to date — it drives health checks
  5. Use required_vars to catch missing env vars before deploy

Data Safety (since v0.29.0)

Git Clean Guard

strut now dry-runs git clean on the VPS checkout before executing the actual clean. If untracked, non-ignored paths are found, the operation aborts with a message listing the affected files and guidance on how to resolve them. This prevents accidental deletion of runtime data that lives inside the checkout.

Override with --force-clean when you're certain the untracked files are safe to remove.

Volume Guard (volguard)

Before deploying, strut compares volume-defining environment variables (any var ending in _PATH, _DATA_PATH, or DATA_VOLUME_*) between the local env file and the remote. If values differ, the deploy aborts with a warning that data may be orphaned or lost.

Override with --confirm-data-move when an intentional volume relocation is planned.

Best Practices

  • Store persistent data outside the checkout directory, or add bind-mount paths to .gitignore.
  • Review strut <stack> diff --env prod before deploying to catch volume path changes early.
  • See Git Clean Safety for a deeper walkthrough of the guard behavior.

Related Pages

Clone this wiki locally