Host provisioning skill contract
Use this file when an agent needs to create a fresh cloud server and turn it into a Sporades Host server. Provider scripts are intentionally thin wrappers around one common contract: create an SSH-reachable Linux server, install the Host server packages, copy the Sporades Host helper runtime, then run the normal sporades host ... commands from the local machine.
This is a skill-type document, not a product feature. Prefer exact commands, structured JSON output, and idempotent checks over provider dashboards. Provider sections must explain how to use that provider's local CLI and how to adapt an available MCP, SDK, Terraform provider, or other automation surface to the same output contract.
Host environment
These instructions depend upon a Linux-like (Linux, MacOS, WSL) host environment.
Common contract
An implementation must accept these inputs, either as environment variables or as explicitly named script parameters:
SPORADES_PROVIDER=digitalocean # digitalocean or hetzner
SPORADES_SERVER_NAME=sporades-host-01
SPORADES_REGION=nyc3 # DigitalOcean region or Hetzner location
SPORADES_SERVER_SIZE=s-1vcpu-1gb # DigitalOcean size or Hetzner server type
SPORADES_SSH_KEY_NAME=workstation
SPORADES_SSH_PUBLIC_KEY="$HOME/.ssh/id_ed25519.pub"
SPORADES_SSH_USER=root
SPORADES_HOST_ALIAS=personal
SPORADES_HOSTED_DOMAIN=example.com
SPORADES_REMOTE_ROOT=/srv/sporades
SPORADES_TLS_MODE=automatic # automatic or cloudflare-origin
SPORADES_CAPSULE_SUBNAME=team-notes # optional deployment targetProvider credentials are intentionally provider-specific:
- DigitalOcean:
DIGITALOCEAN_ACCESS_TOKEN. - Hetzner Cloud:
HCLOUD_TOKEN.
Agents may also accept neutral provider token aliases rather than requiring the .env file to name the provider. SPORADES_API is accepted for either provider, and OCEAN_API is accepted for DigitalOcean:
export DIGITALOCEAN_ACCESS_TOKEN="${DIGITALOCEAN_ACCESS_TOKEN:-${OCEAN_API:-${SPORADES_API:-}}}"
export HCLOUD_TOKEN="${HCLOUD_TOKEN:-${SPORADES_API:-}}"The implementation must produce a shell-consumable output file named sporades-host.env in the current working directory:
SPORADES_PROVIDER=digitalocean
SPORADES_SERVER_ID=123456789
SPORADES_SERVER_IPV4=203.0.113.10
SPORADES_SSH_TARGET=root@203.0.113.10
SPORADES_HOST_ALIAS=personal
SPORADES_HOSTED_DOMAIN=example.com
SPORADES_REMOTE_ROOT=/srv/sporades
SPORADES_TLS_MODE=automaticThe implementation must be safe to retry. If the provider already has a server with SPORADES_SERVER_NAME, reuse it and refresh sporades-host.env instead of creating a second machine.
Provider automation selection
Use the most capable automation surface available in this order:
- A provider-specific MCP tool, if the current agent environment exposes one and it can create or find SSH keys, create or find servers, read the public IPv4 address, and return structured data.
- The provider CLI, which is the canonical fallback for this document.
- A provider SDK, Terraform/OpenTofu, or Ansible module, only when it can be run non-interactively and emit the same
sporades-host.envfile. - Manual console steps only when automation is blocked. Record the final server ID and IPv4 address in
sporades-host.envbefore continuing.
MCP adapters must still honor the common contract. They may replace only the provider creation script. They must not replace the shared Sporades Host server installation script unless they also execute the same SSH, package, helper runtime copy, sporades host add, bootstrap, health, and stats operations.
When using an MCP, write a short local transcript in the agent response or scratch notes that includes:
- provider name,
- tool names invoked,
- reused or created SSH key name,
- reused or created server name and server ID,
- public IPv4 address,
- generated
SPORADES_SSH_TARGET.
Agent workflow
Read
CONTEXT.md,docs/PRD.md, anddocs/server-installation.md.Check the current tool environment for a provider MCP. If present, use it only when it can satisfy the provider adapter contract above.
Confirm the local machine has the selected provider CLI when no MCP is used, plus
ssh,scp,tar, Node.js 22+, npm, and a checked-out Sporades repo.Run exactly one provider creation script or MCP adapter below.
Create or update DNS:
example.com A <server-ip>*.example.com A <server-ip>
For disposable smoke tests, an agent may set
SPORADES_HOSTED_DOMAIN=<server-ip>.sslip.ioafter server creation. That gives wildcard DNS such ashost.<server-ip>.sslip.iowithout editing a DNS provider. Use this only for disposable verification, not a durable Hosted domain.Wait for SSH to accept key auth on
SPORADES_SSH_TARGET. If this is a disposable recreated server and the cloud provider reused an IP, remove the stale local host key first:shssh-keygen -R "$SPORADES_SERVER_IPV4"Only do this when the server was just created by the provider step and the IP reuse is expected.
Run the shared Host server installation script.
If
SPORADES_TLS_MODE=cloudflare-origin, install Cloudflare origin certificate files before bootstrap.Add the local Host profile and run
sporades host bootstrap --json.Optionally register, push, and start
SPORADES_CAPSULE_SUBNAME.Verify with
sporades host health --json,sporades host stats --json, and a public HTTP request to the Capsule URL when a Capsule was deployed.
Do not store provider tokens, private SSH keys, OAuth credentials, Cloudflare origin keys, or .env.sporades.server contents in this repository.
Provider script: DigitalOcean
DigitalOcean automation surface
Use a DigitalOcean MCP when one is available and exposes equivalent Droplet and SSH-key operations. The MCP adapter must:
- Find or create an account SSH key named
SPORADES_SSH_KEY_NAMEfromSPORADES_SSH_PUBLIC_KEY. - Find or create a Droplet named
SPORADES_SERVER_NAME. - Use image
ubuntu-24-04-x64unless the caller explicitly sets a supported replacement. - Use
SPORADES_REGIONandSPORADES_SERVER_SIZE. - Read the Droplet public IPv4 address.
- Write
sporades-host.envwith the common contract fields.
If there is no DigitalOcean MCP, use doctl. Authenticate with either:
export DIGITALOCEAN_ACCESS_TOKEN=<token>
doctl auth init --access-token "$DIGITALOCEAN_ACCESS_TOKEN"or by relying on DIGITALOCEAN_ACCESS_TOKEN in the environment for each command.
Helpful CLI discovery commands:
doctl compute region list
doctl compute size list
doctl compute image list --public
doctl compute ssh-key list
doctl compute droplet listThe DigitalOcean CLI uses doctl compute ssh-key import to add a local public key file to the account, and doctl compute droplet create to create Droplets. Droplet creation requires --size and --image; pass --ssh-keys so the new Host server accepts key-based SSH from first boot.
Default image: ubuntu-24-04-x64.
#!/usr/bin/env bash
set -euo pipefail
export DIGITALOCEAN_ACCESS_TOKEN="${DIGITALOCEAN_ACCESS_TOKEN:-${OCEAN_API:-${SPORADES_API:-}}}"
: "${DIGITALOCEAN_ACCESS_TOKEN:?Set DIGITALOCEAN_ACCESS_TOKEN, OCEAN_API, or SPORADES_API}"
: "${SPORADES_SERVER_NAME:?Set SPORADES_SERVER_NAME}"
: "${SPORADES_REGION:=nyc3}"
: "${SPORADES_SERVER_SIZE:=s-1vcpu-1gb}"
: "${SPORADES_SSH_KEY_NAME:=workstation}"
: "${SPORADES_SSH_PUBLIC_KEY:=$HOME/.ssh/id_ed25519.pub}"
: "${SPORADES_SSH_USER:=root}"
: "${SPORADES_HOST_ALIAS:=personal}"
: "${SPORADES_HOSTED_DOMAIN:?Set SPORADES_HOSTED_DOMAIN}"
: "${SPORADES_REMOTE_ROOT:=/srv/sporades}"
: "${SPORADES_TLS_MODE:=automatic}"
export DIGITALOCEAN_ACCESS_TOKEN
if [ -z "$SPORADES_SSH_PUBLIC_KEY" ] || [ ! -f "$SPORADES_SSH_PUBLIC_KEY" ]; then
if [ -f "$HOME/.ssh/id_ed25519.pub" ]; then
SPORADES_SSH_PUBLIC_KEY="$HOME/.ssh/id_ed25519.pub"
elif [ -f "$HOME/.ssh/id_rsa.pub" ]; then
SPORADES_SSH_PUBLIC_KEY="$HOME/.ssh/id_rsa.pub"
else
echo "No SSH public key found. Set SPORADES_SSH_PUBLIC_KEY." >&2
exit 1
fi
fi
if ! command -v doctl >/dev/null 2>&1; then
echo "doctl is required. Install it, authenticate, and retry." >&2
echo "On macOS with Homebrew: brew install doctl" >&2
exit 1
fi
key_id="$(
doctl compute ssh-key list --format ID,Name --no-header |
awk -v name="$SPORADES_SSH_KEY_NAME" '$2 == name { print $1; exit }'
)"
if [ -z "$key_id" ]; then
key_fingerprint="$(
ssh-keygen -l -E md5 -f "$SPORADES_SSH_PUBLIC_KEY" |
awk '{ print $2 }' |
sed 's/^MD5://'
)"
key_id="$(
doctl compute ssh-key list --format ID,FingerPrint --no-header |
awk -v fingerprint="$key_fingerprint" '$2 == fingerprint { print $1; exit }'
)"
fi
if [ -z "$key_id" ]; then
key_id="$(doctl compute ssh-key import "$SPORADES_SSH_KEY_NAME" \
--public-key-file "$SPORADES_SSH_PUBLIC_KEY" \
--format ID --no-header)"
fi
server_id="$(
doctl compute droplet list --format ID,Name --no-header |
awk -v name="$SPORADES_SERVER_NAME" '$2 == name { print $1; exit }'
)"
if [ -z "$server_id" ]; then
server_id="$(doctl compute droplet create "$SPORADES_SERVER_NAME" \
--region "$SPORADES_REGION" \
--size "$SPORADES_SERVER_SIZE" \
--image ubuntu-24-04-x64 \
--ssh-keys "$key_id" \
--wait \
--format ID --no-header)"
fi
server_ipv4="$(
doctl compute droplet get "$server_id" \
--format PublicIPv4 --no-header
)"
cat > sporades-host.env <<EOF
SPORADES_PROVIDER=digitalocean
SPORADES_SERVER_ID=$server_id
SPORADES_SERVER_IPV4=$server_ipv4
SPORADES_SSH_TARGET=$SPORADES_SSH_USER@$server_ipv4
SPORADES_HOST_ALIAS=$SPORADES_HOST_ALIAS
SPORADES_HOSTED_DOMAIN=$SPORADES_HOSTED_DOMAIN
SPORADES_REMOTE_ROOT=$SPORADES_REMOTE_ROOT
SPORADES_TLS_MODE=$SPORADES_TLS_MODE
EOF
cat sporades-host.envProvider script: Hetzner Cloud
Hetzner Cloud automation surface
Use a Hetzner Cloud MCP when one is available and exposes equivalent server and SSH-key operations. The MCP adapter must:
- Find an existing project SSH key named
SPORADES_SSH_KEY_NAME. - If no name match exists, compute the local
SPORADES_SSH_PUBLIC_KEYfingerprint and reuse any existing project SSH key with the same fingerprint. - Create a project SSH key named
SPORADES_SSH_KEY_NAMEonly when neither the requested name nor the local public key fingerprint already exists. - Find or create a server named
SPORADES_SERVER_NAME. - Use image
ubuntu-24.04unless the caller explicitly sets a supported replacement. - Use
SPORADES_REGIONas the Hetzner location andSPORADES_SERVER_SIZEas the server type. - Read the server public IPv4 address.
- Write
sporades-host.envwith the common contract fields.
If there is no Hetzner Cloud MCP, use hcloud. Authenticate with either:
export HCLOUD_TOKEN=<token>
hcloud context create sporadesor by relying on HCLOUD_TOKEN in the environment for each command.
Helpful CLI discovery commands:
hcloud location list
hcloud server-type list
hcloud image list --type system
hcloud ssh-key list
hcloud server listThe Hetzner Cloud CLI uses hcloud ssh-key create --public-key-from-file to add a local public key file to the project, and hcloud server create to create a server. Use --location; do not use the older datacenter flag for new automation. Hetzner capacity and type availability varies by account, location, and date. If server creation returns resource_unavailable or unsupported location for server type, try another small type/location pair before giving up. Hetzner enforces SSH-key uniqueness by public key, so the script is safe to retry when the same local public key already exists under a different project key name: it reuses the requested name first, falls back to a matching local public key fingerprint, and creates a new project key only when no existing key matches either check.
Default image: ubuntu-24.04. Default smoke-test fallbacks:
ash cpx11
hil cpx11
fsn1 cax11
hel1 cax11
fsn1 ccx13
nbg1 ccx13#!/usr/bin/env bash
set -euo pipefail
export HCLOUD_TOKEN="${HCLOUD_TOKEN:-${SPORADES_API:-}}"
: "${HCLOUD_TOKEN:?Set HCLOUD_TOKEN or SPORADES_API}"
: "${SPORADES_SERVER_NAME:=sporades-host-01}"
: "${SPORADES_REGION:=ash}"
: "${SPORADES_SERVER_SIZE:=cpx11}"
: "${SPORADES_SSH_KEY_NAME:=workstation}"
: "${SPORADES_SSH_PUBLIC_KEY:=}"
: "${SPORADES_SSH_USER:=root}"
: "${SPORADES_HOST_ALIAS:=personal}"
: "${SPORADES_REMOTE_ROOT:=/srv/sporades}"
: "${SPORADES_TLS_MODE:=automatic}"
if [ -z "$SPORADES_SSH_PUBLIC_KEY" ] || [ ! -f "$SPORADES_SSH_PUBLIC_KEY" ]; then
if [ -f "$HOME/.ssh/id_ed25519.pub" ]; then
SPORADES_SSH_PUBLIC_KEY="$HOME/.ssh/id_ed25519.pub"
elif [ -f "$HOME/.ssh/id_rsa.pub" ]; then
SPORADES_SSH_PUBLIC_KEY="$HOME/.ssh/id_rsa.pub"
else
echo "No SSH public key found. Set SPORADES_SSH_PUBLIC_KEY." >&2
exit 1
fi
fi
if ! command -v hcloud >/dev/null 2>&1; then
echo "hcloud is required. Install it, authenticate, and retry." >&2
echo "On macOS with Homebrew: brew install hcloud" >&2
exit 1
fi
key_ref="$(
hcloud ssh-key list -o columns=id,name,fingerprint -o noheader |
awk -v name="$SPORADES_SSH_KEY_NAME" '$2 == name { print $1; exit }'
)"
if [ -z "$key_ref" ]; then
key_fingerprint="$(
ssh-keygen -l -E md5 -f "$SPORADES_SSH_PUBLIC_KEY" |
awk '{ print $2 }' |
sed 's/^MD5://'
)"
key_ref="$(
hcloud ssh-key list -o columns=id,name,fingerprint -o noheader |
awk -v fingerprint="$key_fingerprint" '$3 == fingerprint { print $1; exit }'
)"
fi
if [ -z "$key_ref" ]; then
hcloud ssh-key create \
--name "$SPORADES_SSH_KEY_NAME" \
--public-key-from-file "$SPORADES_SSH_PUBLIC_KEY" >/dev/null
key_ref="$SPORADES_SSH_KEY_NAME"
fi
server_id="$(
hcloud server list -o columns=id,name -o noheader |
awk -v name="$SPORADES_SERVER_NAME" '$2 == name { print $1; exit }'
)"
if [ -z "$server_id" ]; then
created=""
for pair in "$SPORADES_REGION $SPORADES_SERVER_SIZE" "ash cpx11" "hil cpx11" "fsn1 cax11" "hel1 cax11" "fsn1 ccx13" "nbg1 ccx13"; do
set -- $pair
location="$1"
server_type="$2"
if hcloud server create \
--name "$SPORADES_SERVER_NAME" \
--type "$server_type" \
--image ubuntu-24.04 \
--location "$location" \
--ssh-key "$key_ref" \
--start-after-create >/dev/null; then
created="yes"
break
fi
done
if [ -z "$created" ]; then
echo "No Hetzner server type/location fallback succeeded." >&2
exit 1
fi
server_id="$(
hcloud server list -o columns=id,name -o noheader |
awk -v name="$SPORADES_SERVER_NAME" '$2 == name { print $1; exit }'
)"
fi
server_ipv4="$(hcloud server describe "$server_id" -o json | node -e '
let input = "";
process.stdin.on("data", chunk => input += chunk);
process.stdin.on("end", () => {
const data = JSON.parse(input);
console.log(data.public_net.ipv4.ip);
});
')"
: "${SPORADES_HOSTED_DOMAIN:=$server_ipv4.sslip.io}"
cat > sporades-host.env <<EOF
SPORADES_PROVIDER=hetzner
SPORADES_SERVER_ID=$server_id
SPORADES_SERVER_IPV4=$server_ipv4
SPORADES_SSH_TARGET=$SPORADES_SSH_USER@$server_ipv4
SPORADES_HOST_ALIAS=$SPORADES_HOST_ALIAS
SPORADES_HOSTED_DOMAIN=$SPORADES_HOSTED_DOMAIN
SPORADES_REMOTE_ROOT=$SPORADES_REMOTE_ROOT
SPORADES_TLS_MODE=$SPORADES_TLS_MODE
EOF
cat sporades-host.envShared script: install Sporades Host server
Run this from the local Sporades repository after sporades-host.env exists. It follows docs/server-installation.md and assumes a Debian or Ubuntu-style host. It does not create DNS records.
#!/usr/bin/env bash
set -euo pipefail
if [ ! -f sporades-host.env ]; then
echo "sporades-host.env is missing. Run a provider script first." >&2
exit 1
fi
# shellcheck disable=SC1091
. ./sporades-host.env
: "${SPORADES_SSH_TARGET:?Missing SPORADES_SSH_TARGET}"
: "${SPORADES_HOST_ALIAS:?Missing SPORADES_HOST_ALIAS}"
: "${SPORADES_HOSTED_DOMAIN:?Missing SPORADES_HOSTED_DOMAIN}"
: "${SPORADES_REMOTE_ROOT:?Missing SPORADES_REMOTE_ROOT}"
: "${SPORADES_TLS_MODE:=automatic}"
if [ ! -f ./bin/sporades-host-helper.js ] || \
[ ! -f ./Dockerfile.base ]; then
echo "Run this from the Sporades repository root." >&2
exit 1
fi
if command -v sporades >/dev/null 2>&1; then
SPORADES_CLI="${SPORADES_CLI:-sporades}"
else
SPORADES_CLI="${SPORADES_CLI:-node ./bin/sporades.js}"
fi
if [ "${SPORADES_RECREATED_SERVER:-}" = "1" ]; then
ssh-keygen -R "$SPORADES_SERVER_IPV4" >/dev/null 2>&1 || true
fi
until ssh -n -o BatchMode=yes -o StrictHostKeyChecking=accept-new \
"$SPORADES_SSH_TARGET" "true" >/dev/null 2>&1; do
echo "Waiting for SSH on $SPORADES_SSH_TARGET..."
sleep 5
done
ssh -n "$SPORADES_SSH_TARGET" "set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
if command -v cloud-init >/dev/null 2>&1; then
cloud-init status --wait
fi
apt_locks_clear=0
for attempt in \$(seq 1 60); do
if ! fuser /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock >/dev/null 2>&1; then
apt_locks_clear=1
break
fi
echo \"Waiting for apt/dpkg locks... (\$attempt/60)\"
sleep 5
done
if [ \"\$apt_locks_clear\" -ne 1 ]; then
echo \"Timed out waiting for apt/dpkg locks.\" >&2
exit 1
fi
apt-get update
apt-get install -y ca-certificates curl fail2ban gnupg tar
if ! command -v node >/dev/null 2>&1 || ! node --version | grep -Eq '^v2[2-9]\\.'; then
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
fi
apt-get install -y nodejs docker.io caddy
printf '%s\n' \
'[sshd]' \
'enabled = true' \
'port = ssh' \
'logpath = %(sshd_log)s' \
'maxretry = 5' \
'findtime = 10m' \
'bantime = 1h' \
> /etc/fail2ban/jail.d/sporades-sshd.conf
systemctl enable --now docker
systemctl enable --now caddy
systemctl enable --now fail2ban
mkdir -p '$SPORADES_REMOTE_ROOT/bin' '$SPORADES_REMOTE_ROOT/incoming'
"
scp ./bin/sporades-host-helper.js \
"$SPORADES_SSH_TARGET:$SPORADES_REMOTE_ROOT/bin/sporades-host-helper"
scp ./Dockerfile.base \
"$SPORADES_SSH_TARGET:$SPORADES_REMOTE_ROOT/Dockerfile.base"
ssh -n "$SPORADES_SSH_TARGET" "set -euo pipefail
chmod 0755 '$SPORADES_REMOTE_ROOT/bin/sporades-host-helper'
node --version
docker --version
caddy version
fail2ban-client status sshd
tar --version
"
$SPORADES_CLI host add "$SPORADES_HOST_ALIAS" \
--server "$SPORADES_SSH_TARGET" \
--domain "$SPORADES_HOSTED_DOMAIN" \
--remote-root "$SPORADES_REMOTE_ROOT" \
--tls "$SPORADES_TLS_MODE" \
--json
$SPORADES_CLI host use "$SPORADES_HOST_ALIAS"
$SPORADES_CLI host bootstrap --host "$SPORADES_HOST_ALIAS" --json
for attempt in 1 2 3 4 5; do
if $SPORADES_CLI host health --host "$SPORADES_HOST_ALIAS" --json; then
break
fi
echo "Waiting for Host server health route and TLS certificate..."
sleep 5
done
$SPORADES_CLI host health --host "$SPORADES_HOST_ALIAS" --json
$SPORADES_CLI host stats --host "$SPORADES_HOST_ALIAS" --jsonWhen this script is fed to bash over stdin, keep ssh -n; otherwise ssh may consume the rest of the script before later install steps run.
Optional script: install Cloudflare origin certificates
Only use this when SPORADES_TLS_MODE=cloudflare-origin.
#!/usr/bin/env bash
set -euo pipefail
: "${SPORADES_ORIGIN_CERT:?Set SPORADES_ORIGIN_CERT to a local origin.crt path}"
: "${SPORADES_ORIGIN_KEY:?Set SPORADES_ORIGIN_KEY to a local origin.key path}"
. ./sporades-host.env
tls_dir="$SPORADES_REMOTE_ROOT/hosts/$SPORADES_HOSTED_DOMAIN/tls"
ssh "$SPORADES_SSH_TARGET" "mkdir -p '$tls_dir'"
scp "$SPORADES_ORIGIN_CERT" "$SPORADES_SSH_TARGET:$tls_dir/origin.crt"
scp "$SPORADES_ORIGIN_KEY" "$SPORADES_SSH_TARGET:$tls_dir/origin.key"
ssh "$SPORADES_SSH_TARGET" "chmod 0644 '$tls_dir/origin.crt' && chmod 0600 '$tls_dir/origin.key'"Optional script: deploy a Capsule
Run this from a Sporades Capsule project directory after the Host profile has been bootstrapped.
#!/usr/bin/env bash
set -euo pipefail
. ./sporades-host.env 2>/dev/null || true
: "${SPORADES_HOST_ALIAS:?Set SPORADES_HOST_ALIAS}"
: "${SPORADES_CAPSULE_SUBNAME:?Set SPORADES_CAPSULE_SUBNAME}"
sporades host register "$SPORADES_CAPSULE_SUBNAME" \
--host "$SPORADES_HOST_ALIAS" \
--json
if [ -f ./client_secret_google.json ]; then
sporades auth set google --client-json ./client_secret_google.json --json
fi
if [ -f ./.env.sporades.server ]; then
sporades env import --file .env.sporades.server --json
fi
COPYFILE_DISABLE=1 sporades host push \
--host "$SPORADES_HOST_ALIAS" \
--subname "$SPORADES_CAPSULE_SUBNAME" \
--restart \
--verify \
--json
sporades host stats "$SPORADES_CAPSULE_SUBNAME" \
--host "$SPORADES_HOST_ALIAS" \
--jsonIf a Capsule has .env.sporades.server, import it into Sealed Server env before sporades host push. For Google-backed templates such as photo-library, set the OAuth client JSON first with sporades auth set google --client-json ./client_secret_google.json --json, then run sporades env import --file .env.sporades.server --json. Legacy Server env files are not pushed directly; host push consumes local Sealed Server env and packages the Host-encrypted envelope for the Hosted Capsule release.
Verification checklist
ssh "$SPORADES_SSH_TARGET" "node --version"returns Node.js 22+.ssh "$SPORADES_SSH_TARGET" "docker --version && caddy version"succeeds.ssh "$SPORADES_SSH_TARGET" "fail2ban-client status sshd"succeeds.<remote-root>/bin/sporades-host-helperexists and is executable.<remote-root>/Dockerfile.baseexists.sporades host current --jsonpoints at the expected SSH target, Hosted domain, remote root, and TLS mode.sporades host health --host <alias> --jsonsucceeds after DNS is in place.sporades host stats --host <alias> --jsonreports Docker and Caddy as available.- If a Capsule was deployed,
https://<subname>.<hosted-domain>/resolves and serves the Hosted Capsule or a structured Sporades error.
Failure handling
- Server exists but SSH fails: check that the expected public key is attached, wait for cloud-init, and verify security group/firewall rules allow TCP 22.
sporades host healthfails before DNS is configured: finish DNS first. Host bootstrap can prepare files, but health checks need the public domain.- Caddy automatic HTTPS fails: confirm ports 80 and 443 reach the Host server and the Hosted domain plus wildcard resolve to the server IP (double-check firewall rules).
cloudflare-originbootstrap fails: install readableorigin.crtandorigin.keyunder<remote-root>/hosts/<domain>/tls/.- Release push fails: confirm
<remote-root>/incomingexists and the SSH user can write there. - Capsule starts but remains unavailable: run
sporades host logs stdoutandsporades host logs stderrfor the Capsule subname.