A tiny, opinionated CLI that orchestrates a repository's Docker Compose
projects — dependency-ordered startup, per-environment overlays,
variable/volume/network provisioning, backup/restore, and scaffolding, on top
of plain docker compose.
A tugboat is the small vessel that guides larger ships into dock. tug does
that for compose projects: tug up traefik, tug pull bonob, tug configure navidrome.
tug sits on top of docker compose and never becomes a runtime dependency of
your services — it only assembles and spawns docker commands, and your
compose files stay runnable with plain docker compose.
- Dependency-ordered lifecycle.
upstarts a project and its whole dependency chain, dependencies first;down/restart/pull/logsact on one project;all <verb>fans out over everything running. - Per-environment compose overlays.
docker-compose.ymlplus an optionaldocker-compose.<env>.yml, selected automatically. - Provisioning.
configureprompts for and persists required environment variables (with secrets), and creates the docker volumes and networks a project declares — for the project and all its dependencies at once. - Backup & restore of a project's docker volumes to/from compressed archives.
- Scaffolding.
new <name>stamps out a fresh app from templates. - Graph queries & completion.
list,deps(forward/reverse, transitive, JSON for CI),ps, and dynamicbash/zsh/fishcompletion.
Single self-contained binary. The only runtime dependency is the docker
binary on your PATH — no Node.js, no toolchain.
tug is built for a repository laid out like this, where each service is a
directory with a project.json describing its name, dependencies, and the
variables/volumes/networks it needs:
<repoRoot>/
apps/<name>/ # a service: docker-compose.yml + project.json
libs/<name>/ # shared snippets and graph-only projects (e.g. networks)
archive/<name>/ # retired services
.env / .env.local # dotenv layers
.secrets/ # one file per secret
.tugignore # optional: directories to stay out of entirely
Directory names (apps/, libs/, archive/) are convention — discovery scans
the whole repo for project.json files, skipping dot-directories and anything
an optional root .tugignore names. The full contract is in
docs/reference.md; the vocabulary is in
CONTEXT.md.
Prebuilt binaries are static x86_64-unknown-linux-musl and run on any x86_64
Linux. For other platforms, build from source.
Install script (latest release into ~/.local/bin):
curl -fsSL https://raw.githubusercontent.com/raeffs/tug/main/install.sh | shPin a version or change the target directory:
curl -fsSL https://raw.githubusercontent.com/raeffs/tug/main/install.sh \
| TUG_VERSION=v1.0.0 TUG_INSTALL_DIR=/usr/local/bin shManual download (verify the checksum yourself):
VERSION=v1.0.0
base=https://github.com/raeffs/tug/releases/download/$VERSION
curl -fsSLO $base/tug-$VERSION-x86_64-unknown-linux-musl
curl -fsSLO $base/tug-$VERSION-x86_64-unknown-linux-musl.sha256
sha256sum -c tug-$VERSION-x86_64-unknown-linux-musl.sha256
install -m 0755 tug-$VERSION-x86_64-unknown-linux-musl ~/.local/bin/tugFrom source (needs a Rust toolchain):
cargo install --git https://github.com/raeffs/tug # or: cargo build --releasetug completions <shell> prints a completion script for bash, zsh, or
fish. Completion is dynamic — it completes subcommands, flags, and your
repository's discovered project names. Enable it for your shell:
# bash — add to ~/.bashrc:
source <(tug completions bash)
# zsh — write to a directory on your $fpath, before compinit:
tug completions zsh > ~/.zsh/completions/_tug
# fish:
tug completions fish > ~/.config/fish/completions/tug.fishtug must be on your $PATH when completion runs, since the script calls back
out to it. See docs/reference.md for the details.
# From anywhere inside your repository:
tug list # every project and its direct dependencies
tug configure bonob --use-defaults # provision variables/volumes/networks for the chain
tug up bonob # start bonob and its dependencies, in order
tug ps # what's running
tug logs bonob --follow # tail its logs
tug restart bonob # bounce just bonob
tug down bonob # stop just bonobPreview exactly what would run, without touching docker:
tug up bonob --dry-run| Command | What it does |
|---|---|
up <project> |
Start the project and its dependency chain, in order. --no-deps starts the project alone. --force, --no-detach, --wait [--wait-timeout <s>], --dry-run. |
down <project> |
Stop just the project (no cascade). |
restart <project> |
Bounce just the project: down then up. |
pull <project> |
Pull the project's images (no cascade). |
logs <project> |
Stream the project's logs. --follow. |
all down|restart|pull |
Run a verb over every running project, best-effort. |
list |
Every project with its direct dependencies. |
deps <project> |
Query the dependency graph. --reverse, --transitive, --json. |
ps |
The repository's running projects and their status. |
configure <project> |
Provision variables, volumes, and networks for the chain. --use-defaults. |
create-variables / create-volumes / create-networks <project> |
The individual provisioning steps. |
backup-volumes / restore-volumes <project> |
Archive/restore the project's own volumes. |
new <name> |
Scaffold a new app under apps/. |
completions <bash|zsh|fish> |
Print a shell completion script. |
Every command takes --help. Full behaviour — exact docker invocations,
environment resolution, provisioning rules, error handling — is in
docs/reference.md.
configure --use-defaults and up --wait are the CI-facing commands; deps --reverse --transitive --json lets a pipeline compute the affected-project set.
When a job only exercises the app itself, up --wait --no-deps skips booting
the dependency chain — configure still needs the full cascade to resolve
variables the dependencies define.
Install a pinned, checksum-verified binary in a GitHub Actions step:
- name: Install tug
run: |
VERSION=v1.0.0
base=https://github.com/raeffs/tug/releases/download/$VERSION
curl -fsSLO $base/tug-$VERSION-x86_64-unknown-linux-musl
curl -fsSLO $base/tug-$VERSION-x86_64-unknown-linux-musl.sha256
sha256sum -c tug-$VERSION-x86_64-unknown-linux-musl.sha256
install -m 0755 tug-$VERSION-x86_64-unknown-linux-musl /usr/local/bin/tugtug is Rust, with a deliberately minimal dependency set (clap, serde,
serde_json, getrandom, anyhow) — everything else is hand-rolled. The
toolchain is pinned in rust-toolchain.toml.
cargo build
cargo test # unit + integration tests; no docker required
cargo fmt --check
cargo clippy # lints: clippy `all` + `pedantic`The tests need no docker: every external effect goes through a process-spawn
seam (a docker invocation described as data), so tests drive the CLI and
assert exact argv sequences against fixture repositories, and --dry-run is
just a printing implementation of the same seam. See
CONTEXT.md for the vocabulary.
MIT — see LICENSE.