242 lines
10 KiB
Bash
242 lines
10 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# deploy.sh -- deploy the pocket-id stack (caddy + anubis + pocket-id) on
|
|
# Alpine Linux. Single-node, dedicated host: runs everything as root.
|
|
#
|
|
# What this does:
|
|
# 1. Installs docker + docker-cli-compose if missing.
|
|
# 2. Lays down docker-compose.yml, Caddyfile, .env.example in $STACK_DIR.
|
|
# 3. Generates .env on first run with random ENCRYPTION_KEY and
|
|
# ANUBIS_PID_KEY. Existing .env is never overwritten.
|
|
# 4. Prompts for POCKETID_DOMAIN and ACME_EMAIL if not preset.
|
|
# 5. Enables docker on boot, runs `docker compose pull && up -d`.
|
|
# 6. Waits for healthchecks to go green.
|
|
#
|
|
# Self-contained: docker-compose.yml, Caddyfile, and .env.example are
|
|
# embedded at the bottom of this file as a base64-encoded tar.gz. The
|
|
# script extracts them at runtime, so this single file is all you need
|
|
# on the target box.
|
|
#
|
|
# To rebuild after editing the loose files: run ./build.sh in this dir.
|
|
#
|
|
# Idempotent: re-running pulls new images and recreates changed services
|
|
# without touching .env or named volumes.
|
|
#
|
|
# Usage:
|
|
# bash deploy.sh # interactive prompts
|
|
# POCKETID_DOMAIN=id.example.com ACME_EMAIL=me@example.com \
|
|
# bash deploy.sh
|
|
# STACK_DIR=/opt/pocket-id bash deploy.sh
|
|
# SKIP_DOCKER_INSTALL=1 bash deploy.sh # docker already installed
|
|
# FORCE=1 bash deploy.sh # skip confirmations
|
|
|
|
set -euo pipefail
|
|
|
|
: "${STACK_DIR:=/srv/pocket-id}"
|
|
: "${SKIP_DOCKER_INSTALL:=0}"
|
|
: "${FORCE:=0}"
|
|
: "${POCKETID_DOMAIN:=}"
|
|
: "${ACME_EMAIL:=}"
|
|
|
|
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."
|
|
[[ -f /etc/alpine-release ]] || die "This script targets Alpine Linux."
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Extract embedded archive
|
|
# ----------------------------------------------------------------------------
|
|
SCRIPT_DIR=$(mktemp -d -t pocket-id-deploy.XXXXXX)
|
|
trap 'rm -rf "$SCRIPT_DIR"' EXIT
|
|
|
|
extract_archive() {
|
|
grep -a -A 9999999 '^__ARCHIVE_BELOW__$' "$0" \
|
|
| tail -n +2 \
|
|
| base64 -d \
|
|
| tar -xz -C "$SCRIPT_DIR"
|
|
}
|
|
|
|
if grep -q -a '^__ARCHIVE_BELOW__$' "$0"; then
|
|
log "Extracting embedded deployment files..."
|
|
extract_archive
|
|
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
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Prompt for required vars if not set
|
|
# ----------------------------------------------------------------------------
|
|
prompt() {
|
|
local varname="$1" message="$2"
|
|
local -n ref="$varname"
|
|
if [[ -z "${ref:-}" ]]; then
|
|
read -r -p "$message: " ref
|
|
[[ -n "$ref" ]] || die "$varname required."
|
|
fi
|
|
}
|
|
prompt POCKETID_DOMAIN "Public hostname (e.g. id.example.com)"
|
|
prompt ACME_EMAIL "Let's Encrypt email"
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Docker
|
|
# ----------------------------------------------------------------------------
|
|
if [[ "$SKIP_DOCKER_INSTALL" != "1" ]]; then
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
log "Installing docker + docker-cli-compose..."
|
|
apk add -q docker docker-cli-compose openrc
|
|
else
|
|
log "Docker already installed: $(docker --version)"
|
|
fi
|
|
rc-update add docker default >/dev/null 2>&1 || true
|
|
rc-service docker status >/dev/null 2>&1 || rc-service docker start
|
|
fi
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Stack directory + files
|
|
# ----------------------------------------------------------------------------
|
|
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 with generated secrets..."
|
|
install -m 0600 "$SCRIPT_DIR/.env.example" "$ENV_FILE"
|
|
sed -i \
|
|
-e "s|^POCKETID_DOMAIN=.*|POCKETID_DOMAIN=${POCKETID_DOMAIN}|" \
|
|
-e "s|^ACME_EMAIL=.*|ACME_EMAIL=${ACME_EMAIL}|" \
|
|
-e "s|^ENCRYPTION_KEY=.*|ENCRYPTION_KEY=$(openssl rand -base64 32)|" \
|
|
-e "s|^ANUBIS_PID_KEY=.*|ANUBIS_PID_KEY=$(openssl rand -hex 32)|" \
|
|
"$ENV_FILE"
|
|
else
|
|
log ".env exists; leaving secrets alone."
|
|
fi
|
|
|
|
# Validate required values are present.
|
|
missing=()
|
|
for var in POCKETID_DOMAIN ACME_EMAIL ENCRYPTION_KEY ANUBIS_PID_KEY; do
|
|
grep -E "^${var}=.+$" "$ENV_FILE" >/dev/null || missing+=("$var")
|
|
done
|
|
(( ${#missing[@]} == 0 )) || die "Missing values in $ENV_FILE: ${missing[*]}"
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Bring up the stack
|
|
# ----------------------------------------------------------------------------
|
|
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 ${POCKETID_DOMAIN}. DNS for
|
|
that name must already point at this host, and ports 80/443 must be
|
|
reachable from the internet, or the cert request will fail.
|
|
|
|
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
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Wait for health
|
|
# ----------------------------------------------------------------------------
|
|
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
|
|
================================================================
|
|
DEPLOYED
|
|
|
|
URL: https://${POCKETID_DOMAIN}
|
|
Stack dir: $STACK_DIR
|
|
|
|
Manage (run from $STACK_DIR):
|
|
docker compose logs -f
|
|
docker compose restart
|
|
docker compose pull && docker compose up -d # update
|
|
docker compose down # stop, keep volumes
|
|
docker compose down -v # stop, WIPE data
|
|
|
|
Or just re-run this script -- it's idempotent.
|
|
================================================================
|
|
EOF
|
|
|
|
# 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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
|
|
2B7bY/v29m9v1GZbACgAAA==
|