Compare commits

...
2 Commits
Author SHA1 Message Date
valknarandClaude Sonnet 4.6 e3cd2df372 docs: document stacks.sh in README
Replace manual docker compose / systemctl snippets with stacks.sh
equivalents and add a dedicated section covering all commands.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 19:55:05 +02:00
valknarandClaude Sonnet 4.6 067d017ea6 feat(stacks): add --static flag to completion command
Bakes the current stack list into the generated completion script instead
of using runtime directory discovery. Useful for remote hosts where the
stacks dir path differs from the local repo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 19:52:55 +02:00
2 changed files with 132 additions and 52 deletions
+73 -23
View File
@@ -20,11 +20,72 @@ Each stack is independently deployable with its own `compose.yml` and `.env`. Al
## Tools
| Directory | Description |
| File/Directory | Description |
|---|---|
| `stacks.sh` | CLI to manage stacks, services, and scaffolding |
| `_backup` | Daily restic backups to HiDrive (host script + systemd timer) |
| `_update` | Nightly image update check + prune (host script + systemd timer) |
## stacks.sh
`stacks.sh` is the primary management CLI. It wraps `docker compose` with glob-based multi-stack targeting, manages the update and backup systemd services, scaffolds new stacks, and generates shell completions.
```bash
./stacks.sh help
```
**Stack commands** — all accept one or more stack names or glob patterns (omit for all stacks):
```bash
./stacks.sh ls # list all stacks with live container status
./stacks.sh ps gitea # container status table
./stacks.sh up # start all stacks
./stacks.sh up gitea traefik # start specific stacks
./stacks.sh down 'g*' # stop stacks matching glob
./stacks.sh restart 'g*,traefik' # glob + exact name, comma-separated
./stacks.sh pull --parallel # pull all images in parallel
./stacks.sh logs -f gitea # follow logs
./stacks.sh logs -n 100 gitea n8n # tail multiple stacks
./stacks.sh exec gitea gitea gitea admin user list # exec in container
./stacks.sh run passbolt passbolt bin/cake passbolt healthcheck
```
**Service management:**
```bash
./stacks.sh update install # link & enable systemd update timer
./stacks.sh update run # run update now
./stacks.sh update status # show timer/service status
./stacks.sh update logs # show journal logs
./stacks.sh backup install # link & enable systemd backup timer
./stacks.sh backup run # run backup now
./stacks.sh backup snapshots # list restic snapshots
```
**Scaffold a new stack:**
```bash
./stacks.sh new myapp # basic stack with Traefik labels
./stacks.sh new myapp --db postgres # with Postgres service
./stacks.sh new myapp --db postgres --redis # with Postgres + Redis
./stacks.sh new myapp --no-traefik # expose port instead of Traefik
```
Generates `compose.yml` (with healthchecks, `../.data/` volumes, Traefik labels) and `.env.example`.
**Shell completion:**
```bash
# Dynamic — discovers stacks at tab-complete time (default)
./stacks.sh completion zsh --install
# Static — bakes current stack list in; useful on the VPS
./stacks.sh completion zsh --static --install
```
**Global flags:** `--dry-run`, `--parallel`, `--verbose`, `--quiet`
## Deployment
```bash
@@ -53,19 +114,13 @@ rsync -avz _backup/ vps:~/stacks/_backup/
# Initialize restic repo (first time only)
ssh vps 'source ~/stacks/_backup/.env && restic init -r /mnt/hidrive/users/valknar/Backup/stacks'
# Install systemd units
ssh vps 'ln -sf ~/stacks/_backup/stacks-backup.service /etc/systemd/system/ && \
ln -sf ~/stacks/_backup/stacks-backup.timer /etc/systemd/system/ && \
systemctl daemon-reload && systemctl enable --now stacks-backup.timer'
# Install systemd units (or use stacks.sh on the VPS)
ssh vps '~/stacks/stacks.sh backup install'
# Manual test run
ssh vps '~/stacks/_backup/backup.sh'
# Check timer status
ssh vps 'systemctl status stacks-backup.timer'
# View snapshots
ssh vps 'source ~/stacks/_backup/.env && restic -r /mnt/hidrive/users/valknar/Backup/stacks snapshots'
# Manual run / status
ssh vps '~/stacks/stacks.sh backup run'
ssh vps '~/stacks/stacks.sh backup status'
ssh vps '~/stacks/stacks.sh backup snapshots'
```
## Updates
@@ -75,18 +130,13 @@ The `_update` script runs nightly at 2:00 AM. It pulls the latest image for ever
```bash
# Deploy update stack
rsync -avz _update/ vps:~/stacks/_update/
ssh vps 'chmod +x ~/stacks/_update/update.sh'
# Install systemd units
ssh vps 'ln -sf ~/stacks/_update/stacks-update.service /etc/systemd/system/ && \
ln -sf ~/stacks/_update/stacks-update.timer /etc/systemd/system/ && \
systemctl daemon-reload && systemctl enable --now stacks-update.timer'
# Install systemd units (or use stacks.sh on the VPS)
ssh vps '~/stacks/stacks.sh update install'
# Manual test run
ssh vps '~/stacks/_update/update.sh'
# Check timer status
ssh vps 'systemctl status stacks-update.timer'
# Manual run / status
ssh vps '~/stacks/stacks.sh update run'
ssh vps '~/stacks/stacks.sh update status'
```
## Notifications
+59 -29
View File
@@ -726,6 +726,7 @@ REDIS
# ─── Shell Completion ─────────────────────────────────────────────────────────
_completion_bash() {
local stacks_init="$1" # pre-rendered stacks array initialisation block
cat << 'EOF'
# stacks.sh bash completion — source this file or place in /etc/bash_completion.d/
_stacks_complete() {
@@ -736,19 +737,10 @@ _stacks_complete() {
prev="${COMP_WORDS[COMP_CWORD-1]}"
}
# Locate the stacks dir from the invoked script path
local script="${COMP_WORDS[0]}"
local stacks_dir
stacks_dir="$(cd "$(dirname "$(realpath "$script" 2>/dev/null || echo "$script")")" 2>/dev/null && pwd)"
local -a stacks=()
if [[ -d "$stacks_dir" ]]; then
for d in "$stacks_dir"/*/; do
local n; n="$(basename "$d")"
[[ "$n" == _* ]] && continue
[[ -f "$d/compose.yml" ]] && stacks+=("$n")
done
fi
EOF
echo "$stacks_init"
cat << 'EOF'
local commands="ls ps up down restart pull logs exec run config images top stats compose update backup new completion version help"
local update_subs="install uninstall run status logs next"
@@ -758,7 +750,7 @@ _stacks_complete() {
case "${COMP_WORDS[1]}" in
update) COMPREPLY=( $(compgen -W "$update_subs" -- "$cur") ); return ;;
backup) COMPREPLY=( $(compgen -W "$backup_subs" -- "$cur") ); return ;;
completion) COMPREPLY=( $(compgen -W "bash zsh --install" -- "$cur") ); return ;;
completion) COMPREPLY=( $(compgen -W "bash zsh --install --static" -- "$cur") ); return ;;
new)
case "$prev" in
--db) COMPREPLY=( $(compgen -W "postgres mysql mariadb" -- "$cur") ); return ;;
@@ -803,22 +795,15 @@ EOF
}
_completion_zsh() {
local stacks_init="$1" # pre-rendered stacks array initialisation block
cat << 'EOF'
#compdef stacks.sh stacks
_stacks() {
local script="${words[1]}"
local stacks_dir
stacks_dir="$(cd "$(dirname "$(realpath "$script" 2>/dev/null || echo "$script")")" 2>/dev/null && pwd)"
local -a stacks=()
if [[ -d "$stacks_dir" ]]; then
for d in "$stacks_dir"/*/; do
local n="${d:t}"
[[ "$n" == _* ]] && continue
[[ -f "${d}compose.yml" ]] && stacks+=("$n")
done
fi
EOF
echo "$stacks_init"
cat << 'EOF'
local -a commands=(
'ls:List all stacks with live status'
@@ -916,6 +901,7 @@ _stacks() {
completion)
_arguments \
'--install[Install the completion file]' \
'--static[Bake current stack list into completion script]' \
'1:shell:(bash zsh)'
;;
esac
@@ -928,13 +914,14 @@ EOF
}
cmd_completion() {
local shell="" install=false
local shell="" install=false static=false
for arg in "$@"; do
case "$arg" in
bash|zsh) shell="$arg" ;;
--install) install=true ;;
*) die "Unknown argument: $arg (bash|zsh [--install])" ;;
--static) static=true ;;
*) die "Unknown argument: $arg (bash|zsh [--install] [--static])" ;;
esac
done
@@ -944,10 +931,52 @@ cmd_completion() {
warn "No shell specified, defaulting to: $shell"
fi
# Build the stacks array initialisation block — either dynamic (runtime discovery)
# or static (current list baked in at generation time).
local bash_stacks_init zsh_stacks_init
if $static; then
local -a cur_stacks=()
mapfile -t cur_stacks < <(list_all_stacks)
[[ ${#cur_stacks[@]} -eq 0 ]] && die "No stacks found to bake into completion"
local generated_at
generated_at="$(date '+%Y-%m-%d')"
bash_stacks_init=" # Stacks baked in at generation time (${generated_at}) — rerun to update
local -a stacks=(${cur_stacks[*]})"
zsh_stacks_init=" # Stacks baked in at generation time (${generated_at}) — rerun to update
local -a stacks=(${cur_stacks[*]})"
info "Baking ${#cur_stacks[@]} stacks: ${cur_stacks[*]}"
else
bash_stacks_init=' # Dynamically discover stacks from the script location at completion time
local stacks_dir
stacks_dir="$(cd "$(dirname "$(realpath "$script" 2>/dev/null || echo "$script")")" 2>/dev/null && pwd)"
local -a stacks=()
if [[ -d "$stacks_dir" ]]; then
for d in "$stacks_dir"/*/; do
local n; n="$(basename "$d")"
[[ "$n" == _* ]] && continue
[[ -f "$d/compose.yml" ]] && stacks+=("$n")
done
fi'
zsh_stacks_init=' # Dynamically discover stacks from the script location at completion time
local stacks_dir
stacks_dir="$(cd "$(dirname "$(realpath "$script" 2>/dev/null || echo "$script")")" 2>/dev/null && pwd)"
local -a stacks=()
if [[ -d "$stacks_dir" ]]; then
for d in "$stacks_dir"/*/; do
local n="${d:t}"
[[ "$n" == _* ]] && continue
[[ -f "${d}compose.yml" ]] && stacks+=("$n")
done
fi'
fi
local content
case "$shell" in
bash) content="$(_completion_bash)" ;;
zsh) content="$(_completion_zsh)" ;;
bash) content="$(_completion_bash "$bash_stacks_init")" ;;
zsh) content="$(_completion_zsh "$zsh_stacks_init")" ;;
*) die "Unsupported shell: $shell (bash|zsh)" ;;
esac
@@ -1042,7 +1071,8 @@ ${BOLD}UTILITIES${RESET}
--db postgres|mysql|mariadb Add a database service
--redis Add a Redis service
--no-traefik Expose port instead of Traefik labels
${CYAN}completion${RESET} bash|zsh [--install] Generate/install shell completion
${CYAN}completion${RESET} bash|zsh [--install] [--static] Generate/install shell completion
--static Bake current stack list in (no runtime discovery)
${CYAN}version${RESET} Print version
${CYAN}help${RESET} Show this help