#!/bin/bash # # Bitcoin Node for Solo Miners Appliance Installer # An Open Source Linux Appliance from Robbie Ferguson # (c) 2025 Robbie Ferguson set -e APP_ROOT="/opt/btc-solo" WWW_ROOT="/var/www/btc-solo" BITCOIN_CONF="/etc/bitcoin/bitcoin.conf" 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 # --------------------------------------------------------------------------- if [[ "$1" == "--purge" ]]; then systemctl stop ckpool 2>/dev/null || true systemctl stop bitcoind 2>/dev/null || true rm -f /var/lib/btc-solo/.setup-complete rm -f /etc/systemd/system/ckpool.service rm -f /etc/systemd/system/bitcoind.service rm -f /etc/nginx/sites-enabled/btc-solo rm -f /etc/nginx/sites-available/btc-solo rm -f /etc/sudoers.d/btc-solo rm -rf "$APP_ROOT" "$WWW_ROOT" "$CERT_DIR" "$BITCOIN_DATA" rm -f /usr/local/sbin/btc-apply-admin.sh /usr/local/sbin/btc-set-prune.sh /usr/local/sbin/btc-make-cert.sh # Remove bitcoin binaries we installed directly rm -f /usr/local/bin/bitcoind /usr/local/bin/bitcoin-cli /usr/local/bin/bitcoin-tx /usr/local/bin/bitcoin-util 2>/dev/null || true systemctl daemon-reload systemctl restart nginx 2>/dev/null || true echo "Bitcoin Solo Miner Appliance purged." exit 0 fi if [[ $EUID -ne 0 ]]; then echo "Run as root." exit 1 fi echo "[1/8] Updating apt…" apt-get update echo "[2/8] Installing base packages…" apt-get install -y \ curl ca-certificates gnupg \ nginx php-fpm php-cli \ git build-essential autoconf automake libtool pkg-config \ libjansson-dev libevent-dev libcurl4-openssl-dev libssl-dev zlib1g-dev \ openssl # --------------------------------------------------------------------------- # [3/8] Install Bitcoin Core from official binaries # --------------------------------------------------------------------------- echo "[3/8] Installing official Bitcoin Core binaries…" BTC_VER="27.0" BTC_BASE="bitcoin-${BTC_VER}" BTC_TAR="${BTC_BASE}-x86_64-linux-gnu.tar.gz" BTC_URL="https://bitcoincore.org/bin/bitcoin-core-${BTC_VER}/${BTC_TAR}" BTC_SUMS_URL="https://bitcoincore.org/bin/bitcoin-core-${BTC_VER}/SHA256SUMS" mkdir -p /tmp/bitcoin cd /tmp/bitcoin echo "Downloading Bitcoin Core ${BTC_VER}…" curl -LO "${BTC_URL}" curl -LO "${BTC_SUMS_URL}" echo "Verifying checksum…" grep "${BTC_TAR}" SHA256SUMS | sha256sum -c - echo "Extracting…" tar -xzf "${BTC_TAR}" install -m 0755 -o root -g root ${BTC_BASE}/bin/* /usr/local/bin/ # systemd service for bitcoind cat > /etc/systemd/system/bitcoind.service < "$BITCOIN_CONF" </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" < /etc/systemd/system/ckpool.service < /usr/local/sbin/btc-apply-admin.sh <<'EOF' #!/bin/bash CONF="/etc/bitcoin/bitcoin.conf" CKPOOL_CONF="/opt/btc-solo/conf/ckpool.conf" ADMINUSER="$1" ADMINPASS="$2" if [[ -z "$ADMINUSER" || -z "$ADMINPASS" ]]; then echo "Usage: $0 " exit 1 fi # Update bitcoin.conf sed -i '/^rpcuser=/d' "$CONF" sed -i '/^rpcpassword=/d' "$CONF" echo "rpcuser=${ADMINUSER}" >> "$CONF" echo "rpcpassword=${ADMINPASS}" >> "$CONF" # Update ckpool config so it can still talk to bitcoind if [[ -f "$CKPOOL_CONF" ]]; then sed -i "s/\"user\"[[:space:]]*:[[:space:]]*\"[^\"]*\"/\"user\" : \"${ADMINUSER}\"/" "$CKPOOL_CONF" sed -i "s/\"pass\"[[:space:]]*:[[:space:]]*\"[^\"]*\"/\"pass\" : \"${ADMINPASS}\"/" "$CKPOOL_CONF" fi systemctl restart bitcoind systemctl restart ckpool EOF chmod 700 /usr/local/sbin/btc-apply-admin.sh # set prune size cat > /usr/local/sbin/btc-set-prune.sh <<'EOF' #!/bin/bash CONF="/etc/bitcoin/bitcoin.conf" SIZE="$1" if [[ -z "$SIZE" ]]; then echo "Usage: $0 " exit 1 fi sed -i '/^prune=/d' "$CONF" if [[ "$SIZE" != "full" ]]; then echo "prune=${SIZE}" >> "$CONF" fi systemctl restart bitcoind EOF chmod 700 /usr/local/sbin/btc-set-prune.sh # setup donation system cat > /usr/local/sbin/btc-set-donation.sh <<'EOF' #!/bin/bash CONF="/opt/btc-solo/conf/ckpool.conf" ADDR="$1" RATE="$2" if [[ -z "$ADDR" || -z "$RATE" ]]; then echo "Usage: $0 " exit 1 fi # ensure conf exists if [[ ! -f "$CONF" ]]; then echo "ckpool.conf not found at $CONF" exit 1 fi # remove old lines sed -i '/"donaddress"/d' "$CONF" sed -i '/"donrate"/d' "$CONF" # add new ones just before the final } # (very simple appender; good enough for our generated file) sed -i '$i \ ,"donaddress" : "'"$ADDR"'",\n "donrate" : '"$RATE"'' "$CONF" systemctl restart ckpool EOF chmod 700 /usr/local/sbin/btc-set-donation.sh # (re)create self-signed cert cat > /usr/local/sbin/btc-make-cert.sh <<'EOF' #!/bin/bash CERTDIR="/etc/ssl/localcerts" mkdir -p "$CERTDIR" openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ -keyout ${CERTDIR}/btc-solo.key \ -out ${CERTDIR}/btc-solo.crt \ -subj "/C=CA/ST=Ontario/L=Barrie/O=BTC-Solo/OU=IT/CN=$(hostname -f)" chmod 600 ${CERTDIR}/btc-solo.key systemctl restart nginx EOF chmod 700 /usr/local/sbin/btc-make-cert.sh # ensure sudoers.d exists mkdir -p /etc/sudoers.d chmod 750 /etc/sudoers.d # sudoers cat > /etc/sudoers.d/btc-solo <<'EOF' www-data ALL=(root) NOPASSWD: /usr/local/sbin/btc-apply-admin.sh www-data ALL=(root) NOPASSWD: /usr/local/sbin/btc-set-prune.sh www-data ALL=(root) NOPASSWD: /usr/local/sbin/btc-make-cert.sh www-data ALL=(root) NOPASSWD: /usr/local/sbin/btc-set-donation.sh EOF chmod 440 /etc/sudoers.d/btc-solo # --------------------------------------------------------------------------- echo "[6/8] Web UI (dashboard + activate)" # --------------------------------------------------------------------------- # web-owned state dir mkdir -p /var/lib/btc-solo chown www-data:www-data /var/lib/btc-solo # web dashboard mkdir -p "$WWW_ROOT" mkdir -p "$WWW_ROOT/activate" # Main dashboard cat > "$WWW_ROOT/index.php" <<'EOF' Bitcoin Node for Solo Miners

