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 ## 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) | | `_backup` | Daily restic backups to HiDrive (host script + systemd timer) |
| `_update` | Nightly image update check + prune (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 ## Deployment
```bash ```bash
@@ -53,19 +114,13 @@ rsync -avz _backup/ vps:~/stacks/_backup/
# Initialize restic repo (first time only) # Initialize restic repo (first time only)
ssh vps 'source ~/stacks/_backup/.env && restic init -r /mnt/hidrive/users/valknar/Backup/stacks' ssh vps 'source ~/stacks/_backup/.env && restic init -r /mnt/hidrive/users/valknar/Backup/stacks'
# Install systemd units # Install systemd units (or use stacks.sh on the VPS)
ssh vps 'ln -sf ~/stacks/_backup/stacks-backup.service /etc/systemd/system/ && \ ssh vps '~/stacks/stacks.sh backup install'
ln -sf ~/stacks/_backup/stacks-backup.timer /etc/systemd/system/ && \
systemctl daemon-reload && systemctl enable --now stacks-backup.timer'
# Manual test run # Manual run / status
ssh vps '~/stacks/_backup/backup.sh' ssh vps '~/stacks/stacks.sh backup run'
ssh vps '~/stacks/stacks.sh backup status'
# Check timer status ssh vps '~/stacks/stacks.sh backup snapshots'
ssh vps 'systemctl status stacks-backup.timer'
# View snapshots
ssh vps 'source ~/stacks/_backup/.env && restic -r /mnt/hidrive/users/valknar/Backup/stacks snapshots'
``` ```
## Updates ## Updates
@@ -75,18 +130,13 @@ The `_update` script runs nightly at 2:00 AM. It pulls the latest image for ever
```bash ```bash
# Deploy update stack # Deploy update stack
rsync -avz _update/ vps:~/stacks/_update/ rsync -avz _update/ vps:~/stacks/_update/
ssh vps 'chmod +x ~/stacks/_update/update.sh'
# Install systemd units # Install systemd units (or use stacks.sh on the VPS)
ssh vps 'ln -sf ~/stacks/_update/stacks-update.service /etc/systemd/system/ && \ ssh vps '~/stacks/stacks.sh update install'
ln -sf ~/stacks/_update/stacks-update.timer /etc/systemd/system/ && \
systemctl daemon-reload && systemctl enable --now stacks-update.timer'
# Manual test run # Manual run / status
ssh vps '~/stacks/_update/update.sh' ssh vps '~/stacks/stacks.sh update run'
ssh vps '~/stacks/stacks.sh update status'
# Check timer status
ssh vps 'systemctl status stacks-update.timer'
``` ```
## Notifications ## Notifications
+59 -29
View File
@@ -726,6 +726,7 @@ REDIS
# ─── Shell Completion ───────────────────────────────────────────────────────── # ─── Shell Completion ─────────────────────────────────────────────────────────
_completion_bash() { _completion_bash() {
local stacks_init="$1" # pre-rendered stacks array initialisation block
cat << 'EOF' cat << 'EOF'
# stacks.sh bash completion — source this file or place in /etc/bash_completion.d/ # stacks.sh bash completion — source this file or place in /etc/bash_completion.d/
_stacks_complete() { _stacks_complete() {
@@ -736,19 +737,10 @@ _stacks_complete() {
prev="${COMP_WORDS[COMP_CWORD-1]}" prev="${COMP_WORDS[COMP_CWORD-1]}"
} }
# Locate the stacks dir from the invoked script path
local script="${COMP_WORDS[0]}" local script="${COMP_WORDS[0]}"
local stacks_dir EOF
stacks_dir="$(cd "$(dirname "$(realpath "$script" 2>/dev/null || echo "$script")")" 2>/dev/null && pwd)" echo "$stacks_init"
cat << 'EOF'
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
local commands="ls ps up down restart pull logs exec run config images top stats compose update backup new completion version help" 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" local update_subs="install uninstall run status logs next"
@@ -758,7 +750,7 @@ _stacks_complete() {
case "${COMP_WORDS[1]}" in case "${COMP_WORDS[1]}" in
update) COMPREPLY=( $(compgen -W "$update_subs" -- "$cur") ); return ;; update) COMPREPLY=( $(compgen -W "$update_subs" -- "$cur") ); return ;;
backup) COMPREPLY=( $(compgen -W "$backup_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) new)
case "$prev" in case "$prev" in
--db) COMPREPLY=( $(compgen -W "postgres mysql mariadb" -- "$cur") ); return ;; --db) COMPREPLY=( $(compgen -W "postgres mysql mariadb" -- "$cur") ); return ;;
@@ -803,22 +795,15 @@ EOF
} }
_completion_zsh() { _completion_zsh() {
local stacks_init="$1" # pre-rendered stacks array initialisation block
cat << 'EOF' cat << 'EOF'
#compdef stacks.sh stacks #compdef stacks.sh stacks
_stacks() { _stacks() {
local script="${words[1]}" local script="${words[1]}"
local stacks_dir EOF
stacks_dir="$(cd "$(dirname "$(realpath "$script" 2>/dev/null || echo "$script")")" 2>/dev/null && pwd)" echo "$stacks_init"
cat << 'EOF'
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
local -a commands=( local -a commands=(
'ls:List all stacks with live status' 'ls:List all stacks with live status'
@@ -916,6 +901,7 @@ _stacks() {
completion) completion)
_arguments \ _arguments \
'--install[Install the completion file]' \ '--install[Install the completion file]' \
'--static[Bake current stack list into completion script]' \
'1:shell:(bash zsh)' '1:shell:(bash zsh)'
;; ;;
esac esac
@@ -928,13 +914,14 @@ EOF
} }
cmd_completion() { cmd_completion() {
local shell="" install=false local shell="" install=false static=false
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
bash|zsh) shell="$arg" ;; bash|zsh) shell="$arg" ;;
--install) install=true ;; --install) install=true ;;
*) die "Unknown argument: $arg (bash|zsh [--install])" ;; --static) static=true ;;
*) die "Unknown argument: $arg (bash|zsh [--install] [--static])" ;;
esac esac
done done
@@ -944,10 +931,52 @@ cmd_completion() {
warn "No shell specified, defaulting to: $shell" warn "No shell specified, defaulting to: $shell"
fi 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 local content
case "$shell" in case "$shell" in
bash) content="$(_completion_bash)" ;; bash) content="$(_completion_bash "$bash_stacks_init")" ;;
zsh) content="$(_completion_zsh)" ;; zsh) content="$(_completion_zsh "$zsh_stacks_init")" ;;
*) die "Unsupported shell: $shell (bash|zsh)" ;; *) die "Unsupported shell: $shell (bash|zsh)" ;;
esac esac
@@ -1042,7 +1071,8 @@ ${BOLD}UTILITIES${RESET}
--db postgres|mysql|mariadb Add a database service --db postgres|mysql|mariadb Add a database service
--redis Add a Redis service --redis Add a Redis service
--no-traefik Expose port instead of Traefik labels --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}version${RESET} Print version
${CYAN}help${RESET} Show this help ${CYAN}help${RESET} Show this help