# What "hand-building the node around Jeremy's binary" means

## The short version

Jeremy publishes a BlockDAG node release (right now: rc65.6) as a set of pre-built Docker images — the actual node software, a "nodeworker" health/mining supervisor, and a watchdog process. That compiled code is his, untouched, and we never modify it. What we built by hand is everything *around* it: the configuration files, port assignments, and container wiring that tell his software how to run on this specific machine.

Normally you don't have to think about that scaffolding — his `install.sh` installer generates it for you. We couldn't use it.

## Why the installer was off the table

This machine already runs a live production node (node1). Jeremy's installer scans the whole Docker host for any container already running his software, and if it finds one, it refuses to set up a second node with different settings — it assumes you're trying to upgrade the one it found, not run two side by side. There's no flag to turn this off; it's baked into how the installer decides what to do.

So installing rc65.6 as a genuinely separate second node (to test it before ever touching the live one) meant not using the installer at all.

## What we actually did instead

We took the same released images and:

- Wrote our own `.env` and `node.conf` by hand — the files that tell the node its data directory, RPC ports, credentials, and peer list — instead of letting `install.sh` generate them.
- Wrote our own Docker Compose files to define the containers, give them names that don't collide with node1's (`node2`, `watchdog2`), and assign them ports that don't collide with node1 either.
- Manually reassigned every port node1 was already using (RPC, EVM HTTP, EVM WebSocket) so the two nodes could run side by side on one machine without fighting over sockets.

None of this changes Jeremy's code. It's the equivalent of manually wiring up a shipped appliance instead of using its auto-setup — same appliance, hand-built wiring.

## The bug this surfaced

Doing it by hand meant we also had to catch every place a stale or wrong port could sneak in — something the installer would normally get right automatically. We found and fixed several of these, and the trickiest one: the "nodeworker" supervisor's RPC target ports turned out to be hardcoded directly into the image itself (not read from any config file), still pointing at the *old* release's default ports. Since we can't edit Jeremy's image, we fixed it by having Docker append corrected startup flags on top of the container's built-in ones — the node's own flag parser takes the last value it sees, so our correction wins without touching his binary at all.

## Turning it into a pool-mining node

The node itself was only half the job. To actually let a miner point at node2, we also had to stand up its own pool and Postgres containers — and those come from the same "no installer" situation: `install.sh` would normally wire all of this up automatically, alongside the node, in one pass. Hand-building it meant repeating the same kind of careful, one-setting-at-a-time work, just one layer higher in the stack.

A few pitfalls turned up here that are worth knowing about if you're doing this yourself:

- **The watchdog/pool/status containers couldn't see the chain data.** The installer normally points every companion container at the right internal mount path for the node's data directory. Hand-built, ours were still pointing at the literal host-style path from `.env`, which isn't where the data is actually mounted inside those containers. Fixed with a container-scoped environment override pointing each one at the real internal mount path.
- **A hardcoded service name silently zeroed out the peer count.** The pool stack has a setting listing which node service(s) it should watch, and it defaults to the literal name `node`. Since we'd named our second node's container `node2` (to avoid colliding with the real one), everything relying on that default quietly treated the node as not running at all — no error, just an empty result. Fixing the setting to match our actual container name fixed it instantly.
- **Docker socket permissions were never set up.** The installer normally detects the host's `docker` group ID and writes it into `.env` automatically, so the watchdog/status containers can talk to the Docker daemon. Skipping the installer meant this was left at a default that doesn't have permission, and those containers got silent "permission denied" errors that look, from the outside, just like "container not found."
- **A few companion images were still pointing at placeholder tags.** The shipped defaults reference generic tags like `:local` that were never actually loaded on this machine — easy to miss until you try to bring the container up and it can't find the image.
- **A shared Redis volume very nearly got shared between the two nodes' dashboards.** Caught before anything actually wrote to it, and isolated with its own volume name.
- **The pool had its own separate stale-port setting, distinct from the one we'd already fixed for the node.** Even after every other RPC-port reference was corrected, the pool kept failing its own authentication against the node — turned out there's a second, singular version of that setting (used only by the pool's internal router) that also defaults to the old port and has to be set explicitly.

None of these are exotic — they're exactly the kind of small, easy-to-miss wiring the installer normally handles for you without you ever knowing it happened. Hand-building just means each one has to be found and fixed by hand instead.

## Opening the pool up for outside miners

Once node2's pool was healthy and mining locally, the last piece was making it reachable the same way the real production pool is: a plain DNS record pointing at the WAN IP, a router port-forward, and a firewall rule that rate-limits repeat connection attempts from outside the LAN while leaving local traffic untouched. That mirrors exactly how the existing production stratum port works — no special tunnel software required on the miner's end, since it's just a normal internet-routable TCP connection, the same as pointing an ASIC at any public pool.

## If your pool's payouts seem stuck

We also ran into — and fixed — a case where a pool kept mining and crediting blocks normally, but had quietly stopped sending any actual payout transactions, with nothing in the logs to say so. That one's common enough, and general enough to any pool setup (not just a hand-built one), that it gets its own write-up: see `diagnosing-stalled-pool-payouts.md` alongside this document. Short version: check the pool's own database directly rather than trusting the logs, and if payouts really have stalled, restart the pool through the watchdog's activation command rather than a plain container restart.

## Bottom line

Same node software Jeremy built and released, running unmodified. Everything around it — how it's configured, named, and networked to coexist with the existing node, plus its own pool and payout stack — we assembled ourselves because the automated path wasn't built to support two nodes on one machine.
