valknar 8ae9c9e878 fix(update): compare local image store IDs, not running container IDs
docker compose images -q reports the image IDs of currently running
containers, which don't change after a pull — so before == after always
and containers were never recreated.

Fix: resolve each service's image tag to its local SHA256 ID via
docker image inspect, which reads the local image store and correctly
reflects the newly pulled image. Falls back from 'config --images'
(compose v2.19+) to parsing 'config' yaml for older versions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 21:09:56 +02:00

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/.envWEBHOOK_URL
  • _update/.envWEBHOOK_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.

S
Description
Self-contained Docker Compose stacks for pivoine.art infrastructure.
Readme 359 KiB
Languages
Shell 99.6%
Go Template 0.4%