#!/usr/bin/env bash # Bakemono keeper setup for Linux. Installs kubo + ipfs-cluster-follow from scratch, # points a follower at one board, and runs both under systemd. Safe to re-run. # # curl -fsSL https://raw.githubusercontent.com/bakemonoq/bakemono/main/scripts/keeper-setup.sh | sudo bash -s -- https://board.example [/data/dir] # pass a pre-mounted data dir as the 2nd arg to keep the archive (blocks + follower state) off the root disk set -euo pipefail BOARD_URL="${1:-${BAKEMONO_BOARD_URL:-}}" CLUSTER_NAME="${BAKEMONO_CLUSTER_NAME:-bakemono}" IPFS_USER="ipfs" IPFS_HOME="${2:-${BAKEMONO_IPFS_HOME:-/var/lib/ipfs}}" IPFS_PATH="${IPFS_HOME}/.ipfs" main() { preflight ensure_deps install_kubo install_cluster_follow ensure_user init_ipfs init_follower write_units write_denylist_sync start_services done_message } say() { printf '\033[1;35m::\033[0m %s\n' "$*"; } die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } preflight() { [ "$(uname -s)" = "Linux" ] || die "this installer is Linux only" [ "$(id -u)" -eq 0 ] || die "run as root, e.g. pipe into 'sudo bash'" [ -d /run/systemd/system ] || die "systemd is required" BOARD_URL="${BOARD_URL%/}" case "$BOARD_URL" in http://*|https://*) ;; *) die "pass the board URL, e.g. sudo bash -s -- https://board.example" ;; esac case "$(uname -m)" in x86_64|amd64) ARCH="amd64" ;; aarch64|arm64) ARCH="arm64" ;; armv7l|armv6l) ARCH="arm" ;; i386|i686) ARCH="386" ;; *) die "unsupported architecture $(uname -m)" ;; esac case "$IPFS_HOME" in /*) ;; *) die "data dir must be an absolute path, got $IPFS_HOME" ;; esac say "board $BOARD_URL, arch $ARCH, data $IPFS_HOME" } ensure_deps() { need_cmd curl need_cmd tar need_cmd jq } need_cmd() { command -v "$1" >/dev/null 2>&1 && return say "installing $1" if command -v apt-get >/dev/null 2>&1; then apt-get update -qq && apt-get install -y -qq "$1" elif command -v dnf >/dev/null 2>&1; then dnf install -y -q "$1" elif command -v yum >/dev/null 2>&1; then yum install -y -q "$1" elif command -v pacman >/dev/null 2>&1; then pacman -Sy --noconfirm "$1" elif command -v apk >/dev/null 2>&1; then apk add --no-cache "$1" elif command -v zypper >/dev/null 2>&1; then zypper install -y "$1" else die "install $1 manually, no known package manager found" fi } install_kubo() { if command -v ipfs >/dev/null 2>&1; then say "kubo already installed ($(ipfs --version))" return fi install_dist kubo ipfs } install_cluster_follow() { if command -v ipfs-cluster-follow >/dev/null 2>&1; then say "ipfs-cluster-follow already installed" return fi install_dist ipfs-cluster-follow ipfs-cluster-follow } # fetch the latest stable release of a dist.ipfs.tech component and drop its binary into /usr/local/bin install_dist() { local component="$1" binary="$2" version url tmp version="$(curl -fsSL "https://dist.ipfs.tech/${component}/versions" | grep -v -- '-rc' | tail -n1)" [ -n "$version" ] || die "could not resolve latest $component version" say "installing $component $version" tmp="$(mktemp -d)" url="https://dist.ipfs.tech/${component}/${version}/${component}_${version}_linux-${ARCH}.tar.gz" curl -fsSL "$url" -o "${tmp}/pkg.tar.gz" tar -xzf "${tmp}/pkg.tar.gz" -C "$tmp" install -m0755 "${tmp}/${component}/${binary}" "/usr/local/bin/${binary}" rm -rf "$tmp" } ensure_user() { if ! id "$IPFS_USER" >/dev/null 2>&1; then say "creating system user $IPFS_USER" useradd --system --create-home --home-dir "$IPFS_HOME" --shell /usr/sbin/nologin "$IPFS_USER" fi install -d -o "$IPFS_USER" -g "$IPFS_USER" "$IPFS_HOME" } init_ipfs() { if [ -f "${IPFS_PATH}/config" ]; then say "ipfs repo already initialised" else say "initialising ipfs repo" as_ipfs env IPFS_PATH="$IPFS_PATH" ipfs init fi # serve blocks to peers (recent kubo ships the bitswap server off) and answer wants without DHT luck as_ipfs env IPFS_PATH="$IPFS_PATH" ipfs config --json Bitswap.ServerEnabled true as_ipfs env IPFS_PATH="$IPFS_PATH" ipfs config --json Internal.Bitswap.BroadcastControl.Enable false # kubo >= 0.38 renamed Reprovider.Strategy -> Provide.Strategy and FATALs if the deprecated key lingers if as_ipfs env IPFS_PATH="$IPFS_PATH" ipfs config Provide.Strategy roots 2>/dev/null; then as_ipfs env IPFS_PATH="$IPFS_PATH" ipfs config --json Reprovider '{}' 2>/dev/null || true else as_ipfs env IPFS_PATH="$IPFS_PATH" ipfs config Reprovider.Strategy roots fi # your gateway must serve ONLY blocks you already hold - never fetch arbitrary CIDs from the network # on someone's behalf. this is what keeps it "our files only" and stops it becoming an open relay as_ipfs env IPFS_PATH="$IPFS_PATH" ipfs config --json Gateway.NoFetch true # cap the datastore at ~90% of the disk holding the repo, so GC only kicks in near full instead of the 10G default local total_kib max_gb total_kib="$(df -Pk "$IPFS_HOME" | awk 'NR==2{print $2}')" max_gb=$(( total_kib / 1024 / 1024 * 9 / 10 )) [ "${max_gb:-0}" -ge 1 ] && as_ipfs env IPFS_PATH="$IPFS_PATH" ipfs config Datastore.StorageMax "${max_gb}GB" # nopfs reads denylists from here; the sync unit below keeps it current from the board's manifest, so # a taken-down CID is blocked at your gateway immediately, not only after GC frees the block. nopfs # only watches files that exist at daemon start, so seed an empty one now (the sync rewrites it live) install -d -o "$IPFS_USER" -g "$IPFS_USER" "${IPFS_PATH}/denylists" local deny="${IPFS_PATH}/denylists/bakemono.deny" [ -f "$deny" ] || printf 'version: 1\nname: bakemono\n---\n' > "$deny" chown "$IPFS_USER":"$IPFS_USER" "$deny" } init_follower() { if [ -f "${IPFS_HOME}/.ipfs-cluster-follow/${CLUSTER_NAME}/service.json" ]; then say "follower already initialised" return fi say "initialising follower against ${BOARD_URL}/follower.json" as_ipfs env HOME="$IPFS_HOME" ipfs-cluster-follow "$CLUSTER_NAME" init "${BOARD_URL}/follower.json" } as_ipfs() { sudo -u "$IPFS_USER" "$@"; } write_units() { say "writing systemd units" cat >/etc/systemd/system/ipfs.service </etc/systemd/system/ipfs-cluster-follow.service < root -> denylist CID, all in IPFS) into nopfs # so your gateway blocks it at once. if the board is gone there are no new takedowns, and the last # denylist you synced stays in place write_denylist_sync() { say "writing denylist sync" cat >/usr/local/bin/bakemono-denylist-sync <"\$tmp" && [ -s "\$tmp" ]; then cp "\$tmp" "\${DENYDIR}/bakemono.deny" fi rm -f "\$tmp" SYNC chmod 0755 /usr/local/bin/bakemono-denylist-sync cat >/etc/systemd/system/bakemono-denylist.service </etc/systemd/system/bakemono-denylist.timer </dev/null 2>&1; then return; fi sleep 1 done say "ipfs api slow to come up; the follower will retry on its own" } done_message() { cat <