DRAFT -- Pending CLI Stabilization
This document tracks the migration from
stackctl.shto the standalonestackctlbinary. The CLI contracts, release workflow, and GitHub Actions integration are still evolving and have not yet reached a stable 1.0. Sections marked with⚠️ may change before the first stable release.
This guide documents the migration from the repository-local ./stackctl.sh script to the
standalone stackctl binary. It covers configuration migration, command mapping, behavior
differences, and rollback instructions.
AniTrend/local-stack historically shipped a tools/stackctl.sh script plus Python-based
generation and rendering tools (generate_stacks.py, render_compose.py). The stackctl binary
replaces this entire toolchain with a single Deno-compiled binary, eliminating the Python and script
dependencies.
| Before | After |
|---|---|
./stackctl.sh up |
stackctl up |
| Python 3 + dependencies | Single binary, no runtime |
| Per-repo local script | System-wide install (Homebrew) |
| Shell-based config via env vars | ~/.stackctl YAML config |
| Manual profile switching | Built-in profile overlays |
- Docker with Swarm mode enabled (same as before)
- stackctl binary — installed via one of:
- Homebrew:
brew install AniTrend/tap/stackctl - GitHub Releases: download the appropriate tarball
(
stackctl-v<version>-<target-triple>.tar.gz) from the latest release. Supported triples:x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,x86_64-apple-darwin,aarch64-apple-darwin. - Manual:
deno install -n stackctl --allow-read --allow-write --allow-env --allow-run --allow-sys jsr:@anitrend/stackctl
- Homebrew:
- SOPS + age (optional) — only needed for
stackctl secretscommands
# Verify installation
stackctl --version
# Initialize config in your project
stackctl init
# Deploy all stacks
stackctl sync
# Check environment
stackctl doctorThe old stackctl.sh used shell environment variables and .env files:
export COMPOSE_DIR="./docker-compose"
export RENDER_DIR="./.rendered"
export STACKS_DIR="./stacks"
export STACK_PREFIX="mystack"
export STACKCTL_PROFILE="dev"Create a .stackctl file (generated via stackctl init):
project: myproject
stack:
# Root directory for service compose files (each must declare x-stack metadata)
directory: ./stack
# Stack names to manage (empty = all discovered)
names: []
# Default Docker network
network: myproject_default
# Override files (profile or explicit)
overrides: []
render:
# Output directory for rendered YAML
outputDirectory: ./.rendered
# Fail on unresolved variables
strict: false| Old Environment Variable | New Config Field | Example |
|---|---|---|
COMPOSE_DIR |
stack.directory |
./docker-compose |
RENDER_DIR |
render.outputDirectory |
./.rendered |
STACKS_DIR |
No equivalent (generated to stacks/) |
— |
STACK_PREFIX |
project |
mystack |
STACKCTL_PROFILE |
--profile flag or STACKCTL_PROFILE env |
dev |
These commands have reached functional parity with the old stackctl.sh script:
Old (./stackctl.sh) |
New (stackctl) |
Notes |
|---|---|---|
./stackctl.sh up |
stackctl up |
Replaces shell-based deploy |
./stackctl.sh down |
stackctl down |
— |
./stackctl.sh status |
stackctl status |
Now with --json output |
./stackctl.sh logs |
stackctl logs |
Improved streaming |
./stackctl.sh reload |
stackctl reload |
Full config-aware pipeline |
./stackctl.sh doctor |
stackctl doctor |
More comprehensive checks |
The standalone binary adds capabilities that were previously handled by separate Python scripts or not available at all:
| Command | Purpose |
|---|---|
stackctl generate |
Explicit stack regeneration |
stackctl render |
Explicit environment interpolation |
stackctl secrets |
SOPS/age integration |
stackctl env |
.env scaffolding |
stackctl plan |
Inspect operations without executing |
stackctl init |
Config file generation |
stackctl sync |
Full pipeline (generate → render → deploy) |
Record your current stackctl.sh environment:
echo "COMPOSE_DIR=${COMPOSE_DIR:-./docker-compose}"
echo "RENDER_DIR=${RENDER_DIR:-./.rendered}"
echo "STACK_PREFIX=${STACK_PREFIX}"
echo "STACKCTL_PROFILE=${STACKCTL_PROFILE:-dev}"# Interactive detection (scans for docker-compose files)
stackctl init
# Or with explicit values
stackctl init --project myproject --preset standardThis creates .stackctl in your project root. Edit it to match your recorded configuration from
Step 1.
stackctl doctorFixes any issues reported:
- Missing Docker or Swarm mode
- Invalid or missing
.stackctlconfig - Missing override files
- Missing stack directories
# See what would happen without making changes
stackctl sync --dry-run
stackctl up --dry-runReview the output carefully. The pipeline is:
Config → Discover → Generate → Override → Render → Deploy
# Deploy all stacks
stackctl sync
# Or deploy incrementally
stackctl up my-stack-namestackctl status
stackctl logs my-serviceSTACKCTL_PROFILE=prod ./stackctl.sh upProfiles use separate config overlays:
# Using flag
stackctl up --profile prod
# Using environment variable
STACKCTL_PROFILE=prod stackctl upProfile overlays are loaded in this order (later wins):
- Built-in defaults
.stackctl(base).stackctl.<profile>(e.g.,.stackctl.prod).stackctl.local(local overrides, gitignored).stackctl.local.<profile>(local profile overrides)
stackctl supports explicit override files in addition to profile overlays. Override files use
Docker Compose override semantics:
- Scalars: replaced
- Maps: deep-merged
- Sequences: appended
stackctl up --override ./overrides/production.yml --override ./overrides/region-eu.ymlOverride files are applied after profile merging but before render.
stackctl uses the x-stack compose metadata key to group docker-compose files into
named stacks during discovery and generation. Every compose file must declare which
stack it belongs to.
Two forms are supported:
Scalar (legacy) -- a plain stack name:
services:
api:
image: myapp/api
x-stack: apiObject (v1) -- a map with a name field:
services:
api:
image: myapp/api
x-stack:
name: apiBoth forms are equivalent and normalize to the same stack name. Scalar and object forms can coexist within the same stack group; they are merged by normalized name.
The object form currently accepts the name field only. Adding unknown fields
will cause an error:
# Invalid -- "labels" is not a recognized field
x-stack:
name: api
labels: [production]During discovery, files with invalid x-stack metadata are skipped for grouping
and reported as errors. During explicit loading (e.g. stackctl generate), invalid
metadata causes the command to fail with a descriptive message.
The x-stack key is source-only metadata. It is stripped from generated and
rendered stack output and never appears in deployed compose files.
# Remove a specific stack
stackctl down my-stack-name
# Re-deploy previous version
docker stack deploy --compose-file .rendered/my-stack-name.rendered.yml my-stack-name# Homebrew
brew switch stackctl <previous-version>
# Manual
cp /usr/local/bin/stackctl /usr/local/bin/stackctl.new
# ... download previous version
mv stackctl.previous /usr/local/bin/stackctlThe old stackctl.sh remains in your repository and is unaffected by stackctl installation. To
revert:
- Uninstall
stackctl:brew uninstall stackctl - Delete
.stackctlconfig:rm .stackctl - Continue using
./stackctl.shas before
Generated files (stacks/*.yml, .rendered/*.yml) are compatible between both tools for the same
configuration.
✗ Docker is not running or not accessible
Ensure Docker is running and your user has access:
docker info✗ Docker Swarm mode is not active
Initialize Swarm mode:
docker swarm init✗ Stack "myapp" not found in /path/to/project
Check that your compose files declare x-stack metadata and are located within the
configured stack.directory. See Compose Metadata (x-stack)
for supported forms.
# docker-compose.yml -- either form works:
services:
api:
image: myapp/api
x-stack: myapp # scalar form
# or:
# x-stack:
# name: myapp # object formstackctl validates configuration at startup. Run stackctl doctor for a complete diagnostic.
Common issues:
- Missing
project: Set the project name in.stackctl - Missing
stack.network: Set the Docker network name - Empty
stack.names: Leave as[]to discover all stacks, or list specific stack names - Invalid
render.outputDirectory: Must be a valid path
In strict mode (render.strict: true), unused variables cause failure. Switch to non-strict mode or
provide the variables:
# Non-strict mode
echo 'render:\n strict: false' >> .stackctl
# Provide variable
export MY_VAR=value
stackctl upstackctl requires these permissions:
--allow-read— read compose files, config, env files--allow-write— write generated/rendered stacks--allow-env— read environment variables--allow-run— execute Docker, sops, age--allow-sys— system info for doctor
When installed via Homebrew, permissions are pre-configured.
- Old: Relative paths in generated stacks reference the repo root
- New: Paths are absolutized to the project root during rendering
This means .rendered/*.yml files are self-contained and can be used independently of the working
directory.
⚠️ Generated files are not safe to deploy raw. Stack files instacks/(generated) and.rendered/(rendered) contain${VAR}placeholders that must be resolved through the render pipeline before deployment. Deploying a generated stack file directly without runningstackctl renderorstackctl syncwill result in unresolved environment variables in your running services.
stackctl produces deterministic YAML output:
- Keys are sorted alphabetically
- Stack files are ordered by stack name
- Runs produce identical output for identical input
This enables drift detection in CI.
- Old: First error stops the pipeline
- New: All errors are collected and reported at once
- Exit codes: 0=success, 1=validation/drift failure, 2=config error, 3=missing dependency, 4=unexpected error
- Old: Ctrl-C may leave processes running
- New: SIGINT is forwarded to child processes;
secrets deployruns cleanup on interruption
⚠️ The GitHub Actions integration is under active development and its location may change before the first stable release.
Add the setup-stackctl composite action to your workflow to install the stackctl binary on any
GitHub Actions runner (Linux x64/arm64, macOS x64/arm64):
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup stackctl
uses: AniTrend/stackctl/.github/actions/setup-stackctl@v1
with:
version: latest # or a specific version like "0.1.0"
- name: Verify installation
run: stackctl --version
- name: Run stackctl sync
run: stackctl syncThe action selects the correct tarball
(stackctl-v<version>-<target-triple>.tar.gz) for the runner's OS and
architecture, verifies the SHA256 checksum, and adds the binary to PATH for
all subsequent steps.