feat(kanrisha): add two-phase deployment scaffold (host prep + ScoutFS)
Add deployments/kanrisha: a phased, self-contained deploy.sh for a custom app that runs on a ScoutFS filesystem. Alma/Rocky (RHEL family) only for now. Phase 1 (host prep) is complete and automated: EPEL + the host package set (git tar sparse mt-st mtx sg3_utils pciutils mbuffer wget curl zstd jq bash-completion sos lsof), Docker, and ScoutFS via setup-scoutfs.sh with a prompted-or-globals license key. It then prints a handoff: create + mount a ScoutFS filesystem, then re-run with SCOUTFS_PATH set. Phase 2 (deploy the stack behind Caddy/TLS) is wired but its compose/Caddyfile content is a clearly-marked STUB pending the real Kanrisha compose file. The script auto-selects the phase from whether SCOUTFS_PATH is a live ScoutFS mount; idempotent and re-runnable. Wiring: register kanrisha in automations.sh (DEPLOYMENTS + ask_deployment_vars), add SCOUTFS_LICENSE to globals.env.example, and document it in README (deployments table + Alma/Rocky-only note). No oslib.sh change needed -- os_detect already folds rocky into the rhel family. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -108,6 +108,7 @@ deployments/<name>/ # one folder per stack
|
||||
| [`headscale`](deployments/headscale/) | Self-hosted Tailscale control server, OIDC login. | pocket-id (OIDC) |
|
||||
| [`webfinger`](deployments/webfinger/) | Serves `/.well-known/webfinger` for OIDC discovery; redirects the rest. | pocket-id (issuer) |
|
||||
| [`simplex`](deployments/simplex/) | SimpleX SMP + XFTP relay with Tor hidden services + encrypted backups. | globals/age-pubkey.txt |
|
||||
| [`kanrisha`](deployments/kanrisha/) | Custom app on a ScoutFS filesystem; two-phase install (host prep + ScoutFS, then the Docker stack). **Alma/Rocky only.** | ScoutFS (host) |
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -149,6 +150,10 @@ path, hostname, boot hooks, and the sshguard log source/backend.
|
||||
**simplex** remains **Alpine-targeted** — it depends on `awall` and Tor hidden
|
||||
services with Alpine-specific wiring, so it isn't part of the tri-distro set.
|
||||
|
||||
**kanrisha** is the inverse — **Alma/Rocky-targeted**. It installs ScoutFS (a
|
||||
RHEL-only kernel filesystem) and its host package set via `dnf`, so it runs on
|
||||
the RHEL family only for now.
|
||||
|
||||
## Host firewall
|
||||
|
||||
[`scripts/harden-firewall.sh`](scripts/harden-firewall.sh) installs a
|
||||
|
||||
+14
-1
@@ -48,7 +48,7 @@ fi
|
||||
. "$ROOT/scripts/lib.sh"
|
||||
load_globals
|
||||
|
||||
DEPLOYMENTS=(pocket-id beszel headscale webfinger simplex)
|
||||
DEPLOYMENTS=(pocket-id beszel headscale webfinger simplex kanrisha)
|
||||
SCRIPTS=(setup-host harden-ssh harden-jumphost sshuser auto-update)
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
@@ -100,6 +100,19 @@ ask_deployment_vars() {
|
||||
ask XFTP_QUOTA "XFTP disk quota" optional
|
||||
ask SSH_PORT "SSH port" optional
|
||||
ask ALLOWED_IP "Your IP to whitelist in sshguard" optional ;;
|
||||
kanrisha)
|
||||
# License: reuse globals.env value if present, else prompt for it.
|
||||
if [[ -z "${SCOUTFS_LICENSE:-}" ]]; then
|
||||
ask SCOUTFS_LICENSE "ScoutFS license key"
|
||||
else
|
||||
ENVS+=("SCOUTFS_LICENSE=$SCOUTFS_LICENSE")
|
||||
fi
|
||||
# Blank path -> phase 1 (host prep). A live path -> phase 2 (deploy).
|
||||
ask SCOUTFS_PATH "ScoutFS mount path (blank = host prep / phase 1)" optional
|
||||
if [[ -n "${SCOUTFS_PATH:-}" ]]; then
|
||||
ask KANRISHA_DOMAIN "Public hostname (e.g. kanrisha.example.com)"
|
||||
ask ACME_EMAIL "Let's Encrypt email"
|
||||
fi ;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copy to .env and fill in. docker compose picks .env up automatically.
|
||||
# deploy.sh (phase 2) seeds .env from this on first run; an existing .env is
|
||||
# never overwritten.
|
||||
|
||||
# ─── Public hostname ────────────────────────────────────────────────────────
|
||||
# Bare hostname (no scheme) where Kanrisha will be reached. DNS must point here
|
||||
# and 80/443 be reachable before deploy, or the Let's Encrypt request fails.
|
||||
KANRISHA_DOMAIN=kanrisha.example.com
|
||||
|
||||
# Email for Let's Encrypt registration / expiry notifications.
|
||||
ACME_EMAIL=admin@example.com
|
||||
|
||||
# ─── ScoutFS ────────────────────────────────────────────────────────────────
|
||||
# Absolute path to the mounted ScoutFS filesystem Kanrisha operates on. You
|
||||
# create + mount this between phase 1 and phase 2 (see README). Bind-mounted
|
||||
# into the Kanrisha container by docker-compose.yml.
|
||||
SCOUTFS_PATH=/mnt/kanrisha
|
||||
|
||||
# ─── Image tags ─────────────────────────────────────────────────────────────
|
||||
CADDY_TAG=2-alpine
|
||||
# TODO (part two): set the real Kanrisha image + tag once the compose lands.
|
||||
# KANRISHA_IMAGE=
|
||||
# KANRISHA_TAG=latest
|
||||
@@ -0,0 +1,32 @@
|
||||
# Caddyfile for the kanrisha stack.
|
||||
#
|
||||
# Auto-issues a Let's Encrypt cert for $KANRISHA_DOMAIN and reverse-proxies to
|
||||
# the Kanrisha app container.
|
||||
#
|
||||
# STUB: the upstream below points at the placeholder service on :80. Update the
|
||||
# reverse_proxy target (service name + port) to match the real Kanrisha service
|
||||
# in docker-compose.yml when you wire in part two.
|
||||
|
||||
{
|
||||
email {$ACME_EMAIL}
|
||||
}
|
||||
|
||||
{$KANRISHA_DOMAIN} {
|
||||
encode zstd gzip
|
||||
|
||||
reverse_proxy kanrisha:80 { # TODO: real Kanrisha service + port
|
||||
header_up X-Real-IP {http.request.remote.host}
|
||||
}
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
X-Content-Type-Options "nosniff"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
-Server
|
||||
}
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format console
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
# kanrisha
|
||||
|
||||
Kanrisha (custom application) deployed as a Docker Compose stack behind Caddy,
|
||||
running on a **ScoutFS** filesystem. **Alma / Rocky Linux only** for now — it
|
||||
installs ScoutFS (a RHEL-only kernel filesystem) and a host package set via
|
||||
`dnf`.
|
||||
|
||||
> **Status:** the host-prep half (phase 1) is complete; the application stack
|
||||
> (phase 2) is scaffolded with a **stub** `docker-compose.yml` / `Caddyfile`
|
||||
> pending the real Kanrisha compose file. See the TODOs in those files.
|
||||
|
||||
## Two-phase deploy
|
||||
|
||||
Kanrisha runs on a ScoutFS filesystem you create by hand (it depends on your
|
||||
storage layout), so [`deploy.sh`](deploy.sh) is split into two phases and runs
|
||||
whichever fits when you invoke it — decided by whether `SCOUTFS_PATH` points at
|
||||
a live ScoutFS mount.
|
||||
|
||||
**Phase 1 — host prep (automated):**
|
||||
1. `dnf install epel-release`
|
||||
2. `dnf install git tar sparse mt-st mtx sg3_utils pciutils mbuffer wget curl zstd jq bash-completion sos lsof`
|
||||
3. Install Docker.
|
||||
4. Install ScoutFS: `curl -fsSL https://rpm-1.srvno.de/setup-scoutfs.sh | bash -s -- --license <KEY>`
|
||||
5. Print the steps to create your ScoutFS filesystem, then stop.
|
||||
|
||||
**You do (manual):** create + mount a ScoutFS filesystem, e.g. at
|
||||
`/mnt/kanrisha` (and add it to `/etc/fstab`).
|
||||
|
||||
**Phase 2 — deploy the stack (automated):** lay down the compose + Caddyfile,
|
||||
seed `.env`, open 80/443, pull images, bring the stack up behind Caddy/TLS.
|
||||
|
||||
## Required values
|
||||
|
||||
| Variable | Phase | Notes |
|
||||
|----------|-------|-------|
|
||||
| `SCOUTFS_LICENSE` | 1 | License key for `setup-scoutfs.sh`. Prompted, or set in `globals/globals.env`. |
|
||||
| `SCOUTFS_PATH` | 2 | Absolute path to the mounted ScoutFS filesystem. Its presence is what triggers phase 2. |
|
||||
| `KANRISHA_DOMAIN` | 2 | Public hostname (e.g. `kanrisha.example.com`). |
|
||||
| `ACME_EMAIL` | 2 | Let's Encrypt registration email. |
|
||||
|
||||
See [`.env.example`](.env.example) for image tags.
|
||||
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
./automations.sh # Deploy on this host → deploy: kanrisha
|
||||
```
|
||||
|
||||
Or build + run the self-contained artifact:
|
||||
|
||||
```bash
|
||||
./build.sh
|
||||
scp deploy.sh root@host:
|
||||
ssh root@host 'bash deploy.sh' # phase 1 (prompts for license)
|
||||
# ... create + mount your ScoutFS filesystem on the host ...
|
||||
ssh root@host 'SCOUTFS_PATH=/mnt/kanrisha KANRISHA_DOMAIN=kanrisha.example.com \
|
||||
ACME_EMAIL=me@example.com bash deploy.sh' # phase 2
|
||||
```
|
||||
|
||||
Unattended provisioning (phase 1): [`cloud-init.yml`](cloud-init.yml).
|
||||
|
||||
## Notes
|
||||
|
||||
- **Rebuild after editing loose files.** `docker-compose.yml`, `Caddyfile`, and
|
||||
`.env.example` are embedded into `deploy.sh` by [`build.sh`](build.sh); re-run
|
||||
it after any edit or the deploy ships the stale copy.
|
||||
- DNS for `KANRISHA_DOMAIN` must resolve to the host and 80/443 be reachable
|
||||
before phase 2, or the Let's Encrypt cert request fails.
|
||||
- The ScoutFS `mkfs`/`mount` recipe in the phase-1 handoff is a placeholder —
|
||||
fill in your canonical commands.
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# build.sh -- (re)embed docker-compose.yml, Caddyfile, .env.example into
|
||||
# deploy.sh as a base64-encoded tar.gz payload after __ARCHIVE_BELOW__.
|
||||
# Idempotent: strips any existing payload first.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
SCRIPT="$DIR/deploy.sh"
|
||||
MARKER="__ARCHIVE_BELOW__"
|
||||
|
||||
[[ -f "$SCRIPT" ]] || { echo "deploy.sh not found at $SCRIPT" >&2; exit 1; }
|
||||
for f in docker-compose.yml Caddyfile .env.example; do
|
||||
[[ -f "$DIR/$f" ]] || { echo "Missing $DIR/$f" >&2; exit 1; }
|
||||
done
|
||||
|
||||
PAYLOAD=$(tar -czf - -C "$DIR" docker-compose.yml Caddyfile .env.example | base64)
|
||||
|
||||
TMP=$(mktemp)
|
||||
trap 'rm -f "$TMP"' EXIT
|
||||
|
||||
sed "/^${MARKER}\$/,\$d" "$SCRIPT" > "$TMP"
|
||||
{
|
||||
echo "$MARKER"
|
||||
echo "$PAYLOAD"
|
||||
} >> "$TMP"
|
||||
|
||||
mv "$TMP" "$SCRIPT"
|
||||
chmod +x "$SCRIPT"
|
||||
trap - EXIT
|
||||
|
||||
size=$(wc -c < "$SCRIPT")
|
||||
echo "Built $SCRIPT (${size} bytes)"
|
||||
@@ -0,0 +1,44 @@
|
||||
#cloud-config
|
||||
#
|
||||
# Kanrisha (phase 1) -- harden SSH, then run host prep + ScoutFS install on a
|
||||
# fresh Alma / Rocky host.
|
||||
#
|
||||
# This covers PHASE 1 ONLY. Kanrisha needs a ScoutFS filesystem that you create
|
||||
# by hand (it depends on your storage layout), so after first boot:
|
||||
# 1. read the handoff at the end of /var/log/cloud-init-output.log
|
||||
# 2. create + mount your ScoutFS filesystem (e.g. at /mnt/kanrisha)
|
||||
# 3. finish with phase 2:
|
||||
# SCOUTFS_PATH=/mnt/kanrisha KANRISHA_DOMAIN=... ACME_EMAIL=... \
|
||||
# SKIP_PROMPTS=1 bash /opt/automations/deployments/kanrisha/deploy.sh
|
||||
#
|
||||
# Fill in REPO_URL, SCOUTFS_LICENSE, and the values in the runcmd block, then
|
||||
# paste this as the instance user-data.
|
||||
|
||||
packages:
|
||||
- git
|
||||
|
||||
runcmd:
|
||||
- hostnamectl set-hostname kanrisha || true
|
||||
- |
|
||||
set -e
|
||||
REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git
|
||||
REPO_BRANCH=main
|
||||
HARDEN_SSH=1 # harden SSH on this fresh VM (set 0 to skip)
|
||||
SSH_PORT=22
|
||||
ALLOWED_IP= # optional: whitelist your client IP in sshguard
|
||||
SCOUTFS_LICENSE= # <-- your ScoutFS license key (required)
|
||||
git clone --depth 1 --branch "$REPO_BRANCH" "$REPO_URL" /opt/automations
|
||||
cd /opt/automations
|
||||
|
||||
# Harden SSH on this fresh VM: PQ KEX, key-only auth, sshguard. Seeds root
|
||||
# from globals/authorized_keys (or SSH_KEYS_URL).
|
||||
if [ "$HARDEN_SSH" = 1 ]; then
|
||||
SSH_PORT="$SSH_PORT" ALLOWED_IP="$ALLOWED_IP" SKIP_PROMPTS=1 FORCE=1 \
|
||||
bash scripts/harden-ssh.sh
|
||||
fi
|
||||
|
||||
# Phase 1: EPEL + host packages + Docker + ScoutFS. With no SCOUTFS_PATH set
|
||||
# this stops after install and prints the next steps (create the ScoutFS
|
||||
# filesystem, then run phase 2).
|
||||
SCOUTFS_LICENSE="$SCOUTFS_LICENSE" SKIP_PROMPTS=1 \
|
||||
bash deployments/kanrisha/deploy.sh
|
||||
@@ -0,0 +1,374 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# deploy.sh -- deploy Kanrisha (custom app on a ScoutFS filesystem) on
|
||||
# Alma / Rocky Linux. Two-phase, idempotent, self-contained.
|
||||
#
|
||||
# Kanrisha runs on a ScoutFS filesystem that you create by hand (it depends on
|
||||
# your storage layout), so the deploy splits into two phases and this one
|
||||
# script runs whichever is appropriate when you invoke it:
|
||||
#
|
||||
# PHASE 1 -- host prep (no ScoutFS path yet):
|
||||
# 1. Installs EPEL + the host package set Kanrisha needs.
|
||||
# 2. Installs Docker.
|
||||
# 3. Installs ScoutFS from the vendor repo (setup-scoutfs.sh + license).
|
||||
# 4. Prints the steps to create your ScoutFS filesystem, then exits.
|
||||
#
|
||||
# <you create + mount a ScoutFS filesystem, e.g. at /mnt/kanrisha>
|
||||
#
|
||||
# PHASE 2 -- deploy the stack (SCOUTFS_PATH points at that mount):
|
||||
# 5. Lays down docker-compose.yml + Caddyfile + .env in $STACK_DIR.
|
||||
# 6. Opens 80/443, pulls images, brings the stack up behind Caddy/TLS.
|
||||
#
|
||||
# The phase is chosen automatically: if SCOUTFS_PATH is set and is a live
|
||||
# ScoutFS mount, it deploys; otherwise it preps the host. Re-run any time.
|
||||
#
|
||||
# Self-contained: docker-compose.yml, Caddyfile, .env.example are embedded as a
|
||||
# base64 tar.gz after __ARCHIVE_BELOW__. Rebuild with build.sh after editing the
|
||||
# loose source files. (The host-prep phase needs none of them.)
|
||||
#
|
||||
# Alma / Rocky (RHEL family) only for now -- it installs ScoutFS (a RHEL-only
|
||||
# kernel filesystem) and host packages via dnf.
|
||||
#
|
||||
# Usage:
|
||||
# bash deploy.sh # phase 1 (host prep)
|
||||
# SCOUTFS_LICENSE=... bash deploy.sh # phase 1, license preset
|
||||
# SCOUTFS_PATH=/mnt/kanrisha KANRISHA_DOMAIN=k.example.com \
|
||||
# ACME_EMAIL=me@example.com bash deploy.sh # phase 2 (deploy)
|
||||
# SKIP_PROMPTS=1 ... bash deploy.sh # non-interactive (cloud-init)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${STACK_DIR:=/srv/kanrisha}"
|
||||
: "${SKIP_DOCKER_INSTALL:=0}"
|
||||
: "${FORCE:=0}"
|
||||
: "${SKIP_PROMPTS:=0}" # non-interactive: require values via env, no prompts
|
||||
[[ "$SKIP_PROMPTS" == "1" ]] && FORCE=1
|
||||
: "${SCOUTFS_LICENSE:=}"
|
||||
: "${SCOUTFS_PATH:=}"
|
||||
: "${SCOUTFS_SETUP_URL:=https://rpm-1.srvno.de/setup-scoutfs.sh}"
|
||||
: "${KANRISHA_DOMAIN:=}"
|
||||
: "${ACME_EMAIL:=}"
|
||||
|
||||
# Host packages Kanrisha needs in place before ScoutFS + the stack.
|
||||
HOST_PACKAGES=(git tar sparse mt-st mtx sg3_utils pciutils mbuffer wget curl
|
||||
zstd jq bash-completion sos lsof)
|
||||
|
||||
log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; }
|
||||
die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
[[ $EUID -eq 0 ]] || die "Run as root."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OS detection. This deploy.sh is self-contained (scp'd standalone), so the OS
|
||||
# logic is inlined here instead of sourced from scripts/oslib.sh.
|
||||
# ---------------------------------------------------------------------------
|
||||
osfam() {
|
||||
local id="" like=""
|
||||
if [[ -r /etc/os-release ]]; then
|
||||
id="$(. /etc/os-release 2>/dev/null && echo "${ID:-}")"
|
||||
like="$(. /etc/os-release 2>/dev/null && echo "${ID_LIKE:-}")"
|
||||
fi
|
||||
case " $id $like " in
|
||||
*" alpine "*) echo alpine ;;
|
||||
*" debian "*|*" ubuntu "*) echo debian ;;
|
||||
*" rhel "*|*" fedora "*|*" centos "*) echo rhel ;;
|
||||
*) echo "${id:-unknown}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
require_rhel() {
|
||||
[[ "$(osfam)" == "rhel" ]] || \
|
||||
die "Kanrisha supports Alma / Rocky Linux (RHEL family) only for now (detected: $(osfam))."
|
||||
}
|
||||
|
||||
install_docker() {
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
log "Docker already installed: $(docker --version)"
|
||||
return 0
|
||||
fi
|
||||
log "Installing Docker..."
|
||||
command -v curl >/dev/null 2>&1 || dnf install -y -q curl
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable --now docker >/dev/null 2>&1 || systemctl start docker || true
|
||||
}
|
||||
|
||||
# Is the host already prepared (ScoutFS tooling present)?
|
||||
scoutfs_installed() {
|
||||
command -v mkfs.scoutfs >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Is SCOUTFS_PATH a directory sitting on a live ScoutFS mount?
|
||||
scoutfs_path_ready() {
|
||||
[[ -n "$SCOUTFS_PATH" && -d "$SCOUTFS_PATH" ]] || return 1
|
||||
if command -v findmnt >/dev/null 2>&1; then
|
||||
[[ "$(findmnt -no FSTYPE --target "$SCOUTFS_PATH" 2>/dev/null)" == "scoutfs" ]] || return 1
|
||||
else
|
||||
mountpoint -q "$SCOUTFS_PATH" 2>/dev/null || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
open_web_ports() {
|
||||
# Register 80/443 for this stack. Prefer the host firewall
|
||||
# (harden-firewall.sh) when present; else fall back to firewalld/ufw if
|
||||
# active (no-op when neither is).
|
||||
#
|
||||
# NOTE: Caddy publishes 80/443 via Docker, which reaches the host through
|
||||
# nat/FORWARD and BYPASSES the INPUT firewall -- so this is belt-and-braces
|
||||
# for any host-bound bind and self-documents the stack ports.
|
||||
if [[ -d /etc/firewall/ports.d && -x /usr/local/sbin/firewall-apply ]]; then
|
||||
log "Registering 80,443/tcp with host firewall..."
|
||||
printf '80/tcp\n443/tcp\n' > /etc/firewall/ports.d/kanrisha.rule
|
||||
/usr/local/sbin/firewall-apply
|
||||
elif command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
|
||||
log "firewalld active -- allowing http,https..."
|
||||
firewall-cmd -q --add-service=http --permanent
|
||||
firewall-cmd -q --add-service=https --permanent
|
||||
firewall-cmd -q --reload
|
||||
elif command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then
|
||||
log "ufw active -- allowing 80,443/tcp..."
|
||||
ufw allow 80/tcp >/dev/null; ufw allow 443/tcp >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
prompt() {
|
||||
local varname="$1" message="$2"
|
||||
local -n ref="$varname"
|
||||
if [[ -z "${ref:-}" ]]; then
|
||||
[[ "$SKIP_PROMPTS" == "1" ]] && die "$varname required (set it in the environment; running with SKIP_PROMPTS=1)."
|
||||
read -r -p "$message: " ref
|
||||
[[ -n "$ref" ]] || die "$varname required."
|
||||
fi
|
||||
}
|
||||
|
||||
# ===========================================================================
|
||||
# PHASE 1 -- host preparation + ScoutFS
|
||||
# ===========================================================================
|
||||
phase1_prep() {
|
||||
require_rhel
|
||||
log "Phase 1/2: preparing host (EPEL, packages, Docker, ScoutFS)."
|
||||
|
||||
log "Installing EPEL release..."
|
||||
dnf install -y -q epel-release || die "Failed to install epel-release."
|
||||
|
||||
log "Installing ${#HOST_PACKAGES[@]} host packages..."
|
||||
dnf install -y -q "${HOST_PACKAGES[@]}" || die "Failed to install host packages."
|
||||
|
||||
if [[ "$SKIP_DOCKER_INSTALL" != "1" ]]; then
|
||||
install_docker
|
||||
fi
|
||||
|
||||
if scoutfs_installed; then
|
||||
log "ScoutFS already installed (mkfs.scoutfs present); skipping vendor setup."
|
||||
else
|
||||
if [[ -z "$SCOUTFS_LICENSE" ]]; then
|
||||
[[ "$SKIP_PROMPTS" == "1" ]] && \
|
||||
die "SCOUTFS_LICENSE required (set it in the environment or globals.env; running with SKIP_PROMPTS=1)."
|
||||
read -r -p "ScoutFS license key: " SCOUTFS_LICENSE
|
||||
[[ -n "$SCOUTFS_LICENSE" ]] || die "SCOUTFS_LICENSE required."
|
||||
fi
|
||||
log "Installing ScoutFS from ${SCOUTFS_SETUP_URL}..."
|
||||
curl -fsSL "$SCOUTFS_SETUP_URL" | bash -s -- --license "$SCOUTFS_LICENSE" \
|
||||
|| die "ScoutFS setup failed."
|
||||
scoutfs_installed || \
|
||||
warn "setup-scoutfs.sh finished but mkfs.scoutfs is not on PATH yet -- check the vendor output."
|
||||
fi
|
||||
|
||||
print_handoff
|
||||
}
|
||||
|
||||
print_handoff() {
|
||||
cat <<EOF
|
||||
|
||||
================================================================
|
||||
KANRISHA -- HOST PREP COMPLETE (phase 1 of 2)
|
||||
|
||||
Installed: EPEL, host packages, Docker, and ScoutFS.
|
||||
ScoutFS is ready to use.
|
||||
|
||||
NEXT -- create the filesystem Kanrisha will run on:
|
||||
|
||||
1. Identify the device(s) for ScoutFS:
|
||||
lsblk
|
||||
2. Make a ScoutFS filesystem (your metadata + data devices):
|
||||
mkfs.scoutfs ... # <-- your canonical recipe
|
||||
3. Mount it, and add it to /etc/fstab so it survives reboot:
|
||||
mkdir -p /mnt/kanrisha
|
||||
mount -t scoutfs ... /mnt/kanrisha
|
||||
|
||||
THEN -- finish the deploy (phase 2):
|
||||
|
||||
SCOUTFS_PATH=/mnt/kanrisha \\
|
||||
KANRISHA_DOMAIN=kanrisha.example.com \\
|
||||
ACME_EMAIL=you@example.com \\
|
||||
bash deploy.sh
|
||||
|
||||
(or re-run ./automations.sh, pick "deploy: kanrisha", and give the path)
|
||||
================================================================
|
||||
EOF
|
||||
}
|
||||
|
||||
# ===========================================================================
|
||||
# PHASE 2 -- deploy the stack
|
||||
# ===========================================================================
|
||||
phase2_deploy() {
|
||||
require_rhel
|
||||
log "Phase 2/2: deploying the Kanrisha stack on ${SCOUTFS_PATH}."
|
||||
|
||||
prompt KANRISHA_DOMAIN "Public hostname for Kanrisha (e.g. kanrisha.example.com)"
|
||||
prompt ACME_EMAIL "Let's Encrypt email"
|
||||
|
||||
if [[ "$SKIP_DOCKER_INSTALL" != "1" ]]; then
|
||||
install_docker
|
||||
fi
|
||||
command -v docker >/dev/null 2>&1 || \
|
||||
die "Docker is not installed (run phase 1 first, or unset SKIP_DOCKER_INSTALL)."
|
||||
|
||||
# --- extract embedded archive ---
|
||||
SCRIPT_DIR=$(mktemp -d -t kanrisha-deploy.XXXXXX)
|
||||
trap 'rm -rf "$SCRIPT_DIR"' EXIT
|
||||
if grep -q -a '^__ARCHIVE_BELOW__$' "$0"; then
|
||||
log "Extracting embedded deployment files..."
|
||||
grep -a -A 9999999 '^__ARCHIVE_BELOW__$' "$0" | tail -n +2 | base64 -d | tar -xz -C "$SCRIPT_DIR"
|
||||
else
|
||||
die "No embedded archive found. Run build.sh to embed deployment files."
|
||||
fi
|
||||
for f in docker-compose.yml Caddyfile .env.example; do
|
||||
[[ -f "$SCRIPT_DIR/$f" ]] || die "Embedded archive missing $f"
|
||||
done
|
||||
|
||||
open_web_ports
|
||||
|
||||
log "Setting up $STACK_DIR..."
|
||||
install -d -m 0750 "$STACK_DIR"
|
||||
install -m 0640 "$SCRIPT_DIR/docker-compose.yml" "$STACK_DIR/docker-compose.yml"
|
||||
install -m 0640 "$SCRIPT_DIR/Caddyfile" "$STACK_DIR/Caddyfile"
|
||||
|
||||
ENV_FILE="$STACK_DIR/.env"
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
log "Seeding $ENV_FILE..."
|
||||
install -m 0600 "$SCRIPT_DIR/.env.example" "$ENV_FILE"
|
||||
sed -i \
|
||||
-e "s|^KANRISHA_DOMAIN=.*|KANRISHA_DOMAIN=${KANRISHA_DOMAIN}|" \
|
||||
-e "s|^ACME_EMAIL=.*|ACME_EMAIL=${ACME_EMAIL}|" \
|
||||
-e "s|^SCOUTFS_PATH=.*|SCOUTFS_PATH=${SCOUTFS_PATH}|" \
|
||||
"$ENV_FILE"
|
||||
else
|
||||
log ".env exists; leaving it alone."
|
||||
fi
|
||||
|
||||
missing=()
|
||||
for var in KANRISHA_DOMAIN ACME_EMAIL SCOUTFS_PATH; do
|
||||
grep -E "^${var}=.+$" "$ENV_FILE" >/dev/null || missing+=("$var")
|
||||
done
|
||||
(( ${#missing[@]} == 0 )) || die "Missing values in $ENV_FILE: ${missing[*]}"
|
||||
|
||||
if [[ "$FORCE" != "1" ]]; then
|
||||
cat <<EOF
|
||||
|
||||
About to pull images and start the stack from $STACK_DIR.
|
||||
|
||||
Caddy will request a Let's Encrypt cert for ${KANRISHA_DOMAIN}. DNS for
|
||||
that name must already point at this host, and ports 80/443 must be
|
||||
reachable from the internet.
|
||||
|
||||
Continue? [y/N]
|
||||
EOF
|
||||
read -r ans
|
||||
[[ "${ans,,}" == "y" || "${ans,,}" == "yes" ]] || { warn "Aborted."; exit 0; }
|
||||
fi
|
||||
|
||||
cd "$STACK_DIR"
|
||||
log "Pulling images..."
|
||||
docker compose pull
|
||||
log "Starting stack..."
|
||||
docker compose up -d --remove-orphans
|
||||
|
||||
log "Waiting for services to become healthy (up to 120s)..."
|
||||
deadline=$(( $(date +%s) + 120 ))
|
||||
while (( $(date +%s) < deadline )); do
|
||||
status=$(docker compose ps --format '{{.Service}} {{.Health}}' 2>/dev/null || true)
|
||||
unhealthy=$(echo "$status" | awk '$2 != "healthy" && $2 != "" {print $1}')
|
||||
if [[ -z "$unhealthy" && -n "$status" ]]; then
|
||||
log "All services healthy."
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo
|
||||
log "Stack status:"
|
||||
docker compose ps
|
||||
echo
|
||||
cat <<EOF
|
||||
================================================================
|
||||
KANRISHA DEPLOYED
|
||||
|
||||
URL: https://${KANRISHA_DOMAIN}
|
||||
Stack dir: ${STACK_DIR}
|
||||
ScoutFS: ${SCOUTFS_PATH}
|
||||
|
||||
Manage:
|
||||
docker compose logs -f
|
||||
docker compose pull && docker compose up -d # update
|
||||
docker compose down # stop, keep volumes
|
||||
docker compose down -v # stop, WIPE caddy volumes
|
||||
|
||||
Or just re-run this script -- it's idempotent.
|
||||
|
||||
NOTE: the Kanrisha service in docker-compose.yml is a STUB pending the real
|
||||
compose file. Drop it in, run build.sh, then re-run this deploy.
|
||||
================================================================
|
||||
EOF
|
||||
}
|
||||
|
||||
# ===========================================================================
|
||||
# Dispatch -- phase 2 if a live ScoutFS mount was given, else phase 1.
|
||||
# ===========================================================================
|
||||
if scoutfs_path_ready; then
|
||||
phase2_deploy
|
||||
else
|
||||
if [[ -n "$SCOUTFS_PATH" ]]; then
|
||||
warn "SCOUTFS_PATH='$SCOUTFS_PATH' is not a live ScoutFS mount -- running host prep (phase 1)."
|
||||
fi
|
||||
phase1_prep
|
||||
fi
|
||||
|
||||
# IMPORTANT: do not put any code below this exit. Everything after the
|
||||
# __ARCHIVE_BELOW__ marker is the embedded tar.gz payload (base64).
|
||||
exit 0
|
||||
__ARCHIVE_BELOW__
|
||||
H4sIAAAAAAAAA+1YbW/jNhLOZ/2KgRPgHHQlv+VlT70U503SNuhms4i9uBaHg0tLtE1EIlWSiuMG
|
||||
Bvoj7hfeL+kMZb3Em9t+uHa7e/AksCS+DMmZ4TMPGavojms/UmmmDA9WabL3u0sX5eToaK970u2d
|
||||
Hvfcs9vtuXKUQX9wutc77vcGJ8eD/vHJXrc36Hb7e9D9/afyvuTGMg2wd3w6+YdK7vnHGPMTkn24
|
||||
Y1ILs2CAhojuwPchYnG8gvb49egQvqjr2xHaSqXAsgyUhFGkcvv16DDw9r19GI3fvQrBLjhozhL4
|
||||
rlLK9b2IOAgDmVb3IuYxCAkZ0xbsUgUwXlBVwiK+UEnMNeqKVZSnXFrj9PGHjEcWu6G6jINRMM1F
|
||||
EgdmAQtm8DvldiHkHKwCnk6xIZMxZFjHoY/a1AxiniVqVfZgcM8SEcMm5mEmEk6dcf3YMc/gnNYf
|
||||
wC1306JJoJofSzv8WK1pKeyiXnLMZ0IKK5R8QYUSS32dy2q2hZ3GKlOJmq9CfAd4pdUStYH/VWH0
|
||||
8OhoQB/lWOHfMqXtV65nZVKVcc0sN+QEGn2hjP2LKf3hlmNWxvL0BUyFjP1U5ZLsh7OM/aUWlpbD
|
||||
LBw8js5v3o2/Hk3eDsffrtG/2IIaTlewUjlMuV1yXEdhyl7TrOh0T7KUh9VMPe9eJeg1E3pQrMWP
|
||||
mWX1V6TkTMxDz9tYr25ILwAiZXPUV5QcPJ4PLy5+mIyH34R+32dJJiRfu3aoxzL80pNiAq6Dq9Gc
|
||||
trINIZdoAeNjsGYZj10dmdEUAwH40HrZDV92W/U32p1sX5Q0VlJUBx0XEmTZsMNt1HGDNgq1qto2
|
||||
1t6h362KjRk6xdNVcnkvtJIU8eWQ3w3f3F6Nvh1OLm6uh1dvQmgdPG6VrcvZD8+vLyeXWPTaNas/
|
||||
Ny0w+LmMzUTJekGV1+hzgdFrF9GCR3dlC4wutOM/W+fXF60X0FrOuaWn/9ONT8+FtVnY6fT6p0EX
|
||||
/3phv9v762ZJnda/NkoERp3GrRbCoGtKxSLlGKYhHJclmlst0Ngw2BQ4J04wxoWKQ+hhV6zAjXNz
|
||||
cQPtEjcOQ+xYbk8EEGPz6dZ+3IagwKmh1btqF28Ib5bNoV3ZFgOO4ClAnxxW7UklBVABDIhDKk9o
|
||||
O91zbfgEYe1hRfjRzjN0OIcqLLBQ1VqYXJGroQOFofClYXk3CCGr5Dwm21QQ0NwdjSi4uh5+cxn6
|
||||
VjPEnbvOcqFYKtZho4XbPAkhhV0DzYIwutD07E56EhQf2kxb+2P/GeiploMI6ICqTSiFsI8emmlM
|
||||
Is7CQb0Bt8Co2Dst789Oj//3UgXrHzjGb/A/on5b/K/XOzrZ8b+PIfsNuJop7fbtU0ZY8JZhbpUv
|
||||
jMk5EajXnBjHpYz0KrMQccRG6nywlaMcZdjgpE84iUCPoIjqaJgKoAknKjDappN5ZiwCdop0JFFL
|
||||
xGFBxBDpi0PlmjdWrAzBBnN7AO8KNC7Y2xZYM40ZDdplF8I/TAWE8YeE5Cmz0QcyCerDHBG/d3KC
|
||||
JdE+ok5LofkTmut5j0WuT5lI4PGgkaW9NVZuW24Nm/YyUjGHn42NYf6zyLwNODdXU2WKl114hDJX
|
||||
hv+FiBfL3ACvS/5ovAmy3u/9W+zgX72FR8rvgeY/obMtPlNleUD4XTCwtefVPTfzJBlhIo+sP9ZM
|
||||
GhrDH/EoR7q5glbKHnxMO2eD3vHgBLf8l2icKMljPsqnFwptIk2r0vO9f47BgGzIH68y7t9kxKkN
|
||||
1bSkMlLMZnXbWz7jWqMb3qpERCuopWWK6Sgt5kL65Bo/0gpTWVFS68B5arRnc23I0BsLw+SW5RZ3
|
||||
Q4xvVSkGPMYJBa5RCd/0Xn9uGYvycMAfWJr9cSngN/C/f4Jgv4X/x6eDHf5/DEH8V5mjrxQJDrAx
|
||||
FyA9lsEG4apzciaiO1M0Q7hgmBAw/kXEkmQV0Km9OmS3y1Migg6S2aKLY32OqyNAz4RGNojc8Esc
|
||||
EU/4wlg6wbuGwqAySQgHCn/owIpggAi6D//59y/FP7zNp7jf3dHXgXdV84n/4yJeMc3ribelAoMn
|
||||
gJQfUvrQjbS4JD9MXQ7CBnEAF29GkOZEoykJArWmkzy67GW3QzcHZWM2xWw+5YhQfOOWF7DJ7U8z
|
||||
9wbjYYZZyQTeVhI6KzNLCRABhgL54dJlMcr42+rm6EnNCLDxeMMfMqFXIJUVM4wTB+OBV6e+Mxan
|
||||
Qv59S3nt5PJY8ad77X93+nCKSSJHOuJOQLjbyBflxcwzx6fnLnsC+EHlqKu4psFE7voXe+oDdzVE
|
||||
czjcXg4vri8PA3jVuBJyRGYzmWrEionRPdD7JCfwmge1s04qbae+BGq678qdsPF8bT5XD3rVLdRZ
|
||||
eQnlPXMTYbh9hiw2LxiULO4RKyxN0EGGUPPpef6sWULDFsf3z41U7GQnO9nJTnayk53sZCc72clO
|
||||
drKTT0p+BQmmdmwAKAAA
|
||||
@@ -0,0 +1,54 @@
|
||||
# kanrisha stack -- caddy (TLS) + kanrisha (custom app on ScoutFS).
|
||||
#
|
||||
# STUB: the real Kanrisha service is provided in part two. This placeholder
|
||||
# documents the expected shape so build.sh has something to embed and phase 2
|
||||
# of deploy.sh has a valid compose file to stand up Caddy. Replace the
|
||||
# `kanrisha` service with the real definition, then re-run build.sh.
|
||||
#
|
||||
# Topology:
|
||||
# Browser -> caddy:443 -> kanrisha:<port>
|
||||
#
|
||||
# Kanrisha operates on the host's ScoutFS filesystem, bind-mounted read-write
|
||||
# at ${SCOUTFS_PATH} (created by you between phase 1 and phase 2).
|
||||
|
||||
name: kanrisha
|
||||
|
||||
volumes:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:${CADDY_TAG:-2-alpine}
|
||||
container_name: caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
environment:
|
||||
KANRISHA_DOMAIN: "${KANRISHA_DOMAIN}"
|
||||
ACME_EMAIL: "${ACME_EMAIL}"
|
||||
depends_on:
|
||||
- kanrisha
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:2019/config/"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
# TODO (part two): replace this stub with the real Kanrisha service.
|
||||
# - real image + tag (KANRISHA_TAG in .env)
|
||||
# - the port Caddy should reverse_proxy to (update Caddyfile too)
|
||||
# - any env / config / healthcheck the app needs
|
||||
kanrisha:
|
||||
image: ${KANRISHA_IMAGE:-traefik/whoami}:${KANRISHA_TAG:-latest} # STUB image
|
||||
container_name: kanrisha
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# ScoutFS filesystem the app runs on (host path from .env).
|
||||
- "${SCOUTFS_PATH}:/data"
|
||||
@@ -15,6 +15,11 @@ REPO_BRANCH=main
|
||||
ACME_EMAIL=admin@example.com
|
||||
SSH_PORT=22
|
||||
|
||||
# ─── ScoutFS (kanrisha) ─────────────────────────────────────────────────────
|
||||
# License key passed to setup-scoutfs.sh by the kanrisha deployment. Leave
|
||||
# empty to be prompted per host; set it here to reuse one key unattended.
|
||||
SCOUTFS_LICENSE=
|
||||
|
||||
# ─── SSH authorized keys ────────────────────────────────────────────────────
|
||||
# resolve_ssh_keys() prefers this URL when set (fetched live, always current),
|
||||
# e.g. a GitHub keys endpoint (https://github.com/<user>.keys) or a raw
|
||||
|
||||
Reference in New Issue
Block a user