Bitcoin Node for Solo Miners

A Linux Appliance by Robbie Ferguson

Status

Loading status…

Workers

No workers detected yet. This is normal while Bitcoin Core is syncing or if no ASICs are pointed at this server.

EOF # Settings page cat > "$WWW_ROOT/settings.php" <<'EOF' '550 MB (default, small)', (2*1024) => '2 GB', (4*1024) => '4 GB', (8*1024) => '8 GB', (16*1024) => '16 GB', (32*1024) => '32 GB', (64*1024) => '64 GB', (100*1024) => '100 GB', (200*1024) => '200 GB', (300*1024) => '300 GB', (400*1024) => '400 GB', 'full' => 'Full (no pruning)', ]; $current = '550'; $conf = @file_get_contents('/etc/bitcoin/bitcoin.conf'); if ($conf && preg_match('/^prune=(\d+)/m', $conf, $m)) { $current = $m[1]; } elseif ($conf && !preg_match('/^prune=/m', $conf)) { $current = 'full'; } if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['prune'])) { $sel = $_POST['prune']; if (isset($opts[$sel])) { shell_exec('sudo /usr/local/sbin/btc-set-prune.sh ' . escapeshellarg($sel)); $current = $sel; sleep(1); } } $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_ADDR'] ?? gethostname(); $host = preg_replace('/:\d+$/', '', $host); // strip port if present $stratum = 'stratum+tcp://' . $host . ':3333'; ?> Bitcoin Node for Solo Miners [Settings]

Bitcoin Node for Solo Miners

A Linux Appliance by Robbie Ferguson

Stratum Endpoint

