Upload files to "deployments/pocket-id"
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
# Copy to .env and fill in. docker compose picks .env up automatically.
|
||||
# Never commit the populated .env.
|
||||
|
||||
# ─── Public hostname ────────────────────────────────────────────────────────
|
||||
# Bare hostname (no scheme) of the pocket-id deployment. Used by Caddy for
|
||||
# TLS issuance, by anubis as the cookie domain, and to derive APP_URL.
|
||||
POCKETID_DOMAIN=id.example.com
|
||||
|
||||
# Email for Let's Encrypt registration / expiry notifications.
|
||||
ACME_EMAIL=admin@example.com
|
||||
|
||||
# ─── Pocket-ID ──────────────────────────────────────────────────────────────
|
||||
# APP_URL is set automatically from POCKETID_DOMAIN in compose; no need to
|
||||
# set it here unless you run pocket-id standalone.
|
||||
|
||||
# Encryption key. Generate once with: openssl rand -base64 32
|
||||
# Rotating this re-encrypts data on next start; losing it is unrecoverable.
|
||||
ENCRYPTION_KEY=
|
||||
|
||||
# Behind Caddy, this MUST be true so pocket-id reads the real client IP and
|
||||
# scheme from X-Forwarded-* headers. Leave it -- compose overrides anyway.
|
||||
TRUST_PROXY=true
|
||||
|
||||
# Optional: GeoLite2 license key for IP geolocation in the audit log.
|
||||
# Get one free at https://www.maxmind.com/en/geolite2/signup
|
||||
MAXMIND_LICENSE_KEY=
|
||||
|
||||
# UID/GID the pocket-id process runs as inside the container. Match the
|
||||
# owner of ./data on the host if you bind-mount instead of using the named
|
||||
# volume.
|
||||
PUID=1000
|
||||
PGID=1000
|
||||
|
||||
# ─── Anubis ─────────────────────────────────────────────────────────────────
|
||||
# Ed25519 private key (hex) for the anubis PoW sidecar. Generate with:
|
||||
# openssl rand -hex 32
|
||||
# Only needed while the anubis-pid service is enabled in compose.
|
||||
ANUBIS_PID_KEY=
|
||||
|
||||
# ─── Image tags ─────────────────────────────────────────────────────────────
|
||||
# Pin for reproducible deploys.
|
||||
POCKETID_TAG=v2
|
||||
CADDY_TAG=2-alpine
|
||||
ANUBIS_TAG=latest
|
||||
@@ -0,0 +1,39 @@
|
||||
# Caddyfile for pocket-id stack.
|
||||
#
|
||||
# Auto-issues a Let's Encrypt cert for $POCKETID_DOMAIN and reverse-proxies
|
||||
# to anubis-pid (which forwards to pocket-id after the PoW challenge).
|
||||
#
|
||||
# To skip anubis, change the reverse_proxy target to `pocket-id:1411`.
|
||||
|
||||
{
|
||||
email {$ACME_EMAIL}
|
||||
# Uncomment for staging certs while testing (avoids LE rate limits):
|
||||
# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
}
|
||||
|
||||
{$POCKETID_DOMAIN} {
|
||||
encode zstd gzip
|
||||
|
||||
# Forward through anubis (PoW anti-bot) -> pocket-id.
|
||||
reverse_proxy anubis-pid:8923 {
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Forwarded-Host {host}
|
||||
}
|
||||
|
||||
# Sensible security headers. Adjust CSP if you embed pocket-id elsewhere.
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "DENY"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
Permissions-Policy "interest-cohort=()"
|
||||
-Server
|
||||
}
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format console
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/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 the __ARCHIVE_BELOW__
|
||||
# marker. Idempotent: strips any existing payload first.
|
||||
#
|
||||
# Run after editing any of the loose files. The resulting deploy.sh is
|
||||
# self-contained and can be scp'd to the target box on its own.
|
||||
|
||||
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
|
||||
|
||||
# tar -> gzip -> base64. Files only (no leading ./), wrapped at 76 cols
|
||||
# so the embedded blob is git-friendly.
|
||||
PAYLOAD=$(tar -czf - -C "$DIR" docker-compose.yml Caddyfile .env.example | base64)
|
||||
|
||||
# Strip any existing payload (everything from MARKER to EOF), then append a
|
||||
# fresh one. If MARKER isn't present, sed leaves the file unchanged.
|
||||
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,241 @@
|
||||
#!/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 var="$1" prompt="$2" cur="${!var:-}"
|
||||
if [[ -z "$cur" ]]; then
|
||||
read -r -p "$prompt: " cur
|
||||
[[ -n "$cur" ]] || die "$var required."
|
||||
printf -v "$var" '%s' "$cur"
|
||||
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==
|
||||
@@ -0,0 +1,89 @@
|
||||
# pocket-id stack — caddy (TLS) + anubis (PoW anti-bot) + pocket-id.
|
||||
#
|
||||
# Topology:
|
||||
# Internet --> caddy:443 --> anubis-pid:8923 --> pocket-id:1411
|
||||
#
|
||||
# Only caddy publishes ports. pocket-id and anubis are internal-only and
|
||||
# reachable by service name. To bypass anubis (no PoW challenge), point the
|
||||
# Caddyfile reverse_proxy at pocket-id:1411 directly and remove the anubis
|
||||
# block.
|
||||
|
||||
name: pocket-id
|
||||
|
||||
volumes:
|
||||
pocket-id-data:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
|
||||
services:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Caddy — TLS termination + reverse proxy. The only service on 80/443.
|
||||
# Auto-issues Let's Encrypt cert for ${POCKETID_DOMAIN}.
|
||||
# ---------------------------------------------------------------------------
|
||||
caddy:
|
||||
image: caddy:${CADDY_TAG:-2-alpine}
|
||||
container_name: caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp" # HTTP/3
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
environment:
|
||||
POCKETID_DOMAIN: "${POCKETID_DOMAIN}"
|
||||
ACME_EMAIL: "${ACME_EMAIL}"
|
||||
depends_on:
|
||||
- pocket-id
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:2019/config/"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pocket-ID — OIDC provider. No host port published; Caddy fronts it.
|
||||
# TRUST_PROXY=true is required so client IPs and scheme are read from
|
||||
# X-Forwarded-* headers Caddy sets.
|
||||
# ---------------------------------------------------------------------------
|
||||
pocket-id:
|
||||
image: ghcr.io/pocket-id/pocket-id:${POCKETID_TAG:-v2}
|
||||
container_name: pocket-id
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
# Override .env defaults so the container always sees the right values
|
||||
# regardless of how .env is laid out.
|
||||
APP_URL: "https://${POCKETID_DOMAIN}"
|
||||
TRUST_PROXY: "true"
|
||||
volumes:
|
||||
- pocket-id-data:/app/data
|
||||
healthcheck:
|
||||
test: ["CMD", "/app/pocket-id", "healthcheck"]
|
||||
interval: 90s
|
||||
timeout: 5s
|
||||
retries: 2
|
||||
start_period: 10s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Anubis — PoW anti-bot sidecar in front of pocket-id. Generate the key
|
||||
# with `openssl rand -hex 32`. To disable: comment this service out and
|
||||
# change the Caddyfile reverse_proxy target to `pocket-id:1411`.
|
||||
# ---------------------------------------------------------------------------
|
||||
anubis-pid:
|
||||
image: ghcr.io/techarohq/anubis:${ANUBIS_TAG:-latest}
|
||||
container_name: anubis-pid
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
BIND: ":8923"
|
||||
TARGET: "http://pocket-id:1411"
|
||||
DIFFICULTY: "4" # SHA-256 leading zeros; 4 ≈ 1s client work
|
||||
COOKIE_DOMAIN: "${POCKETID_DOMAIN}"
|
||||
METRICS_BIND: ":9090"
|
||||
ED25519_PRIVATE_KEY_HEX: "${ANUBIS_PID_KEY}"
|
||||
depends_on:
|
||||
pocket-id:
|
||||
condition: service_healthy
|
||||
Reference in New Issue
Block a user