Trims the deployed config down to what's actually useful day-to-day: a single Services group with the 10 stacks as plain docker widgets (traefik, coolify, gitea, code, immich, n8n, passbolt, umami, headscale, mailpit). Drops the Data Services group (8 database widgets), the Search widget, and auto-discovery (and the synthetic Discovered group it produced) - all still fully supported by the app itself and demonstrated in the pulsenode repo's own dogfood config, just more than this deployment needs.
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 |
headscale |
Self-hosted Tailscale control server (WireGuard mesh VPN) | headscale |
pulsenode |
Infrastructure dashboard - monitors all the stacks above | pulsenode |
Config files
| File | Description |
|---|---|
.env |
Root config: WEBHOOK_URL, RESTIC_REPOSITORY, RESTIC_PASSWORD (gitignored) |
.env.example |
Template for the root .env |
Management
Stacks are managed via stacks.sh from git@dev.pivoine.art:valknar/scripts.git, available as stacks in PATH.
Stack commands — all accept one or more stack names or glob patterns (omit for all stacks):
stacks ls # list all stacks with live container status
stacks ps gitea # container status table
stacks up # start all stacks
stacks up gitea traefik # start specific stacks
stacks down 'g*' # stop stacks matching glob
stacks restart 'g*,traefik' # glob + exact name, comma-separated
stacks pull --parallel # pull all images in parallel
stacks logs -f gitea # follow logs
stacks logs -n 100 gitea n8n # tail multiple stacks
stacks exec gitea gitea gitea admin user list # exec in container
stacks run passbolt passbolt bin/cake passbolt healthcheck
Service management (reads WEBHOOK_URL, RESTIC_REPOSITORY, RESTIC_PASSWORD from root .env):
stacks update install # write & enable systemd update timer
stacks update run # run update now
stacks update status # show timer/service status
stacks update logs # show journal logs
stacks backup install # write & enable systemd backup timer
stacks backup run # run backup now (auto-detects <stack>_db containers)
stacks backup snapshots # list restic snapshots
Scaffold a new stack:
stacks new myapp # basic stack with Traefik labels
stacks new myapp --db postgres # with Postgres service
stacks new myapp --db postgres --redis # with Postgres + Redis
stacks new myapp --no-traefik # expose port instead of Traefik
Generates compose.yml (with healthchecks, ../.data/ volumes, Traefik labels) and .env.example.
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 'stacks up <stack>'
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
source .env && restic init # initialise restic repo
stacks backup install # write & enable systemd unit + timer
stacks backup run # test run
stacks backup snapshots # list snapshots
stacks backup status # timer/service status
stacks 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 update install # write & enable systemd unit + timer
stacks update run # test run
stacks update status # timer/service status
stacks update logs # journald logs
Notifications
Both update and backup POST to an n8n webhook on completion, which forwards the message to Telegram.
The webhook URL is set via WEBHOOK_URL in the root .env. Both services point to the same n8n workflow at https://n8n.pivoine.art, which accepts { "message": "...", "color": "..." } 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.