#!/usr/bin/env bash # x.com/itsno402 # Simple Docker-based installer for MidnightMiner / DefensioMiner / Midnight Fetcher Bot # Usage: # curl -sL https://no402.com/install.sh | bash set -euo pipefail ############################################## # Helper: prompt with default via /dev/tty ############################################## prompt_default() { local prompt="$1" local default="$2" local input="" if [ -r /dev/tty ]; then printf "%s [%s]: " "$prompt" "$default" > /dev/tty if ! read -r input < /dev/tty; then input="" fi else input="$default" printf "%s [%s]: %s (no-tty default)\n" "$prompt" "$default" "$input" >&2 fi if [ -z "$input" ]; then input="$default" fi printf '%s\n' "$input" } ############################################## # 0) Hard pre-checks (root, docker, compose) ############################################## if [ "$(id -u)" -ne 0 ]; then echo "ERROR: This installer must be run as root (or with sudo)." echo "Example:" echo " curl -sL https://no402.com/install.sh | sudo bash" exit 1 fi if ! command -v docker >/dev/null 2>&1; then echo "ERROR: Docker is not installed or not in PATH." echo "Please install Docker and re-run:" echo " curl -sL https://no402.com/install.sh | bash" exit 1 fi if docker compose version >/dev/null 2>&1; then DOCKER_COMPOSE_CMD="docker compose" elif command -v docker-compose >/dev/null 2>&1; then DOCKER_COMPOSE_CMD="docker-compose" else echo "ERROR: Docker Compose plugin or binary not found." echo "Install either:" echo " - Docker Compose v2 plugin (docker compose)" echo " - Or docker-compose (standalone)" exit 1 fi ############################################## # 1) Basic OS info ############################################## HOST_OS="Unknown" if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release HOST_OS="${PRETTY_NAME:-$NAME}" fi echo "=== Midnight / Defensio / FetcherBot Docker installer ===" echo "Detected host OS: ${HOST_OS}" echo "Docker version: $(docker --version || echo 'unknown')" echo "Docker Compose: $($DOCKER_COMPOSE_CMD version 2>/dev/null || echo 'unknown')" echo ############################################## # 2) Ask for basic configuration ############################################## DEFAULT_BASE_DIR="${PWD}/midnight-miner-docker" BASE_DIR="$(prompt_default "Project directory (will contain config.json, docker-compose.yml, data/logs)" "$DEFAULT_BASE_DIR")" echo > /dev/tty echo "Choose miner implementation:" > /dev/tty echo " 1) MidnightMiner (djeanql/MidnightMiner)" > /dev/tty echo " 2) DefensioMiner (Aervue/DefensioMiner)" > /dev/tty echo " 3) Midnight Fetcher Bot (ADA-Markets/FetcherBot)" > /dev/tty MINER_CHOICE="$(prompt_default "Miner implementation (1, 2 or 3)" "1")" case "$MINER_CHOICE" in 1|"midnight"|"MidnightMiner"|"midnightminer") MINER_IMPL="midnight" ;; 2|"defensio"|"DefensioMiner"|"defensiominer") MINER_IMPL="defensio" ;; 3|"fetcher"|"FetcherBot"|"fetcherbot") MINER_IMPL="fetcher" ;; *) echo "Unknown choice '$MINER_CHOICE', defaulting to MidnightMiner." > /dev/tty MINER_IMPL="midnight" ;; esac NETWORK="defensio" # all three target the Defensio context DEFAULT_WORKERS="8" WORKERS="$(prompt_default "Number of worker processes / threads" "$DEFAULT_WORKERS")" if [ "$MINER_IMPL" = "midnight" ]; then DEFAULT_TMUX_SESSION="dockermm" elif [ "$MINER_IMPL" = "defensio" ]; then DEFAULT_TMUX_SESSION="dockerdm" else DEFAULT_TMUX_SESSION="dockerfb" fi TMUX_SESSION="$(prompt_default "tmux session name (inside container)" "$DEFAULT_TMUX_SESSION")" DEST_ADDR="" if [ "$MINER_IMPL" = "midnight" ] || [ "$MINER_IMPL" = "defensio" ]; then # Consolidation / donation address is REQUIRED for 1+2 while :; do if [ -r /dev/tty ]; then printf "Consolidation / donation destination address (REQUIRED): " > /dev/tty read -r DEST_ADDR < /dev/tty || DEST_ADDR="" else DEST_ADDR="" fi if [ -n "$DEST_ADDR" ]; then break fi echo "Address must not be empty for this miner type." > /dev/tty done fi DONATION=false if [ "$MINER_IMPL" = "midnight" ] || [ "$MINER_IMPL" = "defensio" ]; then DON_ANS="$(prompt_default "Enable developer donation (5%)? (yes/no)" "yes")" if [ "$DON_ANS" = "yes" ] || [ "$DON_ANS" = "Yes" ] || [ "$DON_ANS" = "YES" ]; then DONATION=true fi fi DEFAULT_LOG_FILE="/logs/miner.log" LOG_FILE="$(prompt_default "Log file path inside container" "$DEFAULT_LOG_FILE")" DEFAULT_MAX_DELTA="1200" MAX_DELTA="$(prompt_default "Max miner.log age in seconds before restart (MidnightMiner only)" "$DEFAULT_MAX_DELTA")" FROM_ID="" TO_ID="" BATCH_SIZE="" if [ "$MINER_IMPL" = "defensio" ]; then FROM_ID="$(prompt_default "First wallet ID to use for DefensioMiner (--from)" "1")" TO_ID="$(prompt_default "Last wallet ID to use for DefensioMiner (--to)" "100")" BATCH_SIZE="$(prompt_default "Wallets per solver batch (--batch)" "5")" fi echo > /dev/tty echo "Summary:" > /dev/tty echo " Project dir: $BASE_DIR" > /dev/tty echo " Miner impl: $MINER_IMPL" > /dev/tty echo " Network: $NETWORK" > /dev/tty echo " Workers: $WORKERS" > /dev/tty echo " tmux session: $TMUX_SESSION" > /dev/tty if [ "$MINER_IMPL" = "fetcher" ]; then echo " Dest address: (n/a for FetcherBot)" > /dev/tty else echo " Dest address: $DEST_ADDR" > /dev/tty fi if [ "$MINER_IMPL" = "midnight" ] || [ "$MINER_IMPL" = "defensio" ]; then echo " Donation: $DONATION" > /dev/tty fi echo " Log file: $LOG_FILE" > /dev/tty echo " Max log delta: $MAX_DELTA s" > /dev/tty if [ "$MINER_IMPL" = "defensio" ]; then echo " From ID: $FROM_ID" > /dev/tty echo " To ID: $TO_ID" > /dev/tty echo " Batch size: $BATCH_SIZE" > /dev/tty fi echo > /dev/tty ############################################## # 3) Create project structure ############################################## mkdir -p "$BASE_DIR" mkdir -p "$BASE_DIR/logs" mkdir -p "$BASE_DIR/data" CONFIG_PATH="$BASE_DIR/config.json" WATCHDOG_PATH="$BASE_DIR/watchdog.sh" COMPOSE_PATH="$BASE_DIR/docker-compose.yml" ############################################## # 4) Write config.json ############################################## if [ "$MINER_IMPL" = "midnight" ]; then cat > "$CONFIG_PATH" < "$CONFIG_PATH" < "$CONFIG_PATH" < /dev/tty ############################################## # 5) Write watchdog.sh (inside container) ############################################## cat > "$WATCHDOG_PATH" <<'EOF' #!/usr/bin/env bash # Container watchdog / entrypoint set -euo pipefail CONFIG_FILE="/config/config.json" DATA_DIR="/data" LOG_DIR="/logs" MIDNIGHT_DIR="${DATA_DIR}/MidnightMiner" DEFENSIO_DIR="${DATA_DIR}/DefensioMiner" FETCHER_DIR="${DATA_DIR}/midnight_fetcher_bot_public" STATE_FILE="${DATA_DIR}/.setup_done" if [ ! -f "$CONFIG_FILE" ]; then echo "ERROR: Config file not found: $CONFIG_FILE" exit 1 fi mkdir -p "$DATA_DIR" "$LOG_DIR" eval "$( python3 - "$CONFIG_FILE" <<'PY' import json, sys, shlex cfg_path = sys.argv[1] with open(cfg_path, 'r', encoding='utf-8') as f: cfg = json.load(f) mapping = { "miner_impl": "MINER_IMPL", "network": "NETWORK", "workers": "WORKERS", "destination_address": "DEST_ADDR", "tmux_session": "TMUX_SESSION", "log_file": "LOG_FILE", "max_log_delta": "MAX_DELTA", "donation": "DONATION", "from_id": "FROM_ID", "to_id": "TO_ID", "batch_size": "BATCH_SIZE", } for key, env in mapping.items(): if key not in cfg: continue val = cfg[key] if isinstance(val, bool): val = "true" if val else "false" print(f"{env}={shlex.quote(str(val))}") PY )" : "${MINER_IMPL:=midnight}" : "${NETWORK:=defensio}" : "${WORKERS:=8}" : "${TMUX_SESSION:=dockermm}" : "${MAX_DELTA:=1200}" : "${LOG_FILE:=/logs/miner.log}" : "${DONATION:=true}" : "${DEST_ADDR:=}" : "${FROM_ID:=}" : "${TO_ID:=}" : "${BATCH_SIZE:=}" mkdir -p "$(dirname "$LOG_FILE")" touch "$LOG_FILE" 2>/dev/null || { echo "Cannot write to log file $LOG_FILE" exit 1 } export PATH="/root/.cargo/bin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/bin" time_now() { date '+%d.%m.%Y-%H:%M:%S'; } log() { local ts msg ts="$(time_now)" msg="$1" echo "[$ts] - [$TMUX_SESSION] - $msg" >> "$LOG_FILE" } ############################################## # Ensure base + runtime dependencies every start ############################################## # sudo shim (container runs as root) if ! command -v sudo >/dev/null 2>&1; then log "Creating sudo shim (already running as root)" cat >/usr/local/bin/sudo <<'SUDOEOF' #!/usr/bin/env bash # Simple sudo shim for container (already root) echo "[sudo-shim] Running as root inside container; 'sudo' is a no-op wrapper." >&2 exec "$@" SUDOEOF chmod +x /usr/local/bin/sudo fi ensure_base_deps() { if command -v tmux >/dev/null 2>&1 && command -v git >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then return fi log "Base dependencies missing (git/tmux/curl/etc); installing..." apt-get update -y >>"$LOG_FILE" 2>&1 || log "WARNING: apt-get update failed (continuing)" if ! apt-get install -y git tmux curl ca-certificates build-essential net-tools iproute2 iputils-ping >>"$LOG_FILE" 2>&1; then log "FATAL: unable to install base dependencies (git/tmux/curl/...)" exit 1 fi } ensure_runtime_deps() { if [ "$MINER_IMPL" = "midnight" ]; then pyver="$(python3 - <<'PY' import sys print(f"{sys.version_info.major}.{sys.version_info.minor}") PY )" if [ -n "$pyver" ]; then apt-get install -y "python${pyver}-venv" >>"$LOG_FILE" 2>&1 || true fi apt-get install -y python3-venv >>"$LOG_FILE" 2>&1 || true elif [ "$MINER_IMPL" = "defensio" ] || [ "$MINER_IMPL" = "fetcher" ]; then log "Ensuring Node.js 20.x and Rust toolchain are installed" curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >>"$LOG_FILE" 2>&1 || \ log "WARNING: NodeSource setup script failed" apt-get install -y nodejs >>"$LOG_FILE" 2>&1 || log "WARNING: nodejs install failed" if ! command -v cargo >/dev/null 2>&1; then log "Installing Rust (via rustup)" curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ sh -s -- -y >>"$LOG_FILE" 2>&1 || log "WARNING: rustup installation failed" fi if [ -f "$HOME/.cargo/env" ]; then # shellcheck disable=SC1090 . "$HOME/.cargo/env" fi export PATH="$HOME/.cargo/bin:$PATH" fi } ensure_base_deps ensure_runtime_deps ############################################## # 1) One-time setup (clones & builds in /data) ############################################## if [ ! -f "$STATE_FILE" ]; then log "Loaded config: MINER_IMPL=${MINER_IMPL}, WORKERS=${WORKERS}, TMUX_SESSION=${TMUX_SESSION}" log "Initial setup: preparing repositories and build artifacts in /data" if [ "$MINER_IMPL" = "midnight" ]; then log "Setting up MidnightMiner only" if [ ! -d "$MIDNIGHT_DIR/.git" ]; then log "Cloning MidnightMiner into $MIDNIGHT_DIR" rm -rf "$MIDNIGHT_DIR" git clone https://github.com/djeanql/MidnightMiner "$MIDNIGHT_DIR" >>"$LOG_FILE" 2>&1 else log "Updating MidnightMiner repo" git -C "$MIDNIGHT_DIR" pull --rebase >>"$LOG_FILE" 2>&1 || true fi if [ ! -d "$MIDNIGHT_DIR/venv" ]; then log "Creating Python venv for MidnightMiner" python3 -m venv "$MIDNIGHT_DIR/venv" >>"$LOG_FILE" 2>&1 || { log "python3 -m venv failed for MidnightMiner" exit 1 } "$MIDNIGHT_DIR/venv/bin/pip" install --upgrade pip >>"$LOG_FILE" 2>&1 "$MIDNIGHT_DIR/venv/bin/pip" install -r "$MIDNIGHT_DIR/requirements.txt" >>"$LOG_FILE" 2>&1 fi elif [ "$MINER_IMPL" = "defensio" ]; then log "Setting up DefensioMiner only" if [ ! -d "$DEFENSIO_DIR/.git" ]; then log "Cloning DefensioMiner into $DEFENSIO_DIR" rm -rf "$DEFENSIO_DIR" git clone https://github.com/Aervue/DefensioMiner "$DEFENSIO_DIR" >>"$LOG_FILE" 2>&1 else log "Updating DefensioMiner repo" git -C "$DEFENSIO_DIR" pull --rebase >>"$LOG_FILE" 2>&1 || true fi log "Installing DefensioMiner Node deps and building solver" ( cd "$DEFENSIO_DIR" && npm install >>"$LOG_FILE" 2>&1 ) if ! ( cd "$DEFENSIO_DIR" && npm run build:solver >>"$LOG_FILE" 2>&1 ); then log "npm run build:solver failed or not defined; falling back to 'cargo build --release' in solver/" ( cd "$DEFENSIO_DIR/solver" && cargo build --release >>"$LOG_FILE" 2>&1 ) || \ log "WARNING: cargo build --release in solver/ also failed" fi elif [ "$MINER_IMPL" = "fetcher" ]; then log "Setting up Midnight Fetcher Bot" if [ ! -d "$FETCHER_DIR/.git" ]; then log "Cloning Midnight Fetcher Bot into $FETCHER_DIR" rm -rf "$FETCHER_DIR" git clone https://github.com/ADA-Markets/FetcherBot "$FETCHER_DIR" >>"$LOG_FILE" 2>&1 else log "Updating Midnight Fetcher Bot repo" git -C "$FETCHER_DIR" pull --rebase >>"$LOG_FILE" 2>&1 || true fi log "FetcherBot: installing Node dependencies" ( cd "$FETCHER_DIR" && npm install >>"$LOG_FILE" 2>&1 ) || \ log "WARNING: npm install failed in FetcherBot root" if [ -f "$HOME/.cargo/env" ]; then # shellcheck disable=SC1090 . "$HOME/.cargo/env" fi export PATH="$HOME/.cargo/bin:$PATH" HASH_ENGINE_BUILT=false if [ -d "$FETCHER_DIR/native-HashEngine" ]; then log "FetcherBot: found native-HashEngine at $FETCHER_DIR/native-HashEngine, building with cargo" if ( cd "$FETCHER_DIR/native-HashEngine" && cargo build --release >>"$LOG_FILE" 2>&1 ); then HASH_ENGINE_BUILT=true else log "ERROR: cargo build --release failed in native-HashEngine" fi elif [ -d "$FETCHER_DIR/hashengine/native-HashEngine" ]; then log "FetcherBot: found hashengine/native-HashEngine, building with cargo" if ( cd "$FETCHER_DIR/hashengine/native-HashEngine" && cargo build --release >>"$LOG_FILE" 2>&1 ); then HASH_ENGINE_BUILT=true else log "ERROR: cargo build --release failed in hashengine/native-HashEngine" fi elif [ -d "$FETCHER_DIR/hashengine" ]; then log "FetcherBot: found hashengine at $FETCHER_DIR/hashengine, building with cargo" if ( cd "$FETCHER_DIR/hashengine" && cargo build --release >>"$LOG_FILE" 2>&1 ); then HASH_ENGINE_BUILT=true else log "ERROR: cargo build --release failed in hashengine" fi else log "WARNING: no hash engine directory found (native-HashEngine or hashengine); hash server may be missing" fi if [ "$HASH_ENGINE_BUILT" = false ]; then log "WARNING: hash engine build did not succeed; start.sh may complain about missing hash server binary" fi log "FetcherBot: running Next.js production build" if ! ( cd "$FETCHER_DIR" && npm run build >>"$LOG_FILE" 2>&1 ); then log "ERROR: npm run build failed for FetcherBot" fi fi touch "$STATE_FILE" log "Initial setup for ${MINER_IMPL^} completed" fi ############################################## # 2) Midnight / Defensio / Fetcher loops ############################################## midnight_start_run() { local wallets_file="${MIDNIGHT_DIR}/wallets.json" local cnt_wallets=0 if [ -f "$wallets_file" ]; then cnt_wallets="$(grep -c '"address"' "$wallets_file" || true)" fi log "Starting MidnightMiner session '$TMUX_SESSION' workers=${WORKERS} wallets=${cnt_wallets}" tmux has-session -t "$TMUX_SESSION" 2>/dev/null && tmux kill-session -t "$TMUX_SESSION" || true sleep 2 local network_flag="" if [ "$NETWORK" = "defensio" ]; then network_flag="--defensio" fi local donation_flag="" if [ "$DONATION" = "false" ]; then donation_flag="--no-donation" fi if [ -n "${DEST_ADDR:-}" ]; then log "MidnightMiner: starting miner with auto-consolidation to ${DEST_ADDR}" tmux new-session -d -s "$TMUX_SESSION" \ "cd '${MIDNIGHT_DIR}'; '${MIDNIGHT_DIR}/venv/bin/python' '${MIDNIGHT_DIR}/miner.py' ${network_flag} --workers ${WORKERS} ${donation_flag} --consolidate '${DEST_ADDR}'" else log "MidnightMiner: starting miner without auto-consolidation" tmux new-session -d -s "$TMUX_SESSION" \ "cd '${MIDNIGHT_DIR}'; '${MIDNIGHT_DIR}/venv/bin/python' '${MIDNIGHT_DIR}/miner.py' ${network_flag} --workers ${WORKERS} ${donation_flag}" fi } midnight_main() { local miner_log="${MIDNIGHT_DIR}/miner.log" local delta=0 local timestamp_now timestamp_now="$(date +%s)" if [ -f "$miner_log" ]; then local miner_mtime miner_mtime="$( stat -c %Y "$miner_log" 2>/dev/null \ || stat -f %m "$miner_log" 2>/dev/null \ || echo "$timestamp_now" )" delta=$((timestamp_now - miner_mtime)) fi if tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then if [ "$delta" -gt "$MAX_DELTA" ]; then log "Restarting MidnightMiner; miner.log delta is ${delta}s (> ${MAX_DELTA}s)" midnight_start_run else local wallets_file="${MIDNIGHT_DIR}/wallets.json" local cnt_wallets=0 if [ -f "$wallets_file" ]; then cnt_wallets="$(grep -c '"address"' "$wallets_file" || true)" fi log "MidnightMiner already running; wallets=${cnt_wallets}, miner.log age=${delta}s" fi else midnight_start_run fi } defensio_ensure_wallets() { local state_file="${DEFENSIO_DIR}/.defensio_init_done" local mining_dir="${DEFENSIO_DIR}/wallets/mining" if [ -f "$state_file" ]; then return fi if [ -d "$mining_dir" ]; then if find "$mining_dir" -mindepth 1 -maxdepth 1 -type d | head -n1 | grep -q .; then log "Existing mining wallets detected, skipping auto-generate/register" touch "$state_file" return fi fi if [ -z "$FROM_ID" ] || [ -z "$TO_ID" ]; then log "DefensioMiner: FROM_ID/TO_ID not set, skipping wallet generation" return fi local count=$((TO_ID - FROM_ID + 1)) if [ "$count" -le 0 ]; then log "Invalid FROM_ID/TO_ID (FROM_ID=${FROM_ID}, TO_ID=${TO_ID}); skipping wallet init" return fi log "Initializing DefensioMiner wallets: count=${count}, from=${FROM_ID}, to=${TO_ID}" cd "$DEFENSIO_DIR" npm run generate -- --count "${count}" --network mainnet --mnemonic-length 24 --start-index "${FROM_ID}" >>"$LOG_FILE" 2>&1 npm run register -- --from "${FROM_ID}" --to "${TO_ID}" --force >>"$LOG_FILE" 2>&1 if [ -n "${DEST_ADDR:-}" ]; then log "DefensioMiner: running donation for range ${FROM_ID}-${TO_ID} to ${DEST_ADDR}" npm run donate -- --from "${FROM_ID}" --to "${TO_ID}" --address "${DEST_ADDR}" >>"$LOG_FILE" 2>&1 || \ log "WARNING: npm run donate failed" fi touch "$state_file" log "DefensioMiner wallet init completed" } defensio_start_run() { defensio_ensure_wallets log "Starting DefensioMiner session '$TMUX_SESSION' threads=${WORKERS} range=${FROM_ID}-${TO_ID} batch=${BATCH_SIZE}" tmux has-session -t "$TMUX_SESSION" 2>/dev/null && tmux kill-session -t "$TMUX_SESSION" || true sleep 2 cd "$DEFENSIO_DIR" tmux new-session -d -s "$TMUX_SESSION" \ "cd '${DEFENSIO_DIR}'; ASHMAIZE_THREADS=${WORKERS} npm run start -- --from ${FROM_ID} --to ${TO_ID} --batch ${BATCH_SIZE}" } defensio_main() { if tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then log "DefensioMiner already running in tmux session '$TMUX_SESSION'" else defensio_start_run fi } fetcher_main() { if [ ! -d "$FETCHER_DIR" ]; then log "FetcherBot directory $FETCHER_DIR not found; skipping start" return fi log "FetcherBot: configuring services to use WORKERS=${WORKERS}" ( cd "$FETCHER_DIR" ./start.sh ${WORKERS} >>"$LOG_FILE" 2>&1 || \ log "FetcherBot start.sh returned non-zero; check project logs" ) } ############################################## # 3) Main loop ############################################## while true; do case "$MINER_IMPL" in midnight) midnight_main ;; defensio) defensio_main ;; fetcher) fetcher_main ;; *) log "Unknown MINER_IMPL='${MINER_IMPL}', defaulting to midnight." midnight_main ;; esac sleep 60 done EOF chmod +x "$WATCHDOG_PATH" echo "Wrote watchdog: $WATCHDOG_PATH" > /dev/tty ############################################## # 6) Write docker-compose.yml (ports for fetcher) ############################################## if [ "$MINER_IMPL" = "midnight" ]; then BASE_CONTAINER_NAME="midnight-miner" elif [ "$MINER_IMPL" = "defensio" ]; then BASE_CONTAINER_NAME="defensio-miner" else BASE_CONTAINER_NAME="fetcher-miner" fi CONTAINER_NAME="$BASE_CONTAINER_NAME" suffix=2 while docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER_NAME"; do CONTAINER_NAME="${BASE_CONTAINER_NAME}-${suffix}" suffix=$((suffix + 1)) done cat > "$COMPOSE_PATH" <> "$COMPOSE_PATH" <> "$COMPOSE_PATH" < /dev/tty ############################################## # 7) Start with docker compose + commands.txt ############################################## echo > /dev/tty echo "Project prepared in: $BASE_DIR" > /dev/tty echo " - config.json" > /dev/tty echo " - watchdog.sh" > /dev/tty echo " - docker-compose.yml" > /dev/tty echo " - data/ (repos, wallets, etc.)" > /dev/tty echo " - logs/ (miner logs)" > /dev/tty echo "Container name: $CONTAINER_NAME" > /dev/tty cd "$BASE_DIR" echo > /dev/tty echo "Starting miner stack via: $DOCKER_COMPOSE_CMD up -d" > /dev/tty $DOCKER_COMPOSE_CMD up -d echo > /dev/tty echo "You can watch progress with:" > /dev/tty echo " cd \"$BASE_DIR\"" > /dev/tty echo " $DOCKER_COMPOSE_CMD logs -f" > /dev/tty COMMANDS_TXT="commands.txt" cat > "$COMMANDS_TXT" <> "$COMMANDS_TXT" <:3001 Hash server health endpoint: http://:9001/health Optional firewall rules (run on the host, as root): UFW: ufw allow 3001/tcp ufw allow 9001/tcp iptables: iptables -A INPUT -p tcp --dport 3001 -j ACCEPT iptables -A INPUT -p tcp --dport 9001 -j ACCEPT EOF fi echo > /dev/tty echo "Commands saved to: $BASE_DIR/$COMMANDS_TXT" > /dev/tty