Skip to content

Instantly share code, notes, and snippets.

@w3irdrobot
Last active March 30, 2026 00:20
Show Gist options
  • Select an option

  • Save w3irdrobot/9ae0d44dff0f9b5c54cdedeb2b0d3fa7 to your computer and use it in GitHub Desktop.

Select an option

Save w3irdrobot/9ae0d44dff0f9b5c54cdedeb2b0d3fa7 to your computer and use it in GitHub Desktop.
Deploy Public Pool on
#!/usr/bin/env bash
#
# setup-pool.sh
#
# Provisions an Ubuntu GCP VM with:
# - Bitcoin Core v30.2 (testnet4) via systemd
# - Public Pool (stratum mining pool) via systemd
# - Public Pool UI (static Angular app served by Caddy)
# - Caddy reverse proxy with automatic TLS
#
# Domain: testnet4-mining-pool.lygos.finance
#
# Usage: sudo bash setup-pool.sh
#
set -euo pipefail
###############################################################################
# Configuration
###############################################################################
DOMAIN="testnet4-mining-pool.lygos.finance"
BITCOIN_VERSION="30.2"
BITCOIN_TARBALL="bitcoin-${BITCOIN_VERSION}-x86_64-linux-gnu.tar.gz"
BITCOIN_URL="https://bitcoincore.org/bin/bitcoin-core-${BITCOIN_VERSION}/${BITCOIN_TARBALL}"
BITCOIN_SHA256_URL="https://bitcoincore.org/bin/bitcoin-core-${BITCOIN_VERSION}/SHA256SUMS"
BITCOIN_USER="bitcoin"
BITCOIN_DATADIR="/var/lib/bitcoind"
BITCOIN_CONFDIR="/etc/bitcoin"
BITCOIN_CONF="${BITCOIN_CONFDIR}/bitcoin.conf"
POOL_USER="pool"
POOL_DIR="/opt/public-pool"
POOL_UI_DIR="/opt/public-pool-ui"
POOL_UI_WWW="/var/www/public-pool-ui"
POOL_API_PORT="3334"
POOL_STRATUM_PORT="3333"
BITCOIN_RPC_PORT="48332"
SCRIPTS_DIR="/opt/pool-scripts"
NODE_MAJOR="22"
# Generate random RPC credentials
RPC_USER="poolrpc"
RPC_PASSWORD="$(openssl rand -hex 32)"
###############################################################################
# Preflight checks
###############################################################################
if [[ $EUID -ne 0 ]]; then
echo "ERROR: This script must be run as root (use sudo)." >&2
exit 1
fi
echo "============================================="
echo " Testnet4 Mining Pool Setup"
echo " Domain: ${DOMAIN}"
echo "============================================="
echo ""
###############################################################################
# 1. System update and base dependencies
###############################################################################
echo ">>> [1/8] Updating system and installing base dependencies..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -y
apt-get upgrade -y
apt-get install -y \
curl \
wget \
git \
build-essential \
python3 \
cmake \
jq \
ufw \
ca-certificates \
gnupg
###############################################################################
# 2. Install Node.js 22 LTS
###############################################################################
echo ">>> [2/8] Installing Node.js ${NODE_MAJOR} LTS..."
if ! command -v node &>/dev/null || ! node --version | grep -q "v${NODE_MAJOR}"; then
mkdir -p /etc/apt/keyrings
curl -fsSL "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key" \
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \
| tee /etc/apt/sources.list.d/nodesource.list >/dev/null
apt-get update -y
apt-get install -y nodejs
fi
echo "Node.js version: $(node --version)"
echo "npm version: $(npm --version)"
###############################################################################
# 3. Install and configure Bitcoin Core
###############################################################################
echo ">>> [3/8] Installing Bitcoin Core v${BITCOIN_VERSION}..."
# Create bitcoin system user
if ! id "${BITCOIN_USER}" &>/dev/null; then
useradd --system --create-home --home-dir "${BITCOIN_DATADIR}" --shell /usr/sbin/nologin "${BITCOIN_USER}"
fi
# Download and verify Bitcoin Core
TMPDIR="$(mktemp -d)"
pushd "${TMPDIR}" >/dev/null
echo "Downloading Bitcoin Core..."
wget -q "${BITCOIN_URL}"
wget -q "${BITCOIN_SHA256_URL}"
echo "Verifying SHA256 checksum..."
EXPECTED_HASH="$(grep "${BITCOIN_TARBALL}" SHA256SUMS | awk '{print $1}')"
ACTUAL_HASH="$(sha256sum "${BITCOIN_TARBALL}" | awk '{print $1}')"
if [[ "${EXPECTED_HASH}" != "${ACTUAL_HASH}" ]]; then
echo "ERROR: SHA256 checksum mismatch!" >&2
echo " Expected: ${EXPECTED_HASH}" >&2
echo " Actual: ${ACTUAL_HASH}" >&2
popd >/dev/null
rm -rf "${TMPDIR}"
exit 1
fi
echo "Checksum verified."
echo "Extracting binaries..."
tar -xzf "${BITCOIN_TARBALL}"
install -m 0755 "bitcoin-${BITCOIN_VERSION}/bin/bitcoind" /usr/local/bin/
install -m 0755 "bitcoin-${BITCOIN_VERSION}/bin/bitcoin-cli" /usr/local/bin/
popd >/dev/null
rm -rf "${TMPDIR}"
# Create configuration directories
mkdir -p "${BITCOIN_CONFDIR}"
mkdir -p "${BITCOIN_DATADIR}"
# Write bitcoin.conf
cat > "${BITCOIN_CONF}" <<BTCCONF
# Bitcoin Core configuration — testnet4
chain=testnet4
server=1
txindex=1
daemon=0
# RPC credentials
rpcuser=${RPC_USER}
rpcpassword=${RPC_PASSWORD}
[testnet4]
rpcport=${BITCOIN_RPC_PORT}
rpcbind=127.0.0.1
rpcallowip=127.0.0.1
BTCCONF
# Set ownership and permissions
chown -R "${BITCOIN_USER}:${BITCOIN_USER}" "${BITCOIN_CONFDIR}" "${BITCOIN_DATADIR}"
chmod 710 "${BITCOIN_CONFDIR}" "${BITCOIN_DATADIR}"
chmod 640 "${BITCOIN_CONF}"
# Create systemd service for bitcoind
cat > /etc/systemd/system/bitcoind.service <<'BTCSVC'
[Unit]
Description=Bitcoin Core daemon (testnet4)
Documentation=https://github.com/bitcoin/bitcoin/blob/master/doc/init.md
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/bitcoind \
-pid=/run/bitcoind/bitcoind.pid \
-conf=/etc/bitcoin/bitcoin.conf \
-datadir=/var/lib/bitcoind \
-startupnotify='systemd-notify --ready' \
-shutdownnotify='systemd-notify --stopping'
Type=notify
NotifyAccess=all
PIDFile=/run/bitcoind/bitcoind.pid
Restart=on-failure
TimeoutStartSec=infinity
TimeoutStopSec=600
User=bitcoin
Group=bitcoin
RuntimeDirectory=bitcoind
RuntimeDirectoryMode=0710
ConfigurationDirectory=bitcoin
ConfigurationDirectoryMode=0710
StateDirectory=bitcoind
StateDirectoryMode=0710
# Hardening
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
NoNewPrivileges=true
PrivateDevices=true
MemoryDenyWriteExecute=true
SystemCallArchitectures=native
[Install]
WantedBy=multi-user.target
BTCSVC
systemctl daemon-reload
systemctl enable bitcoind
systemctl start bitcoind
echo "Bitcoin Core started. Waiting for RPC to become available..."
# Wait for bitcoind RPC to be ready (up to 120 seconds)
RPC_READY=false
for _i in $(seq 1 60); do
if bitcoin-cli \
-rpcuser="${RPC_USER}" \
-rpcpassword="${RPC_PASSWORD}" \
-rpcport="${BITCOIN_RPC_PORT}" \
-testnet4 \
getblockchaininfo &>/dev/null; then
RPC_READY=true
break
fi
sleep 2
done
if [[ "${RPC_READY}" != "true" ]]; then
echo "WARNING: bitcoind RPC did not become available within 120 seconds." >&2
echo " The node may still be starting. Continuing setup..." >&2
else
echo "bitcoind RPC is ready."
fi
###############################################################################
# 4. Install and configure Public Pool
###############################################################################
echo ">>> [4/8] Installing Public Pool..."
# Create pool system user
if ! id "${POOL_USER}" &>/dev/null; then
useradd --system --create-home --shell /usr/sbin/nologin "${POOL_USER}"
fi
# Clone and build public-pool
if [[ -d "${POOL_DIR}" ]]; then
echo "Public Pool directory already exists. Pulling latest..."
git -C "${POOL_DIR}" pull
else
git clone https://github.com/benjamin-wilson/public-pool.git "${POOL_DIR}"
fi
pushd "${POOL_DIR}" >/dev/null
npm ci
npm run build
popd >/dev/null
# Create .env file for public-pool
cat > "${POOL_DIR}/.env" <<POOLENV
BITCOIN_RPC_URL=http://127.0.0.1
BITCOIN_RPC_USER=${RPC_USER}
BITCOIN_RPC_PASSWORD=${RPC_PASSWORD}
BITCOIN_RPC_PORT=${BITCOIN_RPC_PORT}
BITCOIN_RPC_TIMEOUT=10000
API_PORT=${POOL_API_PORT}
STRATUM_PORT=${POOL_STRATUM_PORT}
NETWORK=testnet
API_SECURE=false
POOL_IDENTIFIER=Lygos-Testnet4-Pool
POOLENV
# Set ownership
chown -R "${POOL_USER}:${POOL_USER}" "${POOL_DIR}"
# Create systemd service for public-pool
cat > /etc/systemd/system/public-pool.service <<POOLSVC
[Unit]
Description=Public Pool — Stratum Mining Pool
After=network-online.target bitcoind.service
Wants=network-online.target
Requires=bitcoind.service
[Service]
Type=simple
User=${POOL_USER}
Group=${POOL_USER}
WorkingDirectory=${POOL_DIR}
ExecStart=/usr/bin/node dist/main
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
# Environment
EnvironmentFile=${POOL_DIR}/.env
[Install]
WantedBy=multi-user.target
POOLSVC
systemctl daemon-reload
systemctl enable public-pool
systemctl start public-pool
echo "Public Pool started."
###############################################################################
# 5. Build Public Pool UI
###############################################################################
echo ">>> [5/8] Building Public Pool UI..."
# Clone and build public-pool-ui
if [[ -d "${POOL_UI_DIR}" ]]; then
echo "Public Pool UI directory already exists. Pulling latest..."
git -C "${POOL_UI_DIR}" pull
else
git clone https://github.com/benjamin-wilson/public-pool-ui.git "${POOL_UI_DIR}"
fi
# Patch the production environment file to point at our domain
cat > "${POOL_UI_DIR}/src/environments/environment.prod.ts" <<UIENV
export const environment = {
production: true,
API_URL: 'https://${DOMAIN}',
STRATUM_URL: '${DOMAIN}:${POOL_STRATUM_PORT}'
};
UIENV
pushd "${POOL_UI_DIR}" >/dev/null
npm ci
npm run build
popd >/dev/null
# Copy built static files to web root
mkdir -p "${POOL_UI_WWW}"
rm -rf "${POOL_UI_WWW:?}/"*
# Angular outputs to dist/public-pool-ui/browser/ (Angular 18+)
if [[ -d "${POOL_UI_DIR}/dist/public-pool-ui/browser" ]]; then
cp -r "${POOL_UI_DIR}/dist/public-pool-ui/browser/"* "${POOL_UI_WWW}/"
elif [[ -d "${POOL_UI_DIR}/dist/public-pool-ui" ]]; then
cp -r "${POOL_UI_DIR}/dist/public-pool-ui/"* "${POOL_UI_WWW}/"
else
echo "ERROR: Could not find Angular build output." >&2
echo "Contents of ${POOL_UI_DIR}/dist/:" >&2
ls -la "${POOL_UI_DIR}/dist/" >&2
exit 1
fi
chown -R www-data:www-data "${POOL_UI_WWW}"
echo "Public Pool UI built and deployed to ${POOL_UI_WWW}."
###############################################################################
# 6. Install and configure Caddy
###############################################################################
echo ">>> [6/8] Installing and configuring Caddy..."
# Install Caddy from official apt repo
if ! command -v caddy &>/dev/null; then
apt-get install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| tee /etc/apt/sources.list.d/caddy-stable.list >/dev/null
apt-get update -y
apt-get install -y caddy
fi
# Write Caddyfile
cat > /etc/caddy/Caddyfile <<CADDYFILE
${DOMAIN} {
# Proxy API requests to public-pool backend
handle /api/* {
reverse_proxy localhost:${POOL_API_PORT}
}
# Serve the Angular static UI for everything else
handle {
root * ${POOL_UI_WWW}
try_files {path} /index.html
file_server
}
log {
output stdout
format json
}
}
CADDYFILE
# Validate and reload Caddy
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
systemctl reload caddy
echo "Caddy configured and reloaded."
###############################################################################
# 7. Firewall configuration
###############################################################################
echo ">>> [7/8] Configuring firewall..."
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
# SSH
ufw allow 22/tcp
# HTTP (ACME challenge + redirect to HTTPS)
ufw allow 80/tcp
# HTTPS (web UI + API)
ufw allow 443/tcp
# Stratum mining (raw TCP, exposed directly)
ufw allow 3333/tcp
ufw --force enable
echo "Firewall configured."
###############################################################################
# 8. Generate maintenance scripts
###############################################################################
echo ">>> [8/8] Generating maintenance scripts..."
mkdir -p "${SCRIPTS_DIR}"
# --- pool-status.sh ---
cat > "${SCRIPTS_DIR}/pool-status.sh" <<'STATUSEOF'
#!/usr/bin/env bash
# pool-status.sh — Show status of all pool services
set -euo pipefail
echo "=== Bitcoin Core ==="
systemctl status bitcoind --no-pager -l || true
echo ""
echo "=== Bitcoin Blockchain Info ==="
bitcoin-cli -testnet4 getblockchaininfo 2>/dev/null | jq '{chain, blocks, headers, verificationprogress}' || echo "bitcoind RPC not available"
echo ""
echo "=== Public Pool ==="
systemctl status public-pool --no-pager -l || true
echo ""
echo "=== Caddy ==="
systemctl status caddy --no-pager -l || true
echo ""
echo "=== Firewall ==="
ufw status verbose
echo ""
echo "=== Listening Ports ==="
ss -tlnp | grep -E ':(3333|3334|48332|80|443)\s'
STATUSEOF
# --- update-bitcoin.sh ---
cat > "${SCRIPTS_DIR}/update-bitcoin.sh" <<'UPDATEBTCEOF'
#!/usr/bin/env bash
# update-bitcoin.sh — Download and install a new Bitcoin Core version
#
# Usage: sudo ./update-bitcoin.sh <version>
# Example: sudo ./update-bitcoin.sh 31.0
set -euo pipefail
if [[ $EUID -ne 0 ]]; then
echo "ERROR: Must be run as root." >&2
exit 1
fi
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <version>" >&2
echo "Example: $0 31.0" >&2
exit 1
fi
VERSION="$1"
TARBALL="bitcoin-${VERSION}-x86_64-linux-gnu.tar.gz"
URL="https://bitcoincore.org/bin/bitcoin-core-${VERSION}/${TARBALL}"
SHA_URL="https://bitcoincore.org/bin/bitcoin-core-${VERSION}/SHA256SUMS"
TMPDIR="$(mktemp -d)"
pushd "${TMPDIR}" >/dev/null
echo "Downloading Bitcoin Core v${VERSION}..."
wget -q "${URL}"
wget -q "${SHA_URL}"
echo "Verifying checksum..."
EXPECTED="$(grep "${TARBALL}" SHA256SUMS | awk '{print $1}')"
ACTUAL="$(sha256sum "${TARBALL}" | awk '{print $1}')"
if [[ "${EXPECTED}" != "${ACTUAL}" ]]; then
echo "ERROR: Checksum mismatch!" >&2
popd >/dev/null
rm -rf "${TMPDIR}"
exit 1
fi
echo "Stopping bitcoind..."
systemctl stop bitcoind
echo "Installing new binaries..."
tar -xzf "${TARBALL}"
install -m 0755 "bitcoin-${VERSION}/bin/bitcoind" /usr/local/bin/
install -m 0755 "bitcoin-${VERSION}/bin/bitcoin-cli" /usr/local/bin/
popd >/dev/null
rm -rf "${TMPDIR}"
echo "Starting bitcoind..."
systemctl start bitcoind
echo "Bitcoin Core updated to v${VERSION}."
bitcoind --version | head -1
UPDATEBTCEOF
# --- update-public-pool.sh ---
cat > "${SCRIPTS_DIR}/update-public-pool.sh" <<'UPDATEPOOLEOF'
#!/usr/bin/env bash
# update-public-pool.sh — Pull latest public-pool, rebuild, and restart
set -euo pipefail
if [[ $EUID -ne 0 ]]; then
echo "ERROR: Must be run as root." >&2
exit 1
fi
POOL_DIR="/opt/public-pool"
echo "Pulling latest public-pool..."
git -C "${POOL_DIR}" pull
echo "Rebuilding..."
pushd "${POOL_DIR}" >/dev/null
npm ci
npm run build
popd >/dev/null
chown -R pool:pool "${POOL_DIR}"
echo "Restarting public-pool service..."
systemctl restart public-pool
echo "Public Pool updated and restarted."
systemctl status public-pool --no-pager
UPDATEPOOLEOF
# --- update-public-pool-ui.sh ---
cat > "${SCRIPTS_DIR}/update-public-pool-ui.sh" <<'UPDATEUIEOF'
#!/usr/bin/env bash
# update-public-pool-ui.sh — Pull latest public-pool-ui, rebuild, and redeploy
set -euo pipefail
if [[ $EUID -ne 0 ]]; then
echo "ERROR: Must be run as root." >&2
exit 1
fi
POOL_UI_DIR="/opt/public-pool-ui"
POOL_UI_WWW="/var/www/public-pool-ui"
echo "Pulling latest public-pool-ui..."
git -C "${POOL_UI_DIR}" pull
echo "Rebuilding..."
pushd "${POOL_UI_DIR}" >/dev/null
npm ci
npm run build
popd >/dev/null
echo "Deploying static files..."
rm -rf "${POOL_UI_WWW:?}/"*
if [[ -d "${POOL_UI_DIR}/dist/public-pool-ui/browser" ]]; then
cp -r "${POOL_UI_DIR}/dist/public-pool-ui/browser/"* "${POOL_UI_WWW}/"
elif [[ -d "${POOL_UI_DIR}/dist/public-pool-ui" ]]; then
cp -r "${POOL_UI_DIR}/dist/public-pool-ui/"* "${POOL_UI_WWW}/"
else
echo "ERROR: Could not find build output." >&2
ls -la "${POOL_UI_DIR}/dist/" >&2
exit 1
fi
chown -R www-data:www-data "${POOL_UI_WWW}"
echo "Public Pool UI updated and redeployed."
UPDATEUIEOF
# --- backup-pool-db.sh ---
cat > "${SCRIPTS_DIR}/backup-pool-db.sh" <<'BACKUPEOF'
#!/usr/bin/env bash
# backup-pool-db.sh — Backup the public-pool SQLite database
set -euo pipefail
if [[ $EUID -ne 0 ]]; then
echo "ERROR: Must be run as root." >&2
exit 1
fi
POOL_DIR="/opt/public-pool"
BACKUP_DIR="/var/backups/public-pool"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
mkdir -p "${BACKUP_DIR}"
# Find the database file(s)
DB_DIR="${POOL_DIR}/DB"
if [[ ! -d "${DB_DIR}" ]]; then
# Also check for testnet DB directory
DB_DIR="${POOL_DIR}/testnet-DB"
fi
if [[ ! -d "${DB_DIR}" ]]; then
echo "WARNING: No DB directory found in ${POOL_DIR}." >&2
echo "Checking for any .sqlite files..." >&2
DB_DIR="${POOL_DIR}"
fi
BACKUP_FILE="${BACKUP_DIR}/pool-backup-${TIMESTAMP}.tar.gz"
echo "Backing up database from ${DB_DIR}..."
tar -czf "${BACKUP_FILE}" -C "${POOL_DIR}" "$(basename "${DB_DIR}")" 2>/dev/null || \
tar -czf "${BACKUP_FILE}" -C "${POOL_DIR}" . --include='*.sqlite*' --include='*.db*' 2>/dev/null || \
echo "WARNING: Could not find database files to back up."
if [[ -f "${BACKUP_FILE}" ]]; then
echo "Backup saved to: ${BACKUP_FILE}"
echo "Size: $(du -h "${BACKUP_FILE}" | awk '{print $1}')"
# Keep only last 10 backups
BACKUP_COUNT="$(find "${BACKUP_DIR}" -name 'pool-backup-*.tar.gz' | wc -l)"
if [[ "${BACKUP_COUNT}" -gt 10 ]]; then
echo "Pruning old backups (keeping last 10)..."
find "${BACKUP_DIR}" -name 'pool-backup-*.tar.gz' -printf '%T@ %p\n' \
| sort -n \
| head -n "-10" \
| awk '{print $2}' \
| xargs rm -f
fi
fi
BACKUPEOF
# Make all scripts executable
chmod +x "${SCRIPTS_DIR}"/*.sh
###############################################################################
# Save RPC credentials for reference
###############################################################################
CREDS_FILE="${SCRIPTS_DIR}/.rpc-credentials"
cat > "${CREDS_FILE}" <<CREDSEOF
# Bitcoin Core RPC Credentials (auto-generated)
# Used by public-pool to communicate with bitcoind
RPC_USER=${RPC_USER}
RPC_PASSWORD=${RPC_PASSWORD}
RPC_PORT=${BITCOIN_RPC_PORT}
CREDSEOF
chmod 600 "${CREDS_FILE}"
###############################################################################
# Done
###############################################################################
echo ""
echo "============================================="
echo " Setup Complete!"
echo "============================================="
echo ""
echo "Services:"
echo " - bitcoind (testnet4): systemctl status bitcoind"
echo " - public-pool: systemctl status public-pool"
echo " - caddy: systemctl status caddy"
echo ""
echo "Endpoints:"
echo " - Web UI: https://${DOMAIN}/"
echo " - Pool API: https://${DOMAIN}/api/"
echo " - Stratum: ${DOMAIN}:${POOL_STRATUM_PORT} (raw TCP)"
echo ""
echo "RPC credentials saved to: ${CREDS_FILE}"
echo ""
echo "Maintenance scripts in: ${SCRIPTS_DIR}/"
echo " - pool-status.sh — Show status of all services"
echo " - update-bitcoin.sh — Update Bitcoin Core version"
echo " - update-public-pool.sh — Update Public Pool"
echo " - update-public-pool-ui.sh — Update Public Pool UI"
echo " - backup-pool-db.sh — Backup pool database"
echo ""
echo "NOTE: Bitcoin Core is syncing the testnet4 blockchain."
echo " This may take some time. Monitor with:"
echo " bitcoin-cli -testnet4 getblockchaininfo"
echo ""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment