Initial Release: Custom Bitcoin Solo Pool Stack
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
|||||||
|
FROM php:8.3-apache
|
||||||
|
|
||||||
|
# Kopiere das Dashboard in den Webroot
|
||||||
|
COPY html/ /var/www/html/
|
||||||
|
|
||||||
|
# Erstelle den Ordner für die History und setze Rechte
|
||||||
|
RUN mkdir -p /var/www/html/posts && \
|
||||||
|
chown -R www-data:www-data /var/www/html/posts && \
|
||||||
|
chmod -R 775 /var/www/html/posts
|
||||||
|
|
||||||
|
# Definiere das Volume für persistente History-Daten
|
||||||
|
VOLUME /var/www/html/posts
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# ⛏️ Bitcoin Solo-Pool & Custom Dashboard Stack
|
||||||
|
|
||||||
|
Ein autarkes, ressourcenschonendes Bitcoin Solo-Mining-Setup via Docker.
|
||||||
|
Beinhaltet eine eigene **Bitcoin Full Node (bitcoind)**, das **public-pool Backend (NestJS)** für Stratum-Verbindungen und ein **maßgeschneidertes, performantes PHP/JS-Dashboard** als Frontend[cite: 2].
|
||||||
|
|
||||||
|
## 🚀 Warum dieser Stack?
|
||||||
|
Das Standard-Frontend (`public-pool-ui`) belegt oft unnötig Arbeitsspeicher und verursacht bei externen Abfragen CORS-Probleme[cite: 2]. Dieses Setup ersetzt das Frontend durch ein schlankes PHP-Dashboard. Es fungiert als API-Proxy, loggt die Hashrate-Historie in eine lokale JSON und bietet 24/7 Graphen (Chart.js)[cite: 2].
|
||||||
|
|
||||||
|
## ⚙️ Installation
|
||||||
|
|
||||||
|
1. Repository klonen:
|
||||||
|
```bash
|
||||||
|
git clone [https://github.com/tessmania90/bitcoin-solo-pool.git](https://github.com/tessmania90/bitcoin-solo-pool.git)
|
||||||
|
cd bitcoin-solo-pool
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Stack starten:
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚠️ Wichtige Hinweise zum Sync
|
||||||
|
* **Initial Block Download (IBD):** Die Bitcoin-Node muss beim ersten Start synchronisieren (kann >24h dauern)[cite: 2].
|
||||||
|
* Solange der Sync nicht zu 100% fertig ist, verweigert die Node Block-Templates. Das `public-pool` Backend wirft in dieser Zeit Verbindungsfehler[cite: 2]. **Bitte warten, bis der Sync fertig ist!**
|
||||||
|
* **Speicherplatz:** Die Node startet mit `-prune=5000` und verbraucht so nur ca. 17 GB statt >650 GB[cite: 2].
|
||||||
|
|
||||||
|
## 📊 Dashboard & Graphen-Aufzeichnung
|
||||||
|
Das Dashboard ist unter `http://<DEINE-IP>:8080` erreichbar.
|
||||||
|
|
||||||
|
Damit die Hashrate-Historie lückenlos alle 5 Minuten aufgezeichnet wird, richte einen Cronjob auf dem Docker-Host ein (oder nutze Uptime Kuma)[cite: 2]:
|
||||||
|
```bash
|
||||||
|
*/5 * * * * curl -s "[http://127.0.0.1:8080/?api_action=pool](http://127.0.0.1:8080/?api_action=pool)" > /dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔌 Miner verbinden (z.B. Bitaxe, NerdMiner)
|
||||||
|
* **Stratum URL:** `stratum+tcp://<DEINE-IP>:21496`[cite: 2]
|
||||||
|
* **User/Worker:** `<DeineBitcoinAdresse>.<WorkerName>`[cite: 2]
|
||||||
|
* **Passwort:** `x`[cite: 2]
|
||||||
|
|
||||||
|
---
|
||||||
|
*Ein Projekt von [tessmann.dev](https://tessmann.dev)*
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
services:
|
||||||
|
# 1. BITCOIN FULL NODE (bitcoind)
|
||||||
|
bitcoind:
|
||||||
|
image: lncm/bitcoind:v27.0
|
||||||
|
container_name: bitcoind-node
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./bitcoin-data:/root/.bitcoin
|
||||||
|
ports:
|
||||||
|
- "8333:8333" # P2P Netzwerk
|
||||||
|
command:
|
||||||
|
- -server=1
|
||||||
|
- -txindex=0
|
||||||
|
- -prune=5000 # Beschränkt den Speicher auf ca. 17 GB
|
||||||
|
- -rpcbind=0.0.0.0
|
||||||
|
- -rpcallowip=0.0.0.0/0
|
||||||
|
- -rpcuser=pooluser
|
||||||
|
- -rpcpassword=StrengGeheimesPasswort123!
|
||||||
|
- -zmqpubrawblock=tcp://0.0.0.0:28332
|
||||||
|
networks:
|
||||||
|
- mining-net
|
||||||
|
|
||||||
|
# 2. PUBLIC-POOL STRATUM & API BACKEND
|
||||||
|
public-pool:
|
||||||
|
image: benedikteth/public-pool:latest
|
||||||
|
container_name: public-pool-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- bitcoind
|
||||||
|
environment:
|
||||||
|
- BITCOIN_RPC_URL=http://bitcoind:8332
|
||||||
|
- BITCOIN_RPC_USER=pooluser
|
||||||
|
- BITCOIN_RPC_PASSWORD=StrengGeheimesPasswort123!
|
||||||
|
- BITCOIN_RPC_TIMEOUT=10000
|
||||||
|
- BITCOIN_ZMQ_HOST=tcp://bitcoind:28332
|
||||||
|
- API_PORT=3334
|
||||||
|
- STRATUM_PORT=21496
|
||||||
|
- NETWORK=mainnet
|
||||||
|
ports:
|
||||||
|
- "21496:21496" # Stratum V1 Port für Miner
|
||||||
|
networks:
|
||||||
|
- mining-net
|
||||||
|
|
||||||
|
# 3. CUSTOM PHP-DASHBOARD (by Tessmann)
|
||||||
|
dashboard-web:
|
||||||
|
image: tessmann/bitcoin-solo-pool:latest
|
||||||
|
container_name: mining-dashboard
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- ./dashboard-data:/var/www/html/posts
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Berlin
|
||||||
|
networks:
|
||||||
|
- mining-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
mining-net:
|
||||||
|
driver: bridge
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
date_default_timezone_set('Europe/Berlin');
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// API PROXY & AUTOMATISCHER HISTORY-LOGGER
|
||||||
|
// =========================================================================
|
||||||
|
if (isset($_GET['api_action'])) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$api_base = "http://public-pool:3334/api/";
|
||||||
|
$action = $_GET['api_action'];
|
||||||
|
$endpoint = '';
|
||||||
|
|
||||||
|
if ($action === 'history') {
|
||||||
|
$file = __DIR__ . '/posts/hashrate_history.json';
|
||||||
|
echo file_exists($file) ? file_get_contents($file) : '[]';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($action, ['pool', 'network', 'info'])) {
|
||||||
|
$endpoint = $action;
|
||||||
|
} elseif (preg_match('/^client\/[a-zA-Z0-9]+$/', $action)) {
|
||||||
|
$endpoint = $action;
|
||||||
|
} else {
|
||||||
|
echo '{}';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ctx = stream_context_create(['http' => ['timeout' => 3]]);
|
||||||
|
$data = @file_get_contents($api_base . $endpoint, false, $ctx);
|
||||||
|
|
||||||
|
if ($action === 'pool' && $data) {
|
||||||
|
$json = json_decode($data, true);
|
||||||
|
$hr = $json['totalHashRate'] ?? 0;
|
||||||
|
$file = __DIR__ . '/posts/hashrate_history.json';
|
||||||
|
$history = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
|
||||||
|
|
||||||
|
$last_time = end($history)['timestamp'] ?? 0;
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
if ($now - $last_time >= 300) {
|
||||||
|
$history[] = [
|
||||||
|
'timestamp' => $now,
|
||||||
|
'label' => date('d.m. H:i'),
|
||||||
|
'hashrate' => $hr
|
||||||
|
];
|
||||||
|
if (count($history) > 432) array_shift($history);
|
||||||
|
file_put_contents($file, json_encode($history), LOCK_EX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $data ?: '{}';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Bitcoin Solo Pool Dashboard</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<style>
|
||||||
|
body { background: #141210; color: #f3ede7; font-family: sans-serif; padding: 20px; }
|
||||||
|
.card { background: #1c1815; border: 1px solid #382e27; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
|
||||||
|
input { background: #141210; border: 1px solid #382e27; color: #fff; padding: 10px; border-radius: 4px; }
|
||||||
|
button { background: #e05a2b; color: #fff; border: none; padding: 10px 18px; border-radius: 4px; cursor: pointer; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin-top: 15px; }
|
||||||
|
th, td { padding: 10px; border-bottom: 1px solid #382e27; text-align: left; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>⛏️ Bitcoin Solo Mining Dashboard</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>📊 Live Pool Hashrate (36 Stunden Verlauf)</h2>
|
||||||
|
<div style="height: 250px;"><canvas id="hashChart"></canvas></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>🔍 Eigene Worker prüfen</h2>
|
||||||
|
<input type="text" id="wallet-in" placeholder="Bitcoin-Wallet (bc1q...) eingeben" style="width: 320px;">
|
||||||
|
<button onclick="fetchWorkers()">Suchen</button>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Worker</th><th>Sessions</th><th>Hashrate</th><th>Best Difficulty</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="worker-rows">
|
||||||
|
<tr><td colspan="4" style="color: #a89f91;">Bitte Wallet-Adresse suchen.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const ctx = document.getElementById('hashChart').getContext('2d');
|
||||||
|
const chart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: { labels: [], datasets: [{ label: 'Hashrate (H/s)', data: [], borderColor: '#e05a2b', fill: true, backgroundColor: 'rgba(224,90,43,0.1)' }] },
|
||||||
|
options: { responsive: true, maintainAspectRatio: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadHistory() {
|
||||||
|
const res = await fetch('index.php?api_action=history').then(r => r.json());
|
||||||
|
if (Array.isArray(res) && res.length > 0) {
|
||||||
|
chart.data.labels = res.map(p => p.label);
|
||||||
|
chart.data.datasets[0].data = res.map(p => p.hashrate);
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWorkers() {
|
||||||
|
const wallet = document.getElementById('wallet-in').value.trim();
|
||||||
|
if (!wallet) return;
|
||||||
|
const res = await fetch('index.php?api_action=client/' + encodeURIComponent(wallet)).then(r => r.json());
|
||||||
|
const list = res.workers || [];
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
if (list.length === 0) {
|
||||||
|
html = '<tr><td colspan="4">Keine aktiven Worker gefunden.</td></tr>';
|
||||||
|
} else {
|
||||||
|
list.forEach(w => {
|
||||||
|
html += `<tr><td><b>${w.name}</b></td><td>${(w.sessions||[]).length}</td><td>${(w.hashrate||0).toLocaleString()} H/s</td><td>${w.bestDifficulty||0}</td></tr>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.getElementById('worker-rows').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadHistory();
|
||||||
|
setInterval(loadHistory, 300000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user