10 Commits

Author SHA1 Message Date
librelad
65167463f9 fix(chat apps): tag every service so its IP actually substitutes
Installing rocketchat failed with

    invalid IPv4 address: ParseAddr("IP_DATA_2"): unable to parse IP

ipUpdateComposeTags allocates one IP per SERVICE_TAG_N annotation and fills
IP_TAG_i only where SERVICE_TAG_i exists. The four new apps tagged only their
primary service, so every sidecar — matrix's postgres, mattermost's postgres,
rocketchat's mongo, and fifteen of stoat's sixteen — kept a literal IP_DATA_n
in the deployed compose and docker refused to create the container.

Tag every service that carries an ipv4_address, index-aligned with its IP_TAG.
For stoat that also meant moving caddy from SERVICE_TAG_1 to _6 so the indices
line up with the IPs rather than the reading order.

mastodon had the same latent break (IP_TAG_2 and _3 untagged) and is fixed the
same way — it would have failed on first install for the same reason.

SERVICE_TAG carries the compose *key*, not container_name: 'libreportal app
restart <app> <service>' passes it to 'docker compose restart', which only
understands keys.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:48:09 +01:00
librelad
4685320353 feat(secrets): real VAPID keypair for mastodon, slot-numbered DB passwords
VAPID: the two values are the halves of one P-256 keypair, not independent
secrets — the browser verifies that a push is signed by the private key matching
the public key it subscribed with. The RANDOMIZED* generators mint each
placeholder on its own, so they produced two unrelated strings and web push could
never have worked. Generate the pair in mastodon_install_post_setup the way stoat
already does, encoded as Mastodon's webpush gem expects: unpadded URL-safe base64
of the 32-byte private scalar and the 65-byte uncompressed public point, sliced
out of the SEC1 DER. Verified by rebuilding the key from the emitted private half
and re-deriving the public point — openssl accepts it and the point matches.

Generated once and never rotated (rotation would invalidate every subscription),
but a pair of the wrong shape is replaced, so an install carrying the old
unrelated strings heals itself on next install — their public half is 42 chars
where a real point is 87.

Slots: CFG_<APP>_DB_PASSWORD -> CFG_<APP>_DB_PASSWORD_1 and likewise for
DB_ROOT_PASSWORD, across mastodon, owncloud, mattermost, matrix, nextcloud and
bookstack, so a database credential is always a numbered slot and a second one is
just _2. Renaming a key means reconciliation drops the old and adds the new
holding its placeholder, so an existing install regenerates unless the value is
carried over first — documented, including that the old file survives as
.<app>.config.bak.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:42:50 +01:00
librelad
5706498565 fix(secrets): move app credentials into <app>.config, fix slot collision
Five apps (mastodon, owncloud, mattermost, matrix, stoat) took their generated
secrets from the compose-side generator tags PASSWORD_TAG_<n>/RANDOM_TAG_<n>/
HEX_TAG_<n>/VAPID_TAG_<n>. Those mint a fresh secret on every templating run, so
a reinstall handed the app a new database password while its data volume kept the
one initdb was given, and the app came back up unable to open its own database.

Move them to <app>.config as RANDOMIZED* placeholders, reaching the compose via
the #LIBREPORTAL|<APP>_<KEY>_TAG| mechanism tags_processor_app_config_values
already provides. No new handler: the tag name is derived from the config key, so
this is a config line plus a tag per secret. Generation is unchanged — still
random on first install; the value is now remembered instead of re-rolled.

Also fixes two things this exposed:

- The RANDOMIZED* replacers matched unanchored. `sort -u` orders slots lexically
  (1, 10, 11, 2), so slot 1's pattern rewrote the prefix inside slot 10's
  placeholder and slots 10+ ended up holding slot 1's secret with a digit glued
  on — derivable, and invisible because the values weren't byte-identical.
  Anchoring with \b makes match order irrelevant. Verified at 20 slots across
  all four placeholder types: 64 keys, 64 distinct values, no prefix collisions.

- generateRandomPassword drew from base64 without constraining the mix; measured
  over 2000 draws, 1 in 40 contained no digit at all. Retry until the result has
  both a digit and a letter, bounded so a pathological length can't spin.

owncloud gains a fix in passing: its compose seeded the admin account from
PASSWORD_TAG_2 while the WebUI displayed CFG_OWNCLOUD_ADMIN_PASSWORD, which was
generated separately and never used. Both now read the same value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:33:40 +01:00
librelad
c11052b753 fix(mastodon): make tag annotations substitutable
The tag manager reads `#LIBREPORTAL|<TAG>|<VALUE>` and takes <VALUE> as the
current literal to search for on that line, so it must equal the string in the
line body. Mastodon used `unconfigured` as the annotation value against bodies
like `PASSWORD_TAG_1_DATA` — nothing matched, nothing was ever substituted, and
the placeholder shipped as the live database password, SECRET_KEY_BASE, OTP
secret and VAPID keypair.

Nothing caught it either: `unconfigured` doesn't match `_DATA`, so
tagsManagerGetTagState reported the tags as configured, and the stale-tag gate
in dockerComposeUp (which tests the annotation value against
`^[A-Z][A-Z0-9_]*_DATA(_[0-9]+)?$`) let the app start.

Adopt the convention every other app already uses — body placeholder identical
to the annotation value, `<KIND>_DATA_<n>`. All 11 tags now substitute, the app
and postgres services agree on the same generated credentials, and an unfilled
tag is visible to the pre-start gate.

