# atBB Deployment Guide **Version:** 1.2 **Last Updated:** 2026-02-26 **Audience:** System administrators deploying atBB to production > **Related Documentation:** > - [docs/trust-model.md](trust-model.md) — Trust model for self-hosted deployment: what the AppView controls, user data guarantees, and security implications > - [docs/plans/complete/2026-02-11-deployment-infrastructure-design.md](plans/complete/2026-02-11-deployment-infrastructure-design.md) — Architectural decisions and design rationale behind this deployment approach ## Table of Contents 1. [Prerequisites](#1-prerequisites) 2. [Quick Start](#2-quick-start) 3. [Environment Configuration](#3-environment-configuration) 4. [Database Setup](#4-database-setup) 5. [Running the Container](#5-running-the-container) 6. [Reverse Proxy Setup](#6-reverse-proxy-setup) 7. [Monitoring & Logs](#7-monitoring--logs) 8. [Upgrading](#8-upgrading) 9. [Troubleshooting](#9-troubleshooting) 10. [Docker Compose Example](#10-docker-compose-example) 11. [NixOS Deployment](#11-nixos-deployment) --- ## 1. Prerequisites Before deploying atBB, ensure you have the following: ### Infrastructure Requirements - **PostgreSQL 14+** - Managed service recommended: AWS RDS, DigitalOcean Managed Database, Azure Database for PostgreSQL, or similar - Minimum 1GB RAM, 10GB storage (scales with forum size) - SSL/TLS support enabled (`?sslmode=require`) - Database user with CREATE/ALTER/SELECT/INSERT/UPDATE/DELETE permissions - **Domain Name & DNS** - Registered domain name (e.g., `forum.example.com`) - DNS A/AAAA record pointing to your server's public IP - Recommended: wildcard DNS for future subdomains (`*.forum.example.com`) - **Container Runtime** - Docker 20.10+ or Docker Desktop - Minimum 512MB RAM allocated to container (1GB+ recommended) - 2GB disk space for container image and logs ### AT Protocol Requirements **IMPORTANT:** atBB integrates with the AT Protocol network (the decentralized protocol powering Bluesky). You must set up your forum's AT Protocol identity before deployment. #### 1. Choose a Personal Data Server (PDS) Your forum needs a PDS to store its records (forum metadata, categories, moderation actions). Options: - **Self-hosted PDS:** Run your own PDS instance (advanced, recommended for sovereignty) - Guide: https://github.com/bluesky-social/pds - Requires separate server and domain - Full control over data and federation - **Hosted PDS:** Use Bluesky's PDS (`https://bsky.social`) or another provider - Simpler setup, lower maintenance - Suitable for testing and small forums #### 2. Create Forum Account Create an account for your forum on your chosen PDS: ```bash # Example with Bluesky PDS # Visit https://bsky.app and create account with your forum's handle # Handle should match your domain: forum.example.com ``` **Record these values (you'll need them later):** - Forum Handle: `forum.example.com` - Forum Password: (choose a strong password, minimum 16 characters) - Forum DID: `did:plc:xxxxxxxxxxxxx` (found in account settings or PDS admin interface) - PDS URL: `https://bsky.social` (or your PDS URL) #### 3. Understand Lexicon Namespace atBB uses the `space.atbb.*` lexicon namespace for its records: - `space.atbb.forum.forum` — Forum metadata (name, description, rules) - `space.atbb.forum.category` — Forum categories - `space.atbb.post` — User posts and replies - `space.atbb.membership` — User membership records - `space.atbb.modAction` — Moderation actions Your forum's DID will own the forum-level records, while users' DIDs own their posts and memberships. ### Security Requirements - **TLS/SSL Certificate:** Let's Encrypt (free) or commercial certificate - **Firewall:** Restrict inbound ports to 80/443 only - **SSH Access:** Key-based authentication (disable password auth) - **Secrets Management:** Secure storage for environment variables (consider cloud secrets manager) > **Before deploying:** Read [docs/trust-model.md](trust-model.md). It explains what the AppView controls (the Forum DID's credentials and write access), what your users can count on, and the security implications of a compromised server. --- ## 2. Quick Start Follow these steps for a minimal working deployment. Detailed explanations follow in later sections. ### Step 1: Pull the Docker Image ```bash # Pull latest stable version docker pull ghcr.io/malpercio-dev/atbb:latest # Or pin to a specific version (recommended for production) docker pull ghcr.io/malpercio-dev/atbb:v1.0.0 ``` Expected output: ``` latest: Pulling from malpercio-dev/atbb e7c96db7181b: Pull complete ... Status: Downloaded newer image for ghcr.io/malpercio-dev/atbb:latest ``` ### Step 2: Create Environment File ```bash # Copy the template curl -o .env.production https://raw.githubusercontent.com/malpercio-dev/atbb-monorepo/main/.env.production.example # Generate a strong session secret openssl rand -hex 32 # Output: a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 ``` **Edit `.env.production` and fill in these REQUIRED values:** ```bash # Database connection (from your PostgreSQL provider) DATABASE_URL=postgresql://atbb_user:YOUR_DB_PASSWORD@db.example.com:5432/atbb_prod?sslmode=require # AT Protocol credentials (from Prerequisites step) FORUM_DID=did:plc:YOUR_FORUM_DID PDS_URL=https://bsky.social FORUM_HANDLE=forum.example.com FORUM_PASSWORD=YOUR_FORUM_PASSWORD # OAuth configuration (your public domain) OAUTH_PUBLIC_URL=https://forum.example.com # Session security (use the openssl output from above) SESSION_SECRET=a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 ``` **Secure the file:** ```bash chmod 600 .env.production ``` ### Step 3: Run Database Migrations **CRITICAL:** Run migrations BEFORE starting the application. This creates the database schema. ```bash docker run --rm \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:latest \ pnpm --filter @atbb/appview db:migrate ``` Expected output: ``` > @atbb/db@0.1.0 db:migrate > drizzle-kit migrate Reading migrations from migrations/ Applying migration: 0000_initial_schema.sql Migration applied successfully ``` **If this fails, DO NOT proceed.** See [Section 4: Database Setup](#4-database-setup) for troubleshooting. ### Step 4: Start the Container ```bash docker run -d \ --name atbb \ --restart unless-stopped \ -p 8080:80 \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:latest ``` Options explained: - `-d` — Run in background (detached mode) - `--name atbb` — Name the container for easy management - `--restart unless-stopped` — Auto-restart on crashes or server reboot - `-p 8080:80` — Map host port 8080 to container port 80 - `--env-file .env.production` — Load environment variables **Verify the container is running:** ```bash docker ps | grep atbb # Expected: Container with STATUS "Up X seconds" docker logs atbb # Expected: No errors, services starting ``` **Test the application:** ```bash curl http://localhost:8080/api/healthz # Expected: {"status":"ok"} ``` ### Step 5: Configure Reverse Proxy **The container is now running on port 8080, but NOT accessible publicly yet.** You need a reverse proxy to: - Terminate TLS/SSL (HTTPS) - Forward traffic from your domain to the container - Handle automatic certificate renewal **Recommended setup with Caddy (automatic HTTPS):** Install Caddy: ```bash # Ubuntu/Debian sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list sudo apt update sudo apt install caddy ``` Edit `/etc/caddy/Caddyfile`: ``` forum.example.com { reverse_proxy localhost:8080 } ``` Reload Caddy: ```bash sudo systemctl reload caddy ``` **Caddy will automatically obtain a Let's Encrypt certificate and enable HTTPS.** ### Step 6: Verify Deployment Visit your forum: **https://forum.example.com** Expected: atBB home page loads with no errors. **If you see errors, proceed to [Section 9: Troubleshooting](#9-troubleshooting).** --- ## 3. Environment Configuration Complete reference for all environment variables. See `.env.production.example` for detailed comments. ### Required Variables | Variable | Description | Example | |----------|-------------|---------| | `DATABASE_URL` | Database connection string (PostgreSQL or SQLite) | PostgreSQL: `postgresql://user:pass@host:5432/dbname?sslmode=require`; SQLite: `file:./atbb.db` | | `FORUM_DID` | Forum's AT Protocol DID | `did:plc:abcdef1234567890` | | `PDS_URL` | Personal Data Server URL | `https://bsky.social` | | `FORUM_HANDLE` | Forum's AT Protocol handle | `forum.example.com` | | `FORUM_PASSWORD` | Forum account password | (minimum 16 characters, alphanumeric + symbols) | | `OAUTH_PUBLIC_URL` | Public URL for OAuth redirects | `https://forum.example.com` (MUST be HTTPS in production) | | `SESSION_SECRET` | Session encryption key | Generate with: `openssl rand -hex 32` | ### Optional Variables | Variable | Default | Description | |----------|---------|-------------| | `PORT` | `3000` | AppView API port (internal) | | `WEB_PORT` | `3001` | Web UI port (internal) | | `APPVIEW_URL` | `http://localhost:3000` | Internal API URL (keep as localhost for single container) | | `JETSTREAM_URL` | `wss://jetstream2.us-east.bsky.network/subscribe` | AT Protocol firehose URL | | `SESSION_TTL_DAYS` | `30` | Session lifetime in days (1-90 range) | | `REDIS_URL` | (none) | Redis connection string (future: multi-instance deployments) | ### Security Best Practices **SESSION_SECRET Generation:** ```bash # CRITICAL: Never use a predictable value or leave blank openssl rand -hex 32 # Use different secrets for dev/staging/production # Rotating the secret invalidates all active sessions ``` **Password Requirements:** - Minimum 16 characters - Mix of uppercase, lowercase, numbers, symbols - Unique per environment (never reuse) - Store in password manager or secrets vault **Connection String Security:** ```bash # Good: SSL/TLS enforced DATABASE_URL=postgresql://user:pass@host:5432/db?sslmode=require # Bad: Plain text connection (vulnerable to MITM) DATABASE_URL=postgresql://user:pass@host:5432/db ``` **File Permissions:** ```bash # Protect your environment file chmod 600 .env.production # Verify permissions ls -la .env.production # Expected: -rw------- (read/write for owner only) ``` ### Environment Loading Methods **Docker CLI:** ```bash # Recommended: Load from file with --init for better signal handling docker run --init --env-file .env.production ghcr.io/malpercio-dev/atbb:latest # Alternative: Individual variables (for orchestrators) docker run --init \ -e DATABASE_URL="postgresql://..." \ -e FORUM_DID="did:plc:..." \ -e SESSION_SECRET="..." \ ghcr.io/malpercio-dev/atbb:latest ``` **Note:** The `--init` flag enables tini as PID 1, improving signal handling for graceful shutdown. While not strictly required (the container has its own signal handling), it's considered best practice. **Docker Compose:** ```yaml services: atbb: image: ghcr.io/malpercio-dev/atbb:latest env_file: - .env.production ``` **Kubernetes:** ```yaml # Use Secrets (NOT ConfigMaps for sensitive data) apiVersion: v1 kind: Secret metadata: name: atbb-secrets type: Opaque stringData: DATABASE_URL: "postgresql://..." SESSION_SECRET: "..." --- apiVersion: apps/v1 kind: Deployment spec: template: spec: containers: - name: atbb envFrom: - secretRef: name: atbb-secrets ``` --- ## 4. Database Setup atBB supports two database backends: - **PostgreSQL** (recommended for production) — full-featured, suitable for multi-user/multi-server deployments - **SQLite/LibSQL** (lightweight alternative) — single-file database, ideal for small self-hosted forums. Use a `file:` prefix in `DATABASE_URL` (e.g. `file:./atbb.db`) and run the SQLite-specific migrations (`docker-compose.sqlite.yml` for Docker or set `database.type = "sqlite"` in the NixOS module). The rest of this section covers PostgreSQL provisioning. SQLite requires no separate server setup — just point `DATABASE_URL` at a file path. ### PostgreSQL Provisioning #### Option 1: Managed Database (Recommended) **AWS RDS:** 1. Navigate to RDS Console → Create Database 2. Choose PostgreSQL 14+ (latest stable version) 3. Select appropriate instance size: - Small forum (<1000 users): `db.t3.micro` or `db.t4g.micro` - Medium forum (1000-10000 users): `db.t3.small` or `db.t4g.small` - Large forum (10000+ users): `db.t3.medium` or higher 4. Enable "Storage Auto Scaling" (start with 20GB) 5. Enable "Automated Backups" (7-30 day retention) 6. Enable "Publicly Accessible" only if container is in different VPC 7. Security group: Allow PostgreSQL (5432) from container's IP/VPC 8. Create database: `atbb_prod` 9. Create user: `atbb_user` with generated password Connection string format: ``` postgresql://atbb_user:PASSWORD@instance-name.region.rds.amazonaws.com:5432/atbb_prod?sslmode=require ``` **DigitalOcean Managed Database:** 1. Navigate to Databases → Create → PostgreSQL 2. Choose datacenter closest to your Droplet/container 3. Select plan (Basic $15/mo sufficient for small forums) 4. Create database: `atbb_prod` 5. Create user: `atbb_user` with generated password 6. Add trusted source: Your Droplet's IP or "All" for simplicity 7. Download CA certificate (optional, for certificate validation) Connection string provided in dashboard (copy and use directly). **Azure Database for PostgreSQL:** 1. Navigate to Azure Database for PostgreSQL → Create 2. Choose "Flexible Server" (simpler, cheaper) 3. Select region and compute tier (Burstable B1ms sufficient for small forums) 4. Enable "High Availability" for production (optional) 5. Configure firewall: Add your container's public IP 6. Create database: `atbb_prod` Connection string format: ``` postgresql://atbb_user@servername:PASSWORD@servername.postgres.database.azure.com:5432/atbb_prod?sslmode=require ``` #### Option 2: Self-Hosted PostgreSQL **Installation (Ubuntu/Debian):** ```bash # Install PostgreSQL sudo apt update sudo apt install -y postgresql postgresql-contrib # Start and enable service sudo systemctl enable postgresql sudo systemctl start postgresql ``` **Create database and user:** ```bash sudo -u postgres psql -- In psql prompt: CREATE DATABASE atbb_prod; CREATE USER atbb_user WITH PASSWORD 'YOUR_STRONG_PASSWORD'; GRANT ALL PRIVILEGES ON DATABASE atbb_prod TO atbb_user; \q ``` **Enable remote connections (if container is on different host):** Edit `/etc/postgresql/14/main/postgresql.conf`: ``` listen_addresses = '*' # Or specific IP ``` Edit `/etc/postgresql/14/main/pg_hba.conf`: ``` # Add this line (replace 0.0.0.0/0 with specific IP range in production) host atbb_prod atbb_user 0.0.0.0/0 scram-sha-256 ``` Restart PostgreSQL: ```bash sudo systemctl restart postgresql ``` Connection string: ``` postgresql://atbb_user:YOUR_STRONG_PASSWORD@your-server-ip:5432/atbb_prod ``` ### Running Database Migrations Migrations create the database schema (tables, indexes, constraints). **First-time setup:** ```bash docker run --rm \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:latest \ pnpm --filter @atbb/appview db:migrate ``` Options explained: - `--rm` — Remove container after migration completes - `--env-file .env.production` — Load database connection string - `pnpm --filter @atbb/appview db:migrate` — Run Drizzle migrations **Expected output (success):** ``` Reading migrations from /app/packages/db/migrations Applying migration: 0000_initial_schema.sql Applying migration: 0001_add_deleted_flag.sql All migrations applied successfully ``` **Verify migrations:** ```bash # Connect to your database psql "postgresql://atbb_user:PASSWORD@host:5432/atbb_prod?sslmode=require" # List tables \dt # Expected output (12 tables): # Schema | Name | Type | Owner # --------+-----------------------+-------+----------- # public | backfill_errors | table | atbb_user # public | backfill_progress | table | atbb_user # public | boards | table | atbb_user # public | categories | table | atbb_user # public | firehose_cursor | table | atbb_user # public | forums | table | atbb_user # public | memberships | table | atbb_user # public | mod_actions | table | atbb_user # public | posts | table | atbb_user # public | role_permissions | table | atbb_user # public | roles | table | atbb_user # public | users | table | atbb_user ``` ### Migration Troubleshooting **Error: "database does not exist"** ``` FATAL: database "atbb_prod" does not exist ``` Solution: Create the database first (see self-hosted instructions above, or create via cloud console). **Error: "password authentication failed"** ``` FATAL: password authentication failed for user "atbb_user" ``` Solution: Verify credentials in `DATABASE_URL` match database user. **Error: "connection refused"** ``` Error: connect ECONNREFUSED ``` Solution: - Check database host/port are correct - Verify firewall allows connections from container's IP - For cloud databases, ensure "trusted sources" includes your IP **Error: "SSL connection required"** ``` FATAL: no pg_hba.conf entry for host, SSL off ``` Solution: Add `?sslmode=require` to connection string. **Error: "permission denied for schema public"** ``` ERROR: permission denied for schema public ``` Solution: Grant schema permissions: ```sql GRANT USAGE ON SCHEMA public TO atbb_user; GRANT CREATE ON SCHEMA public TO atbb_user; ``` --- ## 5. Running the Container ### Basic Deployment **Production command (recommended):** ```bash docker run -d \ --name atbb \ --restart unless-stopped \ -p 8080:80 \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:latest ``` **Pin to specific version (recommended for stability):** ```bash docker run -d \ --name atbb \ --restart unless-stopped \ -p 8080:80 \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:v1.0.0 ``` **Pin to specific commit SHA (for rollback/testing):** ```bash docker run -d \ --name atbb \ --restart unless-stopped \ -p 8080:80 \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:main-a1b2c3d ``` ### Advanced Options **Custom port mapping:** ```bash # Expose on different host port docker run -d \ --name atbb \ -p 3000:80 \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:latest # Bind to specific interface (localhost only) docker run -d \ --name atbb \ -p 127.0.0.1:8080:80 \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:latest ``` **Resource limits:** ```bash docker run -d \ --name atbb \ --restart unless-stopped \ -p 8080:80 \ --memory="1g" \ --cpus="1.0" \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:latest ``` **Custom network:** ```bash # Create network docker network create atbb-network # Run with network docker run -d \ --name atbb \ --network atbb-network \ -p 8080:80 \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:latest ``` ### Container Management **View logs:** ```bash # All logs docker logs atbb # Follow logs (live) docker logs -f atbb # Last 100 lines docker logs --tail 100 atbb # Logs since timestamp docker logs --since 2026-02-12T10:00:00 atbb ``` **Stop container:** ```bash docker stop atbb ``` **Start stopped container:** ```bash docker start atbb ``` **Restart container:** ```bash docker restart atbb ``` **Remove container:** ```bash # Stop first docker stop atbb # Remove docker rm atbb ``` **Execute commands inside container (debugging):** ```bash # Interactive shell docker exec -it atbb sh # Run single command docker exec atbb ps aux docker exec atbb df -h docker exec atbb cat /etc/nginx/nginx.conf ``` ### Health Checks The container exposes a health endpoint: **Check via curl:** ```bash curl http://localhost:8080/api/healthz ``` **Expected response:** ```json {"status":"ok"} ``` **Check via Docker:** ```bash docker inspect atbb | grep -A 5 Health ``` **Use in monitoring scripts:** ```bash #!/bin/bash # health-check.sh HEALTH=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/healthz) if [ "$HEALTH" != "200" ]; then echo "ALERT: atBB health check failed (HTTP $HEALTH)" # Send alert (email, Slack, PagerDuty, etc.) exit 1 fi echo "OK: atBB is healthy" exit 0 ``` **Run as cron job:** ```bash # Check every 5 minutes */5 * * * * /path/to/health-check.sh >> /var/log/atbb-health.log 2>&1 ``` --- ## 6. Reverse Proxy Setup The container exposes HTTP on port 80. In production, you need a reverse proxy to: - Terminate TLS/SSL (enable HTTPS) - Manage domain routing - Handle certificate renewal - Provide additional security headers ### Caddy (Recommended) **Why Caddy:** - Automatic HTTPS with Let's Encrypt (zero configuration) - Simple configuration syntax - Auto-renewal of certificates - Modern defaults (HTTP/2, security headers) **Installation:** Ubuntu/Debian: ```bash sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list sudo apt update sudo apt install caddy ``` CentOS/RHEL: ```bash dnf install 'dnf-command(copr)' dnf copr enable @caddy/caddy dnf install caddy ``` **Basic Configuration:** Edit `/etc/caddy/Caddyfile`: ``` forum.example.com { reverse_proxy localhost:8080 } ``` **Advanced Configuration (with security headers):** ``` forum.example.com { # Reverse proxy to atBB container reverse_proxy localhost:8080 # Security headers header { # Enable HSTS (force HTTPS) Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" # Prevent clickjacking X-Frame-Options "SAMEORIGIN" # Prevent MIME sniffing X-Content-Type-Options "nosniff" # XSS protection X-XSS-Protection "1; mode=block" # Referrer policy Referrer-Policy "strict-origin-when-cross-origin" # Content Security Policy (adjust as needed) Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" } # Access logs log { output file /var/log/caddy/atbb-access.log format json } } ``` **Apply configuration:** ```bash # Validate configuration sudo caddy validate --config /etc/caddy/Caddyfile # Reload Caddy (no downtime) sudo systemctl reload caddy # Check status sudo systemctl status caddy ``` **Verify HTTPS:** ```bash curl -I https://forum.example.com # Expected: HTTP/2 200 with security headers ``` ### nginx **Installation:** ```bash sudo apt install -y nginx ``` **Configuration:** Create `/etc/nginx/sites-available/atbb`: ```nginx # HTTP -> HTTPS redirect server { listen 80; listen [::]:80; server_name forum.example.com; return 301 https://$server_name$request_uri; } # HTTPS server server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name forum.example.com; # SSL certificates (obtain via certbot) ssl_certificate /etc/letsencrypt/live/forum.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/forum.example.com/privkey.pem; ssl_trusted_certificate /etc/letsencrypt/live/forum.example.com/chain.pem; # SSL settings (Mozilla Modern configuration) ssl_protocols TLSv1.3; ssl_prefer_server_ciphers off; ssl_session_timeout 1d; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; # Security headers add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; # Proxy to atBB container location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # WebSocket support (for future features) proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } # Access logs access_log /var/log/nginx/atbb-access.log combined; error_log /var/log/nginx/atbb-error.log; } ``` **Obtain SSL certificate with Certbot:** ```bash # Install Certbot sudo apt install -y certbot python3-certbot-nginx # Obtain certificate (interactive) sudo certbot --nginx -d forum.example.com # Certbot will automatically: # - Validate domain ownership # - Obtain certificate from Let's Encrypt # - Update nginx configuration # - Set up auto-renewal ``` **Enable site:** ```bash sudo ln -s /etc/nginx/sites-available/atbb /etc/nginx/sites-enabled/ sudo nginx -t # Test configuration sudo systemctl reload nginx ``` ### Traefik **docker-compose.yml with Traefik:** ```yaml version: '3.8' services: traefik: image: traefik:v2.11 command: - "--providers.docker=true" - "--entrypoints.web.address=:80" - "--entrypoints.websecure.address=:443" - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" - "--certificatesresolvers.letsencrypt.acme.email=admin@example.com" - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" ports: - "80:80" - "443:443" volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "./letsencrypt:/letsencrypt" atbb: image: ghcr.io/malpercio-dev/atbb:latest env_file: - .env.production labels: - "traefik.enable=true" - "traefik.http.routers.atbb.rule=Host(`forum.example.com`)" - "traefik.http.routers.atbb.entrypoints=websecure" - "traefik.http.routers.atbb.tls.certresolver=letsencrypt" - "traefik.http.services.atbb.loadbalancer.server.port=80" ``` Start with: ```bash docker-compose up -d ``` --- ## 7. Monitoring & Logs ### Container Logs **View logs:** ```bash # All logs docker logs atbb # Follow logs (real-time) docker logs -f atbb # Filter by timestamp docker logs --since 2026-02-12T10:00:00 atbb docker logs --until 2026-02-12T12:00:00 atbb ``` **Log format:** JSON structured logs Example log entry: ```json { "level": "info", "time": "2026-02-12T14:30:00.000Z", "service": "appview", "msg": "HTTP request", "method": "GET", "path": "/api/forum", "status": 200, "duration": 15 } ``` **Parse logs with jq:** ```bash # Filter by level docker logs atbb | grep '^{' | jq 'select(.level == "error")' # Extract errors from last hour docker logs --since 1h atbb | grep '^{' | jq 'select(.level == "error")' # Count requests by path docker logs atbb | grep '^{' | jq -r '.path' | sort | uniq -c | sort -nr ``` ### Log Persistence **Forward to log aggregator:** Using Docker logging driver (syslog): ```bash docker run -d \ --name atbb \ --log-driver syslog \ --log-opt syslog-address=udp://logserver:514 \ --log-opt tag="atbb" \ -p 8080:80 \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:latest ``` Using Docker logging driver (json-file with rotation): ```bash docker run -d \ --name atbb \ --log-driver json-file \ --log-opt max-size=10m \ --log-opt max-file=3 \ -p 8080:80 \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:latest ``` ### Health Monitoring **Health endpoint:** `GET /api/healthz` Example monitoring script (save as `/usr/local/bin/atbb-health-check`): ```bash #!/bin/bash # atbb-health-check - Monitor atBB health and restart if needed CONTAINER_NAME="atbb" HEALTH_URL="http://localhost:8080/api/healthz" MAX_FAILURES=3 FAILURES=0 while true; do # Check health endpoint HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL") if [ "$HTTP_CODE" != "200" ]; then FAILURES=$((FAILURES + 1)) echo "$(date): Health check failed (HTTP $HTTP_CODE), failures: $FAILURES/$MAX_FAILURES" if [ "$FAILURES" -ge "$MAX_FAILURES" ]; then echo "$(date): Max failures reached, restarting container" docker restart "$CONTAINER_NAME" FAILURES=0 sleep 60 # Wait for restart fi else # Reset failure counter on success if [ "$FAILURES" -gt 0 ]; then echo "$(date): Health check recovered" fi FAILURES=0 fi sleep 60 # Check every minute done ``` Run as systemd service: ```bash sudo chmod +x /usr/local/bin/atbb-health-check cat < $CPU_THRESHOLD" | bc)" -eq 1 ]; then echo "ALERT: CPU usage is ${CPU}% (threshold: ${CPU_THRESHOLD}%)" # Send notification (email, Slack, etc.) fi if [ "$(echo "$MEM > $MEM_THRESHOLD" | bc)" -eq 1 ]; then echo "ALERT: Memory usage is ${MEM}% (threshold: ${MEM_THRESHOLD}%)" # Send notification fi ``` ### Future: Observability Planned enhancements (not yet implemented): - Prometheus metrics endpoint (`/api/metrics`) - OpenTelemetry tracing - Grafana dashboard templates - Alert manager integration --- ## 8. Upgrading ### Upgrade Process **IMPORTANT:** Upgrading will cause brief downtime (sessions are stored in memory and will be lost). **Step 1: Check release notes** ```bash # View releases on GitHub # https://github.com/malpercio-dev/atbb-monorepo/releases # Look for: # - Breaking changes # - Database migration requirements # - New environment variables ``` **Step 2: Backup database** ```bash # Backup current database (critical!) pg_dump "postgresql://atbb_user:PASSWORD@host:5432/atbb_prod?sslmode=require" \ > atbb_backup_$(date +%Y%m%d_%H%M%S).sql # Verify backup ls -lh atbb_backup_*.sql ``` **Step 3: Pull new image** ```bash # Pull specific version docker pull ghcr.io/malpercio-dev/atbb:v1.1.0 # Or pull latest docker pull ghcr.io/malpercio-dev/atbb:latest ``` **Step 4: Run migrations (if required)** ```bash # Check release notes for migration requirements # If migrations are needed: docker run --rm \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:v1.1.0 \ pnpm --filter @atbb/appview db:migrate ``` **Step 5: Stop old container** ```bash docker stop atbb docker rm atbb ``` **Step 6: Start new container** ```bash docker run -d \ --name atbb \ --restart unless-stopped \ -p 8080:80 \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:v1.1.0 ``` **Step 7: Verify upgrade** ```bash # Check logs for errors docker logs atbb # Test health endpoint curl http://localhost:8080/api/healthz # Visit forum in browser # Test key functionality (login, post, etc.) ``` ### Rollback Procedure If upgrade fails, rollback to previous version: **Step 1: Stop broken container** ```bash docker stop atbb docker rm atbb ``` **Step 2: Restore database (if migrations were run)** ```bash # Connect to database psql "postgresql://atbb_user:PASSWORD@host:5432/atbb_prod?sslmode=require" # Drop all tables DROP SCHEMA public CASCADE; CREATE SCHEMA public; # Restore from backup psql "postgresql://atbb_user:PASSWORD@host:5432/atbb_prod?sslmode=require" \ < atbb_backup_20260212_140000.sql ``` **Step 3: Start old version** ```bash docker run -d \ --name atbb \ --restart unless-stopped \ -p 8080:80 \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:v1.0.0 ``` ### Zero-Downtime Upgrades (Future) Once Redis session storage is implemented, you can upgrade with zero downtime: 1. Start new container on different port 2. Test new version 3. Switch reverse proxy to new port 4. Stop old container **Not currently supported** because sessions are in-memory. --- ## 9. Troubleshooting ### Container Won't Start **Symptom:** Container exits immediately after starting **Diagnosis:** ```bash docker logs atbb ``` **Common causes:** 1. **Missing environment variables** ``` Error: DATABASE_URL is required ``` Solution: Verify `.env.production` has all required variables (see Section 3). 2. **Database connection failed** ``` Error: connect ECONNREFUSED ``` Solution: - Verify `DATABASE_URL` is correct - Check firewall allows connections from container's IP - Test connection manually: `psql "postgresql://..."` 3. **Port already in use** ``` Error: bind: address already in use ``` Solution: Change host port mapping: `-p 8081:80` 4. **Migrations not run** ``` Error: relation "forums" does not exist ``` Solution: Run migrations (Section 4). ### Database Connection Issues **Symptom:** Application starts but fails on database queries **Error examples:** ``` FATAL: password authentication failed for user "atbb_user" FATAL: no pg_hba.conf entry for host, SSL off Error: connect ETIMEDOUT ``` **Solutions:** 1. **Test connection manually:** ```bash psql "postgresql://atbb_user:PASSWORD@host:5432/atbb_prod?sslmode=require" ``` If this fails, the issue is NOT with atBB (fix database access first). 2. **Check credentials:** - Verify username/password in `DATABASE_URL` - Ensure user has been created in database 3. **Check SSL settings:** ```bash # If database requires SSL, ensure connection string includes: DATABASE_URL=postgresql://...?sslmode=require ``` 4. **Check network/firewall:** - Verify container can reach database host - Test from within container: `docker exec atbb ping db.example.com` - Check cloud provider security groups/firewall rules ### OAuth Redirect URI Mismatch **Symptom:** Login fails with "redirect URI mismatch" error **Cause:** `OAUTH_PUBLIC_URL` doesn't match the actual domain users access **Solution:** 1. Verify `OAUTH_PUBLIC_URL` in `.env.production`: ```bash OAUTH_PUBLIC_URL=https://forum.example.com # Must match actual domain ``` 2. Common mistakes: - ❌ `http://` instead of `https://` (use HTTPS in production) - ❌ Trailing slash: `https://forum.example.com/` (remove trailing slash) - ❌ Wrong subdomain: `https://www.forum.example.com` vs `https://forum.example.com` 3. Restart container after fixing: ```bash docker restart atbb ``` ### PDS Connectivity Problems **Symptom:** Cannot create posts, forum metadata not syncing **Error in logs:** ``` Error: Failed to connect to PDS: ENOTFOUND Error: Invalid credentials for FORUM_HANDLE ``` **Solutions:** 1. **Verify PDS URL:** ```bash curl https://bsky.social/xrpc/_health # Should return: {"version":"0.x.x"} ``` 2. **Test forum credentials:** ```bash # Use atproto CLI or curl to test auth curl -X POST https://bsky.social/xrpc/com.atproto.server.createSession \ -H "Content-Type: application/json" \ -d '{ "identifier": "forum.example.com", "password": "YOUR_FORUM_PASSWORD" }' # Should return: {"did":"did:plc:...","accessJwt":"..."} ``` 3. **Check environment variables:** ```bash docker exec atbb env | grep -E 'FORUM_|PDS_' # Verify all values are correct ``` ### High Memory Usage **Symptom:** Container using excessive memory (>1GB) **Diagnosis:** ```bash docker stats atbb ``` **Solutions:** 1. **Set memory limit:** ```bash docker update --memory="512m" atbb ``` 2. **Check for memory leak:** - Monitor over time: `docker stats atbb` - If memory grows continuously, report issue with logs 3. **Increase container memory:** ```bash # For large forums, 1-2GB may be normal docker update --memory="2g" atbb ``` ### Logs Filling Disk **Symptom:** Disk space running out due to large log files **Check log size:** ```bash du -sh /var/lib/docker/containers/*/ ``` **Solutions:** 1. **Configure log rotation (recommended):** ```bash # Stop container docker stop atbb docker rm atbb # Restart with log rotation docker run -d \ --name atbb \ --log-opt max-size=10m \ --log-opt max-file=3 \ -p 8080:80 \ --env-file .env.production \ ghcr.io/malpercio-dev/atbb:latest ``` 2. **Manually clean logs:** ```bash # Truncate logs (preserves container) truncate -s 0 $(docker inspect --format='{{.LogPath}}' atbb) ``` 3. **Use external log aggregator** (syslog, fluentd, etc.) ### Container Performance Issues **Symptom:** Slow response times, high CPU usage **Diagnosis:** ```bash docker stats atbb docker top atbb ``` **Solutions:** 1. **Check database performance:** - Slow queries often bottleneck at database - Monitor database server metrics - Add indexes if needed (consult forum performance guide) 2. **Increase resources:** ```bash docker update --cpus="2.0" --memory="1g" atbb ``` 3. **Check reverse proxy settings:** - Ensure proxy is not buffering excessively - Verify HTTP/2 is enabled for better performance 4. **Monitor specific endpoints:** ```bash # Extract slow requests from logs docker logs atbb | grep '^{' | jq 'select(.duration > 1000)' ``` ### Session Errors / Random Logouts **Symptom:** Users randomly logged out, "session expired" errors **Causes:** 1. **Container restarted** — Sessions are in-memory, lost on restart 2. **SESSION_SECRET changed** — Invalidates all sessions 3. **SESSION_SECRET not set** — Each restart generates new secret **Solutions:** 1. **Verify SESSION_SECRET is set:** ```bash docker exec atbb env | grep SESSION_SECRET # Should show a 64-character hex string ``` 2. **If blank, generate and set:** ```bash openssl rand -hex 32 # Add to .env.production # Restart container ``` 3. **Future:** Use Redis for persistent sessions (not yet implemented) ### Getting Help If you cannot resolve an issue: 1. **Collect diagnostics:** ```bash # Container logs docker logs atbb > atbb-logs.txt # Container info docker inspect atbb > atbb-inspect.json # Resource usage docker stats --no-stream atbb ``` 2. **Sanitize sensitive data:** - Remove passwords from logs - Remove `SESSION_SECRET` from environment dumps 3. **Report issue:** - GitHub Issues: https://github.com/malpercio-dev/atbb-monorepo/issues - Include: atBB version, error messages, steps to reproduce - Attach sanitized logs --- ## 10. Docker Compose Example For simpler local testing or single-server deployments, use Docker Compose. **File:** `docker-compose.example.yml` (included in repository) ### What It Provides - PostgreSQL database (local development) - atBB application container - Automatic dependency management (atBB waits for PostgreSQL) - Volume persistence for database - Health checks ### Usage **Step 1: Download files** ```bash # Download docker-compose.example.yml curl -O https://raw.githubusercontent.com/malpercio-dev/atbb-monorepo/main/docker-compose.example.yml # Download .env.production.example curl -O https://raw.githubusercontent.com/malpercio-dev/atbb-monorepo/main/.env.production.example # Rename to .env mv .env.production.example .env ``` **Step 2: Configure environment** ```bash # Generate session secret openssl rand -hex 32 # Edit .env and fill in: nano .env ``` Required changes in `.env`: ```bash # AT Protocol credentials (from Prerequisites) FORUM_DID=did:plc:YOUR_FORUM_DID PDS_URL=https://bsky.social FORUM_HANDLE=forum.example.com FORUM_PASSWORD=YOUR_FORUM_PASSWORD # OAuth (for local testing, use http://localhost) OAUTH_PUBLIC_URL=http://localhost # Session secret (generated above) SESSION_SECRET=a1b2c3d4e5f6... # Database connection will be set by docker-compose # (Uses container name "postgres" as hostname) ``` **Step 3: Start services** ```bash docker-compose -f docker-compose.example.yml up -d ``` Expected output: ``` Creating network "atbb_default" with the default driver Creating volume "atbb_postgres_data" with default driver Creating atbb-postgres ... done Creating atbb-app ... done ``` **Step 4: Run migrations** ```bash docker-compose -f docker-compose.example.yml exec atbb \ pnpm --filter @atbb/appview db:migrate ``` **Step 5: Access forum** Visit: **http://localhost** ### Management Commands **View logs:** ```bash # All services docker-compose -f docker-compose.example.yml logs -f # Specific service docker-compose -f docker-compose.example.yml logs -f atbb docker-compose -f docker-compose.example.yml logs -f postgres ``` **Stop services:** ```bash docker-compose -f docker-compose.example.yml down ``` **Stop and remove data:** ```bash docker-compose -f docker-compose.example.yml down -v # WARNING: This deletes the database volume! ``` **Restart services:** ```bash docker-compose -f docker-compose.example.yml restart ``` **Upgrade to new version:** ```bash # Pull new image docker-compose -f docker-compose.example.yml pull atbb # Run migrations (if required by release notes) docker-compose -f docker-compose.example.yml exec atbb \ pnpm --filter @atbb/appview db:migrate # Restart docker-compose -f docker-compose.example.yml restart atbb ``` ### Production Considerations **DO NOT use docker-compose.example.yml as-is in production.** Limitations: - Database password is weak (change in compose file) - No TLS/SSL for database - No backups configured - Single-server only **For production:** 1. Use managed PostgreSQL (AWS RDS, DigitalOcean, etc.) 2. Run atBB container separately (not with local PostgreSQL) 3. Set up reverse proxy with HTTPS (Caddy/nginx) 4. Use strong passwords and secrets 5. Configure automated backups 6. Set up monitoring and alerting **Modified compose for production (atBB only, external DB):** ```yaml version: '3.8' services: atbb: image: ghcr.io/malpercio-dev/atbb:v1.0.0 container_name: atbb restart: unless-stopped ports: - "127.0.0.1:8080:80" # Bind to localhost only env_file: - .env.production healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/api/healthz"] interval: 30s timeout: 3s retries: 3 ``` --- ## Appendix: Quick Reference ### Required Environment Variables ```bash # PostgreSQL (production recommended) DATABASE_URL=postgresql://user:pass@host:5432/db?sslmode=require # SQLite (lightweight alternative) # DATABASE_URL=file:./atbb.db FORUM_DID=did:plc:xxxxxxxxxxxxx PDS_URL=https://bsky.social FORUM_HANDLE=forum.example.com FORUM_PASSWORD=strong_password_16+_chars OAUTH_PUBLIC_URL=https://forum.example.com SESSION_SECRET=64_hex_chars_from_openssl_rand ``` ### Essential Commands ```bash # Pull image docker pull ghcr.io/malpercio-dev/atbb:latest # Run migrations docker run --rm --env-file .env.production \ ghcr.io/malpercio-dev/atbb:latest \ pnpm --filter @atbb/appview db:migrate # Start container docker run -d --name atbb --restart unless-stopped \ -p 8080:80 --env-file .env.production \ ghcr.io/malpercio-dev/atbb:latest # View logs docker logs -f atbb # Stop/restart docker stop atbb docker restart atbb # Health check curl http://localhost:8080/api/healthz ``` ### Support Resources - **Documentation:** https://github.com/malpercio-dev/atbb-monorepo/tree/main/docs - **Issues:** https://github.com/malpercio-dev/atbb-monorepo/issues - **Releases:** https://github.com/malpercio-dev/atbb-monorepo/releases - **AT Protocol Docs:** https://atproto.com/docs ### Security Checklist Before going to production: - [ ] Generated `SESSION_SECRET` with `openssl rand -hex 32` - [ ] Used strong, unique passwords (minimum 16 characters) - [ ] Enabled database SSL/TLS (`?sslmode=require`) - [ ] Set `OAUTH_PUBLIC_URL` to HTTPS domain (not HTTP) - [ ] Set file permissions: `chmod 600 .env.production` - [ ] Never committed `.env.production` to version control - [ ] Configured reverse proxy with HTTPS (Caddy/nginx) - [ ] Set up database backups - [ ] Configured log rotation - [ ] Set up health monitoring - [ ] Restricted firewall to ports 80/443 only - [ ] Tested backup restoration procedure --- ## 11. NixOS Deployment The atBB flake provides a NixOS module that manages all services declaratively: - **`atbb-appview`** — Hono API server (systemd service) - **`atbb-web`** — Hono web UI server (systemd service) - **`atbb-migrate`** — One-shot database migration service - **PostgreSQL 17** — Local database with peer authentication (optional) - **nginx** — Reverse proxy with automatic ACME/Let's Encrypt TLS (optional) The module is suitable for single-server deployments. Sections 1–10 of this guide describe Docker-based deployment; this section covers the NixOS path exclusively. ### Step 1: Add atBB as a Flake Input In your NixOS system flake: ```nix { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; atbb.url = "github:malpercio-dev/atbb-monorepo"; }; outputs = { nixpkgs, atbb, ... }: { nixosConfigurations.my-server = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ atbb.nixosModules.default ./configuration.nix ]; }; }; } ``` > **Note:** The module is exported as `nixosModules.default`, not `nixosModules.atbb`. ### Step 2: Create the Environment File The module reads secrets from an environment file on the server (never bake secrets into the Nix store). Create the file at a path of your choosing — `/etc/atbb/env` is a reasonable default: ```bash sudo mkdir -p /etc/atbb sudo tee /etc/atbb/env > /dev/null <<'EOF' # Database — Unix socket peer auth (matches services.atbb.user = "atbb") DATABASE_URL=postgres:///atbb?host=/run/postgresql # PGHOST makes postgres.js use the Unix socket directory reliably, # since it does not always honour the ?host= query parameter in URLs. PGHOST=/run/postgresql # Session security SESSION_SECRET= # Forum AT Protocol account credentials FORUM_HANDLE=forum.example.com FORUM_PASSWORD= EOF sudo chmod 600 /etc/atbb/env ``` **Why Unix socket for `DATABASE_URL`?** When `database.enable = true` (the default), the module creates a local PostgreSQL 17 instance and configures peer authentication. Peer auth maps the OS user name to the database user name — no password needed. The connection string `postgres:///atbb?host=/run/postgresql` says: connect to the `atbb` database via the Unix socket at `/run/postgresql`, as the current OS user (`atbb`). **Secrets management:** For automated deployments, consider [sops-nix](https://github.com/Mic92/sops-nix) or [agenix](https://github.com/ryantm/agenix) to provision `/etc/atbb/env` as an encrypted secret rather than managing it manually. ### Step 3: Configure the Module Add to your `configuration.nix`: ```nix { services.atbb = { enable = true; domain = "forum.example.com"; forumDid = "did:plc:your-forum-did"; pdsUrl = "https://bsky.social"; # Path to the environment file created in Step 2 environmentFile = /etc/atbb/env; # Local PostgreSQL (default: true) # Set to false to use an external database via DATABASE_URL database.enable = true; # Run migrations manually after each deploy (safer) # Set to true to run automatically on every appview start autoMigrate = false; }; # Required when enableACME = true (the default) security.acme = { acceptTerms = true; defaults.email = "admin@example.com"; }; } ``` **Important:** When `database.enable = true`, the system user name (`services.atbb.user`, default `"atbb"`) must match the database name (`services.atbb.database.name`, default `"atbb"`). PostgreSQL peer authentication requires this. The module enforces this with an assertion — if you change either value, change both to match. #### Key Options Reference | Option | Default | Description | |--------|---------|-------------| | `domain` | *(required)* | Public domain for the forum | | `forumDid` | *(required)* | Forum's AT Protocol DID | | `pdsUrl` | *(required)* | URL of the forum's PDS | | `environmentFile` | *(required)* | Path to secrets file | | `database.enable` | `true` | Provision local PostgreSQL 17 | | `database.name` | `"atbb"` | Database name | | `autoMigrate` | `false` | Run migrations on appview start | | `enableNginx` | `true` | Configure nginx reverse proxy | | `enableACME` | `true` | Enable Let's Encrypt TLS | | `appviewPort` | `3000` | Internal port for appview | | `webPort` | `3001` | Internal port for web UI | | `user` / `group` | `"atbb"` | System user/group for services | ### Step 4: Deploy Apply your configuration using your preferred NixOS deployment tool: ```bash # Local rebuild sudo nixos-rebuild switch --flake .#my-server # Remote via colmena colmena apply --on my-server # Remote via nixos-rebuild nixos-rebuild switch --flake .#my-server \ --target-host root@forum.example.com ``` ### Step 5: Run Database Migrations The `atbb-migrate` service is a one-shot systemd unit — it runs once and exits. Trigger it manually after each deployment: ```bash sudo systemctl start atbb-migrate # Check migration output sudo journalctl -u atbb-migrate ``` **Expected output:** ``` Reading migrations from /nix/store/.../apps/appview/drizzle Applying migration: 0000_initial_schema.sql ... All migrations applied successfully ``` If you prefer migrations to run automatically on every appview start, set `autoMigrate = true`. Be aware this adds startup latency and prevents appview from starting if migrations fail. ### Step 6: Verify Services ```bash # Check all atBB services systemctl status atbb-appview atbb-web # View live logs journalctl -fu atbb-appview journalctl -fu atbb-web # Test the API curl http://localhost:3000/api/healthz # Expected: {"status":"ok"} # Verify nginx is routing correctly curl https://forum.example.com/api/healthz ``` ### Using Caddy Instead of nginx If you prefer Caddy, disable the built-in nginx proxy and configure `services.caddy` yourself: ```nix { services.atbb = { # ... other options enableNginx = false; # disable built-in nginx virtualHost enableACME = false; # Caddy manages TLS automatically }; services.caddy = { enable = true; virtualHosts."forum.example.com".extraConfig = '' # AT Protocol well-known endpoints → appview # Must reach appview (not web UI) for OAuth to work handle /.well-known/* { reverse_proxy localhost:${toString config.services.atbb.appviewPort} } # REST API → appview handle /api/* { reverse_proxy localhost:${toString config.services.atbb.appviewPort} } # Web UI — catch-all handle { reverse_proxy localhost:${toString config.services.atbb.webPort} } ''; }; } ``` See `nix/Caddyfile.example` in the repository for the equivalent standalone Caddyfile. ### Upgrading To upgrade atBB, update the flake input and redeploy: ```bash # Update atBB to latest nix flake update atbb # Redeploy sudo nixos-rebuild switch --flake .#my-server # Run migrations for the new version sudo systemctl start atbb-migrate ``` NixOS handles the service restart automatically when the package changes. Because `atbb-appview` and `atbb-web` are declared with `Restart = "on-failure"`, a failed startup will not leave broken processes running. --- **End of Deployment Guide** For questions or issues not covered here, please open an issue at: https://github.com/malpercio-dev/atbb-monorepo/issues