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:
2026-06-14 20:35:37 -05:00
co-authored by Claude Opus 4.8
parent 7e6c1ce7d1
commit 70d3019a1c
10 changed files with 655 additions and 1 deletions
+5
View File
@@ -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
View File
@@ -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
}
+23
View File
@@ -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
+32
View File
@@ -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
}
}
+70
View File
@@ -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.
+34
View File
@@ -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)"
+44
View File
@@ -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
+374
View File
@@ -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__
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
drKTT0p+BQmmdmwAKAAA
+54
View File
@@ -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"
+5
View File
@@ -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