Existing 0.1.0 installs keep their literal credentials until re-installed, and
their Postgres was initialised with them, so a plain re-install desynchronises
the compose from the volume. Document both recovery paths in upgrade notes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 05:36:28 +01:00
librelad
8a0c3641d0 fix(nextcloud,mastodon): track a moving stable line like every other app
Both templates were stale in different ways, and the new tag enumeration
surfaced it: nextcloud sat on 31-fpm-alpine with 34 out, mastodon on
v4.2.0 with v4.6 out.

  nextcloud  31-fpm-alpine -> 34-fpm-alpine
  mastodon   v4.2.0        -> v4.6

The mastodon one was the real problem: v4.2.0 is an EXACT patch pin, so
it never moved at all — no security patches, ever. v4.6 is a moving
minor-line tag (the same shape as stalwart's v0.16), so auto-update now
delivers patches within the line.

Deliberately NOT floated to :latest or :stable. Both projects require
stepped upgrades — Nextcloud in particular refuses to skip a major — so
a tag that crosses majors on its own would break the app on a routine
container recreate. A major-pinned, patch-moving tag is the correct
shape here, not a limitation.

Verified against the registry: all three pinned apps now report nothing
newer, and each tag still moves (v0.16 2026-08-10, 34-fpm-alpine
2026-08-03, v4.6 2026-08-06).

Templates only, so this changes NEW installs. Existing installs keep
their tag and will now be told a newer line exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:39:36 +01:00
librelad
56102ac5c0 feat(updater): P1b — CFG_<APP>_VERSION drives the anchor image tag
Add a `#LIBREPORTAL|<APP>_VERSION_TAG|<current-tag>` sentinel to each app's
anchor (`<slug>-service`) image line, so a `CFG_<APP>_VERSION` config drives
that tag through the existing tagging system — the config becomes the source,
the compose tag is derived (like ports/IPs/domains already are). This is the
"install/pin a specific version" knob.

- Placeholder is the literal current tag (e.g. `31-fpm-alpine`), NOT a `*_DATA`
  token: an unset var leaves the line untouched (inert comment, keeps the real
  tag) and never trips the up_app stale-tag scanner.
- Only the anchor line is tagged (sidecars mariadb/redis/nginx stay
  tracked-by-digest, not user-version-picked). ollama correctly targets
  `ollama/ollama`, not the companion open-webui.
- Untagged anchors normalized to `:latest` (semantic no-op) so they're
  templatable too. The manager (libreportal) is skipped — it updates via its
  release channel, not a docker tag.

32 apps wired. Verified: CFG_NEXTCLOUD_VERSION=32 → `nextcloud:32` (prefix
preserved, idempotent, sidecars untouched); all compose files still valid YAML.
Inert until a version config is set, so no behaviour change on existing installs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-17 23:13:57 +01:00
librelad
12b4d6823e feat(backup): file-capture labels for linkding, vaultwarden, headscale, mastodon
Now that uid auto-discover is in (d424473), adding more apps is just naming the
container + path — no uid guessing. Four more apps get complete backups of their
private data dirs (previously: incomplete on the file side because libreportal
can't read sub-UID-owned content from the host).

- linkding-service     -> /etc/linkding/data           (overlaps with sqlite db, same pattern as gitea — dump replays over the captured tree on restore, harmless)
- vaultwarden-service  -> /data                         (same overlap pattern)
- headscale-service    -> /var/lib/headscale            (same overlap pattern)
- mastodon-service     -> /mastodon/public/system       (uploads; postgres handled separately by backup.db)

Coverage now: nextcloud, bookstack, gitea, owncloud, linkding, vaultwarden,
headscale, mastodon. Skipped jellyfin — it has multiple internal sqlite DBs and
no backup.db declared; adding just backup.files without backup.db / backup.live
wouldn't activate live capture, and adding backup.live blind could yield torn
sqlites. That one wants proper DB declaration first.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 16:13:46 +01:00
librelad
69f7289b4a feat(backup): declare server databases + fail safe to stop on dump failure
- Add libreportal.backup.db labels to the MariaDB/Postgres apps (nextcloud,
  owncloud, bookstack, mastodon, invidious) so they back up live + consistent.
- If a declared dump cannot be taken (DB down, wrong path), the backup falls
  back to stop-snapshot-start for that run instead of snapshotting torn data —
  a misconfiguration degrades to 'safe with downtime', never to 'unsafe'.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 15:12:55 +01:00
librelad
dec3055b63 feat(routing): dynamic per-port subdomains + router-block toggle
Replace the static one-host-per-app model with per-port routers: each
Traefik-managed port carries a subdomain (12-col PORT format) and gets a
DOMAINSUBNAME_TAG_<n> host, so one container can serve unlimited hosts.
tagsProcessorPortSubdomains stamps per-port hosts (subdomain @/empty = apex,
multi-level allowed); tagsProcessorPortRouterBlocks comments out
# TRAEFIK_PORT_<n>_BEGIN/END blocks for non-Traefik ports so unfilled
placeholders never ship (mirrors GLUETUN_OFF). Convert all 27 router apps
(subdomains seeded from HOST_NAME; headscale admin. prefix -> subdomain).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-22 00:45:01 +01:00
librelad
875a60f90f LibrePortal v0.1.0 — initial release
A free, open, self-hosted app platform (GNU AGPLv3): one-click app deploys,
Traefik reverse proxy with automatic SSL, rootless Docker support, gluetun
VPN routing, and a web dashboard to manage it all.

Free & open forever to self-host; optional paid hosted services fund it.
See PROMISE.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-21 20:37:54 +01:00