Upload files to "deployments/pocket-id"

This commit is contained in:
2026-05-04 17:15:05 +00:00
parent aef47c835a
commit d0977bcbe8
5 changed files with 454 additions and 0 deletions
+44
View File
@@ -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
+39
View File
@@ -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
}
}
+41
View File
@@ -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)"
+241
View File
@@ -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==
+89
View File
@@ -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