This document covers taking repowatch from source to a running system
service: prerequisites, the make install/make activate flow, the two
ways to run it (a single daemon vs. split status API + timer), the nginx
cache in front of it, backups, and upgrades. For config.yaml itself, see
configuration.md; for authentication and access control,
see access.md.
- Prerequisites
- Install
- What
make activatedoes - Two ways to run it
- nginx cache and auto-reconciliation
- Backups
- Upgrading
- Running without systemd
- Packaging (
DESTDIR) - Hardening notes
For distribution-specific package commands and first login, start with
Quick start. The shipped configuration has repos: []; it
starts an administrative service and an empty nginx site. Add sources through
YAML or the dashboard after installation.
The shipped profile enables purge and cache inventory: install and load both
ngx_cache_purge and the njs HTTP module from packages compatible with nginx.
make check does not prove their availability; activation's nginx -t does.
- Python 3.11+ with
venvandpipavailable (some distros splitpipout of the base Python package —make checkwill tell you if it's missing). nginx, unless you're running the caching layer yourself and settingWITH_NGINX=0(see below).systemd, unless you're supervising the process yourself withWITH_SYSTEMD=0.gpgv, if any repository hasverify_signature: true(apt, pacman, RPM-MD/dnf, apt-rpm, Slackware).openssl, orapk-tools >= 3.0, if any apk repository hasverify_signature: true— seeapk_signature_backendin configuration.md.- Enough local disk for
state_db(SQLite — must be local disk, not NFS; see configuration.md) and for the nginx package cache itself (NFS is fine for the cache directory, just not forstate_db).
Run make check at any point — it's a read-only diagnostic (OK/WARN/
FAIL per item: Python version, required stdlib modules, gpgv/openssl/
nginx/systemctl availability, filesystem permissions, an existing
installation's consistency, nginx include conflicts) and never touches
disk or the network beyond reading local files.
sudo make check # read-only diagnostics for system paths
sudo make install PREFIX=/usr/local # builds wheels, lays down files — no services touched
sudoedit /etc/repowatch/config.yaml # review settings before activation
sudo make activate # creates the system user, enables and starts services
sudo -u repowatch /usr/local/bin/repowatch set-passwordsudo is required for make install itself with the defaults shown above
— PREFIX=/usr/local, SYSCONFDIR=/etc, LOCALSTATEDIR=/var are all
real system paths a normal user can't write to. It's only optional if
every one of those is overridden to somewhere you already own (e.g. for
local testing, or packaging into a DESTDIR — see
Packaging).
make install is intentionally inert with respect to any running system:
it builds/installs into a venv under PREFIX, writes the CLI wrapper,
copies systemd unit templates and scripts, and writes an install manifest
($PREFIX/share/repowatch/install.json) — but it does not create the
repowatch system user, does not touch systemctl, and does not write or
link any nginx config. make activate is the separate, explicit step that
does all of that; it requires root and refuses to run under DESTDIR.
Paths are independently configurable (all via make variables, forwarded
as environment variables to the installer):
| Variable | Default | Meaning |
|---|---|---|
PREFIX |
/usr/local |
Application venv, CLI wrapper, install manifest. |
SYSCONFDIR |
/etc |
config.yaml lives at $SYSCONFDIR/repowatch/config.yaml. |
LOCALSTATEDIR |
/var |
state_db/backups at $LOCALSTATEDIR/lib/repowatch. |
CACHE_DIR |
$LOCALSTATEDIR/cache/nginx/repowatch |
nginx's proxy_cache_path, when WITH_NGINX=1. |
SYSTEMD_UNIT_DIR |
/etc/systemd/system |
Where unit files are linked. |
NGINX_CONF / NGINX_ENABLED_DIR |
auto-detected | Explicit override if make check can't unambiguously find your nginx's main config / sites-enabled-style include directory. |
WITH_NGINX |
1 |
Set to 0 to skip nginx integration entirely — bring your own caching layer/config. |
WITH_SYSTEMD |
1 |
Set to 0 to skip systemd unit installation/activation — repowatch run doesn't depend on systemd at all, run it under any supervisor. |
DESTDIR |
(empty) | Staging root for packaging — see Packaging. |
Re-running make install against an already-installed layout is safe (it
detects and preserves an existing config.yaml rather than overwriting it),
but changing PREFIX/SYSCONFDIR/LOCALSTATEDIR after the fact is refused
by make check — that's a data-migration decision the installer won't make
silently on your behalf. "Safe" here means it won't corrupt anything, not
that it always proceeds: the same preflight also refuses outright if any
repowatch service or timer is currently active — stop them first (or use
make upgrade below, which handles the stop/start itself as part of one
transaction) rather than reinstalling underneath a running process.
- Creates the
repowatchsystem user/group if they don't already exist (--system, no login shell, no home directory). chownsconfig.yaml's directory,config.yamlitself, and the state directory (includingbackups/) to that user — deliberately not recursive over an existing package cache (which may be large, and may sit on NFS).- If
WITH_NGINX=1: renders the cacheserverblock from yourconfig.yaml(seenginx/render.py/ configuration.md), validates it withnginx -t, and symlinks it into your nginx's enabled- sites directory — rolling back the symlink/file if validation fails. Initial bootstrap publishesactive.conf,purge.conf,dedup.map,probe.confandprobe.jsbefore validation. It reads existing dedup data without creating SQLite as root; a fresh installation uses an empty map. Failed writes/validation remove newly created bootstrap files and the link, preserving pre-existing files. Also writes a small root-ownedpolicy.json(cache dir, access log path, nginx binary, site-link path) that later reconciliation runs against — see the next section. - If
WITH_SYSTEMD=1: links the unit files from$PREFIX/share/repowatch/systemd/into$SYSTEMD_UNIT_DIR, reloads systemd, and enables+startsrepowatch.serviceandrepowatch-backup.timer(and, if nginx is also managed,repowatch-nginx.timer).
make activate re-runs check-config against the installed venv before
touching anything, and refuses to proceed if make check reports any
failure — it will not activate a broken installation.
repowatch.service (the default from make activate) runs
repowatch run — a single process that does everything: the scheduled
index checks, cache warming, the syslog listener (if syslog_listener. enabled), and the status.json/dashboard HTTP server, all together.
The daemon keeps an independent task for each repository and reloads configuration
every ten seconds. A long warm does not hold up that reload or the separate
retention scheduler. Index checks and cache work share check_concurrency, with
capacity reserved for checks; see the configuration reference.
A repository's next check waits for its own previous check and cache work to finish.
Most setting edits take effect on the next operation. Disabling prefetch stops
queued automatic downloads after the next scheduler reload; active transfers
may finish. Removing a repository cancels and
drains its active task. Ordinary interrupted warming is not a persistent retry queue.
repowatch-status.service + repowatch-check.timer is the split
alternative: a long-running serve-status process for the HTTP side, paired
with a timer that runs check-once periodically instead of the built-in
scheduler. The real tradeoff here is that this combination has no syslog
listener — that only runs inside repowatch run — so warmed_packages
won't reflect real client downloads and /api/requests stays empty, not
"no dashboard" as the naming might suggest (the dashboard is served by
repowatch-status.service either way). Reach for this split if you want
independent restart/scaling of "answer status queries" vs. "go check
upstreams", or if you'd rather drive checks from your own scheduler instead
of repowatch run's internal one.
Only one of the two shapes should be enabled against the same state_db at
a time — make activate enables the first one; switching to the second is a
manual systemctl disable --now repowatch.service followed by enabling the
other two units.
repowatch.service ships with baseline systemd hardening
(NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp),
with ReadWritePaths scoped to exactly state_db's directory and
config.yaml's directory — the latter is required because the dashboard's
POST /api/repos writes config.yaml directly at runtime.
Neither shape needs systemd at all, actually — see
Running without systemd for repowatch supervise, a third option for environments without it.
The nginx server block itself is rendered once at make activate time
(see above), from your config.yaml. But config.yaml can keep changing
afterward — new repositories, changed url_templates, edited nginx
settings — through the dashboard, without a redeploy. repowatch-nginx.timer
(every 15s) checks whether regeneration is needed. Unchanged ticks compare a
fingerprint of the configuration, privileged policy, installed Python code and
probe template, and catalog revisions when deduplication is enabled. They also
hash all five managed output files, so missing or altered files trigger repair.
Client request writes and successful checks of unchanged catalogs do not cause
the expensive duplicate query or rendering to run again. Discovering or losing a
package hash does invalidate the catalog revision, even without a version change.
Changed inputs run the renderer; changed output is validated with nginx -t and
reloaded transactionally. A failed apply cannot authorize the fast path, and a
catalog change during rendering prevents caching that reconciliation result.
The successful input fingerprint is kept in the root-owned status.json, so it
survives separate timer invocations. sudo repowatch nginx-apply --force bypasses
this optimization and repeats validation/reload. Use it after out-of-band database
edits or changes to nginx configuration outside the five managed files. Normal
no-op ticks do not reload nginx; the CLI and systemd may still log their outcome.
Harmless dedup pairs mapping a URI to itself are debug messages; conflicting
canonical targets and case-colliding paths remain warnings.
This reconciliation runs as a separate root-owned oneshot systemd unit
(repowatch-nginx.service, invoked by the timer), not as a sudo call from
the main repowatch.service process — the main process keeps running as the
unprivileged repowatch user with no elevated privileges of its own. If a
render or nginx -t fails, the previously-active config is left in place
and the failure surfaces in that unit's systemd logs
(journalctl -u repowatch-nginx.service); the daemon itself isn't affected.
If you're not using the generator (WITH_NGINX=0, or nginx.enabled: false
in config.yaml), none of this applies — write and manage your own nginx
config. repowatch -c config.yaml nginx-render prints a concrete example for
your own repos[] (the caching approach: proxy_cache, short TTL on mutable
index files, long TTL on immutable package files, IPv4-only upstream
resolution — see configuration.md for why).
repowatch-backup.timer runs daily (with up to 30 minutes of randomized
delay, and Persistent=true to catch up after downtime) and backs up
state_db — the SQLite file holding repository snapshots, change history,
and warmed-package state — using SQLite's own online backup API
(sqlite3 <db> ".backup '<dest>'"), not a plain file copy. That matters
because a plain cp/rsync over a file the daemon is actively writing to
(WAL mode) risks capturing it mid-transaction; the backup API produces a
consistent snapshot without blocking the running service. Backups are
gzip-compressed and rotated by age (RETENTION_DAYS, default 14 —
independent of event_retention_days, which retains individual history rows
in the live database).
config.yaml itself is not backed up by this timer — back it up the way you
back up any other config file under version control or your usual config-
management tooling. (The upgrade procedure below separately snapshots it as
part of every upgrade attempt, but that's incidental to the upgrade, not a
substitute for your own config backups.)
Restoring a backup:
sudo systemctl stop repowatch.service # or repowatch-status.service + repowatch-check.timer
sudo -u repowatch gunzip -k /var/lib/repowatch/backups/state-<timestamp>.sqlite3.gz
sqlite3 /var/lib/repowatch/backups/state-<timestamp>.sqlite3 'PRAGMA integrity_check;'
sudo -u repowatch cp /var/lib/repowatch/backups/state-<timestamp>.sqlite3 /var/lib/repowatch/state.sqlite3
sudo systemctl start repowatch.serviceRestoring only rolls back state_db (history, warmed-package tracking,
issued tokens, admin sessions) — it does not touch config.yaml, so make
sure the config you're running matches what you expect separately.
sudo make upgrade-plan MANIFEST=/usr/local/share/repowatch/install.json WHEELHOUSE=build/wheels
sudo make upgrade MANIFEST=/usr/local/share/repowatch/install.json WHEELHOUSE=build/wheelsThis is an offline upgrade path — it expects a pre-built wheelhouse with
target-compatible dependencies already present (make wheel on a matching
environment, or copied over ahead of time); it never reaches out to the
network itself.
upgrade-plan loads the install manifest, resolves
PREFIX/SYSCONFDIR/LOCALSTATEDIR/etc. against it, and prints exactly
what upgrade --apply would operate on — then runs a set of cheap, read-only
checks: does config.yaml load in the currently installed venv, does that
venv exist, does the wheelhouse contain exactly one repowatch-*.whl, and are
repowatch.service/nginx.service already active (both required before
--apply will proceed). It exits non-zero and prints each failing check as a
PROBLEM line if any of these don't hold. This is still not a full
rehearsal of the upgrade — it does NOT build the candidate venv from the
wheelhouse, does NOT install the new release's dependencies, and can't catch
an incompatibility that only shows up once the candidate is actually built
(that only happens inside step 2 below, with --apply). Use it to catch a
wrong MANIFEST/WHEELHOUSE path, a missing/misnamed wheel, or services
that aren't up yet, before you commit to the real upgrade.
make upgrade (root required):
- Refuses to run unless the current installation has the required active services
and passes installation preflight and unless
state_dblives outside the installation/config directories being replaced. - Builds a candidate venv in a temp directory and validates it against
the current
config.yamlbefore touching the running service — an incompatible candidate is caught with zero downtime. - Only then: stops services, snapshots the installed files and does an
online backup of
state_dbinto a timestamped directory understate_db'sbackups/, installs the new version, re-activates (including nginx re-render/reload if applicable), and runs a smoke check. - On most failures, it restores the snapshotted files and database from
that same backup directory and resumes the services in their prior
enabled/active state, rather than leaving you with a half-upgraded
installation. Two caveats worth knowing: the smoke check tolerates a
503from/healthzas a pass (it only confirms the service answers with valid JSON, not that every repository is healthy), so a post-upgrade instance that's technically up but degraded won't trigger a rollback on that basis alone; and the rollback step itself isn't wrapped in its own exception handling — if restoring files, restoring the database, or reloading nginx during rollback fails, that failure propagates instead of being silently swallowed, but it also means rollback isn't unconditionally guaranteed to finish cleanly on every kind of failure.
If a managed service is still activating or deactivating (for example a running
repowatch-nginx.service reconciliation), the upgrade refuses to proceed.
Wait for that operation to finish and retry; do not bypass the state check.
It refuses to run against a DESTDIR-staged layout or with WITH_SYSTEMD=0
— this specific automated path assumes a systemd-managed installation is
what's being upgraded in place.
repowatch self-update --repo erophey7/repowatch --check # just report, don't install
repowatch self-update --repo erophey7/repowatch # install if newerA separate, lighter mechanism from make upgrade above: it checks the given
GitHub repository's latest (non-draft, non-prerelease) release, downloads its
wheel and the <wheel>.sha256 checksum file published alongside it in the
same release, verifies the checksum, and pip installs the wheel into
whichever environment repowatch itself is currently running from. It never
touches config.yaml, state_db, or nginx, and it doesn't require the
source repository or a wheelhouse to be present — just network access to
GitHub.
A release without exactly one .whl asset, without a matching .sha256
asset, or with a checksum that doesn't match is refused outright — there is
no "install anyway" fallback. This protects against a corrupted download or
a network-level tamper, but not against a compromised GitHub account:
the checksum file travels in the same release as the wheel it describes, so
whoever could replace one could replace both. If that stronger guarantee
matters for your deployment, use the offline make upgrade path above (with
a wheelhouse you built and reviewed yourself) instead.
self-update reinstalls repowatch and resolves its Python dependencies via
pip install --upgrade --force-reinstall. Dependency versions can change
even when the release has not raised its minimum requirements. It leaves
config.yaml, state_db, and nginx untouched. It does not restart the
service: restart it afterwards using your usual supervisor.
repowatch run itself has no systemd dependency at all — it's a single
process with its own asyncio scheduler (runtime.scheduler.run_forever), same as
make install WITH_SYSTEMD=0 already assumes. What's missing without
systemd is a replacement for the other two units: repowatch-nginx.timer
(periodic nginx reconciliation) and repowatch-backup.timer (daily backup).
repowatch supervise is a single foreground command that replaces all
three, for any environment with no systemd (and, in the general case
covered here, not even cron — a minimal or container environment being the
main target):
repowatch -c /etc/repowatch/config.yaml supervise \
--pid-file /var/run/repowatch/repowatch.pid \
--backup-dir /var/lib/repowatch/backups --backup-interval-hours 24 \
--nginx --nginx-policy /etc/nginx/repowatch/policy.jsonThis runs exactly what repowatch run runs (status API, optional syslog
listener, the check scheduler) plus, if the corresponding flags are given,
a backup loop and an nginx-reconciliation loop in the same process — see
repowatch supervise --help for the full flag list (intervals, retention).
Nothing is auto-detected: leave out --backup-dir/--nginx to skip those
parts entirely, same as not enabling the corresponding systemd timer.
Both run and supervise bind HTTP/TLS and the enabled UDP socket before
starting the watcher. Syslog must finish its initial index load within 30 seconds.
A bind/TLS/startup failure aborts startup and releases prepared sockets. During
operation, an unexpected watcher or mandatory listener exit terminates the
service with an error; configure the process manager to restart it. Listener
exits are checked every 100 ms when the event loop can run. Signals and external
cancellation cancel and drain child coroutines, stop listener loops and remove
the PID file. Blocking work already dispatched to a thread remains subject to
its own timeout; this is not a hard real-time shutdown guarantee.
Unexpected non-SQLite errors in an individual repository operation are logged
and isolated from other repositories. Shared SQLite failures are fatal and
cancel and drain running repository tasks, so the service cannot continue
retention with a known failed database or listener. check-once returns nonzero for these isolated
operation failures; existing fetch-error logging behavior remains unchanged.
The --nginx root requirement, and what you give up without systemd.
Under systemd, nginx reconciliation deliberately runs as a separate
root-owned oneshot unit (repowatch-nginx.service), so the main daemon
never needs elevated privileges (see
nginx cache and auto-reconciliation
and Hardening notes above) — that split exists because
systemd can supervise two independently-privileged units against one
config.yaml. The combined supervise --nginx command performs both jobs in one process,
so it requires the whole process to run as root (it
refuses to start otherwise, same check nginx-apply already makes) — there
is no way to keep the rest of supervise unprivileged while it also
reconciles nginx in-process.
If that tradeoff isn't acceptable, don't pass --nginx: run supervise
unprivileged for the main daemon (and, if you want, --backup-dir too —
backups only need write access to state_db's directory, not root), and
apply nginx changes through whatever periodic mechanism you do have
available — repowatch nginx-apply --policy ... as root from cron if
you have it, or your own small root-owned loop/timer if you don't. The
pieces are independent; --nginx is only a convenience for the case where
running as root isn't a concern (a common posture for a single-purpose
container, for instance).
PID file and manual start/stop, for environments with no supervisor to hand the command to at all:
repowatch -c /etc/repowatch/config.yaml supervise --pid-file /var/run/repowatch/repowatch.pid &
# ...later:
kill "$(cat /var/run/repowatch/repowatch.pid)"supervise writes its PID to --pid-file on start and removes it on a
clean shutdown (SIGTERM/SIGINT); it doesn't restart itself if it
crashes — if you have any process supervisor available (even a minimal
one), point it at repowatch supervise as the command to run, and let it
handle restart-on-crash. Two short examples:
OpenRC
#!/sbin/openrc-run
command="/usr/local/bin/repowatch"
command_args="-c /etc/repowatch/config.yaml supervise --pid-file /var/run/repowatch/repowatch.pid"
command_background=true
pidfile="/var/run/repowatch/repowatch.pid"runit
#!/bin/sh
# /etc/sv/repowatch/run
exec /usr/local/bin/repowatch -c /etc/repowatch/config.yaml supervise(runit itself tracks the process's own PID via the pipe it holds open, so
--pid-file is optional here — pass it only if something else also wants
to read it, e.g. a health check script.)
Standalone backups without supervise. repowatch backup <dir> [--retention-days N] is also a first-class, one-shot command on its own —
useful if you have cron but not systemd: 0 3 * * * repowatch -c /etc/repowatch/config.yaml backup /var/lib/repowatch/backups. It performs
the same pure-Python online SQLite backup (no dependency on the sqlite3
CLI binary or scripts/backup-state.sh, which requires the make install
layout) that supervise --backup-dir uses internally.
make install DESTDIR=/path/to/stage PREFIX=/usr/local ... lays out files
under the staging root for downstream packaging (e.g. building a distro
package), without writing anything outside it and without any of the
activate steps (which are refused entirely under DESTDIR). Runtime paths
baked into the installed files (the CLI wrapper, systemd units, nginx
policy) are always the target paths (PREFIX, etc.) — DESTDIR never
leaks into them, only into where install itself writes on the build host.
repowatch.serviceruns as the dedicated, unprivilegedrepowatchsystem user, withProtectSystem=strictand a minimalReadWritePaths.- Privileged operations — writing the nginx site config, reloading nginx —
are isolated in a separate root-owned oneshot unit
(
repowatch-nginx.service) triggered by its own timer, not by granting the main processsudoor filesystem access outside its state/config directories. admin_password_hashis a PBKDF2 hash, never the plaintext password — see access.md for the full authentication model, session/CSRF handling, and host-token scoping.- The dashboard and status API themselves aren't rate-limited by design — see the "What's deliberately not there" section of access.md for the reasoning; put a reverse proxy or firewall in front if you need that.
Disabling a repository's prefetch stops new queued automatic package downloads
once the daemon reloads the edit (normally on the next ten-second tick). Active
transfers may finish, and index checking and cleanup continue. Manual warming
is independent. See demand and update selection.