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
#
# Bitcoin Solo Miner Appliance Installer
# An Open Source Appliance from Robbie Ferguson
# Bitcoin Node for Solo Miners Appliance Installer
# An Open Source Linux Appliance from Robbie Ferguson
# (c) 2025 Robbie Ferguson
set -e
@ -13,6 +13,9 @@ CERT_DIR="/etc/ssl/localcerts"
CKPOOL_USER="ckpool"
CKPOOL_PORT="3333"
BITCOIN_DATA="/var/lib/bitcoind"
BTC_RPC_USER="user"
BTC_RPC_PASS="pass"
PRUNE_SIZE="550"
# ---------------------------------------------------------------------------
# PURGE
@ -99,17 +102,22 @@ EOF
mkdir -p /etc/bitcoin
mkdir -p "${BITCOIN_DATA}"
# temporary admin creds; user will change on first boot
# Bitcoin Core Configuration
cat > "$BITCOIN_CONF" <<EOF
server=1
txindex=0
rpcuser=user
rpcpassword=pass
rpcallowip=127.0.0.1
rpcbind=127.0.0.1
listen=1
prune=550
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
systemctl daemon-reload
@ -155,6 +163,16 @@ if [[ ! -x "/opt/btc-solo/ckpool/src/ckpool" ]]; then
fi
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"
cat > "$APP_ROOT/conf/ckpool.conf" <<EOF
@ -173,6 +191,13 @@ EOF
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
[Unit]
Description=ckpool solo mining server
@ -182,7 +207,7 @@ After=network.target bitcoind.service
User=${CKPOOL_USER}
Group=${CKPOOL_USER}
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
[Install]
@ -321,7 +346,7 @@ chown www-data:www-data /var/lib/btc-solo
mkdir -p "$WWW_ROOT"
mkdir -p "$WWW_ROOT/activate"
# main dashboard
# Main dashboard
cat > "$WWW_ROOT/index.php" <<'EOF'
<?php
$flag = '/var/lib/btc-solo/.setup-complete';
@ -329,6 +354,177 @@ if (!file_exists($flag)) {
header('Location: /activate/');
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 = [
'550' => '550 MB (default, small)',
@ -370,7 +566,7 @@ $stratum = 'stratum+tcp://' . $host . ':3333';
<html>
<head>
<meta charset="utf-8">
<title>Local Bitcoin Solo Mining</title>
<title>Bitcoin Node for Solo Miners [Settings]</title>
<style>
body { font-family: sans-serif; background: #111; color: #eee; margin: 0; }
header { background: #222; padding: 1rem 2rem; }
@ -384,17 +580,17 @@ $stratum = 'stratum+tcp://' . $host . ':3333';
</head>
<body>
<header>
<h1>Local Bitcoin Solo Mining</h1>
<p>Point your ASICs to this server and keep 100% of your block.</p>
<h1>Bitcoin Node for Solo Miners</h1>
<p style="margin:0;">A Linux Appliance by Robbie Ferguson</p>
</header>
<div class="card">
<h2>Stratum Endpoint</h2>
<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>
</div>
<div class="card">
<h2>Prune Size</h2>
<h2>Blockchain Prune Size</h2>
<form method="post">
<label for="prune">Current setting:</label>
<select name="prune" id="prune">
@ -410,11 +606,11 @@ $stratum = 'stratum+tcp://' . $host . ':3333';
</form>
</div>
<div class="card">
<h2>Operator / Donation</h2>
<p><small>Default is 2% to the project address. Set to 0 to keep the full block.</small></p>
<h2>Donate to Bald Nerd</h2>
<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
$conf = @file_get_contents('/opt/btc-solo/conf/ckpool.conf');
$currAddr = 'YOUR_HARDCODED_DONATION_ADDRESS';
$currAddr = '1MoGAsK8bRFKjHrpnpFJyqxdqamSPH19dP';
$currRate = 2;
if ($conf) {
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>
</form>
</div>
<div class="card">
<h2>System</h2>
<p>Check <code>systemctl status bitcoind</code> and <code>systemctl status ckpool</code>.</p>
</div>
</body>
</html>
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
cat > "$WWW_ROOT/activate/index.php" <<'EOF'
<?php
@ -574,6 +814,30 @@ rm -f /etc/nginx/sites-enabled/default || true
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"
# ---------------------------------------------------------------------------