Pithead v1.0.1
⬆️ A newer release is available — install v1.0.2
v1.0.2 is a packaging-only patch (identical images) with the complete install bundle + the stable
pithead.tar.gzdownload. Use it instead of the instructions below.
[1.0.1] - 2026-06-13
First installable stable release. Supersedes 1.0.0, whose published images were arm64-only
(built on an Apple-Silicon host) and whose install bundle was incomplete — it could not run on
x86_64. No feature changes from 1.0.0; the fixes are entirely in the release pipeline (correct
linux/amd64 image builds + a complete install bundle). 1.0.0 is kept as a superseded tombstone at
the bottom.
Bundled, SHA-pinned upstream components: P2Pool v4.16, Monero v0.18.5.0, XMRig-proxy
6.26.0, Tari/minotari_node v5.3.1-mainnet, Caddy 2.11.4, docker-socket-proxy v0.4.2.
The exact image digests for each release ship in the GitHub Release's ingredients manifest.
monerod follows P2Pool v4.16's recommendations where they're compatible with the Tor-first model:
in-peers=64 (the inbound/open-files cap) is honored exactly, and the optional clearnet initial sync
(#183) runs monerod as a clearnet node should — out-peers=32 + P2Pool's recommended priority nodes
(p2pmd.xmrvsbeast.com, nodes.hashvault.pro) for that window. The DNS-based recommendations
(priority hostnames, DNS blocklist/checkpointing) stay off in Tor mode — they leak clearnet DNS
(#161) — with monerod's compiled-in checkpoints as the substitute. ./pithead doctor now also checks
that the system clock is NTP-synchronized (clock skew gets shares/blocks rejected), per P2Pool's
"synchronize your clock before mining" guidance.
Install / Upgrade
New install — clone the repo (or download the pithead-v1.0.1.tar.gz install bundle from the GitHub Release's assets) and follow the quickstart (./pithead setup).
Upgrade an existing (source) checkout:
git pull
./pithead upgrade./pithead upgrade re-renders the generated config itself — the P2Pool v4.16 monerod settings (out-peers, the clearnet-window priority nodes) and the new doctor clock check are picked up automatically — then rebuilds and recreates only the containers that changed. No separate ./pithead apply is needed for this release, and your config.json plus preserved secrets (Tor onions, RPC credentials, proxy token) are kept untouched. Run ./pithead apply only when you edit config.json — e.g. to opt into the new, default-off clearnet initial sync. Verify afterwards with ./pithead status and ./pithead doctor.
See docs/operations.md for the full lifecycle reference.
Added
-
Optional clearnet initial sync (#183). A default-off, per-component opt-in
(monero.clearnet_initial_sync/tari.clearnet_initial_sync) that lets a node do its one-time
initial block download over clearnet — much faster than over bandwidth-capped Tor circuits, which
can crawl at near-zero blocks/sec — then return to Tor for all ongoing operation. When on, Monero
drops its Tor P2Pproxy=(loweringout-peers48 → 16) but keepstx-proxy=tor, so
transaction-origin privacy is preserved and wallets are never exposed; Tari switches its transport
to TCP and re-enables theseeds.tari.comDNS seed (its onionpeer_seedsare unreachable without
Tor). The only exposure is node-existence — your host IP is visible to that chain's P2P network
for the sync window. Privacy-first by construction: off by default, a⚠disruptive-change
confirmation onapply, a persistent "CLEARNET INITIAL SYNC ACTIVE — node IP exposed" banner in
status/up, adoctorwarning (and a green "Tor-only" check when off), and a matching warning in
the monerod container logs — so a node is never silently left on clearnet. Exposed in
config.advanced.example.json; documented with a full threat model indocs/privacy.md, plus
docs/configuration.md,docs/getting-started.md, anddocs/hardware.md. -
Automatic clearnet → Tor switch-back once synced (#234). The clearnet initial sync (above) no
longer needs a manual flip back. The dashboard watches each chain's sync state and, the first time
a clearnet node reports fully synced, writes a persistent marker and restarts the daemon — which
comes back up Tor-only and stays there across restarts,apply, and reboots (the marker, not
the flag, is the source of truth, so a synced node can never be silently re-exposed). Monero and
Tari transition independently. Fail-safe: the marker is persisted before the restart and a failed
switch is retried, never leaving a node stranded on clearnet; thestatus/upbanner anddoctor
warning clear themselves once a node is genuinely back on Tor. Re-arm a fresh clearnet sync by
toggling the flag off and on. Mechanism: a sharedclearnet-statedir (dashboard rw, monerod/tari
ro) drives the daemon entrypoints' Tor-vs-clearnet decision; pithead always renders the canonical
Tor config. Closes #234. -
Dashboard "new release available" badge (#224). The dashboard periodically checks GitHub for
the latest Pithead release and, if it's newer than the running version, shows a header badge next to
the version badge — "New release vX.Y.Z available ↗" — linking straight to the release page.
Notify-only: it never updates anything; you upgrade with./pithead upgradeon your own terms
(the one-click upgrade is the separate #59). On by default because the check is routed over
Tor (the same bridge SOCKS as the XvB fetch,socks5hso the DNS lookup goes through Tor too) —
GitHub sees a Tor exit, not your IP, so it leaks nothing about you; it's cached hourly and fails
silently offline. Setdashboard.check_for_updates: falseto opt out. Documented in
docs/configuration.md+docs/privacy.md. -
Release pipeline —
scripts/release.sh(make release) (#44). A single entry point, run from
the private build/test server, that cuts a versioned release end to end: preflight (clean tree,
SemVer fromVERSION, tag-not-already-released, resolve the component pins) → blocking test gate
(make test+ the #54 live integration matrix) → build the 5 first-party images with OCI labels +
PITHEAD_RELEASE=1→ push to a staging:vX.Y.Z-rc.Ntag and capture digests → smoke-verify the
pushed images from the registry → promote by digest to:vX.Y.Z+:latest(no rebuild, so
the release is bit-for-bit what was tested) → publish the GitHub Release with an ingredients
manifest (promoted digests + upstream pins) and a pinned install bundle. Safe by construction:
--dry-runpreviews the whole plan, it never starts the live stack on the build host, and it never
prints the registry token. Images publish toghcr.io/p2pool-starter-stack/pithead-*
(override viaPITHEAD_REGISTRY/PITHEAD_IMAGE_PREFIX). Release installs pull the published
images — no local build: each compose service carries animage: …/pithead-<svc>:${STACK_VERSION}
ref alongsidebuild:, and pithead auto-selects the mode — a source checkout (Dockerfiles present)
builds locally and tags:dev(--pull never); a release bundle (no Dockerfiles, just
pithead+VERSION+compose+build/tari/) resolvesSTACK_VERSIONfromVERSIONand pulls
:vX.Y.Z(--pull missing). So a release is./pithead setupwith no compile wait. -
Chart hashrate-averaging-window toggle (#168). A new Avg control on the dashboard chart
plots any of xmrig-proxy's five native averaging windows — 1m / 10m / 1h / 12h / 24h. It's
separate from the existing time-Range buttons: Range sets how much time the x-axis spans, Avg
sets how smooth each point is (short windows react to a rig dropping/joining within a poll or two;
long windows show the underlying trend). 10m is the default, so the chart is unchanged unless
you switch — and the choice persists across reloads. Every option plots a true average for its
window (the old headline "15m" was really the proxy's 10m relabeled; that's gone). All five windows
are now captured per poll and persisted in their own history columns (additive migration, so
existing databases upgrade in place); per-window history is forward-only, and the 12h/24h
averages need that much rig uptime to fill — both are signposted in the UI. Display/observability
only; the switching algorithm is untouched. -
Optional dashboard login (#8). A new
dashboard.authblock puts a Caddy HTTP basic-auth prompt
in front of the dashboard. It's opt-in and off by default — an emptydashboard.auth.password
keeps today's behavior (no login), which is right for a private LAN appliance; set a password when
the box is shared or reachable beyond the LAN. pithead bcrypt-hashes the password with the
already-pinned Caddy image (caddy hash-password) and persists only the hash in.env
(base64-encoded so bcrypt's$doesn't spam compose interpolation warnings) — the plaintext lives
solely in your owner-onlyconfig.json. A sha256 fingerprint of the plaintext keeps the hash
stable acrossapplyruns (re-hashed only when the password actually changes, so the Caddyfile
doesn't churn). The username/password are validated before render, the secret is never echoed
in the change preview, andapplywarns if a password is set withoutdashboard.secure: true
(basic-auth is cleartext over plain HTTP). Documented in
docs/configuration.md#exposing-the-dashboard-safely. -
"Raffle Eligible" indicator on the dashboard (#158). A Hero-band + Simple-Overview box that
answers "am I set up to both win and collect an XvB raffle payout?". It reads Yes only when
XvB is on and both gates are met: you're donating at least the donor tier (XvB's credited
1h and 24h averages have cleared the lowest threshold — the same figure as Current Tier)
and you hold a P2Pool PPLNS share (XvB's "VIP" gate — without it a win is skipped, you take a
fail, and you're dropped after the 3rd, regardless of tier). It reads No when donating but a gate
is unmet, and N/A (XvB off) when XvB is disabled. A loud "⚠ No PPLNS share — XvB wins skipped"
badge fires for the make-or-break case (donating with no share) so donations aren't wasted.
Deliberately stricter than XvB's bare "VIP = just a share" so a green Yes is trustworthy; named
"Raffle Eligible" rather than XvB's "VIP" to avoid colliding with thevipdonation tier.
Display/observability only — the donation controller already protects the share reserve; this
surfaces it. -
Two xmrig-proxy config knobs: optional stratum authentication (#152) and dev-fee transparency
(#173).p2pool.stratum_password(default off) turns the open:3333stratum port into
authenticated stratum — only rigs that send the matchingpassmay mine, which also shrinks the
#122 worker-name SSRF surface."auto"generates and persists a stable secret (surfaced after
applyand stored in.env), or set your own string; the password is cleartext over stratum, so
it's access control, not encryption — pair it withstratum_bind/a firewall.proxy.donate_level
exposes xmrig-proxy's built-in dev-fee donation (compiled-in default 0% — no fee), now rendered
explicitly so it's visible and operator-controllable — default0(off), or1–99to
donate to the xmrig developers; this is not the XvB donation (xvb.*, steered by the optimizer).
Both render to
xmrig-proxy CLI flags (--access-password/--donate-level), are validated before render, and
are documented indocs/configuration.md+docs/workers.md#authentication. -
Expanded the
pitheadshell test suite over data-safety / security-relevant paths (#140). The
backup/restoreround-trip is now covered end to end (stubbedtar/du/df/docker/sudo):
archive layout (the irreplaceable bits in, blockchains out), the leading-/strip, a true
restore-in-place round-trip, and the low-free-space pre-check (cancel +--yesproceed). Added
generate_caddyfileHTTPS-vs-HTTP (tls internal) branch tests and unit tests for the previously
uncovereddetect_os/detect_host_timezone/deps_satisfiedhost-detection helpers. Test-only
— would have caught the #127backuperrexit bug. (upgrade#128 anddoctor#127 were already
covered.) -
CI now builds every container image, shellchecks all container scripts, and runs hadolint
(#124). Previously only the dashboard's test stage was built, so a broken Dockerfile /COPY
path / entrypoint in monerod, P2Pool, Tor, or xmrig-proxy would only surface at deploy time — e.g.
the Tor image's #180 entrypoint restructure shipped with no CI safety net. Abuild-imagesmatrix
nowdocker builds all fivebuild/*images on every PR;make lint(the CI shellcheck step)
gained the sevenbuild/*/*.shentrypoints + healthchecks; and ahadolintjob lints the
Dockerfiles (with a documented.hadolint.yamlignore list for rules that conflict with the
digest-pinning approach). CI shellcheck now runs viamake lintso the file list can't drift. -
The live integration harness now asserts the runtime privacy + resource posture, closing the
gap where these could regress silently past the tier-1 config checks: each service'smem_limitis
actually in effect (#132), monerod makes no clearnet DNS (checkpoints off, no priority-node
hostnames; #161), and Tari's resolver is the dead-address sinkhole rather than a clearnet
nameserver (#162). Heavier live-coverage follow-ups filed as #201 (deploy on a non-default subnet),
#202 (fault-inject a DB-write failure →db_healthy), and #203 (empty proxy token → fail closed). -
Memory ceilings on every service (#132). Only Tari was bounded before; now monerod, P2Pool, the
dashboard, Tor, xmrig-proxy, and both Docker socket proxies each carry amem_limit(with
memswap_limit == mem_limitfor a clean, swap-free OOM-kill). A leak or spike now OOM-restarts the
offending container in its own cgroup instead of letting the host OOM-killer pick a victim — which
could be monerod, the revenue service. The ceilings are generous (observed steady-state is far
lower — monerod ~0.3 GiB RSS since its DB is reclaimable page cache, dashboard ~0.06, p2pool ~0.35);
monerod's is tunable via the newmonero.mem_limitconfig (default a generous 6 GB; its
OOM-triggering memory is small — ~0.1 GiB at rest, ~1–3 GiB during sync — while its multi-GB DB is
reclaimable mmap'd page cache, so initial-sync verification never trips it). The other (small)
services carry fixed conservative ceilings. -
docs/privacy.md— a single source-of-truth network-egress reference (#164, the v1.0 close-out
of the #160 privacy epic). It maps every off-box connection: whether it's Tor-routed, its default,
and how to harden it — runtime egress (monerod/Tari over Tor with their clearnet DNS leaks closed;
the XvB stats fetch now over Tor) plus the two clearnet yield paths still open in v1.0 (P2Pool
outbound peers #165, XvB donation mining #166 — both Tor-by-default in v1.1) and the one-time
build/install IP exposure. The absolute "your home IP isn't exposed" claims in the README,
architecture, and FAQ are corrected to the honest Tor-first, not yet Tor-only reality, and all
three cross-link the new guide. -
pithead setupanddoctornow warn when the host has a public IP and stratum:3333is
bound to all interfaces (#113). The stratum port is plain, unauthenticated stratum and must never
face the public internet: a NAT'd home host has no public IP on its interfaces and stays silent,
while a VPS/cloud or publicly-addressed host gets a clear nudge to firewall:3333to the LAN or
narrowp2pool.stratum_bind(it also stays quiet if the bind is already narrowed). Warn-only,
never fatal;doctorprints a ✓ when not exposed. A pure, unit-testedis_public_ipclassifier
handles the RFC1918 / CGNAT / ULA / link-local / loopback exclusions (IPv4 + IPv6). -
A four-tier test strategy for simulating every runtime situation (#54), documented in
docs/testing-strategy.mdwith a full scenario catalog:- Live config-matrix suite (
tests/integration/, tier 4) that drives a real, synced
server through the config matrix and asserts the stack behaves — containers healthy, nodes
synced, miners mining, dashboard reading correct live state,statusexit codes, secrets
preserved. Runs over SSH or--local; the blocking pre-release gate. A--fault-injection
phase deliberately breaks monerod (stop / SIGSTOP / remove) to assertpithead status'
down/unhealthy/missing verdicts and the failover→recovery cycle.make test-integration. - Controllable fake monerod/Tari + a contract test (
tests/integration/fakes/, tier 2)
that points the real dashboard clients at the fakes and asserts they parse every state —
docker-free, runs on every PR.make test-fakes. - Fake-daemon docker mini-stack (
tests/integration/mini-stack/, tier 3) running the real
dashboard + docker-control proxy against the fakes, asserting sync hold/release and Tari
reject/readmit end-to-end with real containers (make test-mini-stack). Validated green
(11/11) on a real Docker host, and isolated (namespaced container names + non-colliding
ports) so it can run safely beside a live deployment. - New dashboard unit tests for the required-Tari sync gate, the #35-latch × #31-failover
interaction, and simultaneous double outages. - A generated test inventory (
docs/test-inventory.md,make test-inventory) listing
every test/scenario across all suites, kept honest by a CI drift check. - A non-destructive
--checkmode for the live harness (assert the box's current state —
no config change/apply/restore); the safe first run / ongoing health check. Validated with
a 22/22 green run against a real synced, mining box, which calibrated the harness to trust
monerod's own sync flag as the readiness gate, andproxy_workersfor mining liveness
(stratum.connscan read 0 while mining). - A developer testing guide (
docs/testing-guide.md): per-change recipes, conventions, and
the calibration gotchas learned on real hardware. - Regression guards for past bugs/security fixes: extended the #90 hardening section of
tests/stack/test_compose.shwith per-service least-privilege checks for the Docker socket
proxies (the read proxy can't POST; the control proxy is start/stop-only; both mount the
socket read-only) and the Tari[m]inotariself-match guard — alongside the existing
no-new-privileges / cap_drop / credential-free-healthcheck assertions. Plus a
dashboard.host"auto"-revert test and the schema-migration test that caught the DB upgrade
bug above. - Release/validation-server tooling: a
--readinessmode for the live harness (non-destructive
assessment that a box is fit to be a release server — synced chains reusable, snapshot-capable
filesystem, disk headroom, secrets owner-only, dashboard localhost-only), a
docs/release-server.mdguide (why end-to-end validation needs a dedicated server vs. what
GitHub Actions runs free on every PR, the hardening checklist, and the safe self-hosted-
runner setup), and arelease-gate.ymlworkflow that runs the tier-4 matrix on a self-hosted
runner only on trusted code (manual dispatch / push to main — never on a fork PR). - A
--safety-backuprollback net for the live harness: takes a realpithead backupbefore
the destructive scenarios and automatically rolls the box back (down → restore → up) if
anything fails, removing the archive on success — so the destructive matrix can run on a
precious box. The--lifecyclephase also does abackup→restoreround-trip (assert the
pool reverts and secrets survive), exercising both verbs end-to-end. UPDATE_INTERVALis now env-configurable (lets the mini-stack loop fast in CI).
- Live config-matrix suite (
-
Dashboard header shows the host's IP address next to the hostname when the configured
dashboard.hostis a name, ashostname @ ip(e.g.pithead.local @ 192.168.1.42), so you can still reach the
dashboard when the hostname doesn't resolve from your phone or another machine on the LAN. The
address is detected on the host (the dashboard runsnetwork_mode: host), and is omitted when
the host is already an IP or can't be determined (#119). -
P2Pool Earnings (estimated) card on the dashboard's Advanced view: expected XMR
per day / month / year from P2Pool mining only, computed from your P2Pool hashrate
and the live Monero block reward + network difficulty, plus an expected time-to-share.
Explicitly scoped to P2Pool — XvB donations are excluded (the what-if hashrate defaults
to your P2Pool 1h average, the same figure shown in the header / Overview, which already
excludes any XvB-donated slice, so an active XvB split doesn't inflate the estimate and
the number stays consistent with the rest of the dashboard) and Tari merge-mining is
excluded. Includes a what-if hashrate input and a clear "estimates, not guarantees"
disclaimer. Tari (#117) and the XvB tier estimate (#118) are deferred (#12). -
Dashboard header now shows which build is running, as a muted badge on both the
syncing and main screens: a clean release showsvX.Y.Z(from the top-level
VERSIONfile), while any dev/working-tree build showsdev · branch @ hashso
it's never mistaken for a release. The version is baked into the dashboard image at
build time (build-arg → env + OCI labels), so the running container is
self-describing. -
Per-worker share stats in the dashboard's Workers table: accepted / rejected (with invalid
folded in) counts per rig, a ⚠ flag on a high reject rate, and a Proxy totals footer
(pool-wide accepted / rejected / invalid + best difficulty) collected from the xmrig-proxy
/summaryendpoint. The proxy already reported all of this; it was being parsed and discarded
(#82). -
Responsive dashboard layout: the web UI now reflows for phone-sized screens — the header
stacks, the card grid collapses to a single column, the disk bar goes full-width, and the
workers table scrolls horizontally within its card instead of overflowing the page. Desktop
is unchanged. -
Dashboard branding (#81): the header now leads with the Pithead mark + wordmark and demotes
the host IP to a subtitle, and a hero KPI band above the dashboard surfaces the headline
numbers — total hashrate, shares in the PPLNS window, blocks found, XvB tier, and mining mode. -
Release & versioning scaffold: top-level
VERSIONfile (single source of truth),
this changelog, anddocs/releasing.mddocumenting the release process. The
GHCR publishing pipeline andmake release/pithead releasecommand are still
to come (seedocs/releasing.md). -
p2pool.stratum_bindconfig option to choose which host interface the stratum port
(3333) is published on (default0.0.0.0; set a LAN IP or127.0.0.1to narrow it). -
Liveness healthcheck for the p2pool container (probes the stratum port), so a stalled
p2pool is now visible inpithead statusand the dashboard. -
pithead doctornow checks that Docker is enabled to start at boot (systemd) and warns if not —
restart: unless-stoppedonly brings the stack back after a reboot when the daemon does too,
which matters for an unattended miner (#137). -
Low-disk warning badge in the dashboard header (#138): a heads-up at 85% used of the data
filesystem and a prominent critical alert at 95%, on both the sync and main screens — the disk
bar alone is easy to miss, and a full data disk corrupts the Monero database mid-write.
Changed
- Dashboard cards reordered by operator relevance (#159, completing the #156→#159 chain). Cards
rendered in the order they were added, so pool-wide and network-wide context (Global P2Pool Stats,
XMR Network) sat above and between the things that matter for running this stack. The board now
leads with the fleet at-a-glance — the hashrate chart and the Workers table go full-width up
top (this stack may drive many machines) — then this stack's own detail cards (my P2Pool node, my
XvB tier/VIP/routed, my earnings, my Tari), with Global P2Pool Stats and XMR Network demoted to
reference position at the bottom. The Simple Overview's stats are likewise reordered to lead with
the decision-relevant numbers (total hashrate, mode, workers, tier, VIP, shares) before the routed
split and reference fields. "Mine" first, "the world" last — and a cleaner hero shot for launch. - Dashboard now shows routed hashrate everywhere for display, credited only where XvB's verdict
matters (#156). Two different XvB numbers were being conflated: routed = what the xmrig-proxy
actually sent to a pool (v_xvb/v_p2pool, a common basis for both pools that sums to your total),
and credited = what XvB's API reports back (avg_1h/avg_24h, XvB-only, on its own "credit
factor" basis). The header, Simple Overview, and chart previously showed credited XvB next to
routed P2Pool — two incomparable bases side by side. They now show routed for both pools
(explicitly labelled "(routed)"), derived fromv_xvbthe same way P2Pool's averages already were
(new_avg_xvb_over_window). Credited now appears in exactly two places: the swap-algorithm
decision logic (unchanged — it must steer off credited per #9/#70) and the Advanced "XvB Donation
Stats" card, where routed and credited 1h/24h are now juxtaposed so the live credit factor is
visible. The Current Tier label stays credited-derived everywhere (the raffle assigns tiers off
credited — an intentional exception). No schema change; reads sensibly (routed = 0) when XvB is off. - The Compose project name is now pinned to
pithead(name:indocker-compose.yml), so
the stack's images, network and volumes are prefixedpithead*regardless of the checkout
directory — instead of inheriting the directory's name (which left older checkouts named after
the repo's previous name).pithead up/apply/upgradedetect a stack still running under
the old, directory-derived project name and migrate it automatically (only that project's
containers are removed so the renamed project can take over — bind-mounted chain data and the
Tor onion keys are untouched). One-time after the rename, Caddy re-issues its local TLS cert
under the new project, so re-trust the dashboard cert if you'd installed the old one. - Hardened the leaf containers (caddy, xmrig-proxy, dashboard, docker-proxy, docker-control)
withno-new-privileges. All except the dashboard alsocap_drop: [ALL](caddy keeps
NET_BIND_SERVICEfor:80/:443); the dashboard keeps its default capabilities because it
writes its SQLite history into a host-user-owned volume as root. Caddy and the two Docker
socket proxies additionally run with a read-only root filesystem (ephemeraltmpfsfor
scratch; Caddy's certs persist incaddy_data). - Log rotation (
json-file, 10 MB × 3) now applies to every service —caddy,
docker-proxy, anddocker-controlpreviously fell back to Docker's uncapped default, so
their logs could grow without bound and fill the disk on a long-running host (#123).
Fixed
-
Release images are now built for
linux/amd64(x86_64). 1.0.0 was built on an Apple-Silicon host
with a plaindocker build, so its images were labelledarm64and would not run on x86_64 servers
(no matching manifest for linux/amd64) — even though they contained x86_64 binaries. The stack is
x86_64-only by nature (monero/p2pool/xmrig-proxy shiplinux-x64binaries, and xmrig-proxy has no
arm64 build at all), so the pipeline now builds withbuildx --platform linux/amd64(forcing amd64
even on an arm64 release host) and the smoke stage fails the release if a pushed image isn't amd64
(#243, corrected from a multi-arch attempt). -
The pinned install bundle now ships every config template the compose mounts. 1.0.0's bundle was
missingbuild/monero/bitmonero.conf.template— the monero image doesn't bake it in, so a bundle
./pithead setupmounted an empty dir there and monerod couldn't start.make_bundlenow derives the
shipped paths from the compose file so no runtime mount can be omitted (#242). -
Disconnected workers never fell off the "Workers Alive" table (#182 regression). A worker that
stopped mining was supposed to linger as DOWN for an hour and then drop off, but it never did:
xmrig-proxy keeps a disconnected rig in its/workerslist (with a frozen hashrate) for hours, and
the dashboard's lifecycle dropped the aged-out worker from its internal state while the proxy still
reported it — so the next poll recreated it with a freshlast_active, resetting the 1-hour clock.
The ghost row flickered off for a single cycle each hour and came back as DOWN indefinitely. The
lifecycle now keeps an aged-out worker in state as long as the proxy still reports it (preserving
when it actually went offline), so it falls off once and stays off; a genuine reconnect still
re-adds it fresh. Found during a release-gate coverage audit; covered by a new regression test. -
Chart Avg windows other than 10m no longer flat-line at 0 on wide ranges (#168 regression). The
chart downsampler (used whenever a range has more points than the canvas tier — i.e. the 24h / 1w /
1mo ranges) bucket-averaged only the legacyv/v_p2pool/v_xvbcolumns and silently dropped
the per-window columns added in #168. So selecting 1 Min / 1 Hr / 12 Hr / 24 Hr as the Avg
window on those ranges plotted a flat zero line (the y-axis then auto-scaling to the Shares markers),
even though the underlying data was present — only the default 10 Min window survived. The
downsampler now carries every per-window hashrate column through, derived from the window map so
future windows can't regress the same way. Display only. -
pithead upgradeno longer marks an already-deployed stack as "not set up." Each upgrade
re-renders.env, andrender_envwritesDEPLOYMENT_COMPLETED=${DEPLOYMENT_COMPLETED:-false}.
Becauseload_preserved_statedoesn't carry that flag (unlike the Tor onions / RPC creds / proxy
token it preserves), the shell variable was unset and everyupgradesilently rewrote it to
false. The nextrequire_deployedcommand (up,apply, or anotherupgrade) then aborted with
"The stack isn't fully set up yet — run setup first," and a barepitheaddropped into setup
instead of help — forcing a needless, heavyweightsetup(Tor re-provision, GRUB edit) before every
upgrade.upgradenow re-assertsDEPLOYMENT_COMPLETED=truebefore the render, mirroringapply;
it's only ever reached pastrequire_deployed, so the stack is deployed by definition. A new
black-box test pins the behavior (it fails on the old code). -
Dashboard "Workers Alive" panel now has the standard gap beneath it. Every dashboard section is
a.cardwrapped in a.grid, and.gridsupplies the consistent 20px bottom margin between
sections. The Workers Alive table was the one section rendered as a bare.cardwith no.grid
wrapper, so the space between it and the cards below collapsed to zero — out of step with the rest
of the layout. It's now wrapped in.gridlike the chart above it, restoring even spacing. -
Dashboard "Current Tier" no longer overstates your XvB tier on a hashrate drop (#157). The XvB
raffle qualifies a tier on both the 1h and 24h credited average (and terminates a win if the 1h
drops below the round minimum), but the dashboard resolved Current Tier from the 24h average alone.
On a hashrate drop the 1h falls first while the laggy 24h still reads the old tier — so the dashboard
showed a tier the miner had effectively already lost, exactly when an accurate signal matters most.
Current Tier now reflectsmin(1h, 24h); ramp-up (where 24h is the lower, conservative read) is
unchanged. Display-only — the donation controller still steers off the credited 1h average (#9/#70). -
Dashboard share-health panel no longer flickers to empty on a malformed proxy
/summary(#141).
The pool-wide accepted/rejected/invalid/best totals are designed so a bad poll keeps the last-good
value — but that only held when the fetch raised. A non-raising malformed body (a non-dict:null,
a list, a string) parsed to{}and overwrote the last-good, blanking the panel. The parse now
routes through_merge_proxy_summary, which keeps the prior totals unless the new parse is usable
(a valid summary reporting genuine zeros is still adopted). -
Dashboard "Stats fetched from xmrvsbeast.com (Updated: …)" timestamp now reflects the real fetch
(#136). It was bumped on every algo cycle because the controller writesdonation_fractioneach
loop, even though the actual xmrvsbeast.com fetch only runs every 10th iteration — so the "Updated"
time ticked fresh (~every 30s) even while the site was unreachable for hours, hiding stale data. The
last_updatetimestamp now bumps only on a genuine fetch (whenavg_1h/avg_24harrive), not on
the per-cycle local writes (mode,donation_fraction,fail_count). -
HugePages header badge no longer reads red when reserved-but-not-yet-used (#175). Right after
boot, or while Monero syncs and the miner is held, HugePages are correctly allocated but0are in
use yet — the badge showed "Allocated (0 / N)" in red, implying an error. The reserved-but-unused
state now renders green (it's the normal startup state); red is reserved for the genuinely-bad
HugePages_Total == 0("Disabled") case. The "Unknown" (meminfo unreadable) state is unchanged. -
Hashrate chart no longer reads blue-purple when mining 100% to P2Pool (#184). The chart is a
stacked area where each band draws a colored top border-line; when a series is flat-zero (XvB off,
or P2Pool zero in XvB-only mode) its border was painted along the other series' edge — so an
all-P2Pool window looked blue-purple, implying a split that wasn't there. Each band's border is now
suppressed per-segment wherever the band has zero height, so a single-pool window reads as one solid
color (the truthful picture) without having to manually hide the empty series. -
"Workers Alive" table now tells the truth about offline workers (#169 / #182). Two related
bugs: (1) a disconnected worker's Uptime kept climbing — it was computed as seconds-since-last-share,
so the longer a rig was down the bigger its "uptime" read (it was really downtime); offline rows
now showDOWN. (2) Dead rows never left the table — it's titled "Workers Alive" but a rig
that connected once (e.g. under a typo'd name) lingered forever. A new dashboard-side
WorkerLifecycletracks each worker'sconnected_since(giving online rigs a true,
monotonically-increasing uptime even when their direct API is unreachable — replacing the
misleading last-share estimate) andlast_active(so an offline worker falls off after
WORKER_FALLOFF_SEC, default 1h, operating on the live proxy list — not the deadknown_workers
path #144). A reconnect re-adds the worker and restarts its uptime. Rawuptimestays on the row
for client-side column sorting. -
A fully-synced monerod no longer shows as "loading" in the dashboard's Monero panel. A synced
node reportstarget_height: 0(no target), so the panel'sdonecheck — which compared
percent >= 100against a target, and derived the state string fromhas_targetfirst — never
fired, leaving the normal steady state stuck at "loading" indefinitely (surfaced in the #180
gouda validation; mining and worker-gating were unaffected — those use monerod's RPC flag
directly). The sync state now trusts monerod's authoritative caught-up signal (reachable && not is_syncing), and the live integration harness asserts the panel reads "done" — closing the
test gap that let this escape both the unit suite and the e2e matrix. -
Install no longer fails on hosts whose LAN already uses
172.28.0.0/24(#180). The Docker
bridge subnet is now configurable vianetwork.subnet(default172.28.0.0/24); set a free
X.Y.Z.0/24andpitheadrebases the whole stack onto it. Previously the hardcoded subnet/IPs
madedocker compose upfail outright withPool overlaps with other one on this address space.
The structured fixed-IP layout is preserved (services keep their.25–.31octets), so the
host-networked dashboard's bridge addressing and the #122 worker-SSRF CIDR guard still hold — only
the/24base moves. A singleNETWORK_PREFIXflows through every config path: compose
interpolation, monerod/Tor containerenvsubst/sedat start, and the Tari config render. And if
a host does collide,pithead up/applynow catch Docker's overlap error and print the exact
network.subnetfix (with an example) instead of a raw Docker stack trace. -
Dashboard now records every P2Pool share instead of at most one per 30 s poll: it tracks the
cumulativeshares_foundcounter and records the per-poll delta as N distinct shares. A
higher-hashrate or nano-sidechain node finding 2+ shares within one poll window no longer
undercounts the PPLNS window or skews the XvB controller's share gate (#129). -
Dashboard now surfaces broken persistence: a
db_healthyfield in/api/stateand a loud
"DB write failing" badge when the SQLite DB can't be initialized or written, instead of appearing
healthy while silently losing history/shares/stats on the next restart (#131). -
pithead upandpithead doctornow warn when a data directory named in.envdoesn't exist —
the tell-tale of a relocated/copied install or a second checkout, which would otherwise silently
start a fresh sync and orphan the dashboard history (data dirs are absolute paths in.env) (#126). -
pithead upgradenow re-renders the generated config (.env, Caddyfile, Tari config) before
rebuilding, so a release that changes a config template, restructures the Caddyfile, or adds an
.envvar takes effect —upgradewas previously justup --build, running new images against
the stale generated config from the last setup/apply (#128). -
pithead applyno longer silently strands the stack after a faileddocker compose up: it leaves
an "apply incomplete" marker so a re-run re-attempts the recreate (instead of no-opping on the
already-committed.env) and prints explicit recovery guidance on failure (#125). -
pithead backupno longer aborts whendu/dfexit non-zero on an unreadable file or a
transient FS error — the disk-space pre-check now degrades gracefully (its "proceeding without
a space check" fallback was previously unreachable underset -e) (#127). -
pithead doctornow exits non-zero when a critical check FAILS, so it can be used as a
cron/CI/monitoring health gate (it previously always exited 0); warnings alone still exit 0 (#127). -
Dashboard P2Pool pool-type detection (Main/Mini/Nano) now matches the peer's port exactly
instead of as a substring of the whole peer string, so a peer on an unrelated port that merely
contains the digits can't misclassify the sidechain (which drives block-time and the PPLNS
window the XvB controller uses) (#142). -
pithead reset-dashboardnow resolves the data directories it wipes from.env(the live
deployment) instead of re-readingconfig.json— editing a*.data_dirinconfig.json
before resetting (without anapply) can no longer wipe a directory the stack never used. It
also refuses to run rather than guess if.envdoesn't name them (#139). -
Dashboard pruned/full label (#32) always showed Full for a local node: the dashboard parsed
MONERO_PRUNEwith== "true", but pithead rendersconfig.json'smonero.pruneas1/0
(the form monerod's CLI wants), so a correctly pruned node read as Full. The label is purely
cosmetic (the node is pruned either way); the parser now accepts1/true/yes/on. Surfaced
on a live pruned deployment whose badge read "XMR Full". -
Dashboard pruned/full label (#32) always showed Full on local nodes: the dashboard parsed
MONERO_PRUNEwith== "true", but pithead writes it as1/0, so a pruned node read as
Full. Now accepts1/true/yes/on. Found by the live integration harness on a real box. -
Dashboard DB upgrade path: opening a database created by an early (pre-
timestamp) schema
threwno such column: timestampand aborted the migration, leaving the DB half-upgraded —
_create_tablesbuilt theidx_tsindex on a column_migrate_dbhadn't added yet. Indexes
are now created after migrations. Found by a new schema-migration intent test.
Security
- Hardened the dashboard against an SSRF primitive (#122). A connecting miner fully controls its
stratum worker name/IP, yet thenetwork_mode: hostdashboard fetched per-worker stats at a host
taken verbatim from that input — so a miner could steer outbound GETs at127.0.0.1, the
internal docker bridge (the socket proxies on172.28.0.30/.31), or a cloud-metadata IP. The
worker probe now targets only a validated real miner IP (rejecting loopback, link-local,
multicast, unspecified, reserved, and the172.28.0.0/16bridge), never uses the
miner-controlled name as a request host, and caps the name before echoing it back as a Bearer
token. - The xmrig-proxy HTTP control API now fails closed on a missing token (#153). The API is
writable (--http-no-restricted, required for XvB pool-switching) and reachable on the bridge +
host, so it must always be authenticated. Compose now interpolates--http-access-token(and
--http-port) with:?, so a hand-edited or pre-token stale.envwith an empty
PROXY_AUTH_TOKENmakes the stack refuse to start — instead of exposing an unauthenticated
control API. pithead still auto-generates the token onapply; atest_compose.shassertion
guards both the token-present and empty-token paths. - Closed clearnet DNS leaks in the node configs (#161, #162). monerod drops its priority-node
HOSTNAMES (resolved by the local resolver before--proxy), disables DNS checkpoints
(disable-dns-checkpoints, since removingenforce-dns-checkpointingalone doesn't stop the
checkpoints.moneropulse.*lookups) and the update check (check-updates=disabled), and adds
Tor-node anonymity hygiene (pad-transactions,hide-my-port). Tari disables DNS seeds
(dns_seeds = [], bootstrapping from onionpeer_seedsover Tor), prunes the clearnet
/ip4//ip6/peer seeds, corrects a comment that implied DNS-over-TLS (it was plaintext UDP/53),
and drops the inertcheck_for_updatesgRPC method. The last clearnet DNS path — the Tari Pulse
service's ~120 scheckpoints.tari.comTXT lookup (an advisory deep-reorg check with no in-binary
off-switch) — is closed by pointing the container's resolver at a dead local address (dns: 127.0.0.1); the lookup fails without a packet leaving the host and Tari tolerates it (returns
"passed", verified intari_pulse_service), so zero clearnet DNS and no functional impact. The
container already overrode Docker's127.0.0.11, so no service discovery is broken. Trade-off:
loses the Pulse deep-reorg advisory, the same class as monerod'sdisable-dns-checkpoints. - The dashboard's XvB stats fetch (
xmrvsbeast.com, with the wallet as a query param) now routes
over Tor (socks5h, so the hostname resolves via Tor too) instead of clearnet from the
host-networked container — closing a real-IP ↔ wallet correlation leak. It's also gated behind
XVB_ENABLED, so disabling XvB stops the egress entirely. DeadTARI_EXPLORER_URLremoved (#163). - The monerod RPC credentials are no longer interpolated into the compose healthcheck command
(they were readable viadocker inspect); the healthcheck now reads them from the container
environment via a script. - Documented that the stratum port defaults to all interfaces and should be firewalled to the
LAN — see Connecting Miners › Firewall. - All externally-pulled base/runtime images are now pinned by immutable
@sha256digest
(caddy, docker-socket-proxy, the Tari node, and theubuntu/python/alpinebuild bases),
so a re-pushed tag or a registry MITM can't silently change the running image (#135). dashboard.hostis now validated (hostname/IP characters only) before it's rendered into the
Caddyfile, so a value containing whitespace, a newline, or{/}can no longer break the
Caddyfile or inject reverse-proxy directives — mirroring thestratum_bindvalidation (#130).
Ingredients — Pithead v1.0.1
- Version: 1.0.1
- Commit:
a8b3e449b749f5f62c639abaab18708a1bbad4be - Built: 2026-06-14T05:16:46Z
Published images (ghcr.io/p2pool-starter-stack, tags v1.0.1 + latest)
ghcr.io/p2pool-starter-stack/pithead-tor:v1.0.1- digest:
ghcr.io/p2pool-starter-stack/pithead-tor@sha256:93f3d5be065da407740512b274c5f0e3f8fda9f8f20df0ead4a7343ac836676c
- digest:
ghcr.io/p2pool-starter-stack/pithead-monero:v1.0.1- digest:
ghcr.io/p2pool-starter-stack/pithead-monero@sha256:10844f6a9b3413d847fe05390160f048ab2f93fa2be11b8dd6d3c67fbf6b3036
- digest:
ghcr.io/p2pool-starter-stack/pithead-p2pool:v1.0.1- digest:
ghcr.io/p2pool-starter-stack/pithead-p2pool@sha256:9a2d59a1959b6d00445659eac95d9bf9c462774ba3ee77a7067847b411244c8c
- digest:
ghcr.io/p2pool-starter-stack/pithead-xmrig-proxy:v1.0.1- digest:
ghcr.io/p2pool-starter-stack/pithead-xmrig-proxy@sha256:10062dd65b681e92c9fec4c3cdfbc71e0fcc7dccf734a6dafb3b8d315466a492
- digest:
ghcr.io/p2pool-starter-stack/pithead-dashboard:v1.0.1- digest:
ghcr.io/p2pool-starter-stack/pithead-dashboard@sha256:651eb72d5adaab29216626dc0221416acd659518dcac02e3394827ad8a03a40c
- digest:
Upstream component pins
- p2pool:
v4.16 - monerod:
v0.18.5.0 - xmrig-proxy:
6.26.0 - tor base:
alpine:latest@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11 - tari:
quay.io/tarilabs/minotari_node:v5.3.1-mainnet@sha256:824fd6ec21d618805317d7eede374d6782906eeae17d2fc8aaad4df6205f94e0 - caddy:
caddy:2.11.4@sha256:cfeb0b281bc44a5a51fecde39e9e577c60d863c0b6196e6bbdf58fd00960887f - docker-socket-proxy:
tecnativa/docker-socket-proxy:v0.4.2@sha256:1f3a6f303320723d199d2316a3e82b2e2685d86c275d5e3deeaf182573b47476