Dedicated Scancode Host
As of v0.27.6, the scancode per-file license/copyright scan can run on its
own machine via aveloxis scancode-worker, adjacent to the primary aveloxis
server. This page is the complete recipe.
Why
Scancode’s resource profile is nothing like the rest of the pipeline:
Disk: every scan is a fresh multi-GB shallow clone under
scancode_clone_dir(June 2026 incident artifacts on the primary host included leaked clone dirs and 9.5 GB stderr files).CPU: each scan is a Python multiprocessing pool pinned at 100% for minutes to hours; kernel-class repos legitimately run to the 24-hour adaptive-timeout cap.
No API budget: scancode clones anonymously — it never consumes the GitHub/GitLab key pool the main pipeline lives on.
Moving it to an adjacent machine isolates the disk/CPU blast radius. No new
coordination is needed: both machines talk to the same PostgreSQL, claims go
through FOR UPDATE SKIP LOCKED, and the v0.27.6 scancode_locked_host
column ensures each machine only adjudicates the liveness of ITS OWN
in-flight scans (a PID recorded on host A is meaningless on host B;
cross-host locks fall back to the derived stale-lock age window).
1. PostgreSQL remote access
On the database host, allow the scancode machine to connect:
# postgresql.conf — listen beyond localhost (skip if already remote-accessible):
listen_addresses = '*' # or a specific interface address
# pg_hba.conf — one line for the scancode host (use its actual address;
# do NOT open 0.0.0.0/0 — chaoss.tv's postgres logs show constant
# credential brute-forcing against exposed 5432 ports):
host aveloxis aveloxis 192.0.2.42/32 scram-sha-256
Reload postgres (SELECT pg_reload_conf(); — listen_addresses needs a
full restart). Verify from the scancode host:
psql "postgres://aveloxis:PASSWORD@db-host:5432/aveloxis?sslmode=prefer" -c "SELECT 1"
2. Install aveloxis + the scancode toolchain
# aveloxis binary (Go 1.26+) — the SAME version the primary runs,
# never @latest: the worker never migrates, so a newer binary than the
# schema stamp refuses to start (v0.29.68; it logged an ERROR and read
# columns the database did not have yet). Release tags are v-prefixed
# (v0.29.4), so set the bare number `aveloxis version` prints on the
# primary and let the @v prefix supply the tag form:
PRIMARY_VERSION=0.29.4 # what `aveloxis version` prints on the primary
go install "github.com/aveloxis/aveloxis/cmd/aveloxis@v${PRIMARY_VERSION}"
# git is required for the shallow clones; then the analysis tools
# (scancode needs Python 3.10+ and pipx):
aveloxis install-tools
install-tools installs scancode-toolkit-mini into a pipx venv and injects
the typecode-libmagic plugin. At every worker startup the v0.27.6
preflight additionally discovers the wheel’s matched (libmagic.so, magic.mgc) pair inside the venv and pins it via the
TYPECODE_LIBMAGIC_PATH / TYPECODE_LIBMAGIC_DB_PATH environment variables
on every scancode subprocess — the deterministic fix for the
version-mismatch warning storms that wedged the June 2026 fleet.
3. Minimal config template
The scancode host’s aveloxis.json needs ONLY the database block and the
scancode_* knobs — no API keys, no web/api/mail blocks:
{
"log_level": "info",
"database": {
"host": "db-host",
"port": 5432,
"user": "aveloxis",
"password": "…",
"dbname": "aveloxis",
"sslmode": "prefer"
},
"collection": {
"scancode_workers": 6,
"scancode_start_interval_s": 90,
"scancode_cadence_days": 180,
"scancode_clone_dir": "/srv/scancode-clones",
"scancode_run_timeout_hours": 2,
"scancode_run_timeout_cap_hours": 24,
"scancode_timeout_cap_strikes": 3,
"scancode_max_in_memory": 5000,
"scancode_ignore_globs": []
}
}
Size scancode_clone_dir for scancode_workers × largest expected clone
and raise scancode_max_in_memory if the host is RAM-rich (see the
configuration reference).
4. Turn scancode OFF on the primary server
In the PRIMARY server’s aveloxis.json:
"collection": {
"scancode_workers": 0
}
The explicit 0 disables the in-serve scancode pool (v0.27.6 — an absent
key keeps the default of 2; pre-v0.27.6 an explicit 0 was silently clamped
to 2). Restart aveloxis serve; its log confirms:
scancode worker disabled on this process (scancode_workers: 0) — run `aveloxis scancode-worker` on a dedicated host instead
Running pools on BOTH machines is also safe (SKIP LOCKED prevents duplicate claims) — it’s just rarely what you want, since the primary regains the disk/CPU load you moved.
5. Run it
aveloxis start scancode-worker -c /etc/aveloxis/aveloxis.json # background, logs to ~/.aveloxis/scancode-worker.log
aveloxis stop scancode-worker -c /etc/aveloxis/aveloxis.json # graceful stop (-c: the post-stop backend check reads the same config)
aveloxis scancode-worker -c /etc/aveloxis/aveloxis.json # foreground (what the systemd unit runs)
The command writes ~/.aveloxis/aveloxis-scancode-worker.pid, checks the
schema version (it never runs migrations — the v0.21.5 contract; run
aveloxis migrate from the primary when upgrading), runs the health
preflight + auto-remediation, sweeps stale clone dirs, and starts claiming.
start all / stop all never include the worker.
What NOT to run on this host
aveloxis start serve (or aveloxis serve). It is the full scheduler
regardless of which knobs the config carries: it runs the startup
migration and then collects with the fleet’s API keys. The 2026-09-09
incident did exactly that from a scancode-only config pointed at the
production database: the binary was one version ahead of the schema
stamp, so the migration ran in full beside the primary’s live workers
and deadlocked; the stop serve that followed then matched the
primary’s backends by application_name alone and printed terminate
recipes for them. Since v0.29.4 that shape — a binary AHEAD of the
stamp — is refused twice: by the start serve deploy gate when the
stamp is behind the binary, and by serve’s startup migration when
another aveloxis-serve is connected. Since 2026-09-11 a start serve
built at the primary’s version (which §2 asks for) is refused as well:
nothing needs migrating, but it would still start a second full
scheduler against the fleet, and that is the objection. Until then it
was only warned about — and a second machine consequently ran a full
stack against the production database for ten days, deadlocking the
primary’s staging inserts against its schema DDL, with the warning
sitting in a log on the wrong host the whole time. A deliberate second
serve is now explicit: aveloxis serve --allow-second-serve. The flag is
per-invocation and is deliberately NOT forwarded by aveloxis start serve, so it cannot become a persistent setting that hides the
condition again.
The gate and serve’s log both say when another aveloxis-serve is
already connected and from which client addresses, each tagged with the
code’s own verdict: an “(other address)” entry is normally the primary
— “wrong command on this host”; a “(this host)” entry is a serve on
THIS host, either one still running or a backend of one just stopped
here still draining (aveloxis stop reports those). Both labels
report the host verdict, not a comparison of the address text
alone — behind a pooler the marker is what separates two hosts sharing
one address, so an “(other address)” entry can carry an address string
identical to your own. Only aveloxis start refuses to double-start a component, so check ps and the
pidfile before waiting for a “(this host)” entry to clear.
stop scopes its backend check to this host.
When NO “(other address)” entry appears — every listed entry is “(this host)”, or an address this role cannot see — the verdict withdraws the “wrong command” reading entirely, because nothing in the listing identifies a primary. That is the ordinary shape when a second serve starts on the same machine, and the reverse case is real: if this host’s serve came up first and the primary’s came back afterwards, the PRIMARY is the entry tagged “(other address)”.
Both readings, and stop’s, need the database role this host connects
as to SEE the primary’s sessions: pg_stat_activity shows a session’s
client_addr only to roles that HOLD that session’s role’s privileges
and to roles that hold pg_read_all_stats’s — everyone else reads
NULL, the same NULL a unix socket shows.
Connect as the primary’s role (the shared-DSN recipe above does), or grant this host’s role the stats privileges:
GRANT pg_read_all_stats TO <role>;
Holding is the test, not membership: a NOINHERIT role, or one
granted WITH INHERIT FALSE, is a member of pg_read_all_stats with
none of its privileges and still reads NULL. On PostgreSQL 16 and
later a grant’s inherit option is fixed at GRANT time, so if the grant
already exists and does not inherit, re-grant it with
GRANT pg_read_all_stats TO <role> WITH INHERIT TRUE. Check with
SELECT pg_has_role(current_user, 'pg_read_all_stats', 'USAGE').
Without the privileges the entries read “client address not visible to
role …”, stop prints a no-verdict note for such backends, and
nothing is ever offered for termination on that evidence.
Warning
Behind a transaction pooler, upgrade the primary before you trust this
host’s verdicts. A pooler in transaction or statement mode
(pgbouncer, pgcat, Odyssey) replaces pg_stat_activity.client_addr with
its own address for every backend, so the primary’s serve and this
host’s worker share one address. The address rule alone then makes
“(other address)” never appear, the deploy gate read the primary as a
serve on THIS host, and aveloxis stop here wait on — and offer a
pg_terminate_backend recipe for — the primary’s pool. That is the
2026-09-09 incident’s shape.
This host is protected by the @<hostname> marker each component now
puts in its application_name: it survives the collapse, because
PostgreSQL never rewrites what the client sent. The marker’s job is
narrow and one-directional — it can separate two hosts the address
rule collapsed together, and it can never merge two the address
rule kept apart. A marker collision (for example, two container hosts
running one compose file with the same hostname) degrades to the address
rule. Behind a transaction pooler that fallback can classify the other
host as this host and offer a termination recipe, so assign unique hostnames
(or connect directly to PostgreSQL) before relying on this verdict.
But the marker only helps when both sides carry one. A primary still running a pre-round-12 binary tags un-suffixed, and an un-marked backend falls back to the address rule — which behind a pooler places it here.
So: upgrade the primary to the same build before relying on this host’s verdicts on a pooled deployment, or point both hosts’ DSNs at PostgreSQL directly.
systemd unit example
/etc/systemd/system/aveloxis-scancode.service:
[Unit]
Description=Aveloxis dedicated scancode worker
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=aveloxis
ExecStart=/home/aveloxis/go/bin/aveloxis scancode-worker -c /etc/aveloxis/aveloxis.json
Restart=on-failure
RestartSec=30
# scancode subprocesses are the actual memory consumers; adjust for
# scancode_workers × --max-in-memory sizing.
MemoryMax=48G
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now aveloxis-scancode
journalctl -u aveloxis-scancode -f
6. One-time libmagic version lock
The June/July 2026 warning-storm incident chain started when the system
file/libmagic packages moved ahead of the typecode-libmagic wheel’s
vintage. The v0.27.6 env pinning makes the mismatch impossible for the
scans themselves, but freezing the system packages removes the churn
entirely on a single-purpose host:
sudo apt-mark hold file libmagic1 libmagic-mgc
(Undo later with apt-mark unhold.) On macOS test hosts the equivalent is
brew pin libmagic.
Monitoring
Health:
SELECT * FROM aveloxis_ops.aveloxis_status WHERE status_name = 'scancode';— whilestatus = 'broken'the dispatcher claims nothing and re-probes every 15 minutes (auto-resumes on a passing probe).In-flight scans:
SELECT repo_id, scancode_locked_host, scancode_locked_at FROM aveloxis_data.repos WHERE scancode_locked_at IS NOT NULL;Skips:
SELECT repo_id, scancode_skip_reason FROM aveloxis_data.repos WHERE scancode_skip_reason <> '';Sidelined spinners:
SELECT repo_id, scancode_timeout_attempts FROM aveloxis_data.repos WHERE scancode_timeout_attempts > 0 ORDER BY 2 DESC;
See also: architecture/scancode for the worker’s internals and the four-state crash recovery, commands reference for the CLI surface.