Early dashboard development
This commit is contained in:
93
README.md
Normal file
93
README.md
Normal 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. You’ll 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
304
installer
@ -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"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user