Docker & Crypto 22. August 2026 • 18 Min. Lesezeit

Eigener Bitcoin Solo Pool: Full Node (bitcoind), public-pool Backend & individuelles PHP-Dashboard im Docker-Stack

Wer Bitcoin-Solo-Mining mit Hardware wie dem NerdMiner, Bitaxe oder ausgemusterten GPU/ASIC-Rigs betreiben möchte, benötigt kein externes Mining-Netzwerk. Mit der Open-Source-Software public-pool und einer eigenen Bitcoin Full Node (bitcoind) lässt sich eine komplett unabhängige, non-custodial Solo-Mining-Infrastruktur aufbauen: Findet ein Miner einen gültigen Block, geht die Block-Belohnung (Coinbase Transaction) ohne Umwege und ohne Pool-Gebühren direkt an die eigene Wallet.

In der Praxis bringt das vorgefertigte Standard-Setup oft Nachteile mit sich: Das mitgelieferte Web-Frontend (public-pool-ui) belegt unnötig Arbeitsspeicher, lässt sich schwer in bestehende Server-Dashboards integrieren und führt bei externen API-Abfragen schnell zu CORS-Problemen. In diesem Leitfaden bauen wir den vollständigen Stack von Grund auf: von der Bitcoin-Node über das Mining-Backend bis hin zum maßgeschneiderten, performanten PHP-Dashboard.


1. Die Architektur im Überblick

Das Gesamtsystem besteht aus drei aufeinander abgestimmten Komponenten, die über ein internes Docker-Bridge-Netzwerk miteinander kommunizieren:

  • Bitcoin Core Full Node (bitcoind): Validiert die Blockchain, verwaltet den Mempool und liefert Blockvorlagen über RPC (Port 8332) sowie ZMQ-Block-Events (Port 28332).
  • public-pool Backend (NestJS): Verwaltet die Stratum-Verbindungen der Miner (Port 21496), konstruiert Coinbase-Transaktionen und stellt eine REST-API bereit (Port 3334).
  • Custom Apache/PHP Dashboard: Ersetzt das ressourcenhungrige Standard-Frontend (public-pool-ui). Es dient als gehärteter API-Proxy, speichert historische Hashrate-Daten und visualisiert Worker via Chart.js im Browser.

2. Speicherbedarf & Sync-Dauer (WICHTIG!)

Bevor wir den Stack starten, zwei elementare Praxishinweise zur Bitcoin-Node, die oft zu Verwirrung führen:

  • Speicherplatz (Pruning): Eine vollständige Blockchain (ohne Pruning) belegt gut und gerne 650 GB bis 700 GB Speicherplatz. Das ist für reines Solo-Mining nicht zwingend notwendig! Es hat sich bewährt, die Node mit dem Parameter -prune=5000 zu starten. Dadurch verwirft die Node alte Blockdaten und der Speicherbedarf des bitcoind-Containers schrumpft auf sehr verträgliche ca. 17 GB.
  • Initial Block Download (IBD) & Verbindungsfehler: Die Node muss beim ersten Start die gesamte Historie seit 2009 herunterladen. Dieser Sync dauert auf einem performanten Server mit reiner SSD-Ausstattung gut und gerne 24 Stunden oder länger.

    Achtung: Solange dieser Sync nicht zu 100 % durchgelaufen ist, verweigert bitcoind die Bereitstellung von Block-Templates. Das bedeutet: Das public-pool API-Backend bekommt keinen Connect zur Node und wirft in den Logs Fehler oder wartet in einer Endlosschleife. Erst wenn der Node-Sync restlos abgeschlossen ist, verbindet sich der public-pool erfolgreich und man sieht, dass das Setup funktioniert!

3. Die vollständige docker-compose.yml

Erstelle auf deinem Host-System ein zentrales Projektverzeichnis (z. B. /opt/mining-stack) mit folgender Verzeichnisstruktur:

/opt/mining-stack/
├── docker-compose.yml
├── bitcoin-data/
├── html/
│   └── index.php
└── data/

