From c87123a4944a80bcc393a072d400dd5b5f758710 Mon Sep 17 00:00:00 2001 From: Guido X Jansen Date: Sat, 14 Feb 2026 15:10:54 +0100 Subject: [PATCH] feat(ops): add backup/restore scripts, smoke test, and documentation (#6) Scripts: - backup.sh: PostgreSQL dump with optional age encryption, auto-cleanup - restore.sh: Interactive restore with confirmation, GDPR compliance note - smoke-test.sh: Validates local or remote deployment (services, API, SSL) Documentation: - installation.md: Step-by-step server setup and first deploy - configuration.md: All environment variables with types and defaults - upgrading.md: Standard upgrade, pinned versions, rollback procedure - administration.md: Monitoring, logs, common tasks, troubleshooting - backups.md: Automated backups, encryption, restore, disaster recovery --- docs/administration.md | 163 +++++++++++++++++++++++++++++++++++++++++ docs/backups.md | 136 ++++++++++++++++++++++++++++++++++ docs/configuration.md | 96 ++++++++++++++++++++++++ docs/installation.md | 121 ++++++++++++++++++++++++++++++ docs/upgrading.md | 79 ++++++++++++++++++++ scripts/backup.sh | 99 +++++++++++++++++++++++++ scripts/restore.sh | 115 +++++++++++++++++++++++++++++ scripts/smoke-test.sh | 154 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 963 insertions(+) create mode 100644 docs/administration.md create mode 100644 docs/backups.md create mode 100644 docs/configuration.md create mode 100644 docs/installation.md create mode 100644 docs/upgrading.md create mode 100755 scripts/backup.sh create mode 100755 scripts/restore.sh create mode 100755 scripts/smoke-test.sh diff --git a/docs/administration.md b/docs/administration.md new file mode 100644 index 0000000..4610448 --- /dev/null +++ b/docs/administration.md @@ -0,0 +1,163 @@ +# Administration Guide + +Day-to-day administration of a running Barazo instance. + +## Monitoring + +### Service Status + +```bash +# Check all services +docker compose ps + +# Check specific service +docker compose ps barazo-api +``` + +All services should show `healthy` status. + +### Logs + +```bash +# All services +docker compose logs -f + +# Specific service (last 100 lines, follow) +docker compose logs -f --tail 100 barazo-api + +# Specific service without follow +docker compose logs --tail 50 postgres +``` + +### Smoke Test + +Run the smoke test to verify everything is working: + +```bash +./scripts/smoke-test.sh https://your-domain.com +``` + +### Resource Usage + +```bash +docker stats --no-stream +``` + +## Backups + +### Automated Backups (Recommended) + +Set up a daily backup via cron: + +```bash +crontab -e +``` + +Add this line (runs daily at 2 AM): + +``` +0 2 * * * cd /path/to/barazo-deploy && ./scripts/backup.sh --encrypt >> /var/log/barazo-backup.log 2>&1 +``` + +Requires `BACKUP_PUBLIC_KEY` in your `.env` file. + +### Manual Backup + +```bash +./scripts/backup.sh # Unencrypted (local storage only) +./scripts/backup.sh --encrypt # Encrypted (safe for off-server storage) +``` + +Backups are saved to `./backups/` with timestamps. Old backups are automatically cleaned up after 7 days (configurable via `BACKUP_RETAIN_DAYS`). + +### Restore + +```bash +./scripts/restore.sh backups/barazo-backup-20260214-020000.sql.gz +``` + +See [Backup & Restore](backups.md) for full documentation. + +## Common Tasks + +### Restart a Service + +```bash +docker compose restart barazo-api +``` + +### Stop Everything + +```bash +docker compose down # Stops containers (preserves data) +docker compose down -v # Stops containers AND deletes all data +``` + +### Connect to Database + +```bash +docker compose exec postgres psql -U barazo +``` + +### View Caddy Access Logs + +```bash +docker compose logs caddy +``` + +### Force SSL Certificate Renewal + +Caddy renews certificates automatically. If you need to force renewal: + +```bash +docker compose restart caddy +``` + +### Update Images Without Restart + +```bash +docker compose pull # Pull new images +docker compose up -d # Restart only changed services +``` + +## Troubleshooting + +### Service Won't Start + +```bash +# Check the logs +docker compose logs + +# Check if port is already in use +sudo lsof -i :80 +sudo lsof -i :443 +``` + +### Out of Disk Space + +```bash +# Check disk usage +df -h + +# Clean up Docker resources +docker system prune -f # Remove stopped containers, unused images +docker volume prune -f # Remove unused volumes (careful!) +``` + +### High Memory Usage + +Check which service is consuming memory: + +```bash +docker stats --no-stream +``` + +Consider enabling resource limits in `docker-compose.yml` (uncomment the `mem_limit` lines). + +### Database Is Slow + +Connect to PostgreSQL and check for long-running queries: + +```bash +docker compose exec postgres psql -U barazo -c "SELECT pid, now() - query_start AS duration, query FROM pg_stat_activity WHERE state = 'active' ORDER BY duration DESC LIMIT 5;" +``` diff --git a/docs/backups.md b/docs/backups.md new file mode 100644 index 0000000..7e64a11 --- /dev/null +++ b/docs/backups.md @@ -0,0 +1,136 @@ +# Backup & Restore + +How to back up and restore your Barazo forum data. + +## What Gets Backed Up + +| Data | Backed up | Priority | +|------|-----------|----------| +| PostgreSQL (topics, replies, users, settings) | Yes | Critical | +| Valkey (cache, sessions) | No | Low -- regenerated automatically | +| Caddy (SSL certificates) | No | Medium -- re-obtained automatically by Let's Encrypt | +| Tap (firehose cursor) | No | Low -- re-syncs from the relay | +| Plugins | No | Medium -- reinstallable from npm | + +The backup script creates a PostgreSQL dump. This contains all forum data (topics, replies, users, categories, settings, moderation logs). + +## Creating Backups + +### Manual Backup + +```bash +# Unencrypted (keep on server only) +./scripts/backup.sh + +# Encrypted (safe to store off-server) +./scripts/backup.sh --encrypt +``` + +Backups are saved to `./backups/` with filenames like `barazo-backup-20260214-020000.sql.gz`. + +### Automated Backups + +Add a cron job for daily backups: + +```bash +crontab -e +``` + +``` +# Daily at 2 AM, encrypted +0 2 * * * cd /path/to/barazo-deploy && ./scripts/backup.sh --encrypt >> /var/log/barazo-backup.log 2>&1 +``` + +### Backup Encryption + +Backups contain user content and potentially PII. Encrypt before storing off-server. + +**Setup age encryption:** + +```bash +# Generate a keypair (do this once) +age-keygen -o barazo-backup-key.txt + +# The public key is printed to the terminal -- add it to .env +# BACKUP_PUBLIC_KEY="age1..." + +# Store the private key file securely (NOT on the same server) +# You need this to decrypt backups +``` + +**Install age:** + +```bash +# Ubuntu/Debian +sudo apt install age + +# macOS +brew install age +``` + +### Backup Retention + +By default, backups older than 7 days are automatically deleted. Change this with `BACKUP_RETAIN_DAYS` in `.env`: + +```bash +BACKUP_RETAIN_DAYS=30 # Keep 30 days +BACKUP_RETAIN_DAYS=0 # Never delete (manage manually) +``` + +## Restoring from Backup + +### Standard Restore + +```bash +./scripts/restore.sh backups/barazo-backup-20260214-020000.sql.gz +``` + +### Encrypted Backup Restore + +```bash +BACKUP_PRIVATE_KEY_FILE=/path/to/barazo-backup-key.txt \ + ./scripts/restore.sh backups/barazo-backup-20260214-020000.sql.gz.age +``` + +### What the Restore Script Does + +1. Stops the API and Web services (prevents writes during restore) +2. Drops and recreates the database +3. Restores the SQL dump +4. Restarts the API and Web services +5. Verifies the database has tables + +### After Restoring + +**GDPR compliance:** If the backup is older than the most recent data, you must re-apply deletions that occurred after the backup date. Check the `deletion_log` table for events after the backup timestamp. + +## Disaster Recovery + +If your server is completely lost: + +1. **Provision a new server** (same OS, Docker installed) +2. **Clone the deploy repo** and copy your `.env` file +3. **Start services:** `docker compose up -d` +4. **Restore the database** from your most recent backup +5. **Run the smoke test** to verify: `./scripts/smoke-test.sh https://your-domain.com` + +Caddy will automatically obtain a new SSL certificate. The Tap service will re-sync from the relay. Valkey cache will rebuild as users access the forum. + +## Verifying Backups + +Periodically verify that your backups are valid: + +```bash +# List backup files +ls -lh backups/ + +# Test a backup by restoring to a separate database +docker compose exec postgres psql -U barazo -d postgres \ + -c "CREATE DATABASE barazo_test;" +gunzip -c backups/barazo-backup-LATEST.sql.gz \ + | docker compose exec -T postgres psql -U barazo -d barazo_test -q +docker compose exec postgres psql -U barazo -d barazo_test \ + -c "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public';" +docker compose exec postgres psql -U barazo -d postgres \ + -c "DROP DATABASE barazo_test;" +``` diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..a833a53 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,96 @@ +# Configuration Reference + +All Barazo environment variables with descriptions, defaults, and examples. + +## Community Identity + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `COMMUNITY_NAME` | Yes | `"My Community"` | Display name for your forum | +| `COMMUNITY_DOMAIN` | Yes | -- | Domain name (e.g., `forum.example.com`). Used by Caddy for SSL. | +| `COMMUNITY_DID` | No | -- | AT Protocol DID. Created automatically during first setup. | +| `COMMUNITY_MODE` | No | `single` | `single` for one community, `global` for aggregator mode | + +## Database (PostgreSQL) + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `POSTGRES_USER` | Yes | -- | PostgreSQL superuser name | +| `POSTGRES_PASSWORD` | Yes | -- | PostgreSQL superuser password | +| `POSTGRES_DB` | Yes | -- | Database name | +| `POSTGRES_PORT` | No | `5432` | Host port mapping (dev compose only) | +| `DATABASE_URL` | Yes | -- | Connection string for the API. Format: `postgresql://user:pass@postgres:5432/dbname` | +| `MIGRATION_DATABASE_URL` | No | -- | Connection string for migrations (DDL role, if using role separation) | + +## Cache (Valkey) + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `VALKEY_PASSWORD` | Prod: Yes | -- | Valkey authentication password. Dangerous commands are disabled. | +| `VALKEY_PORT` | No | `6379` | Host port mapping (dev compose only) | +| `VALKEY_URL` | No | Auto | Connection URL for the API. Auto-constructed in compose. | + +## AT Protocol + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `RELAY_URL` | No | `wss://bsky.network` | Bluesky relay URL for firehose | +| `TAP_ADMIN_PASSWORD` | Yes | -- | Tap admin API password | +| `TAP_PORT` | No | `2480` | Host port mapping (dev compose only) | +| `OAUTH_CLIENT_ID` | Yes | -- | Your forum's public URL (e.g., `https://forum.example.com`) | +| `OAUTH_REDIRECT_URI` | Yes | -- | OAuth callback URL (e.g., `https://forum.example.com/api/auth/callback`) | + +## Frontend + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `NEXT_PUBLIC_API_URL` | Yes | -- | Public API URL as seen by browsers (e.g., `https://forum.example.com/api`) | +| `NEXT_PUBLIC_SITE_URL` | Yes | -- | Public site URL (e.g., `https://forum.example.com`) | + +## Image Versions + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `BARAZO_API_VERSION` | No | `latest` | API Docker image tag. Pin to a specific version in production (e.g., `1.2.3`). | +| `BARAZO_WEB_VERSION` | No | `latest` | Web Docker image tag. Pin to a specific version in production. | + +## Search + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `EMBEDDING_URL` | No | -- | Enables hybrid semantic search. Example: `http://ollama:11434/api/embeddings` | +| `AI_EMBEDDING_DIMENSIONS` | No | `768` | Vector dimensions (must match your embedding model) | + +## Encryption + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `AI_ENCRYPTION_KEY` | Conditional | -- | AES-256-GCM key for encrypting BYOK API keys at rest. Required if BYOK features are used. Generate: `openssl rand -base64 32` | + +## Cross-Posting + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `FEATURE_CROSSPOST_FRONTPAGE` | No | `false` | Enable Frontpage cross-posting (Bluesky cross-posting is always available) | + +## Plugins + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `PLUGINS_ENABLED` | No | `true` | Set to `false` to disable all plugins | +| `PLUGIN_REGISTRY_URL` | No | `https://registry.npmjs.org` | npm registry URL for plugin installation | + +## Monitoring + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `GLITCHTIP_DSN` | No | -- | GlitchTip/Sentry DSN for error reporting | +| `LOG_LEVEL` | No | `info` | Pino log level: `trace`, `debug`, `info`, `warn`, `error`, `fatal` | + +## Backups + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `BACKUP_PUBLIC_KEY` | No | -- | age public key for encrypting backups. Generate: `age-keygen -o key.txt` | +| `BACKUP_DIR` | No | `./backups` | Directory for backup files | +| `BACKUP_RETAIN_DAYS` | No | `7` | Days to keep old backups before cleanup | diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..a171096 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,121 @@ +# Installation Guide + +Step-by-step guide to deploying a Barazo forum on your own server. + +## Prerequisites + +- **VPS or dedicated server** with a public IP address + - Minimum: 2 vCPU, 4 GB RAM, 20 GB SSD (Hetzner CX22 recommended) + - Linux (Ubuntu 22.04+ or Debian 12+ recommended) +- **Domain name** with a DNS A record pointing to your server's IP +- **Docker** v24+ and **Docker Compose** v2 +- **SSH access** to your server + +## 1. Install Docker + +If Docker is not already installed: + +```bash +# Install Docker (official script) +curl -fsSL https://get.docker.com | sh + +# Add your user to the docker group (avoids sudo for docker commands) +sudo usermod -aG docker $USER + +# Log out and back in for group change to take effect +exit +# SSH back in + +# Verify +docker --version +docker compose version +``` + +## 2. Clone the Repository + +```bash +git clone https://github.com/barazo-forum/barazo-deploy.git +cd barazo-deploy +``` + +## 3. Configure Environment + +```bash +cp .env.example .env +nano .env # or your preferred editor +``` + +**Required variables to set:** + +| Variable | What to set | +|----------|-------------| +| `COMMUNITY_NAME` | Your forum's display name | +| `COMMUNITY_DOMAIN` | Your domain (e.g., `forum.example.com`) | +| `POSTGRES_PASSWORD` | Strong random password | +| `VALKEY_PASSWORD` | Strong random password | +| `TAP_ADMIN_PASSWORD` | Strong random password | +| `DATABASE_URL` | Update the password to match `POSTGRES_PASSWORD` | +| `OAUTH_CLIENT_ID` | `https://your-domain.com` | +| `OAUTH_REDIRECT_URI` | `https://your-domain.com/api/auth/callback` | +| `NEXT_PUBLIC_API_URL` | `https://your-domain.com/api` | +| `NEXT_PUBLIC_SITE_URL` | `https://your-domain.com` | + +Generate passwords with: + +```bash +openssl rand -base64 24 +``` + +## 4. Start Services + +```bash +docker compose up -d +``` + +This starts all 6 services: PostgreSQL, Valkey, Tap, API, Web, and Caddy. + +First startup may pull several Docker images (allow a few minutes on slower connections). + +## 5. Verify Installation + +```bash +# Check all services are running +docker compose ps + +# Run smoke test +./scripts/smoke-test.sh https://your-domain.com +``` + +All services should show `healthy` status. Caddy automatically obtains an SSL certificate from Let's Encrypt on first request. + +Visit `https://your-domain.com` in your browser. You should see the Barazo forum. + +## 6. First-Time Setup + +The first user to complete the setup wizard becomes the community administrator. Open your forum URL and follow the on-screen instructions to: + +1. Sign in with your AT Protocol account (Bluesky) +2. Set community name and description +3. Create initial categories +4. Configure moderation settings + +## Troubleshooting + +**Caddy fails to obtain SSL certificate:** + +- Verify your DNS A record points to the server's IP: `dig +short your-domain.com` +- Ensure ports 80 and 443 are open in your firewall +- Check Caddy logs: `docker compose logs caddy` + +**Services fail to start:** + +- Check logs: `docker compose logs` +- Verify `.env` has no syntax errors +- Ensure all required variables are set (no `CHANGE_ME` remaining) + +**Database connection errors:** + +- Verify `DATABASE_URL` password matches `POSTGRES_PASSWORD` +- Check PostgreSQL logs: `docker compose logs postgres` + +See also: [Troubleshooting](https://github.com/barazo-forum/barazo-deploy#troubleshooting) in README. diff --git a/docs/upgrading.md b/docs/upgrading.md new file mode 100644 index 0000000..138ef72 --- /dev/null +++ b/docs/upgrading.md @@ -0,0 +1,79 @@ +# Upgrade Guide + +How to upgrade your Barazo installation to a new version. + +## Standard Upgrade + +```bash +cd barazo-deploy + +# Pull new images +docker compose pull + +# Restart with new versions +docker compose up -d + +# Verify +docker compose ps +./scripts/smoke-test.sh https://your-domain.com +``` + +Database migrations run automatically when the API starts. No manual migration step is needed. + +## Pinned Version Upgrade + +If you pin image versions in `.env` (recommended for production): + +```bash +# Edit .env to update versions +nano .env +# Change BARAZO_API_VERSION=1.2.3 to BARAZO_API_VERSION=1.3.0 +# Change BARAZO_WEB_VERSION=1.2.3 to BARAZO_WEB_VERSION=1.3.0 + +# Pull and restart +docker compose pull +docker compose up -d +``` + +## Pre-Upgrade Checklist + +1. **Read the changelog** for the new version -- check for breaking changes +2. **Create a backup** before upgrading: + ```bash + ./scripts/backup.sh + ``` +3. **Test on staging first** if you have a staging environment + +## Rollback + +If the upgrade causes issues: + +```bash +# Stop services +docker compose down + +# Edit .env to revert to previous version +nano .env +# Change versions back to previous values + +# Pull previous images +docker compose pull + +# Restore database from pre-upgrade backup +./scripts/restore.sh backups/barazo-backup-YYYYMMDD-HHMMSS.sql.gz + +# Start services +docker compose up -d + +# Verify +docker compose ps +``` + +## Breaking Changes + +Major version bumps (e.g., 1.x to 2.x) may include breaking changes that require manual steps. These are documented in the release notes and CHANGELOG.md. + +Common breaking changes to watch for: +- **Environment variable renames** -- update your `.env` file +- **Database schema changes** -- migrations run automatically, but rollback may require the backup +- **Caddy configuration changes** -- check if Caddyfile needs updates diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100755 index 0000000..df29bcf --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Barazo Backup Script +# +# Creates a compressed PostgreSQL backup with timestamp. +# Optionally encrypts with age for off-server storage. +# +# Usage: +# ./scripts/backup.sh # Plain backup (local storage only) +# ./scripts/backup.sh --encrypt # Encrypted backup (requires BACKUP_PUBLIC_KEY) +# +# Environment: +# BACKUP_DIR Backup directory (default: ./backups) +# BACKUP_RETAIN_DAYS Days to keep old backups (default: 7) +# BACKUP_PUBLIC_KEY age public key for encryption (required with --encrypt) +# COMPOSE_FILE Docker Compose file (default: docker-compose.yml) + +set -euo pipefail + +BACKUP_DIR="${BACKUP_DIR:-./backups}" +BACKUP_RETAIN_DAYS="${BACKUP_RETAIN_DAYS:-7}" +COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" +ENCRYPT=false + +# Parse arguments +for arg in "$@"; do + case "$arg" in + --encrypt) ENCRYPT=true ;; + --help|-h) + echo "Usage: $0 [--encrypt]" + echo "" + echo "Options:" + echo " --encrypt Encrypt backup with age (requires BACKUP_PUBLIC_KEY)" + echo "" + echo "Environment variables:" + echo " BACKUP_DIR Backup directory (default: ./backups)" + echo " BACKUP_RETAIN_DAYS Days to keep old backups (default: 7)" + echo " BACKUP_PUBLIC_KEY age public key for encryption" + echo " COMPOSE_FILE Docker Compose file (default: docker-compose.yml)" + exit 0 + ;; + *) + echo "Unknown argument: $arg" >&2 + exit 1 + ;; + esac +done + +# Validate encryption prerequisites +if [ "$ENCRYPT" = true ] && [ -z "${BACKUP_PUBLIC_KEY:-}" ]; then + echo "Error: --encrypt requires BACKUP_PUBLIC_KEY environment variable" >&2 + exit 1 +fi + +if [ "$ENCRYPT" = true ] && ! command -v age &>/dev/null; then + echo "Error: age is required for encryption. Install: https://github.com/FiloSottile/age" >&2 + exit 1 +fi + +# Create backup directory +mkdir -p "$BACKUP_DIR" + +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +BACKUP_FILE="$BACKUP_DIR/barazo-backup-$TIMESTAMP.sql.gz" + +echo "Starting backup at $(date)" + +# Check PostgreSQL is running +if ! docker compose -f "$COMPOSE_FILE" exec -T postgres pg_isready -U "${POSTGRES_USER:-barazo}" &>/dev/null; then + echo "Error: PostgreSQL is not running" >&2 + exit 1 +fi + +# Dump PostgreSQL +echo "Dumping PostgreSQL..." +docker compose -f "$COMPOSE_FILE" exec -T postgres \ + pg_dump -U "${POSTGRES_USER:-barazo}" "${POSTGRES_DB:-barazo}" \ + | gzip > "$BACKUP_FILE" + +BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1) +echo "Backup created: $BACKUP_FILE ($BACKUP_SIZE)" + +# Encrypt if requested +if [ "$ENCRYPT" = true ]; then + echo "Encrypting backup..." + age -r "$BACKUP_PUBLIC_KEY" -o "${BACKUP_FILE}.age" "$BACKUP_FILE" + rm "$BACKUP_FILE" + BACKUP_FILE="${BACKUP_FILE}.age" + echo "Encrypted backup: $BACKUP_FILE" +fi + +# Clean up old backups +if [ "$BACKUP_RETAIN_DAYS" -gt 0 ]; then + DELETED=$(find "$BACKUP_DIR" -name "barazo-backup-*" -mtime +"$BACKUP_RETAIN_DAYS" -delete -print | wc -l | tr -d ' ') + if [ "$DELETED" -gt 0 ]; then + echo "Cleaned up $DELETED old backup(s) (older than $BACKUP_RETAIN_DAYS days)" + fi +fi + +echo "Backup complete at $(date)" diff --git a/scripts/restore.sh b/scripts/restore.sh new file mode 100755 index 0000000..7c5ca48 --- /dev/null +++ b/scripts/restore.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# Barazo Restore Script +# +# Restores a PostgreSQL backup from a file created by backup.sh. +# +# Usage: +# ./scripts/restore.sh backups/barazo-backup-20260214-020000.sql.gz +# ./scripts/restore.sh backups/barazo-backup-20260214-020000.sql.gz.age +# +# For encrypted backups (.age), set BACKUP_PRIVATE_KEY_FILE to the path +# of your age private key file. +# +# WARNING: This will overwrite the current database contents. + +set -euo pipefail + +COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" + +if [ $# -lt 1 ]; then + echo "Usage: $0 " >&2 + echo "" >&2 + echo "Examples:" >&2 + echo " $0 backups/barazo-backup-20260214-020000.sql.gz" >&2 + echo " $0 backups/barazo-backup-20260214-020000.sql.gz.age" >&2 + echo "" >&2 + echo "Environment variables:" >&2 + echo " BACKUP_PRIVATE_KEY_FILE Path to age private key (for .age files)" >&2 + echo " COMPOSE_FILE Docker Compose file (default: docker-compose.yml)" >&2 + exit 1 +fi + +BACKUP_FILE="$1" + +if [ ! -f "$BACKUP_FILE" ]; then + echo "Error: Backup file not found: $BACKUP_FILE" >&2 + exit 1 +fi + +# Check if encrypted +IS_ENCRYPTED=false +if [[ "$BACKUP_FILE" == *.age ]]; then + IS_ENCRYPTED=true + if [ -z "${BACKUP_PRIVATE_KEY_FILE:-}" ]; then + echo "Error: Encrypted backup requires BACKUP_PRIVATE_KEY_FILE environment variable" >&2 + exit 1 + fi + if [ ! -f "$BACKUP_PRIVATE_KEY_FILE" ]; then + echo "Error: Private key file not found: $BACKUP_PRIVATE_KEY_FILE" >&2 + exit 1 + fi + if ! command -v age &>/dev/null; then + echo "Error: age is required for decryption. Install: https://github.com/FiloSottile/age" >&2 + exit 1 + fi +fi + +# Confirm +echo "WARNING: This will overwrite the current database." +echo "Backup file: $BACKUP_FILE" +echo "" +read -p "Continue? (y/N) " -r +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Restore cancelled." + exit 0 +fi + +# Check PostgreSQL is running +if ! docker compose -f "$COMPOSE_FILE" exec -T postgres pg_isready -U "${POSTGRES_USER:-barazo}" &>/dev/null; then + echo "Error: PostgreSQL is not running. Start it with: docker compose -f $COMPOSE_FILE up -d postgres" >&2 + exit 1 +fi + +echo "Starting restore at $(date)" + +# Stop API and Web to prevent writes during restore +echo "Stopping API and Web services..." +docker compose -f "$COMPOSE_FILE" stop barazo-api barazo-web 2>/dev/null || true + +# Restore +DB_NAME="${POSTGRES_DB:-barazo}" +DB_USER="${POSTGRES_USER:-barazo}" + +echo "Dropping and recreating database..." +docker compose -f "$COMPOSE_FILE" exec -T postgres \ + psql -U "$DB_USER" -d postgres -c "DROP DATABASE IF EXISTS $DB_NAME;" +docker compose -f "$COMPOSE_FILE" exec -T postgres \ + psql -U "$DB_USER" -d postgres -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;" + +echo "Restoring from backup..." +if [ "$IS_ENCRYPTED" = true ]; then + age -d -i "$BACKUP_PRIVATE_KEY_FILE" "$BACKUP_FILE" \ + | gunzip \ + | docker compose -f "$COMPOSE_FILE" exec -T postgres psql -U "$DB_USER" -d "$DB_NAME" -q +else + gunzip -c "$BACKUP_FILE" \ + | docker compose -f "$COMPOSE_FILE" exec -T postgres psql -U "$DB_USER" -d "$DB_NAME" -q +fi + +# Restart services +echo "Restarting API and Web services..." +docker compose -f "$COMPOSE_FILE" up -d barazo-api barazo-web + +# Verify +echo "Verifying restore..." +sleep 5 +TABLE_COUNT=$(docker compose -f "$COMPOSE_FILE" exec -T postgres \ + psql -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public';") +echo "Tables in database: $(echo "$TABLE_COUNT" | tr -d ' ')" + +echo "" +echo "Restore complete at $(date)" +echo "" +echo "IMPORTANT: If this backup is older than the latest data, check the" +echo "deletion_log table and re-apply any deletions that occurred after" +echo "the backup timestamp to maintain GDPR compliance." diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100755 index 0000000..af4f4e7 --- /dev/null +++ b/scripts/smoke-test.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# Barazo Smoke Test +# +# Validates that a running Barazo instance is healthy. +# Run after deployment or upgrade to verify all services are working. +# +# Usage: +# ./scripts/smoke-test.sh # Test local (docker compose ps) +# ./scripts/smoke-test.sh https://forum.example.com # Test remote URL +# +# Exit codes: +# 0 All checks passed +# 1 One or more checks failed + +set -euo pipefail + +COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" +REMOTE_URL="${1:-}" +PASSED=0 +FAILED=0 + +pass() { + echo " PASS: $1" + PASSED=$((PASSED + 1)) +} + +fail() { + echo " FAIL: $1" + FAILED=$((FAILED + 1)) +} + +echo "Barazo Smoke Test" +echo "=================" +echo "" + +# --- Docker Compose checks (local only) --- +if [ -z "$REMOTE_URL" ]; then + echo "Local deployment checks:" + + # Check all services are running + SERVICES=$(docker compose -f "$COMPOSE_FILE" ps --format json 2>/dev/null || echo "") + if [ -z "$SERVICES" ]; then + fail "Docker Compose services not running" + else + for SERVICE in postgres valkey barazo-api barazo-web caddy; do + STATUS=$(docker compose -f "$COMPOSE_FILE" ps --format json "$SERVICE" 2>/dev/null | grep -o '"Health":"[^"]*"' | head -1 || echo "") + if echo "$STATUS" | grep -q "healthy"; then + pass "$SERVICE is healthy" + elif docker compose -f "$COMPOSE_FILE" ps --format json "$SERVICE" 2>/dev/null | grep -q "running"; then + pass "$SERVICE is running (no healthcheck)" + else + fail "$SERVICE is not running or unhealthy" + fi + done + fi + + echo "" + + # Check PostgreSQL connection + echo "Database checks:" + if docker compose -f "$COMPOSE_FILE" exec -T postgres pg_isready -U "${POSTGRES_USER:-barazo}" &>/dev/null; then + pass "PostgreSQL is accepting connections" + else + fail "PostgreSQL is not accepting connections" + fi + + # Check Valkey connection + if docker compose -f "$COMPOSE_FILE" exec -T valkey valkey-cli ping 2>/dev/null | grep -q "PONG"; then + pass "Valkey is responding" + else + fail "Valkey is not responding" + fi + + echo "" +fi + +# --- HTTP checks --- +if [ -n "$REMOTE_URL" ]; then + BASE_URL="$REMOTE_URL" + echo "Remote deployment checks ($BASE_URL):" +else + BASE_URL="http://localhost:3000" + echo "HTTP checks (via localhost):" +fi + +# API health +echo "" +echo "API checks:" +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/api/health" 2>/dev/null || echo "000") +if [ "$HTTP_CODE" = "200" ]; then + pass "API health endpoint returns 200" +else + fail "API health endpoint returned $HTTP_CODE (expected 200)" +fi + +# API health/ready should be blocked externally (403 from Caddy) or 200 internally +if [ -n "$REMOTE_URL" ]; then + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/api/health/ready" 2>/dev/null || echo "000") + if [ "$HTTP_CODE" = "403" ]; then + pass "/api/health/ready blocked externally (403)" + else + fail "/api/health/ready returned $HTTP_CODE (expected 403)" + fi +fi + +# Frontend +echo "" +echo "Frontend checks:" +if [ -n "$REMOTE_URL" ]; then + HOMEPAGE=$(curl -s "$BASE_URL" 2>/dev/null || echo "") +else + HOMEPAGE=$(curl -s "http://localhost:3001" 2>/dev/null || echo "") +fi + +if echo "$HOMEPAGE" | grep -qi "barazo\|html"; then + pass "Frontend returns HTML content" +else + fail "Frontend did not return expected HTML" +fi + +# SSL check (remote only) +if [ -n "$REMOTE_URL" ] && [[ "$REMOTE_URL" == https://* ]]; then + echo "" + echo "SSL checks:" + DOMAIN=$(echo "$REMOTE_URL" | sed 's|https://||' | sed 's|/.*||') + SSL_EXPIRY=$(echo | openssl s_client -servername "$DOMAIN" -connect "$DOMAIN:443" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2 || echo "") + if [ -n "$SSL_EXPIRY" ]; then + pass "SSL certificate valid (expires: $SSL_EXPIRY)" + else + fail "Could not verify SSL certificate" + fi + + # HTTPS redirect + HTTP_REDIRECT=$(curl -s -o /dev/null -w "%{redirect_url}" "http://$DOMAIN" 2>/dev/null || echo "") + if [[ "$HTTP_REDIRECT" == https://* ]]; then + pass "HTTP redirects to HTTPS" + else + fail "HTTP does not redirect to HTTPS" + fi +fi + +# --- Summary --- +echo "" +echo "=================" +TOTAL=$((PASSED + FAILED)) +echo "Results: $PASSED/$TOTAL passed" + +if [ "$FAILED" -gt 0 ]; then + echo "" + echo "$FAILED check(s) failed. Review the output above." + exit 1 +fi + +echo "All checks passed." -- 2.51.2