From 1842bd332c751bedd1601bd25fb9d61d7d8ccb4b Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:39:45 -0700 Subject: [PATCH] fix(selfhost): templatize the docker-prune systemd unit for multi-host use Renames loopover-docker-prune.{service,timer}.example to the %i-instantiated loopover-docker-prune@.{service,timer}.example, using %I for WorkingDirectory/ExecStart so one pair of unit files can be enabled per self-host instance path (systemd-escape -p) instead of requiring a hand-copied, uniquely-named unit per operator. Closes #4894 --- .../content/docs/self-hosting-operations.mdx | 13 +++++-- packages/loopover-miner/DEPLOYMENT.md | 2 +- systemd/loopover-docker-prune.service.example | 27 -------------- systemd/loopover-docker-prune.timer.example | 13 ------- .../loopover-docker-prune@.service.example | 37 +++++++++++++++++++ systemd/loopover-docker-prune@.timer.example | 14 +++++++ 6 files changed, 61 insertions(+), 45 deletions(-) delete mode 100644 systemd/loopover-docker-prune.service.example delete mode 100644 systemd/loopover-docker-prune.timer.example create mode 100644 systemd/loopover-docker-prune@.service.example create mode 100644 systemd/loopover-docker-prune@.timer.example diff --git a/apps/loopover-ui/content/docs/self-hosting-operations.mdx b/apps/loopover-ui/content/docs/self-hosting-operations.mdx index 86a2e3c1c8..02f8c3e75b 100644 --- a/apps/loopover-ui/content/docs/self-hosting-operations.mdx +++ b/apps/loopover-ui/content/docs/self-hosting-operations.mdx @@ -444,13 +444,18 @@ less than 7 days is left alone, so a recent deploy is never at risk): +This is a templated (instantiated) unit — the instance name is your loopover install path, +systemd-escaped, so the same pair of installed unit files can prune Docker resources for more +than one loopover install on a host, or be copied verbatim across a fleet, without editing a +per-install path by hand. + Run it manually at any time with `docker system df` before and after to see what it reclaimed: `sh scripts/selfhost-docker-prune.sh`. diff --git a/packages/loopover-miner/DEPLOYMENT.md b/packages/loopover-miner/DEPLOYMENT.md index 40e2ffd6d8..8ef06b5622 100644 --- a/packages/loopover-miner/DEPLOYMENT.md +++ b/packages/loopover-miner/DEPLOYMENT.md @@ -170,7 +170,7 @@ sudo systemctl daemon-reload sudo systemctl enable --now loopover-miner.service ``` -Because `loop` is a **long-running daemon that schedules its own cycles**, it is a persistent `Type=simple` service (with `Restart=on-failure`) — **not** a oneshot unit driven by a `.timer`, unlike the periodic `loopover-docker-prune.*.example` hygiene job in [`systemd/`](../../systemd/). Keep `GITHUB_TOKEN` (and any coding-agent credentials) in a root-owned `0600` `EnvironmentFile`, never in the unit file. Follow the loop with `journalctl -u loopover-miner -f`; `systemctl stop` sends SIGTERM, which the loop handles cleanly at its next kill-switch check. +Because `loop` is a **long-running daemon that schedules its own cycles**, it is a persistent `Type=simple` service (with `Restart=on-failure`) — **not** a oneshot unit driven by a `.timer`, unlike the periodic `loopover-docker-prune@.*.example` hygiene job in [`systemd/`](../../systemd/). Keep `GITHUB_TOKEN` (and any coding-agent credentials) in a root-owned `0600` `EnvironmentFile`, never in the unit file. Follow the loop with `journalctl -u loopover-miner -f`; `systemctl stop` sends SIGTERM, which the loop handles cleanly at its next kill-switch check. Want the dashboard too? [`systemd/loopover-miner-ui.service.example`](../../systemd/loopover-miner-ui.service.example) is a companion unit that serves `apps/loopover-miner-ui` persistently over the same local state — see that app's [README](../../apps/loopover-miner-ui/README.md#running-as-a-persistent-service). diff --git a/systemd/loopover-docker-prune.service.example b/systemd/loopover-docker-prune.service.example deleted file mode 100644 index 50b6a23891..0000000000 --- a/systemd/loopover-docker-prune.service.example +++ /dev/null @@ -1,27 +0,0 @@ -# Runs scripts/selfhost-docker-prune.sh on a schedule (paired with loopover-docker-prune.timer.example) to -# reclaim unused Docker images and build cache before disk fills up on a long-running self-host deployment. -# -# Install (adjust the path below to wherever you cloned/deployed loopover): -# sudo cp systemd/loopover-docker-prune.service.example /etc/systemd/system/loopover-docker-prune.service -# sudo cp systemd/loopover-docker-prune.timer.example /etc/systemd/system/loopover-docker-prune.timer -# sudo $EDITOR /etc/systemd/system/loopover-docker-prune.service # fix WorkingDirectory / ExecStart path -# sudo systemctl daemon-reload -# sudo systemctl enable --now loopover-docker-prune.timer -# -# This is a HOST-level unit, not a Docker Compose service: reclaiming images/build-cache needs real Docker -# daemon access, which no container in docker-compose.yml is granted (see that file's docker-proxy and -# runner service comments for why raw /var/run/docker.sock exposure into a container is avoided). - -[Unit] -Description=LoopOver self-host Docker resource hygiene (image + build-cache prune) -After=docker.service -Requires=docker.service - -[Service] -Type=oneshot -# REQUIRED: point this at wherever you cloned loopover. -WorkingDirectory=/opt/loopover -ExecStart=/bin/sh /opt/loopover/scripts/selfhost-docker-prune.sh -# Optional: override the default 7-day (168h) safety window before an unused image/build-cache entry is -# eligible for removal. -# Environment=LOOPOVER_DOCKER_PRUNE_RETAIN_HOURS=168 diff --git a/systemd/loopover-docker-prune.timer.example b/systemd/loopover-docker-prune.timer.example deleted file mode 100644 index f9f959b0cc..0000000000 --- a/systemd/loopover-docker-prune.timer.example +++ /dev/null @@ -1,13 +0,0 @@ -# Pairs with loopover-docker-prune.service.example. See that file for install instructions. - -[Unit] -Description=Run loopover-docker-prune.service daily - -[Timer] -OnCalendar=daily -Persistent=true -# Spread the run over a window instead of firing at exactly midnight on every host. -RandomizedDelaySec=30m - -[Install] -WantedBy=timers.target diff --git a/systemd/loopover-docker-prune@.service.example b/systemd/loopover-docker-prune@.service.example new file mode 100644 index 0000000000..e6437c4684 --- /dev/null +++ b/systemd/loopover-docker-prune@.service.example @@ -0,0 +1,37 @@ +# Runs scripts/selfhost-docker-prune.sh on a schedule (paired with loopover-docker-prune@.timer.example) to +# reclaim unused Docker images and build cache before disk fills up on a long-running self-host deployment. +# +# TEMPLATED UNIT (#4894): the `@` in the filename makes this a systemd instantiated unit, so ONE pair of +# installed unit files can prune Docker resources for any number of loopover installs on the same host (or +# be copied verbatim to every host in a fleet) without hand-editing a WorkingDirectory/ExecStart path per +# instance. The instance name (the string after `@` when you enable it) IS the install path, systemd-escaped +# -- `%I` below expands back to the real, unescaped path. +# +# Install (repeat the last two lines once per loopover install path on this host): +# sudo cp systemd/loopover-docker-prune@.service.example /etc/systemd/system/loopover-docker-prune@.service +# sudo cp systemd/loopover-docker-prune@.timer.example /etc/systemd/system/loopover-docker-prune@.timer +# sudo systemctl daemon-reload +# INSTANCE=$(systemd-escape -p /opt/loopover) # -> "opt-loopover"; repeat per install path if more than one +# sudo systemctl enable --now "loopover-docker-prune@${INSTANCE}.timer" +# +# A single-instance host works exactly the same way -- there is no non-templated form anymore; just pick one +# instance name (e.g. `systemd-escape -p /opt/loopover`) and enable that. +# +# This is a HOST-level unit, not a Docker Compose service: reclaiming images/build-cache needs real Docker +# daemon access, which no container in docker-compose.yml is granted (see that file's docker-proxy and +# runner service comments for why raw /var/run/docker.sock exposure into a container is avoided). + +[Unit] +Description=LoopOver self-host Docker resource hygiene (image + build-cache prune) for %I +After=docker.service +Requires=docker.service + +[Service] +Type=oneshot +# %I is the unescaped instance name -- the loopover install path you passed to `systemd-escape -p`. +WorkingDirectory=%I +ExecStart=/bin/sh %I/scripts/selfhost-docker-prune.sh +# Optional: override the default 7-day (168h) safety window before an unused image/build-cache entry is +# eligible for removal. Per-instance overrides need a matching per-instance drop-in +# (systemctl edit "loopover-docker-prune@.service") rather than editing this shared template. +# Environment=LOOPOVER_DOCKER_PRUNE_RETAIN_HOURS=168 diff --git a/systemd/loopover-docker-prune@.timer.example b/systemd/loopover-docker-prune@.timer.example new file mode 100644 index 0000000000..e4012b913f --- /dev/null +++ b/systemd/loopover-docker-prune@.timer.example @@ -0,0 +1,14 @@ +# Pairs with loopover-docker-prune@.service.example. See that file for install instructions -- this is +# also a templated (instantiated) unit, enabled per instance: `loopover-docker-prune@.timer`. + +[Unit] +Description=Run loopover-docker-prune@%i.service daily + +[Timer] +OnCalendar=daily +Persistent=true +# Spread the run over a window instead of firing at exactly midnight on every host. +RandomizedDelaySec=30m + +[Install] +WantedBy=timers.target