Lege im Hauptverzeichnis die docker-compose.yml an. Sie definiert die Bitcoin Node, das public-pool Backend und den Apache-Webserver in einem gemeinsamen Netzwerk:

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
      - "3334:3334"   # Interne REST-API
    networks:
      - mining-net

  # 3. EIGENES PHP-DASHBOARD (Ersetzt public-pool-ui)
  dashboard-web:
    image: php:8.3-apache
    container_name: mining-dashboard
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - ./html:/var/www/html
      - ./data:/var/www/html/posts
    environment:
      - TZ=Europe/Berlin
    networks:
      - mining-net

networks:
  mining-net:
    driver: bridge

4. Das Dashboard: index.php erstellen

Erstelle im Ordner html die Datei index.php. Wichtig ist hierbei die globale Zeitzonen-Kompensation (date_default_timezone_set('Europe/Berlin')), da Docker-Container standardmäßig in UTC laufen. Ohne diese Anpassung würden im Graphen andernfalls falsche (um 2 Stunden versetzte) Sommerzeit-Timestamps landen.

<?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>

5. Deployment & Berechtigungen

Setze vor dem ersten Start die Berechtigungen für das Datenverzeichnis, damit der Webserver-Prozess (www-data / UID 33) die Verlaufsdatei schreiben kann:

chmod -R 775 ./data && chown -R 33:33 ./data

Starte den kompletten Stack im Hintergrund:

docker compose up -d

6. 24/7 Graphen-Aufzeichnung sicherstellen

Da PHP zustandslos arbeitet, wird der 5-Minuten-History-Check standardmäßig nur ausgeführt, wenn jemand die Seite aufruft. Um einen lückenlosen Graphen zu garantieren, richten wir einen automatischen Trigger ein:

  1. Via Uptime Kuma / Monitoring: Lege einen HTTP(s)-Monitor auf die URL http://<DEINE-IP>:8080/index.php?api_action=pool mit einem Prüfintervall von 300 Sekunden an.
  2. Via Host-Cronjob (Alternative): Trage per crontab -e auf deinem Server folgende Zeile ein:
    */5 * * * * curl -s "http://127.0.0.1:8080/?api_action=pool" > /dev/null

7. Wichtige Praxistipps & Troubleshooting

  • Leeres Dashboard nach dem Start? Keine Panik! Selbst wenn die Full Node fertig synchronisiert ist, liefert die API des Pools (und damit auch unsere PHP-Seite) erst dann Daten und Hashrates, wenn sich der allererste Worker erfolgreich verbunden und Hash-Shares gesendet hat. Vorher bleiben der Graph und die Kacheln auf null.
  • Miner mit dem Pool verbinden: Trage in der Weboberfläche deines Miners (z. B. Bitaxe) folgende Stratum-Parameter ein:
    • Stratum URL: stratum+tcp://<DEINE-SERVER-IP>:21496
    • User / Worker-Name: <DeineBitcoinAdresse>.<WorkerName>
    • Passwort: x (oder leer lassen)
  • Warum weichen Hashrates ab? Das Backend berechnet die globale Pool-Hashrate über ein langes Zeitfenster (z. B. 15–30 Minuten), während Worker-Hashrates auf kurzfristigen Share-Intervallen (1–5 Minuten) basieren. Abweichungen sind statistisch bedingt völlig normal.
💡 Support & Consulting: Du benötigst Unterstützung bei der Einrichtung oder Absicherung deiner Linux- und Virtualisierungsinfrastruktur? 👉 tessmann-digital.de

☕ Support the Lab & Kaffeekasse

Dir haben meine How-Tos, Tools oder der Mining-Pool geholfen? Ich betreibe dieses Lab komplett werbe- und trackingfrei in meiner Freizeit. Wenn du meine Arbeit und die Serverkosten unterstützen möchtest, freue ich mich riesig über einen virtuellen Kaffee!

Support via PayPal