Generic CLI tool for managing Docker stacks on VPS infrastructure. Bash + BATS test suite.
Two filesystem trees collaborate at runtime:
- Strut_Home (
~/.strut/) — the engine:strutentrypoint,lib/*.shmodules,templates/ - Project_Root (user's dir) — their config:
strut.conf,stacks/,.envfiles
The strut entrypoint resolves Strut_Home via symlink resolution, then walks up from $PWD to find strut.conf (Project_Root). Config is loaded before any lib modules.
| File | Purpose |
|---|---|
strut |
CLI entrypoint — command dispatch, usage |
lib/config.sh |
Project config discovery (find_project_root, load_strut_config) |
lib/registry.sh |
Pluggable registry auth (ghcr/dockerhub/ecr/none) |
lib/utils.sh |
Colors, logging, SSH helpers, compose builders |
lib/deploy.sh |
Deploy orchestration, VPS release, repo sync |
lib/deploy_blue_green.sh |
Blue-green deploy: dual-project, health-gated proxy swap, state-file rollback |
lib/health.sh |
Dynamic health checks from services.conf |
lib/cmd_*.sh |
Command handlers (deploy, stop, init, scaffold, tui, etc.) |
lib/docker.sh |
Docker pull, prune helpers |
lib/volumes.sh |
Dynamic volume management from volume.conf |
lib/keys.sh + lib/keys/ |
Key management (SSH, API, env, db, GitHub) |
lib/backup.sh + lib/backup/ |
Backup/restore for Postgres, Neo4j, MySQL, SQLite |
lib/drift.sh + lib/drift/ |
Config drift detection and auto-fix |
lib/migrate.sh + lib/migrate/ |
VPS migration wizard (8 phases) |
templates/ |
Scaffold templates for new stacks |
install.sh |
One-liner installer (clone + symlink) |
VERSION |
Semver, read by strut --version |
strut <stack> <command> [--env <name>] [--dry-run] [--services <profile>]
strut # interactive TUI (fzf+select)
strut --no-tui | STRUT_NO_TUI=1 # disable TUI
Top-level: init, upgrade, --version, list, scaffold, audit, migrate, monitoring,
secrets-filter (transparent git clean/smudge for at-rest secrets)
Per-stack: deploy, stop, update, health, logs, status, shell, exec,
backup, restore, db:pull, db:push, db:schema, drift, volumes, keys, domain
release — alias for deploy (see below)
deploy resolves its target from the stack's topology, like status/health/
logs do (should_dispatch_remote, lib/utils.sh): a stack mapped to a VPS
runs the full pipeline on that host (vps_release — sync → migrate → deploy →
health → auto-rollback), everything else deploys against the local Docker
daemon. --local (alias --force-local) forces local; --no-sync /
--no-migrate skip pipeline steps.
release is a permanently-supported alias for deploy — it existed as a
separate verb when deploy always meant "run here", which made
wrong-target deploys a repeated mistake (strut#415). It differs only in
refusing a stack that resolves to no VPS instead of falling back to local.
STRUT_REMOTE_EXEC=1 is stamped on every ./strut invocation sent over SSH.
It is the authoritative "we are the target" signal — is_running_on_vps
otherwise infers this from hostname/IP, which cannot match a Tailscale
MagicDNS name or CNAME, and without the marker such a host would dispatch
to itself unbounded. Any new remote ./strut call site must set it.
strut.conf— project-level: registry type, org, branch, bannerservices.conf— service ports, health paths, DB flags (drives health engine)required_vars— env vars validated before deployvolume.conf— volume paths and ownership mappingsrepos.conf— GitHub repos for key managementbackup.conf— backup schedule and retentionenv/hosts/<alias>.env— tracked, non-secret per-host env layer. Applied on every deploy where the stack resolves to a host alias (via[stacks]→[hosts]instrut.conf, or--host <alias>), last-wins over the base env file. Unlike the legacystacks/<stack>/.<host>.envoverride (still supported, lower precedence), this path is not gitignored, so it survives a clean git-redeploy.common.env(project root, optional) — shared, non-secret env values across every stack (load_common_env,lib/utils.sh). Lowest precedence: loaded before the stack/project env file and the per-host layer, both of which override it. Not swept by the.env/.*.envgitignore rules (no leading dot) — meant to be committed. Seetemplates/common.env.template..<env>.enc.env(stack dir or project root) — committed, at-rest-encrypted env file for thesecrets-filtergit clean/smudge workflow (strut#178). Plaintext in the working tree, age ciphertext in the git blob. Preferred over.<env>.envbyresolve_env_fileat the same level, and explicitly un-ignored (!*.enc.env/!.*.enc.env) so it can be committed understacks/<stack>/where the plain.*.envrule would otherwise sweep it.env/stack.gen.enc.env,env/hosts/<alias>.gen.enc.env— committed, age/gpg-encrypted values generated once bystrut <stack> gen <VAR>(lib/cmd_gen.sh) and applied as the final layer of the env chain (env_apply_gen_layer,lib/utils.sh,strut#179), overriding the base env file (including a resolved.<env>.enc.env),env/hosts/<alias>.env, andcommon.envon conflicting keys — stack-scope applies first, host-scope last (most specific wins). Decrypted on demand via_secrets_unlock; decrypt failure (e.g. no age identity) warns and skips rather than failing the deploy, so read-only commands still work without the key.
- No hardcoded service names, ports, orgs, or paths in the engine
- All behavior driven by config files (
strut.conf,services.conf, etc.) lib/config.showns all config loading and defaultslib/registry.showns all registry auth dispatch- Health checks dynamically discover services from
services.conf - Required vars validation is optional (skip if no
required_varsfile)
bats tests/ # Run all tests
bats tests/test_config.bats # Run specific fileBATS test suite with property-based tests (100-iteration randomized loops). Key test files:
| Test | Properties |
|---|---|
test_config.bats |
Config walk-up, parsing defaults, symlink resolution |
test_registry.bats |
Registry dispatch routing, invalid type rejection |
test_health_discovery.bats |
Service/DB/port discovery from services.conf |
test_init.bats |
Init flag propagation to strut.conf |
test_entrypoint.bats |
Version round-trip, upgrade guard |
test_no_hardcodes.bats |
Static grep — no Climate-Hub references in engine |
test_scaffold.bats |
Org substitution, required_vars consistency |
- All
lib/*.shfiles start withset -euo pipefail fail()exits with code 1 to stderr;warn()continues to stdoutlog()prefix is[strut]; banner readsBANNER_TEXTfrom config- SSH commands use
build_ssh_optsfor consistent option building - Compose commands use
resolve_compose_cmdfor consistent project naming DRY_RUN=true+run_cmd/run_cmd_evalfor preview mode
- Do not add
Co-Authored-By: Claude …(or any AI/tool attribution) trailers to commits or PR bodies in this repo. History was filter-repo'd to remove them; don't reintroduce.
9 procedural skills for operational workflows: vps-deployment, vps-debugging, database-backups, key-rotation, stack-validation, drift-detection, monitoring-setup, domain-ssl, vps-audit-migration.