valknar 5d6a752f71 expose Traefik dashboard on traefik.pivoine.art, vpn-only gated
Previous attempt tried a separate internal-only :8080 entrypoint for
pulsenode to reach the API directly - that turned out to 404 no
matter what (--api.dashboard=true alone didn't register a working
router there, and adding --api=true plus an explicit entrypoint still
didn't produce a matching router; verified against a local traefik:v3
container before giving up on that path rather than keep guessing
against the live one).

Cleaner approach: expose it through the same web/web-secure
entrypoints everything else already uses, via a new file-provider
router (traefik/dynamic/dashboard.yaml) matching Traefik's own
documented self-referencing pattern - Host(`traefik.pivoine.art`) &&
(PathPrefix(`/api`) || PathPrefix(`/dashboard`)) routed to the built-in
api@internal service, gated by vpn-only same as pulsenode's own router.

vpn-only's sourceRange now also includes falcon_network's own subnet
(172.18.0.0/16) alongside the tailnet ranges - a container on our own
internal docker network is as trusted as a tailnet peer, and without
this pulsenode's own request to the API (a container-to-container
call, not a VPN-sourced one) would get the same 403 a random internet
visitor would. pulsenode's traefik widget now points at
https://traefik.pivoine.art/api instead of the internal :8080 attempt.
2026-08-17 18:56:53 +02:00
2026-08-17 15:42:08 +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
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.

S
Description
Self-contained Docker Compose stacks for pivoine.art infrastructure.
Readme
935 KiB
Languages
CSS 100%