From 618ef685341c30f55a8fd30216eacf85f12aab00 Mon Sep 17 00:00:00 2001 From: Matthew Jackson Date: Mon, 18 May 2026 21:48:35 -0700 Subject: [PATCH] initial: unraid-1 bootstrap (garage + basement + watchtower + host-agent) --- README.md | 34 +++++++++ unraid-1/basement.template.xml | 48 ++++++++++++ unraid-1/garage.template.xml | 63 ++++++++++++++++ unraid-1/garage.toml | 35 +++++++++ unraid-1/host-agent.template.xml | 62 +++++++++++++++ unraid-1/install.sh | 126 +++++++++++++++++++++++++++++++ unraid-1/watchtower.template.xml | 54 +++++++++++++ 7 files changed, 422 insertions(+) create mode 100644 README.md create mode 100644 unraid-1/basement.template.xml create mode 100644 unraid-1/garage.template.xml create mode 100644 unraid-1/garage.toml create mode 100644 unraid-1/host-agent.template.xml create mode 100755 unraid-1/install.sh create mode 100644 unraid-1/watchtower.template.xml diff --git a/README.md b/README.md new file mode 100644 index 0000000..6db64d8 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# pq/scripts + +Public bootstrap scripts. Only public repo on git.docker.pq.io — everything +else is private. This one is public so machines on the internal network +can `curl` raw URLs anonymously, without embedding tokens. + +Distribution is over the LAN (gitea is not internet-reachable); "public" +here just means anonymous access from the internal network. + +## Layout + +``` +unraid-1/ + install.sh # bootstrap entrypoint + garage.toml # garage S3 config (single-node, lmdb, rf=1) + garage.template.xml # unraid CA template — garage container + basement.template.xml # unraid CA template — basement-ui (garage admin UI) + watchtower.template.xml # unraid CA template — image auto-updater + host-agent.template.xml # unraid CA template — per-host metrics + fan ctrl +``` + +## Usage + +On the unraid-1 console: + +```sh +bash <(curl -sL https://git.docker.pq.io/pq/scripts/raw/branch/main/unraid-1/install.sh) +``` + +The installer is idempotent. Re-run any time to re-fetch templates or pick +up new ones. `FORCE=1` rotates garage's secrets. + +After install: Unraid UI → Docker → Add Container → Template dropdown → +Apply each of `my-garage`, `my-basement`, `my-watchtower`, `my-host-agent`. diff --git a/unraid-1/basement.template.xml b/unraid-1/basement.template.xml new file mode 100644 index 0000000..935509e --- /dev/null +++ b/unraid-1/basement.template.xml @@ -0,0 +1,48 @@ + + + + basement + ghcr.io/mattjackson/basement-ui:latest + https://github.com/mattjackson/basement-ui + bridge + + sh + false + + + basement-ui: admin UI for the local garage S3 container. Caddy on classe terminates TLS for basement.pq.io and reverse-proxies to this container's port 8080. + Network:Web Cloud: + http://[IP]:[PORT:8080]/ + + https://garagehq.deuxfleurs.fr/img/logo.svg + --restart=unless-stopped --add-host=host.docker.internal:host-gateway --label com.centurylinklabs.watchtower.enable=true + + + + + + +Admin UI for the garage container running on this host. + +Reaches garage's admin API at http://host.docker.internal:3903 (the host-gateway alias resolves to unraid-1's docker bridge IP, where the garage container publishes port 3903). The admin token below is shared with the garage container — install.sh pre-fills both. + +Publicly served at https://basement.pq.io via Caddy on classe (reverse proxy → unraid-1:8080). + + + 8080 + + http://host.docker.internal:3903 + __GARAGE_ADMIN_TOKEN__ + diff --git a/unraid-1/garage.template.xml b/unraid-1/garage.template.xml new file mode 100644 index 0000000..ad97499 --- /dev/null +++ b/unraid-1/garage.template.xml @@ -0,0 +1,63 @@ + + + + garage + dxflrs/garage:v1.0.1 + https://hub.docker.com/r/dxflrs/garage + bridge + + sh + false + + https://garagehq.deuxfleurs.fr/ + Single-node Garage S3 server. Caddy on classe terminates TLS for s3.pq.io and reverse-proxies to this container's port 3900. + Backup: Cloud: Network:Other + + + https://garagehq.deuxfleurs.fr/img/logo.svg + --restart=unless-stopped --label com.centurylinklabs.watchtower.enable=true + + + + + + +Single-node Garage S3 (lmdb engine, replication_factor=1). Path-style endpoint: https://s3.pq.io/<bucket>/<key>. + +After the container starts the FIRST time only, initialise the cluster layout from the unraid console: + + docker exec garage /garage status + # copy the node-id from the output + docker exec garage /garage layout assign -z dc1 -c 1T <node-id> + docker exec garage /garage layout apply --version 1 + +Then create a bucket and an access key: + + docker exec garage /garage bucket create my-bucket + docker exec garage /garage key create my-key + docker exec garage /garage bucket allow --read --write --owner my-bucket --key my-key + docker exec garage /garage key info --show-secret my-key + + + 3900 + 3902 + 3903 + + /mnt/user/appdata/garage/meta + /mnt/user/s3/data + /mnt/user/appdata/garage/garage.toml + + __GARAGE_RPC_SECRET__ + __GARAGE_ADMIN_TOKEN__ + __GARAGE_METRICS_TOKEN__ + diff --git a/unraid-1/garage.toml b/unraid-1/garage.toml new file mode 100644 index 0000000..c02ca22 --- /dev/null +++ b/unraid-1/garage.toml @@ -0,0 +1,35 @@ +# Garage v1 single-node config. Lives on unraid-1. +# Secrets (rpc_secret, admin_token, metrics_token) come from env vars +# defined in .env — NOT in this file. See Garage docs: +# https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/ + +metadata_dir = "/var/lib/garage/meta" +data_dir = "/var/lib/garage/data" + +# lmdb is the v1 default; sled was deprecated. lmdb is more crash-resilient +# and the recommended engine for new deployments. +db_engine = "lmdb" + +# Single-node: every object stored once. No HA. +replication_factor = 1 + +rpc_bind_addr = "[::]:3901" +rpc_public_addr = "127.0.0.1:3901" +# rpc_secret -> GARAGE_RPC_SECRET (env) + +[s3_api] +s3_region = "garage" +api_bind_addr = "[::]:3900" +# Path-style: https://s3.pq.io// +# Virtual-host style: https://.s3.pq.io/ (requires DNS wildcard) +root_domain = ".s3.pq.io" + +[s3_web] +bind_addr = "[::]:3902" +root_domain = ".web.s3.pq.io" +index = "index.html" + +[admin] +api_bind_addr = "[::]:3903" +# admin_token -> GARAGE_ADMIN_TOKEN (env) +# metrics_token -> GARAGE_METRICS_TOKEN (env) — used by Prometheus scrape diff --git a/unraid-1/host-agent.template.xml b/unraid-1/host-agent.template.xml new file mode 100644 index 0000000..6f85288 --- /dev/null +++ b/unraid-1/host-agent.template.xml @@ -0,0 +1,62 @@ + + + + host-agent + ghcr.io/mattjackson/host-agent:latest + https://github.com/mattjackson/host-agent + host + + sh + true + + https://github.com/mattjackson/host-agent + Per-host Prometheus exporters (node, cadvisor, ipmi, smartctl, nvidia-gpu) plus Dell PowerEdge fan controller. Each sub-service self-disables if hardware prerequisites are missing — same image runs on Dell servers and consumer Unraid hardware. + Tools: Status: Productivity: + + + https://raw.githubusercontent.com/prometheus/prometheus/main/web/ui/static/img/prometheus_logo_grey.svg + --restart=unless-stopped --cgroupns=host -v /:/host:ro,rslave --label com.centurylinklabs.watchtower.enable=true + + + + + + +host-agent: single container, s6-supervised, bundles per-host metrics + Dell fan control. + +On unraid-1 (consumer hardware): fan-controller + ipmi_exporter + nvidia_gpu_exporter self-disable. node_exporter (CPU/mem/disk/net), cadvisor (per-container), smartctl_exporter (drive SMART), and vmagent (remote_write) run and push to your Prometheus. + +The root mount (/:/host:ro,rslave) is wired via ExtraParams since Unraid's Path config doesn't expose mount propagation flags; rslave is required for node_exporter's rootfs view to track later host mounts. + + + /sys + /var/run/docker.sock + /run/containerd + /var/lib/docker + /dev + /mnt/user/appdata/host-agent + + http://docker-1.internal.pq.io:9090/api/v1/write + + all + utility + diff --git a/unraid-1/install.sh b/unraid-1/install.sh new file mode 100755 index 0000000..b60c886 --- /dev/null +++ b/unraid-1/install.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# unraid-1 installer — sets up the unraid box with the standard fleet of +# containers (garage S3, basement UI, watchtower, host-agent). +# +# Usage on the unraid console: +# bash <(curl -sL https://git.docker.pq.io/pq/scripts/raw/branch/main/unraid-1/install.sh) +# +# What it does: +# 1. Validates this is an unraid host with the array started. +# 2. Confirms the "s3" share exists (parity-protected; you create it). +# 3. mkdir's /mnt/user/appdata/garage/{meta,} and /mnt/user/s3/data. +# 4. Generates 3 garage secrets (rpc, admin, metrics) → secrets.env (preserved). +# 5. Fetches garage.toml → /mnt/user/appdata/garage/garage.toml. +# 6. Fetches 4 unraid container templates (garage / basement / watchtower / +# host-agent), substitutes secrets where needed, writes them to +# /boot/config/plugins/dockerMan/templates-user/my-*.xml. +# +# After: Unraid UI → Docker → Add Container → Template dropdown → Apply each. +# +# Idempotent: re-running keeps existing secrets and garage.toml. FORCE=1 +# rotates the garage secrets (you'll then need to re-Apply garage + basement). + +set -euo pipefail + +BASE="${UNRAID1_INSTALL_BASE:-https://git.docker.pq.io/pq/scripts/raw/branch/main/unraid-1}" + +APPDATA=/mnt/user/appdata/garage +DATADIR=/mnt/user/s3/data +TEMPLATE_DIR=/boot/config/plugins/dockerMan/templates-user +TOML_DEST="$APPDATA/garage.toml" +SECRETS="$APPDATA/secrets.env" + +say() { printf "install.sh: %s\n" "$*"; } +die() { printf "install.sh: %s\n" "$*" >&2; exit 1; } + +# ── Sanity ── +[ -d /boot/config/plugins/dockerMan ] || die "no /boot/config/plugins/dockerMan — not an unraid host?" +[ -d /mnt/user ] || die "/mnt/user missing — is the array started?" +[ -d /mnt/user/s3 ] || die "share /mnt/user/s3 missing. Create it in unraid UI first (Shares → Add Share → 's3', cache:yes recommended), then re-run." + +mkdir -p "$APPDATA/meta" "$DATADIR" "$TEMPLATE_DIR" + +# ── garage.toml ── +if [ -f "$TOML_DEST" ] && [ "${FORCE:-}" != "1" ]; then + say "$TOML_DEST exists — keeping (FORCE=1 to overwrite)" +else + say "fetching $BASE/garage.toml" + curl -fsSL "$BASE/garage.toml" -o "$TOML_DEST" +fi + +# ── Secrets ── +if [ -f "$SECRETS" ] && [ "${FORCE:-}" != "1" ]; then + say "reusing existing secrets at $SECRETS (FORCE=1 to rotate)" + # shellcheck disable=SC1090 + . "$SECRETS" +else + GARAGE_RPC_SECRET=$(openssl rand -hex 32) + GARAGE_ADMIN_TOKEN=$(openssl rand -hex 32) + GARAGE_METRICS_TOKEN=$(openssl rand -hex 32) + umask 077 + cat > "$SECRETS" < "$dest" + else + cp "$tmp" "$dest" + fi + rm -f "$tmp" + say "wrote $dest" +} + +fetch_template \ + "$BASE/garage.template.xml" \ + "$TEMPLATE_DIR/my-garage.xml" \ + -e "s|__GARAGE_RPC_SECRET__|$GARAGE_RPC_SECRET|" \ + -e "s|__GARAGE_ADMIN_TOKEN__|$GARAGE_ADMIN_TOKEN|" \ + -e "s|__GARAGE_METRICS_TOKEN__|$GARAGE_METRICS_TOKEN|" + +fetch_template \ + "$BASE/basement.template.xml" \ + "$TEMPLATE_DIR/my-basement.xml" \ + -e "s|__GARAGE_ADMIN_TOKEN__|$GARAGE_ADMIN_TOKEN|" + +fetch_template \ + "$BASE/watchtower.template.xml" \ + "$TEMPLATE_DIR/my-watchtower.xml" + +fetch_template \ + "$BASE/host-agent.template.xml" \ + "$TEMPLATE_DIR/my-host-agent.xml" + +cat <<'EOF' + +──────────────────────────────────────── +Done. Apply each template in Unraid UI → Docker → Add Container → Template dropdown: + + 1. "my-garage" → Apply. Initialise cluster after first start: + docker exec garage /garage status # copy node id + docker exec garage /garage layout assign -z dc1 -c 1T + docker exec garage /garage layout apply --version 1 + Smoke test from your laptop: + aws --endpoint-url https://s3.pq.io --region garage s3 ls + + 2. "my-basement" → Apply. Reachable at https://basement.pq.io. + + 3. "my-watchtower" → Apply. Auto-updates the labelled containers above + below. + + 4. "my-host-agent" → Apply. Pushes node/cadvisor/smartctl metrics to your + Prometheus (defaults to docker-1.internal.pq.io:9090). +──────────────────────────────────────── +EOF diff --git a/unraid-1/watchtower.template.xml b/unraid-1/watchtower.template.xml new file mode 100644 index 0000000..930eea1 --- /dev/null +++ b/unraid-1/watchtower.template.xml @@ -0,0 +1,54 @@ + + + + watchtower + nickfedor/watchtower:1.17.0 + https://hub.docker.com/r/nickfedor/watchtower + bridge + + sh + false + + https://github.com/nicholas-fedor/watchtower + Auto-updates running containers when newer images are published. Opt-in: only watches containers labelled com.centurylinklabs.watchtower.enable=true (garage + basement both carry this). + Tools: Utilities: + + + https://raw.githubusercontent.com/containrrr/watchtower/main/logo.png + --restart=unless-stopped + + + + + + +Watches all containers with the label `com.centurylinklabs.watchtower.enable=true` and pulls + restarts them when a newer image tag is published. + +Configured identically to the watchtower service on classe (docker-server/docker-compose.yml) so unraid + classe behave the same way: + + - polls every 300s (5min — fleet-wide default; gentle on ghcr/dockerhub rate limits) + - cleans up the old image after replacing a container + - opt-in only (WATCHTOWER_LABEL_ENABLE=true) — does NOT touch unlabelled containers + +Does not watch itself: a new Watchtower version requires re-Applying this template (or manually pulling). + + + /var/run/docker.sock + + 300 + true + true +