Username: BTC address for payout

Password: anything

Blockchain Prune Size

Note: Prune size only limits stored block data. You should also expect the node to use up to 500 MB for chainstate and metadata. Actual disk usage will be higher than the value selected here.

Changing this will restart bitcoind.

Donate to Bald Nerd

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.

EOF # AJAX status info cat > "$WWW_ROOT/status.php" <<'EOF' 'not activated']); exit; } header('Content-Type: application/json'); // ---- 1) bitcoin core info ---- $btcInfo = null; $out = shell_exec('/usr/local/bin/bitcoin-cli -conf=/etc/bitcoin/bitcoin.conf getblockchaininfo 2>/dev/null'); if ($out) { $btcInfo = json_decode($out, true); } // ---- 2) service health ---- 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'), ]; // ---- 3) system info (non-sensitive) ---- // uptime $uptime_seconds = null; if (is_readable('/proc/uptime')) { $up = trim(file_get_contents('/proc/uptime')); $parts = explode(' ', $up); $uptime_seconds = (int)floatval($parts[0]); } // load average $load = function_exists('sys_getloadavg') ? sys_getloadavg() : null; // memory $mem_total = null; $mem_avail = null; if (is_readable('/proc/meminfo')) { $meminfo = file('/proc/meminfo', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($meminfo as $line) { if (strpos($line, 'MemTotal:') === 0) { $mem_total = (int)filter_var($line, FILTER_SANITIZE_NUMBER_INT) * 1024; } elseif (strpos($line, 'MemAvailable:') === 0) { $mem_avail = (int)filter_var($line, FILTER_SANITIZE_NUMBER_INT) * 1024; } } } // disk (root) $disk_root_total = @disk_total_space('/'); $disk_root_free = @disk_free_space('/'); // disk (bitcoind datadir) $btcDir = '/var/lib/bitcoind'; $disk_btc_total = @disk_total_space($btcDir); $disk_btc_free = @disk_free_space($btcDir); // bitcoind dir size $btc_dir_size = null; $du = shell_exec('du -sb ' . escapeshellarg($btcDir) . ' 2>/dev/null'); if ($du) { $parts = preg_split('/\s+/', trim($du)); if (isset($parts[0]) && ctype_digit($parts[0])) { $btc_dir_size = (int)$parts[0]; } } // ---- 4) output ---- echo json_encode([ 'initialblockdownload' => $btcInfo['initialblockdownload'] ?? null, 'blocks' => $btcInfo['blocks'] ?? null, 'headers' => $btcInfo['headers'] ?? null, 'verificationprogress' => $btcInfo['verificationprogress'] ?? null, 'pruned' => $btcInfo['pruned'] ?? null, 'services' => $services, 'system' => [ 'uptime_seconds' => $uptime_seconds, 'loadavg' => $load, 'mem_total_bytes' => $mem_total, 'mem_available_bytes' => $mem_avail, 'disk_root' => [ 'total_bytes' => $disk_root_total, 'free_bytes' => $disk_root_free, ], 'disk_bitcoind' => [ 'total_bytes' => $disk_btc_total, 'free_bytes' => $disk_btc_free, 'used_bytes' => $btc_dir_size, ], ], ], JSON_UNESCAPED_SLASHES); EOF # activation page cat > "$WWW_ROOT/activate/index.php" <<'EOF' Create New Admin User

Create New Admin User

This sets the Bitcoin node’s administrative (RPC) credentials used to control the server itself.

Keep these private — they grant full access to your node.

These credentials are not used by miners to connect; miners only need your Bitcoin address and can use any password.

EOF # --------------------------------------------------------------------------- echo "[7/8] NGINX with HTTPS + redirect" # --------------------------------------------------------------------------- mkdir -p "$CERT_DIR" openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ -keyout ${CERT_DIR}/btc-solo.key \ -out ${CERT_DIR}/btc-solo.crt \ -subj "/C=CA/ST=Ontario/L=Barrie/O=BTC-Solo/OU=IT/CN=$(hostname -f)" >/dev/null 2>&1 chmod 600 ${CERT_DIR}/btc-solo.key cat > /etc/nginx/sites-available/btc-solo < /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 echo "================================================================" echo " Bitcoin Solo Miner Appliance installed." echo echo " 1) Open: https://$(hostname -I | awk '{print $1}')/activate/" echo " 2) Set Admin Username / Admin Password" echo " 3) (Optional) Regenerate the cert" echo " 4) You will be redirected to the dashboard." echo echo " Stratum endpoint for ASICs: stratum+tcp://$(hostname -I | awk '{print $1}'):${CKPOOL_PORT}" echo "================================================================"