Commit Graph
100 Commits
Author SHA1 Message Date
valknar 79f0c1a293 fix(gitea): cache-server needs external_secret, not external_secret_file
The cache-server subcommand only checks the literal external_secret
field at startup and errors out if just external_secret_file is set
(unlike the daemon, which handles _file fine). Move its config out of
git entirely - it now has to hold the actual secret value - generated
straight into .data on the VPS instead.
2026-08-24 11:39:06 +02:00
valknar e1a540f110 fix(gitea): bypass run.sh entrypoint for the cache-server container
run.sh (act_runner's default image entrypoint) ignores any command args
entirely - it always registers then execs `act_runner daemon`
regardless, with no cache-server mode. The cache-server container was
silently running as a second runner daemon instead. Override the
entrypoint directly to tini + act_runner, skipping run.sh.
2026-08-24 11:30:22 +02:00
valknar 0d85ef73b9 fix(gitea): runner config for caching 4 2026-08-24 11:26:54 +02:00
valknar 99ff1c0c4e fix: wrong act runner config 2026-08-24 09:12:42 +00:00
valknar 6353f662fa fix(gitea): runner config for caching 3 2026-08-24 11:08:48 +02:00
valknar 5b00a9809d fix(gitea): runner config for caching 2 2026-08-24 10:58:15 +02:00
valknar 30ac8954ad fix(gitea): runner config for caching 2026-08-24 10:30:33 +02:00
valknarandClaude Sonnet 5 9af932cd70 chore(obsidian): drop blog reference link from local.ini.example
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 21:01:29 +02:00
valknarandClaude Sonnet 5 a9f9a6765b feat(obsidian): add CouchDB stack for Obsidian LiveSync sync
Self-hosted sync backend for the Obsidian LiveSync plugin, following
https://blog.admin-intelligence.de/selbsthosting-von-obsidian-mit-couchdb-kostenlose-synchronisation-einrichten/.
local.ini.example carries the required chttpd/cors config; local.ini itself
is gitignored since CouchDB rewrites it in place (hashed admin password,
node uuid) on first start, so a tracked copy would never stay clean. The
config also has to be mounted read-write, not :ro: CouchDB's entrypoint
chowns everything under /opt/couchdb and aborts under set -e if that fails.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 20:54:55 +02:00
valknarandClaude Sonnet 5 00cc3583ec fix(traefik): stop fail2ban banning legit traffic on stray 4xx
statuscode "400,401,403-499" counted almost every client error as a
brute-force strike, so normal 404s/400s from routine app behavior could
rack up 5 hits in 10 minutes and trip a 3h ban (returned as 429) for real
users. Narrow to 401/403 (actual auth failures) and raise maxretry to 15
so it still catches real brute-forcing without false-positiving on noise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 18:44:20 +02:00
valknarandClaude Sonnet 5 f151cd9aae fix(gitea): color ui warning/success/error/info message text, not just header
.ui.negative.message .header only tinted the header; the message body and
the warning/success/info variants had no color rule at all across every
Catppuccin theme.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 18:35:13 +02:00
valknarandClaude Sonnet 5 d80358e238 fix(pulsenode): use coder icon for Code Server, globe for Web Apps
Requires pulsenode v0.4.5 (BrandIcon's new "globe" generic icon and
the coder brand icon).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpxhtQY3CExQdMs4j7MmJe
2026-08-18 10:31:43 +02:00
valknarandClaude Sonnet 5 d9a0a902e3 feat(pulsenode): rename Infra to System, add Services/Web Apps quick-link bookmarks
Drop the standalone Code Server/Passbolt/Umami docker widgets in
favor of a compact multi-link bookmark card for every self-hosted
service, plus a second one linking out to the Coolify-hosted web
apps (pivoine.art, bar.pivoine.art, etc.). Requires pulsenode
v0.4.4+ for the bookmark widget's new `links` field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpxhtQY3CExQdMs4j7MmJe
2026-08-18 10:17:03 +02:00
valknar 4ffa078da0 feat(pulsenode): move traefik into Services as a service widget
Matches pulsenode v0.4.3, which folded the dedicated traefik widget
type into the same combined docker+API pattern as the other six
services. No apiUrl needed anymore - base URL is derived from
containerName like every other service widget.
2026-08-17 21:34:36 +02:00
valknar ad245518ba feat(pulsenode): split Infra/Services groups, add traefik icon+href
Separates system/traefik (host+proxy infra) from the per-service
widgets into their own group, and gives the traefik widget an icon
and public dashboard link now that pulsenode's traefik widget
supports both.
2026-08-17 21:23:21 +02:00
valknar 336a5e46b8 feat(pulsenode): switch coolify/gitea/immich/n8n/headscale to service widgets
Uses the new combined docker+API widget type shipped in pulsenode
v0.4.0. Umami and Passbolt stay as plain docker widgets - Umami has no
static API key for self-hosted instances (needs a login flow, skipped
for now), Passbolt has no widget-relevant API to poll.
2026-08-17 21:05:12 +02:00
valknar 4c439a4deb fix(traefik): restore --ping=true, causing the dashboard to vanish
Docker's provider drops a container's own docker-label routers when
Docker reports that container unhealthy. --ping=true had been dropped
somewhere along the way, so /ping 404'd, the healthcheck failed,
traefik was marked unhealthy, and its own traefik-dashboard-web(-secure)
routers disappeared as a result. Also restores acme.tlschallenge=true
(needed for cert renewal) and drops the debug log level used for
earlier troubleshooting.
2026-08-17 20:22:35 +02:00
valknar 6a03d2698f chore: re-enable dashboard 2026-08-17 20:14:47 +02:00
valknar e7368f0eda chore(traefik): remove dangling traefik-internal router labels
Leftover from an earlier failed attempt (referenced a since-removed
"internal" entrypoint, logging harmless "EntryPoint doesn't exist"
errors). The api.insecure + named "traefik" entrypoint approach now
in place supersedes it.
2026-08-17 20:11:36 +02:00
valknar b77a65119b fix: claude mess 2 2026-08-17 18:09:01 +00:00
valknar dccbd22020 fix: claude mess 2026-08-17 17:49:42 +00:00
valknar 3761924242 fix(traefik): expose API on an internal-only entrypoint for container-to-container access
The public traefik.pivoine.art path round-trips through the host's
published port, and Docker's hairpin NAT there rewrites the source IP
before it reaches vpn-only's allowlist, so pulsenode's own request was
getting a 403 despite falcon_network being allowlisted. Port 8080 is
not published to the host, so it's reachable only from other
falcon_network containers - no TLS/vpn-only needed, and it's not
exposed externally.
2026-08-17 19:29:43 +02:00
valknar bed01b4fdf streamline traefik dashboard routing: docker labels instead of file provider
Switches traefik.pivoine.art routing from a standalone file-provider
router (dynamic/dashboard.yaml) to docker-compose labels on traefik's
own service - the same pattern every other stack already uses for its
own routing, rather than a one-off exception. Traefik discovers itself
just like any other container on falcon_network. Both routers point
at the built-in api@internal service via .service= instead of a
loadbalancer target, since there's no backend container port to route
to. Also dropped the (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
condition - the whole traefik.pivoine.art host is dedicated to this,
so a bare Host() match is simpler and no less correct.

Also fixes why VPN access still failed: traefik.pivoine.art was never
added to Headscale's MagicDNS extra_records, so even a tailnet-
connected browser resolved it via public DNS and hit vpn-only from a
non-tailnet source IP - same root cause as the earlier
pulsenode.pivoine.art fix, just missed for this hostname.
2026-08-17 19:05:08 +02:00
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
valknar b0dba5bf40 fix(traefik): explicitly define the traefik entrypoint on :8080
--api.dashboard=true alone wasn't enough - the API returned 404 on
every path after enabling it. Traefik apparently doesn't reliably
auto-create the default "traefik" entrypoint the way older docs
describe; defining it explicitly is the documented, reliable way to
get the API/dashboard router actually listening. Still not in ports:,
so still only reachable from other containers on falcon_network.
2026-08-17 18:48:41 +02:00
valknar f207009efd add system and traefik widgets to pulsenode; enable Traefik API
- traefik/compose.yml: flip --api.dashboard=true so Traefik's internal
  API is reachable. No entrypoint named "traefik" is defined, so
  Traefik auto-creates its default one on :8080 - not published to the
  host (only 80/443 are), so it's reachable only from other containers
  on falcon_network, i.e. exactly (and only) pulsenode.
- pulsenode/config.yml: adds a system widget and swaps the plain
  Traefik docker card for the real traefik widget (router/entrypoint/
  middleware view via that now-enabled API), both at the top of
  Services.

Verified locally before deploying: a plain container (no pid: host,
no /proc,/sys mounts) already reports host-accurate CPU count, memory
total, and root disk usage via systeminformation, since Docker doesn't
namespace /proc/stat, /proc/meminfo, or overlayfs df by default. No
privilege escalation needed for the system widget after all - tested
side by side against the real host's nproc/free/df output. Also
confirmed the merged config.yml validates against the app's own zod
schema, not just as YAML.
2026-08-17 18:46:51 +02:00
valknar 858d20e16e remove Mailpit from pulsenode dashboard 2026-08-17 18:22:49 +02:00
valknar 5fc922b477 simplify pulsenode dashboard: one Services group, no db/search/discovery
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.
2026-08-17 18:19:39 +02:00
valknar 83056e0fdc sync pulsenode config.yml: brand icons, drop stale accent override
Mirrors the pulsenode app repo's updated config.yml (v0.3.0): removes
the leftover --pn-accent: #38bdf8 override that was masking the new
neon-green default, and adds icon: entries for every service that has
a brand match in lib/brand-icons.ts.
2026-08-17 18:02:05 +02:00
valknar d7ba6ec040 fix(headscale): add MagicDNS override for pulsenode.pivoine.art
vpn-only (the ipAllowList middleware pulsenode now uses) checks the
connection's source IP, but being on the tailnet doesn't reroute
traffic to a normal public DNS hostname through the tunnel - Tailscale
only reroutes destinations it actually knows about. pulsenode.pivoine.art
had no such record, so it always resolved publicly for everyone,
tailnet or not, and Traefik never saw a tailnet-range source IP to
allow. Same fix already in place for triggershell.falcon.pivoine.art:
an extra_records override served only over MagicDNS, pointing to the
VPS's own tailnet IP so tailnet clients' connections actually traverse
the tunnel and satisfy the allowlist.
2026-08-17 16:02:29 +02:00
valknar 318f48036d refactor: move vpn-only middleware from falcon.yaml to security.yaml
falcon-vpn-only was defined inline in falcon.yaml even though it's a
generic ipAllowList restriction with nothing falcon-specific about it
- security.yaml is where the other reusable middlewares (security-
headers, no-index, rate-limit) already live. Renamed to vpn-only to
match their bare naming style now that it's not tied to one file, and
updated falcon.yaml's two router references to vpn-only@file (same
cross-file pattern already used there for security-headers@file).

Also applies it to the pulsenode stack: it's a dashboard surfacing
container names/health for every service here, and there's no auth in
front of it yet, so gating it to the tailnet is a real mitigation for
that gap rather than leaving it open on the public hostname.
2026-08-17 15:54:40 +02:00
valknar 1c5ee1002a feat: add pulsenode stack
Infrastructure dashboard monitoring all the stacks in this repo, built
via stacks new pulsenode and then hand-adjusted for what a generic
scaffold can't know: pulls the published image
(dev.pivoine.art/valknar/pulsenode:latest) instead of building, mounts
/var/run/docker.sock:ro plus its own config/ directory read-only, runs
read-only/non-root/cap-dropped (group_add on DOCKER_GID to get socket
access without running as root), and uses its own healthcheck.mjs
rather than curl (not present in its minimal image).

config/config.yml is real, not a placeholder: docker/database widgets
for every container across all ten stacks here, discovery enabled
against falcon_network as a safety net for anything added later.
2026-08-17 15:42:08 +02:00
valknar 6e96e33875 feat(traefik,headscale): restrict falcon.pivoine.art to Tailscale mesh only
Add a Traefik IP allowlist so *.falcon.pivoine.art rejects any source
outside the Tailscale/Headscale CIDR ranges, even though DNS still
resolves publicly. Pair it with a headscale MagicDNS override so
tailnet clients resolve the hostname straight to the VPS's tailscale
IP and route correctly.

Requires Docker's userland-proxy disabled on the VPS host
(/etc/docker/daemon.json) so Traefik sees real client source IPs
instead of the docker bridge gateway — done manually, not tracked
in this repo.
2026-08-16 16:51:53 +02:00
valknar 184200299d feat(traefik): route *.falcon.pivoine.art to apps on falcon over Tailscale
Falcon (home machine) stays off the public internet; the VPS Traefik
terminates TLS/ACME as usual and forwards to falcon's tailnet IP.
2026-08-16 16:32:21 +02:00
valknarandClaude Sonnet 5 4951492f4c feat(headscale): add self-hosted Tailscale control server stack
CLI-managed (no web UI), sqlite-backed, fronted by Traefik like the other stacks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 15:55:36 +02:00
valknarandClaude Sonnet 4.6 aaa4564a0b revert(gitea): remove custom images, use default Gitea logo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 19:27:12 +02:00
valknarandClaude Sonnet 4.6 824c78535b fix(gitea): transparent background, flower maximized to fill square
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 19:01:14 +02:00
valknarandClaude Sonnet 4.6 b46f553457 feat(gitea): replace icons with square Catppuccin Frappé peony icon
Dark Frappé base (#303446) background with the pivoine.art peony
centered and padded. Replaces logo.svg/png, favicon.svg/png, and
apple-touch-icon.png.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 18:52:35 +02:00
valknarandClaude Sonnet 4.6 66536adde7 fix(gitea): set default theme to catppuccin-frappe-blue
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 18:04:21 +02:00
valknarandClaude Sonnet 4.6 9065421264 feat(gitea): replace custom themes with Catppuccin v1.0.2
70 CSS files covering all 4 flavors (Latte/Frappé/Macchiato/Mocha)
× 14 accents + 14 auto variants. Default theme set to Mocha Mauve.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 18:01:35 +02:00
valknarandClaude Sonnet 4.6 ec92d55e3c fix(mailpit): bind UI to port 8025 so healthcheck passes; revert email FROM changes
Port 0 disabled the web UI but broke the built-in /readyz healthcheck.
Binding to 8025 keeps the UI accessible within the Docker network only
(no Traefik labels) and lets the container report healthy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 19:34:40 +02:00
valknarandClaude Sonnet 4.6 1be281ec97 fix(mail): change FROM address to hi@pivoine.art for IONOS relay compatibility
IONOS rejects relay from VPS IPs when sender doesn't match authenticated
account. Switch gitea and passbolt FROM to the actual IONOS account address.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 19:09:00 +02:00
valknarandClaude Sonnet 4.6 7ae681cc83 docs(stacks): update README — stacks.sh moved to scripts repo, use stacks alias
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 15:53:16 +02:00
valknarandClaude Sonnet 4.6 056db5d6ea refactor(stacks): move stacks.sh to ~/scripts repo
stacks.sh now lives in git@dev.pivoine.art:valknar/scripts.git
and resolves STACKS_DIR from env or defaults to ~/stacks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 15:47:30 +02:00
valknarandClaude Sonnet 4.6 a7a3fb8582 fix(stacks): show correct status when backup service has failed
Same fix as the update service: gate "not installed" on unit file
existence rather than systemctl exit code, which is non-zero for
failed services too.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 02:21:44 +02:00
valknarandClaude Sonnet 4.6 04a79ea198 fix(stacks): show correct status when update service has failed
systemctl status exits non-zero for failed services (exit 3), not
only missing ones (exit 4), causing the || warn branch to always fire
after a failed run. Gate the "not installed" warning on whether the
unit file exists instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 02:21:00 +02:00
valknarandClaude Sonnet 4.6 a08c5fca39 fix(stacks): prevent update run from aborting when images not in local cache
docker image inspect returns exit code 1 for images not yet pulled.
With set -euo pipefail, this silently killed the script mid-loop.
Add || true so an uncached image yields empty before/after (correct
first-run behaviour: pull fires, before != after, stack gets started).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 02:17:49 +02:00
valknarandClaude Sonnet 4.6 f65034a2e2 refactor(gitea): move VPS IPv4 address to .env as VPS_IPV4
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 20:42:26 +02:00
valknarandClaude Sonnet 4.6 fa0f5cba4b fix(gitea): update SSH_PORT to 22 to match new external port
Clone URLs now show the correct standard port 22.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 20:27:01 +02:00
valknarandClaude Sonnet 4.6 8797a42d61 fix(gitea): bind SSH explicitly to IPv4 to avoid conflict with sshd on IPv6
Docker binds to both [::]:22 and 0.0.0.0:22 without an explicit IP,
conflicting with sshd on [::]:22.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 20:25:59 +02:00
valknarandClaude Sonnet 4.6 1e5d5254a6 feat(gitea): expose SSH on port 22
sshd is now IPv6-only (ListenAddress ::, bindv6only=1), freeing
IPv4 port 22 for Gitea SSH.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 20:25:42 +02:00
valknarandClaude Sonnet 4.6 0f943b5a62 fix(coolify): switch PHP-FPM from ondemand to dynamic mode
ondemand kills all workers after 10s idle, causing cold Laravel
bootstrap (10-54s) on every request after any quiet period.
dynamic keeps min_spare_servers=1 alive at all times; the other
PM_* values (start_servers=2, min=1, max_spare=3) were already
set but ignored under ondemand.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 11:06:07 +02:00
valknarandClaude Sonnet 4.6 8d33836714 fix(gitea): block scraper IP and add rate limiting
74.7.243.196 was crawling valknar/home SVG icon tree at 1.8 req/s.
- Denylist the IP in fail2ban dynamic config
- Add rate-limit@file middleware to Gitea's Traefik router to throttle
  future scrapers on public repos

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 10:27:56 +02:00
valknarandClaude Sonnet 4.6 3265d69a6c fix(traefik): upgrade fail2ban plugin to v0.9.0 and fix config schema
v0.7.1 caused 403 on all routes. v0.9.0 renamed whitelist→allowlist,
changed ip to a comma-separated string, and added enabled/statuscode fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 10:09:37 +02:00
valknarandClaude Sonnet 4.6 586f137cf9 feat(traefik): add fail2ban plugin with global middleware
Registers tomMoulard/fail2ban v0.7.1 as an experimental plugin and
applies it globally on the web-secure entrypoint alongside security-headers.
Bans IPs after 5 failures within 10 minutes for 3 hours; whitelists
RFC1918 ranges to protect internal/Docker traffic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 10:05:11 +02:00
valknarandClaude Sonnet 4.6 b69951f917 fix(help): align update subcommands and add backup descriptions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 23:49:57 +02:00
valknarandClaude Sonnet 4.6 d7829cb5ae docs: fix README backup/update/notifications sections
- Source .env before restic init so RESTIC_REPOSITORY is available
- Remove stale references to deleted _backup/.env and _update/.env
- Update Notifications section to point at root .env

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 21:13:00 +02:00
valknarandClaude Sonnet 4.6 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
valknarandClaude Sonnet 4.6 fcff6f3298 refactor: absorb _backup and _update into stacks.sh
- 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>
2026-06-16 20:56:04 +02:00
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
valknarandClaude Sonnet 4.6 3251f27edb feat: add stacks.sh stack manager CLI
Bash script for managing Docker Compose stacks with glob filtering,
parallel execution, service management, scaffolding, and shell completion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 19:13:43 +02:00
valknarandClaude Sonnet 4.6 cf32e669cb fix(gitea): use .RunModeIsProd and hardcode Umami values in header template
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 10:34:21 +02:00
valknarandClaude Sonnet 4.6 e613d766ec fix(gitea): revert to UMAMI_ID/UMAMI_SRC and use .UmamiSrc/.UmamiId template vars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 10:26:39 +02:00
valknarandClaude Sonnet 4.6 53608a7195 fix(gitea): use .UmamiSrc/.UmamiScript template vars for Umami analytics
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 10:24:57 +02:00
valknarandClaude Sonnet 4.6 735adfabea feat(gitea): add conditional Umami analytics via env vars in header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 10:17:19 +02:00
valknarandClaude Sonnet 4.6 a9eb3de90d docs: add code stack to README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 18:18:17 +02:00
valknarandClaude Sonnet 4.6 c27a012d54 fix(code): consolidate volumes into single /home/coder mount
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 18:05:43 +02:00
valknarandClaude Sonnet 4.6 d367540eac fix(code): mount workspace volume to /home/coder/workspace
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 18:01:19 +02:00
valknarandClaude Sonnet 4.6 c0aa3acbfc fix(code): mount workspace volume to /home/coder for full home persistence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 17:59:53 +02:00
valknarandClaude Sonnet 4.6 d9bbfea9c4 fix(code): remove separate workspace volume
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 17:53:22 +02:00
valknarandClaude Sonnet 4.6 27c9e1c8a5 feat(code): add code-server stack with Traefik and Anthropic API support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 17:31:58 +02:00
valknarandClaude Sonnet 4.6 d4327bd152 fix(update): detect real image changes and improve webhook messages
Use image ID diff instead of grepping "Pulled" (which appears even
when images are already up to date). Add timestamp, stack count, and
updated/total ratio to all notification messages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 07:58:55 +02:00
valknarandClaude Sonnet 4.6 2130069836 fix(update): set executable bit on update.sh in git index
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 07:41:24 +02:00
valknarandClaude Sonnet 4.6 6eb603205a fix(update): make update.sh executable and fix Pulled grep pattern
- Add +x permission to update.sh (was causing systemd 203/EXEC failure)
- Remove $ anchor from grep so trailing space in docker output is matched
- Add .data/ to .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 16:19:36 +00:00
valknarandClaude Sonnet 4.6 f231dd5432 feat: externalize EMAIL_FROM and EMAIL_USER to .env for gitea and passbolt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 18:38:22 +02:00
valknarandClaude Sonnet 4.6 afec1c03f5 feat(gitea): set mailer FROM address
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 18:01:08 +02:00
valknarandClaude Sonnet 4.6 9c7a34e9c0 feat: remove Affine stack
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 13:43:21 +02:00
valknarandClaude Sonnet 4.6 40dfe5776a feat(gitea): add Umami analytics via direct custom header template
Mount templates/custom/ directly to Gitea's default GITEA_CUSTOM templates
path. No entrypoint override or envsubst needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 12:58:25 +02:00
valknarandClaude Sonnet 4.6 e5e6a2c302 feat(gitea): add Umami analytics via custom header template
Injects the Umami tracking script into every Gitea page using a custom
header.tmpl generated at startup via envsubst. Script URL and website ID
are externalized to UMAMI_SCRIPT_URL / UMAMI_WEBSITE_ID env vars.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 12:49:26 +02:00
valknarandClaude Sonnet 4.6 9c65cfc9e3 revert(passbolt): remove clock-skew patch — metadata key already created
The patched PublicKeyValidationService.php and its volume mount are
no longer needed now that the metadata key exists in the database.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:48:41 +02:00
valknarandClaude Sonnet 4.6 15ce2e3f4b chore: remove accidentally committed log file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:47:34 +02:00
valknarandClaude Sonnet 4.6 300c685d50 feat: remove Vaultwarden — migrated to Passbolt
Credentials migrated to Passbolt CE at passbolt.pivoine.art.
Removed stack files and README entry. Data wiped on VPS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:47:07 +02:00
valknarandClaude Sonnet 4.6 5c398ee77c fix(passbolt): add 300s clock-skew tolerance to key creation date check
The isNotCreatedInTheFutureRule has zero tolerance, causing the
browser extension to fail when generating a metadata key if the
browser clock is even 1 second ahead of the server. Patching
isDateInFuture to allow 300 seconds tolerance and mounting the
file as a read-only volume so the fix survives image updates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:16:56 +02:00
valknarandClaude Sonnet 4.6 2e31c1dcc9 fix(passbolt): persist GPG keyring as volume to survive restarts
Passbolt's entrypoint creates /var/lib/passbolt/.gnupg/pubring.kbx
as root while PHP-FPM runs as www-data. Without a volume this file
is recreated with wrong ownership on every container recreate, breaking
all GPG operations. Mounting the dir as a volume keeps the chown
33:33 fix permanent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:44:20 +02:00
valknarandClaude Sonnet 4.6 6f12bf9af7 fix(passbolt): disable metadata encryption for new instance setup
Passbolt 5.x's isNotCreatedInTheFutureRule has zero tolerance for
clock skew — even 1 second between browser and server causes the
metadata key creation to fail during first setup. Disabling the
automatic metadata setup for new instances allows the browser
extension to complete account setup successfully.

Encrypted metadata can be enabled from the admin panel post-setup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:24:51 +02:00
valknarandClaude Sonnet 4.6 758e69300f fix(passbolt): add TZ env var (Europe/Amsterdam)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:06:01 +02:00
valknarandClaude Sonnet 4.6 ae81935376 fix(passbolt): clean setup with correct GPG fingerprint
Passbolt 5.x does not auto-persist the server key fingerprint across
container restarts (no passbolt.php is written). The fingerprint env var
is required and corresponds to the key auto-generated on first clean start.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 20:39:55 +02:00
valknarandClaude Sonnet 4.6 d8cfcd23d1 fix(passbolt): fix DB hostname, encoding, and GPG fingerprint
- Use container_name passbolt_db instead of service name db (service names
  are ambiguous on the shared falcon_network — 6 other stacks also have a
  service named db)
- Add DATASOURCES_DEFAULT_ENCODING=utf8 to override MySQL's utf8mb4 default
- Add DATASOURCES_QUOTE_IDENTIFIER=true for PostgreSQL identifier quoting
- Set PASSBOLT_GPG_SERVER_KEY_FINGERPRINT for the auto-generated server key
- Add PASSBOLT_GPG_SERVER_KEY_EMAIL for correct server key identity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 20:14:57 +02:00
valknarandClaude Sonnet 4.6 a1f0f7091b feat(passbolt): add Passbolt CE stack
Password manager with GPG encryption. Uses PostgreSQL for consistency
with other stacks. Backed up alongside existing databases. Vaultwarden
kept running during migration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 20:00:05 +02:00
valknarandClaude Sonnet 4.6 31841d1ac3 fix(compose): remove empty labels keys left after watchtower label removal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 19:44:01 +02:00
valknarandClaude Sonnet 4.6 4c522961a9 feat(_update): replace watchtower with custom nightly update script
Removes the watchtower container in favour of a host-side script that
runs daily at 2:00 AM via systemd timer.  Mirrors the _backup pattern:
auto-discovers stacks, pulls images, recreates changed containers,
prunes dangling images, and notifies via n8n → Telegram.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 19:42:25 +02:00
valknarandClaude Sonnet 4.6 cb241c9696 feat(affine): add SMTP config, AFFINE_SERVER_EXTERNAL_URL; remove blinko dir
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 06:57:45 +02:00
valknarandClaude Sonnet 4.6 4b99e21a99 feat: replace blinko with affine, route notifications to telegram
- Add AFFiNE stack (affine.pivoine.art): main app, migration job,
  redis, pgvector postgres
- Remove blinko stack
- Update backup.sh: swap blinko_db → affine_db
- Update README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 06:28:27 +02:00
valknarandClaude Sonnet 4.6 11ceb46e4a chore(backup): replace emoji codes with actual Unicode emojis
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 06:04:09 +02:00
valknarandClaude Sonnet 4.6 5985d8619f fix(backup): use message field in webhook payload, matching watchtower
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 22:36:16 +02:00
valknarandClaude Sonnet 4.6 eed45f1627 docs: update README — add notifications section for n8n/Telegram
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 22:30:40 +02:00
valknarandClaude Sonnet 4.6 39636e85c3 feat: replace mattermost with n8n, route notifications to telegram
- Add n8n stack (n8n.pivoine.art) with postgres, traefik, watchtower
- Remove mattermost stack entirely
- Update backup.sh: swap mattermost_db → n8n_db, rename
  MATTERMOST_WEBHOOK → WEBHOOK_URL, simplify notify() payload
- Update watchtower: mattermost:// → generic+https:// for n8n webhook
- Add _backup/.env.example
- Update README

Webhook URLs in _backup/.env and watchtower/.env contain
FILL_AFTER_N8N_SETUP — update once n8n workflow is created.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:36:58 +02:00
valknarandClaude Sonnet 4.6 5482236a9a chore(coolify): remove SSH config volume mounts
No longer needed after Coolify update — verified on VPS, container
stays healthy without them.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:05:11 +02:00
valknarandClaude Sonnet 4.6 dc52de77bc feat(coolify): track SSH config and known_hosts in repo
Mount from stack directory instead of .data, consistent with how
gitea handles runner-config.yaml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:02:12 +02:00