Early dashboard development

This commit is contained in:
2025-11-07 16:48:55 -05:00
parent 28d177d4ea
commit 0e74cd8551
2 changed files with 377 additions and 20 deletions

93
README.md Normal file
View File

@ -0,0 +1,93 @@
# Bitcoin Node for Solo Miners
*A Linux Appliance by Robbie Ferguson*
## Overview
**Bitcoin Node for Solo Miners** is a self-contained Debian-based appliance that lets you host your own Bitcoin node for your solo miners with no third-party pool (or fees) required.
This system installs and configures:
- **Bitcoin Core** (with pruning)
- **ckpool** (running in solo mining mode)
- A custom, secure **HTTPS web dashboard** for monitoring and configuration
This appliance is designed for hobbyists, home miners, and small operations who want to mine independently while maintaining full control of their rewards and privacy. Running your own independent mining node helps strengthen the decentralization of both the Bitcoin blockchain and the global mining network.
---
## Features
- 🧱 **Full or Pruned Node Support** — Select from 550 MB to full blockchain retention.
-**Solo Mining with ckpool** — Connect ASICs directly via Stratum (`stratum+tcp://your-node:3333`).
- 🔒 **Secure HTTPS Interface** — Self-signed TLS certificate generated on first boot.
- 🧭 **Web Dashboard** — View node sync progress, service health, and pool status.
- 💰 **Optional Donation (Pool Fee)** — At your own discretion, donate from 0 5% of a block reward to Robbie Ferguson.
- 🧩 **Automatic Service Management** — Fully integrated systemd services for `bitcoind` and `ckpool`.
---
## Installation
Download and run the installer script as root on a clean **Debian 13 (Trixie)** system:
```bash
chmod +x installer
sudo ./installer
```
When installation completes:
1. Open your browser to `https://<your-server-ip>/activate/`
2. Create your admin credentials (used for web access and RPC)
3. Optionally regenerate your HTTPS certificate
4. Youll be redirected to the dashboard once activation is complete
---
## Configuration
Access the settings panel anytime using the link on the dashboard.
From here you can:
- Adjust pruning size (550 MB Full)
- Change donation (pool fee) percentage
- Regenerate your certificate
- Restart core services as needed
---
## Connecting Miners
Point your ASIC miners to:
```
stratum+tcp://<your-server-ip>:3333
```
**Username:** your Bitcoin wallet address to receive payout (this can be different for each miner if desired)
**Password:** anything
Your miners can begin hashing once the Bitcoin node finishes syncing.
---
## Security Notes
- The web interface is HTTPS-only and disabled until activation is complete.
- RPC/admin credentials are generated at first activation
- Self-signed certificates are created automatically on first boot and can be regenerated later.
---
## System Requirements
| Component | Minimum | Recommended |
|------------|----------|-------------|
| OS | Debian 13 (Trixie) | Debian 13 (Trixie) |
| CPU | 2 cores | 4+ cores |
| RAM | 2 GB | 4 GB+ |
| Storage | 16 GB (pruned) | 500 GB+ (full node) |
| Network | Broadband | High-speed fiber |
---
## License
This project is released under the **Apache 2.0 License**.
Copyright © 2025 Robbie Ferguson
---
## Credits
- [Bitcoin Core](https://bitcoincore.org)
- [ckpool](https://bitbucket.org/ckolivas/ckpool)
- Linux appliance integration by Robbie Ferguson

304
installer
View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# #
# Bitcoin Solo Miner Appliance Installer # Bitcoin Node for Solo Miners Appliance Installer
# An Open Source Appliance from Robbie Ferguson # An Open Source Linux Appliance from Robbie Ferguson
# (c) 2025 Robbie Ferguson # (c) 2025 Robbie Ferguson
set -e set -e
@ -13,6 +13,9 @@ CERT_DIR="/etc/ssl/localcerts"
CKPOOL_USER="ckpool" CKPOOL_USER="ckpool"
CKPOOL_PORT="3333" CKPOOL_PORT="3333"
BITCOIN_DATA="/var/lib/bitcoind" BITCOIN_DATA="/var/lib/bitcoind"
BTC_RPC_USER="user"
BTC_RPC_PASS="pass"
PRUNE_SIZE="550"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# PURGE # PURGE
@ -99,17 +102,22 @@ EOF
mkdir -p /etc/bitcoin mkdir -p /etc/bitcoin
mkdir -p "${BITCOIN_DATA}" mkdir -p "${BITCOIN_DATA}"
# temporary admin creds; user will change on first boot # Bitcoin Core Configuration
cat > "$BITCOIN_CONF" <<EOF cat > "$BITCOIN_CONF" <<EOF
server=1 server=1
txindex=0 txindex=0
rpcuser=user
rpcpassword=pass
rpcallowip=127.0.0.1 rpcallowip=127.0.0.1
rpcbind=127.0.0.1 rpcbind=127.0.0.1
listen=1 listen=1
prune=550
daemon=0 daemon=0
prune=${PRUNE_SIZE}
rpcuser=${BTC_RPC_USER}
rpcpassword=${BTC_RPC_PASS}
rpcbind=127.0.0.1
maxconnections=250 # Allow up to 250 connections to speed up sync
maxuploadtarget=0 # Unlimited upload speed
dbcache=2048 # 2 GB RAM for sync
maxmempool=300 # Use less RAM post-sync
EOF EOF
systemctl daemon-reload systemctl daemon-reload
@ -155,6 +163,16 @@ if [[ ! -x "/opt/btc-solo/ckpool/src/ckpool" ]]; then
fi fi
id -u "$CKPOOL_USER" >/dev/null 2>&1 || useradd -r -s /bin/false "$CKPOOL_USER" id -u "$CKPOOL_USER" >/dev/null 2>&1 || useradd -r -s /bin/false "$CKPOOL_USER"
usermod -aG $CKPOOL_USER www-data
# try to restart the running PHP-FPM service so www-data picks up new group
php_fpm_service=""
# 1) ask systemd what php-fpm services exist
php_fpm_service=$(systemctl list-units --type=service --all \
| awk '/php.*fpm\.service/ {print $1; exit}')
if [ -n "$php_fpm_service" ]; then
systemctl restart "$php_fpm_service"
fi
mkdir -p "$APP_ROOT/conf" "$APP_ROOT/logs" mkdir -p "$APP_ROOT/conf" "$APP_ROOT/logs"
cat > "$APP_ROOT/conf/ckpool.conf" <<EOF cat > "$APP_ROOT/conf/ckpool.conf" <<EOF
@ -173,6 +191,13 @@ EOF
chown -R "$CKPOOL_USER":"$CKPOOL_USER" "$APP_ROOT" chown -R "$CKPOOL_USER":"$CKPOOL_USER" "$APP_ROOT"
# create the socket folder for CKPool so we can later parse the JSON output
mkdir -p /var/run/ckpool
chown ${CKPOOL_USER}:${CKPOOL_USER} /var/run/ckpool
# make ckpmsg available to PHP
ln -sf /opt/btc-solo/ckpool/src/ckpmsg /usr/local/bin/ckpmsg
cat > /etc/systemd/system/ckpool.service <<EOF cat > /etc/systemd/system/ckpool.service <<EOF
[Unit] [Unit]
Description=ckpool solo mining server Description=ckpool solo mining server
@ -182,7 +207,7 @@ After=network.target bitcoind.service
User=${CKPOOL_USER} User=${CKPOOL_USER}
Group=${CKPOOL_USER} Group=${CKPOOL_USER}
WorkingDirectory=${APP_ROOT}/ckpool WorkingDirectory=${APP_ROOT}/ckpool
ExecStart=${APP_ROOT}/ckpool/src/ckpool -c ${APP_ROOT}/conf/ckpool.conf -B ExecStart=${APP_ROOT}/ckpool/src/ckpool -c ${APP_ROOT}/conf/ckpool.conf -B -s /var/run/ckpool
Restart=on-failure Restart=on-failure
[Install] [Install]
@ -321,7 +346,7 @@ chown www-data:www-data /var/lib/btc-solo
mkdir -p "$WWW_ROOT" mkdir -p "$WWW_ROOT"
mkdir -p "$WWW_ROOT/activate" mkdir -p "$WWW_ROOT/activate"
# main dashboard # Main dashboard
cat > "$WWW_ROOT/index.php" <<'EOF' cat > "$WWW_ROOT/index.php" <<'EOF'
<?php <?php
$flag = '/var/lib/btc-solo/.setup-complete'; $flag = '/var/lib/btc-solo/.setup-complete';
@ -329,6 +354,177 @@ if (!file_exists($flag)) {
header('Location: /activate/'); header('Location: /activate/');
exit; exit;
} }
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_ADDR'] ?? gethostname();
$host = preg_replace('/:\d+$/', '', $host);
$stratum = 'stratum+tcp://' . $host . ':3333';
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Bitcoin Node for Solo Miners</title>
<style>
body { font-family: sans-serif; background: #111; color: #eee; margin: 0; }
header { background: #222; padding: 1rem 2rem; display:flex; justify-content:space-between; align-items:center; }
h1 { margin: 0; }
a { color: #7ad; text-decoration: none; }
.card { background: #1b1b1b; margin: 1rem 2rem; padding: 1rem 1.5rem; border-radius: 12px; }
code { background: #333; padding: 0.25rem 0.5rem; border-radius: 6px; }
.status-ok { color: #8f8; }
.status-warn { color: #ffb347; }
.status-err { color: #ff6b6b; }
.progress-wrap { background:#000; border-radius:6px; height:18px; overflow:hidden; }
.progress-bar { background:#4caf50; height:100%; width:0%; transition:width .4s ease; }
ul.status-list { list-style:none; padding-left:0; }
ul.status-list li { margin-bottom:.25rem; }
</style>
</head>
<body>
<header>
<div>
<h1>Bitcoin Node for Solo Miners</h1>
<p style="margin:0;">A Linux Appliance by Robbie Ferguson</p>
</div>
<div>
<a href="/settings.php">Settings</a>
</div>
</header>
<div class="card" id="node-status">
<h2>Status</h2>
<p>Loading status…</p>
</div>
<div class="card" id="workers">
<h2>Workers</h2>
<p>No workers detected yet. This is normal while Bitcoin Core is syncing or if no ASICs are pointed at this server.</p>
</div>
<script>
// per-tab tracker with sliding window
const syncTracker = { samples: [] };
const WINDOW_SECONDS = 900; // 15 minutes
function formatEta(seconds) {
if (seconds == null || !isFinite(seconds)) return 'estimating…';
if (seconds < 60) return seconds.toFixed(0) + 's';
const mins = seconds / 60;
if (mins < 60) return mins.toFixed(1) + 'm';
const hours = mins / 60;
return hours.toFixed(1) + 'h';
}
function updateSamples(progress) {
const now = Date.now() / 1000;
syncTracker.samples.push({ t: now, p: progress });
const cutoff = now - WINDOW_SECONDS;
syncTracker.samples = syncTracker.samples.filter(s => s.t >= cutoff);
}
function estimateEta(progress) {
const samples = syncTracker.samples;
if (samples.length < 2) return null;
const first = samples[0];
const last = samples[samples.length - 1];
const dP = last.p - first.p;
const dT = last.t - first.t;
if (dP <= 0 || dT <= 5) return null;
const rate = dP / dT; // % per second
if (rate <= 0) return null;
const remaining = Math.max(0, 100 - progress);
return remaining / rate;
}
function renderServices(svcs) {
if (!svcs) return '';
const map = {
bitcoind: 'Bitcoin Core',
ckpool: 'CKPool'
};
let out = '<h3>Service Health</h3><ul class="status-list">';
for (const key of Object.keys(map)) {
if (!(key in svcs)) continue;
const state = svcs[key];
const ok = state === 'active';
out += '<li>' + (ok ? '✅' : '❌') + ' ' + map[key] + ' — ' + state + '</li>';
}
out += '</ul>';
return out;
}
async function loadStatus() {
try {
const res = await fetch('/status.php', {cache: 'no-store'});
const data = await res.json();
const box = document.getElementById('node-status');
if (data.error) {
box.innerHTML = '<h2>Status</h2><p class="status-err">' + data.error + '</p>' + renderServices(data.services);
return;
}
const ibd = !!data.initialblockdownload;
const blocks = data.blocks ?? '';
const pruned = data.pruned ? '(pruned)' : '';
let progress = 0;
if (typeof data.verificationprogress === 'number') {
progress = data.verificationprogress * 100;
} else if (data.blocks && data.headers && data.headers > 0) {
progress = (data.blocks / data.headers) * 100;
}
// update window + estimate
updateSamples(progress);
const etaSeconds = ibd ? estimateEta(progress) : null;
let html = '<h2>Status</h2>';
if (ibd) {
html += '<p class="status-warn">⚠ Bitcoin Core is syncing. Your miners will not be able to submit shares or receive work until the node is fully synced.</p>';
} else {
html += '<p class="status-ok">✔ Bitcoin Core is in sync.</p>';
}
html += '<p>Blocks: ' + blocks + ' ' + pruned + '</p>';
html += '<div class="progress-wrap"><div class="progress-bar" id="syncbar"></div></div>';
html += '<p><small>Sync progress: ' + progress.toFixed(2) + '%';
if (ibd) {
html += ' • Estimated time left: ' + formatEta(etaSeconds);
}
html += '</small></p>';
// append service health
html += renderServices(data.services);
box.innerHTML = html;
const bar = document.getElementById('syncbar');
if (bar) {
bar.style.width = Math.min(progress, 100).toFixed(2) + '%';
}
} catch (e) {
const box = document.getElementById('node-status');
box.innerHTML = '<h2>Status</h2><p class="status-err">Failed to fetch status.</p>';
}
}
loadStatus();
setInterval(loadStatus, 5000);
</script>
</body>
</html>
EOF
# Settings page
cat > "$WWW_ROOT/settings.php" <<'EOF'
<?php
$flag = '/var/lib/btc-solo/.setup-complete';
if (!file_exists($flag)) {
header('Location: /activate/');
exit;
}
$opts = [ $opts = [
'550' => '550 MB (default, small)', '550' => '550 MB (default, small)',
@ -370,7 +566,7 @@ $stratum = 'stratum+tcp://' . $host . ':3333';
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Local Bitcoin Solo Mining</title> <title>Bitcoin Node for Solo Miners [Settings]</title>
<style> <style>
body { font-family: sans-serif; background: #111; color: #eee; margin: 0; } body { font-family: sans-serif; background: #111; color: #eee; margin: 0; }
header { background: #222; padding: 1rem 2rem; } header { background: #222; padding: 1rem 2rem; }
@ -384,17 +580,17 @@ $stratum = 'stratum+tcp://' . $host . ':3333';
</head> </head>
<body> <body>
<header> <header>
<h1>Local Bitcoin Solo Mining</h1> <h1>Bitcoin Node for Solo Miners</h1>
<p>Point your ASICs to this server and keep 100% of your block.</p> <p style="margin:0;">A Linux Appliance by Robbie Ferguson</p>
</header> </header>
<div class="card"> <div class="card">
<h2>Stratum Endpoint</h2> <h2>Stratum Endpoint</h2>
<p><code><?php echo htmlspecialchars($stratum); ?></code></p> <p><code><?php echo htmlspecialchars($stratum); ?></code></p>
<p>Username: your BTC address</p> <p>Username: BTC address for payout</p>
<p>Password: anything</p> <p>Password: anything</p>
</div> </div>
<div class="card"> <div class="card">
<h2>Prune Size</h2> <h2>Blockchain Prune Size</h2>
<form method="post"> <form method="post">
<label for="prune">Current setting:</label> <label for="prune">Current setting:</label>
<select name="prune" id="prune"> <select name="prune" id="prune">
@ -410,11 +606,11 @@ $stratum = 'stratum+tcp://' . $host . ':3333';
</form> </form>
</div> </div>
<div class="card"> <div class="card">
<h2>Operator / Donation</h2> <h2>Donate to Bald Nerd</h2>
<p><small>Default is 2% to the project address. Set to 0 to keep the full block.</small></p> <p><small>If you happen to win a BTC block, you can donate a small percentage to Robbie Ferguson (developer of this appliance, host of Category5 Technology TV) with big thanks. You can set to 0 if you prefer to keep the full block to yourself.</small></p>
<?php <?php
$conf = @file_get_contents('/opt/btc-solo/conf/ckpool.conf'); $conf = @file_get_contents('/opt/btc-solo/conf/ckpool.conf');
$currAddr = 'YOUR_HARDCODED_DONATION_ADDRESS'; $currAddr = '1MoGAsK8bRFKjHrpnpFJyqxdqamSPH19dP';
$currRate = 2; $currRate = 2;
if ($conf) { if ($conf) {
if (preg_match('/"donaddress"\s*:\s*"([^"]+)"/', $conf, $m)) $currAddr = $m[1]; if (preg_match('/"donaddress"\s*:\s*"([^"]+)"/', $conf, $m)) $currAddr = $m[1];
@ -445,14 +641,58 @@ $stratum = 'stratum+tcp://' . $host . ':3333';
<button type="submit">Save</button> <button type="submit">Save</button>
</form> </form>
</div> </div>
<div class="card">
<h2>System</h2>
<p>Check <code>systemctl status bitcoind</code> and <code>systemctl status ckpool</code>.</p>
</div>
</body> </body>
</html> </html>
EOF EOF
# AJAX status info
cat > "$WWW_ROOT/status.php" <<'EOF'
<?php
// no session needed; this is polled often
$flag = '/var/lib/btc-solo/.setup-complete';
if (!file_exists($flag)) {
http_response_code(403);
header('Content-Type: application/json');
echo json_encode(['error' => 'not activated']);
exit;
}
// Bitcoin Core status
$cmd = '/usr/local/bin/bitcoin-cli -conf=/etc/bitcoin/bitcoin.conf getblockchaininfo 2>/dev/null';
$out = shell_exec($cmd);
$info = $out ? json_decode($out, true) : null;
// service health (simple is-active check)
function svc($name) {
$res = trim(shell_exec('systemctl is-active ' . escapeshellarg($name) . ' 2>/dev/null'));
return $res === 'active' ? 'active' : $res;
}
$services = [
'bitcoind' => svc('bitcoind'),
'ckpool' => svc('ckpool')
];
header('Content-Type: application/json');
if (!$info) {
echo json_encode([
'error' => 'bitcoin-cli failed',
'services' => $services
]);
exit;
}
echo json_encode([
'initialblockdownload' => $info['initialblockdownload'] ?? null,
'blocks' => $info['blocks'] ?? null,
'headers' => $info['headers'] ?? null,
'verificationprogress' => $info['verificationprogress'] ?? null,
'pruned' => $info['pruned'] ?? null,
'services' => $services
]);
EOF
# activation page # activation page
cat > "$WWW_ROOT/activate/index.php" <<'EOF' cat > "$WWW_ROOT/activate/index.php" <<'EOF'
<?php <?php
@ -574,6 +814,30 @@ rm -f /etc/nginx/sites-enabled/default || true
systemctl restart nginx systemctl restart nginx
echo "Configuring Log Rotation"
cat > /etc/logrotate.d/btc-solo <<'EOF'
/opt/btc-solo/logs/*.log {
weekly
rotate 8
compress
missingok
notifempty
copytruncate
}
EOF
cat > /etc/logrotate.d/nginx <<'EOF'
/var/log/nginx/*.log {
weekly
rotate 8
compress
missingok
notifempty
copytruncate
}
EOF
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
echo "[8/8] Done" echo "[8/8] Done"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------