- Inline update logic (pull → compare digests → up -d → prune → notify) - Inline backup logic with dynamic Postgres detection: any running <stack>_db container is dumped using the <stack>/<stack> convention - Systemd unit files are now generated on `install` from embedded heredocs pointing at stacks.sh itself — no external scripts needed - Root .env (WEBHOOK_URL, RESTIC_REPOSITORY, RESTIC_PASSWORD) replaces the per-service .env files in _backup/ and _update/ - Remove _backup/ and _update/ directories entirely - Update README accordingly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stacks
Self-contained Docker Compose stacks for pivoine.art infrastructure.
Each stack is independently deployable with its own compose.yml and .env. All persistent data lives in ../.data/<stack>/.
Stacks
| Stack | Description | Services |
|---|---|---|
traefik |
Reverse proxy, TLS termination | traefik |
mailpit |
SMTP relay (no web UI) | mailpit |
umami |
Web analytics | umami, db |
immich |
Photo & video management | immich, ml, redis, db |
n8n |
Workflow automation & notification relay | n8n, db |
gitea |
Git hosting + CI runner | gitea, runner, db |
coolify |
Deployment platform | coolify, realtime, redis, db |
passbolt |
Password manager (GPG-encrypted, team sharing) | passbolt, db |
code |
Browser-based VS Code IDE with Anthropic API access | code |
Tools
| File | Description |
|---|---|
stacks.sh |
CLI to manage stacks, services, scaffolding, updates, and backups |
.env |
Root config: WEBHOOK_URL, RESTIC_REPOSITORY, RESTIC_PASSWORD (gitignored) |
.env.example |
Template for the root .env |
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.
./stacks.sh help
Stack commands — all accept one or more stack names or glob patterns (omit for all stacks):
./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 (reads WEBHOOK_URL, RESTIC_REPOSITORY, RESTIC_PASSWORD from root .env):
./stacks.sh update install # write & 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 # write & enable systemd backup timer
./stacks.sh backup run # run backup now (auto-detects <stack>_db containers)
./stacks.sh backup snapshots # list restic snapshots
Scaffold a new stack:
./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:
# 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
# Copy example env and fill in secrets
cp <stack>/.env.example <stack>/.env
# Sync a stack to VPS
rsync -avz <stack>/ vps:~/stacks/<stack>/
# Start a stack
ssh vps 'cd ~/stacks/<stack> && docker compose up -d'
Network
All stacks share the external falcon_network Docker network for inter-service communication (e.g. traefik routing, mailpit SMTP).
Backup
Runs daily at 3:00 AM via a systemd timer. Detects Postgres databases automatically by convention (<stack>_db container, user <stack>, database <stack>), dumps each one, then runs a full restic backup of .data/. Retention: 7 daily, 4 weekly, 6 monthly. Notifications go to Telegram via n8n.
# First-time setup on VPS
cp .env.example .env && $EDITOR .env # set RESTIC_REPOSITORY, RESTIC_PASSWORD, WEBHOOK_URL
restic init # initialise restic repo (uses vars from .env)
./stacks.sh backup install # write & enable systemd unit + timer
./stacks.sh backup run # test run
./stacks.sh backup snapshots # list snapshots
./stacks.sh backup status # timer/service status
./stacks.sh backup logs # journald logs
Updates
Runs nightly at 2:00 AM via a systemd timer. Pulls the latest image for every stack, recreates any container whose image changed, prunes dangling images, and sends a Telegram notification via n8n.
./stacks.sh update install # write & enable systemd unit + timer
./stacks.sh update run # test run
./stacks.sh update status # timer/service status
./stacks.sh update logs # journald logs
Notifications
The update script and the backup script both POST to an n8n webhook, which forwards messages to Telegram.
The webhook URL is set in:
_backup/.env→WEBHOOK_URL_update/.env→WEBHOOK_URL
Both point to the same n8n workflow at https://n8n.pivoine.art. The workflow accepts { "message": "..." } and forwards it to Telegram.
Data
Persistent data is stored in ~/stacks/.data/<stack>/ on the VPS using bind mounts. Database stacks use dedicated Postgres instances with simple credentials.