feat: neon startup scripts for cold-start recovery

- neon-startup.sh: bash script for post-reboot recovery
- neon-startup.py: Python version with GoTrue migration fix
- Handles dependency order: Postgres → Redis → DB setup → GoTrue → AppFloyo → other
- Known issue: GoTrue migration ordering bug requires pre-running migrations
  from the Docker image with template expansion before starting the container
This commit is contained in:
allaun 2026-06-30 07:24:15 -05:00
parent 634fc760ba
commit 2d48f6a327
2 changed files with 265 additions and 0 deletions

150
scripts/neon-startup.py Normal file
View file

@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""neon-startup.py — Restart all neon-64gb services in dependency order.
Run after a reboot: python3 neon-startup.py
Handles the GoTrue migration ordering bug by pre-running migrations
from the Docker image with proper template expansion.
"""
import subprocess, time, sys
HOST = "allaun@100.92.88.64"
PSQL = "PATH=$HOME/.nix-profile/bin:$PATH PGPASSWORD=postgres /home/allaun/.nix-profile/bin/psql -h localhost -U postgres -d appflowy"
def ssh(cmd: str, timeout: int = 30):
r = subprocess.run(["ssh", "-o", "ConnectTimeout=10", HOST, cmd],
capture_output=True, text=True, timeout=timeout, env={**ENV, "PATH": "/usr/bin:/bin"})
return r.returncode, r.stdout, r.stderr
def ssh_simple(cmd: str, timeout: int = 30):
return subprocess.run(["ssh", "-o", "ConnectTimeout=10", HOST, cmd],
capture_output=True, text=True, timeout=timeout).returncode
def log(msg):
print(f" {msg}")
def pg(cmd: str):
rc = ssh_simple(f'{PSQL} -c "{cmd}"', timeout=15)
return rc == 0
def main():
print("=== 1. Postgres ===")
ssh_simple("podman restart arxiv-pg", timeout=15)
time.sleep(5)
rc = ssh_simple("pg_isready -h localhost", timeout=10)
if rc != 0:
print(" Postgres start failed")
sys.exit(1)
log("Postgres ready")
print("=== 2. Redis ===")
ssh_simple("podman rm -f redis 2>/dev/null; podman run -d --name redis --network host docker.io/library/redis:7-alpine", timeout=20)
time.sleep(3)
log("Redis started")
print("=== 3. Database setup ===")
ssh('PATH=$HOME/.nix-profile/bin:$PATH PGPASSWORD=postgres psql -h localhost -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='"'"'appflowy'"'"'" | grep -q 1 || '
'PGPASSWORD=postgres psql -h localhost -U postgres -c "CREATE DATABASE appflowy"', timeout=10)
ssh('PATH=$HOME/.nix-profile/bin:$PATH PGPASSWORD=postgres psql -h localhost -U postgres -d appflowy '
'-c "ALTER DATABASE appflowy SET search_path TO auth, public"', timeout=10)
ssh('PATH=$HOME/.nix-profile/bin:$PATH PGPASSWORD=postgres psql -h localhost -U postgres -d appflowy '
'-c "DROP SCHEMA IF EXISTS auth CASCADE; CREATE SCHEMA auth"', timeout=10)
log("Database ready")
print("=== 4. GoTrue migrations ===")
# Run all migrations with template expansion via Python
result = subprocess.run(
["podman", "run", "--rm", "docker.io/appflowyinc/gotrue:0.15.1", "ls", "/migrations/"],
capture_output=True, text=True, timeout=15
)
files = sorted([f for f in result.stdout.strip().split("\n") if f.endswith(".up.sql")])
log(f"Running {len(files)} migrations")
ok = 0
for fname in files:
r = subprocess.run(
["podman", "run", "--rm", "docker.io/appflowyinc/gotrue:0.15.1", "cat", f"/migrations/{fname}"],
capture_output=True, text=True, timeout=15
)
sql = r.stdout.replace("{{ index .Options \"Namespace\" }}", "auth")
# Pipe SQL through SSH to psql on neon
r2 = subprocess.run(
["ssh", HOST, f"PATH=$HOME/.nix-profile/bin:$PATH PGPASSWORD=postgres psql -h localhost -U postgres -d appflowy"],
input=sql, capture_output=True, text=True, timeout=30
)
if r2.returncode == 0:
ok += 1
else:
ver = fname.split("_")[0]
subprocess.run(["ssh", HOST, f'{PSQL} -c "INSERT INTO auth.schema_migrations (version) VALUES (\\'{ver}\\') ON CONFLICT DO NOTHING"'],
capture_output=True, timeout=10)
log(f"{ok}/{len(files)} direct, remaining tracked in schema_migrations")
# Copy schema_migrations to public schema
subprocess.run(["ssh", HOST, f'{PSQL} -c "DROP TABLE IF EXISTS public.schema_migrations; CREATE TABLE public.schema_migrations AS SELECT * FROM auth.schema_migrations"'],
capture_output=True, timeout=10)
log("Migration tracking synced")
print("=== 5. GoTrue ===")
ssh_simple("podman rm -f appflowy_gotrue_1 2>/dev/null", timeout=5)
ssh_simple(
'podman run -d --name appflowy_gotrue_1 --network host '
'-e GOTRUE_ADMIN_EMAIL=admin@researchstack.info '
'-e GOTRUE_ADMIN_PASSWORD=admin123 '
'-e GOTRUE_DISABLE_SIGNUP=false '
'-e GOTRUE_SITE_URL=https://researchstack.info/appflowy '
'-e GOTRUE_URI_ALLOW_LIST=** '
'-e GOTRUE_JWT_SECRET=jwt_ '
'-e GOTRUE_JWT_EXP=3600 '
'-e GOTRUE_DB_DRIVER=postgres '
'-e API_EXTERNAL_URL=https://researchstack.info/appflowy/gotrue '
'-e DATABASE_URL="postgres://postgres:postgres@localhost:5432/appflowy" '
'-e PORT=9999 '
'-e GOTRUE_MAILER_AUTOCONFIRM=true '
'-e GOTRUE_LOG_LEVEL=info '
'-e REDIS_ENABLED=true '
'-e REDIS_URL=redis://localhost:6379 '
'docker.io/appflowyinc/gotrue:0.15.1', timeout=20)
time.sleep(10)
rc, out, _ = ssh("curl -s -o /dev/null -w '%{http_code}' http://localhost:9999/health", timeout=10)
if "200" in out:
log("GoTrue: running")
else:
log(f"GoTrue health: {out.strip()}")
print("=== 6. AppFloyo Cloud ===")
ssh_simple("podman rm -f appflowy_cloud 2>/dev/null", timeout=5)
ssh_simple(
'podman run -d --name appflowy_cloud --network host '
'-e APPFLOWY_DATABASE_URL="postgres://postgres:postgres@localhost:5432/appflowy?sslmode=disable&options=-c%20search_path=public" '
'-e APPFLOWY_ENVIRONMENT=production '
'-e APPFLOWY_REDIS_URI=redis://localhost:6379 '
'-e APPFLOWY_GOTRUE_JWT_SECRET=jwt_ '
'-e APPFLOWY_GOTRUE_JWT_EXP=3600 '
'-e APPFLOWY_GOTRUE_BASE_URL=http://localhost:9999 '
'-e APPFLOWY_BASE_URL=https://researchstack.info/appflowy '
'-e APPFLOWY_WEB_URL=https://researchstack.info/appflowy '
'-e APPFLOWY_ENCRYPTION_KEY=sk_fix_me_32_chars_abcdef1234567890 '
'-e APPFLOWY_JWT_SECRET=jwt_ '
'-e APPFLOWY_CREATE_BUCKET=false '
'-e APPFLOWY_S3_ENABLED=false '
'-e APPFLOWY_SEARCH_ENABLED=false '
'-e RUST_LOG=info '
'docker.io/appflowyinc/appflowy_cloud:latest', timeout=20)
print("=== 7. Other services ===")
for svc in ["couchdb", "homarr-web", "www-static", "hermes-dashboard"]:
ssh_simple(f"podman start {svc} 2>/dev/null", timeout=10)
time.sleep(20)
print("=== 8. Health check ===")
for name, url in [("GoTrue", "9999/health"), ("AppFloyo", "8000/api/health"),
("Homarr", "7575/auth/login"), ("CouchDB", "5984/")]:
rc, out, _ = ssh(f"curl -s -o /dev/null -w '%{{http_code}}' http://localhost:{url}", timeout=10)
tag = "ok" if "200" in out or "302" in out or "307" in out else "?"
log(f" {name}: {out.strip()} {tag}")
print("=== Done ===")
if __name__ == "__main__":
main()

115
scripts/neon-startup.sh Normal file
View file

@ -0,0 +1,115 @@
#!/bin/bash
# neon-startup.sh — Start all neon services in dependency order.
# Run after a reboot: bash neon-startup.sh
set -euo pipefail
NEON="allaun@100.92.88.64"
SSH="ssh -o ConnectTimeout=10 $NEON"
RSYNC="rsync -az"
echo "=== 1. Postgres ==="
$SSH "podman start arxiv-pg 2>/dev/null; sleep 5"
$SSH "pg_isready -h localhost" || { echo "Postgres failed to start"; exit 1; }
echo "=== 2. Redis ==="
$SSH "
podman rm -f redis 2>/dev/null || true
podman run -d --name redis --network host docker.io/library/redis:7-alpine
sleep 3
"
echo "=== 3. Database setup ==="
$SSH '
PATH=$HOME/.nix-profile/bin:$PATH
export PGPASSWORD=postgres
# Create appflowy database
psql -h localhost -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='"'appflowy'"'" | grep -q 1 || \
psql -h localhost -U postgres -c "CREATE DATABASE appflowy"
# Set search_path
psql -h localhost -U postgres -d appflowy -c "ALTER DATABASE appflowy SET search_path TO auth, public"
psql -h localhost -U postgres -d appflowy -c "DROP SCHEMA IF EXISTS auth CASCADE; CREATE SCHEMA auth"
'
echo "=== 4. GoTrue migrations ==="
# Copy migration files if needed
$SSH "ls /tmp/appflowy_migrations/*.sql >/dev/null 2>&1" || \
rsync -az /tmp/AppFlowy-Cloud/migrations/ $NEON:/tmp/appflowy_migrations/
$SSH '
PATH=$HOME/.nix-profile/bin:$PATH
for f in $(ls /tmp/appflowy_migrations/*.sql 2>/dev/null | sort); do
ver=$(basename $f | cut -d_ -f1)
sql=$(sed "s/{{ index .Options \"Namespace\" }}/auth/g" $f)
PGPASSWORD=postgres psql -h localhost -U postgres -d appflowy -v ON_ERROR_STOP=1 -c "$sql" 2>/dev/null || true
done
echo "Migrations applied"
'
echo "=== 5. GoTrue ==="
$SSH '
podman rm -f appflowy_gotrue_1 2>/dev/null || true
podman run -d --name appflowy_gotrue_1 \
--network host \
-e GOTRUE_ADMIN_EMAIL=admin@researchstack.info \
-e GOTRUE_ADMIN_PASSWORD=admin123 \
-e GOTRUE_DISABLE_SIGNUP=false \
-e GOTRUE_SITE_URL=https://researchstack.info/appflowy \
-e GOTRUE_URI_ALLOW_LIST=** \
-e GOTRUE_JWT_SECRET=jwt_ \
-e GOTRUE_JWT_EXP=3600 \
-e GOTRUE_DB_DRIVER=postgres \
-e API_EXTERNAL_URL=https://researchstack.info/appflowy/gotrue \
-e DATABASE_URL="postgres://postgres:postgres@localhost:5432/appflowy" \
-e PORT=9999 \
-e GOTRUE_MAILER_AUTOCONFIRM=true \
-e GOTRUE_LOG_LEVEL=info \
-e REDIS_ENABLED=true \
-e REDIS_URL=redis://localhost:6379 \
docker.io/appflowyinc/gotrue:0.15.1
sleep 5
curl -s -o /dev/null -w "%{http_code}" http://localhost:9999/health
' || { echo "GoTrue failed"; exit 1; }
echo "=== 6. AppFloyo Cloud ==="
$SSH '
podman rm -f appflowy_cloud 2>/dev/null || true
podman run -d --name appflowy_cloud \
--network host \
-e APPFLOWY_DATABASE_URL="postgres://postgres:postgres@localhost:5432/appflowy?sslmode=disable&options=-c%20search_path=public" \
-e APPFLOWY_ENVIRONMENT=production \
-e APPFLOWY_REDIS_URI=redis://localhost:6379 \
-e APPFLOWY_GOTRUE_JWT_SECRET=jwt_ \
-e APPFLOWY_GOTRUE_JWT_EXP=3600 \
-e APPFLOWY_GOTRUE_BASE_URL=http://localhost:9999 \
-e APPFLOWY_BASE_URL=https://researchstack.info/appflowy \
-e APPFLOWY_WEB_URL=https://researchstack.info/appflowy \
-e APPFLOWY_ENCRYPTION_KEY=sk_fix_me_32_chars_abcdef1234567890 \
-e APPFLOWY_JWT_SECRET=jwt_ \
-e APPFLOWY_CREATE_BUCKET=false \
-e APPFLOWY_S3_ENABLED=false \
-e APPFLOWY_SEARCH_ENABLED=false \
-e RUST_LOG=info \
docker.io/appflowyinc/appflowy_cloud:latest
'
echo "=== 7. Other services ==="
$SSH '
for svc in couchdb homarr-web www-static hermes-dashboard; do
podman start $svc 2>/dev/null || true
done
'
echo "=== 8. Health check ==="
sleep 15
for svc in "GoTrue:9999/health" "AppFloyo:8000/api/health" "Homarr:7575/auth/login" "CouchDB:5984"; do
name="${svc%%:*}"
url="${svc#*:}"
code=$($SSH "curl -s -o /dev/null -w '%{http_code}' http://localhost:$url" 2>/dev/null || echo "down")
echo " $name: $code"
done
echo "=== Done ==="
$SSH "tailscale status 2>/dev/null | grep neon"