WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at main 2020 lines 53 kB view raw view rendered
1# atBB Deployment Guide 2 3**Version:** 1.2 4**Last Updated:** 2026-02-26 5**Audience:** System administrators deploying atBB to production 6 7> **Related Documentation:** 8> - [docs/trust-model.md](trust-model.md) — Trust model for self-hosted deployment: what the AppView controls, user data guarantees, and security implications 9> - [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 10 11## Table of Contents 12 131. [Prerequisites](#1-prerequisites) 142. [Quick Start](#2-quick-start) 153. [Environment Configuration](#3-environment-configuration) 164. [Database Setup](#4-database-setup) 175. [Running the Container](#5-running-the-container) 186. [Reverse Proxy Setup](#6-reverse-proxy-setup) 197. [Monitoring & Logs](#7-monitoring--logs) 208. [Upgrading](#8-upgrading) 219. [Troubleshooting](#9-troubleshooting) 2210. [Docker Compose Example](#10-docker-compose-example) 2311. [NixOS Deployment](#11-nixos-deployment) 24 25--- 26 27## 1. Prerequisites 28 29Before deploying atBB, ensure you have the following: 30 31### Infrastructure Requirements 32 33- **PostgreSQL 14+** 34 - Managed service recommended: AWS RDS, DigitalOcean Managed Database, Azure Database for PostgreSQL, or similar 35 - Minimum 1GB RAM, 10GB storage (scales with forum size) 36 - SSL/TLS support enabled (`?sslmode=require`) 37 - Database user with CREATE/ALTER/SELECT/INSERT/UPDATE/DELETE permissions 38 39- **Domain Name & DNS** 40 - Registered domain name (e.g., `forum.example.com`) 41 - DNS A/AAAA record pointing to your server's public IP 42 - Recommended: wildcard DNS for future subdomains (`*.forum.example.com`) 43 44- **Container Runtime** 45 - Docker 20.10+ or Docker Desktop 46 - Minimum 512MB RAM allocated to container (1GB+ recommended) 47 - 2GB disk space for container image and logs 48 49### AT Protocol Requirements 50 51**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. 52 53#### 1. Choose a Personal Data Server (PDS) 54 55Your forum needs a PDS to store its records (forum metadata, categories, moderation actions). Options: 56 57- **Self-hosted PDS:** Run your own PDS instance (advanced, recommended for sovereignty) 58 - Guide: https://github.com/bluesky-social/pds 59 - Requires separate server and domain 60 - Full control over data and federation 61 62- **Hosted PDS:** Use Bluesky's PDS (`https://bsky.social`) or another provider 63 - Simpler setup, lower maintenance 64 - Suitable for testing and small forums 65 66#### 2. Create Forum Account 67 68Create an account for your forum on your chosen PDS: 69 70```bash 71# Example with Bluesky PDS 72# Visit https://bsky.app and create account with your forum's handle 73# Handle should match your domain: forum.example.com 74``` 75 76**Record these values (you'll need them later):** 77- Forum Handle: `forum.example.com` 78- Forum Password: (choose a strong password, minimum 16 characters) 79- Forum DID: `did:plc:xxxxxxxxxxxxx` (found in account settings or PDS admin interface) 80- PDS URL: `https://bsky.social` (or your PDS URL) 81 82#### 3. Understand Lexicon Namespace 83 84atBB uses the `space.atbb.*` lexicon namespace for its records: 85- `space.atbb.forum.forum` — Forum metadata (name, description, rules) 86- `space.atbb.forum.category` — Forum categories 87- `space.atbb.post` — User posts and replies 88- `space.atbb.membership` — User membership records 89- `space.atbb.modAction` — Moderation actions 90 91Your forum's DID will own the forum-level records, while users' DIDs own their posts and memberships. 92 93### Security Requirements 94 95- **TLS/SSL Certificate:** Let's Encrypt (free) or commercial certificate 96- **Firewall:** Restrict inbound ports to 80/443 only 97- **SSH Access:** Key-based authentication (disable password auth) 98- **Secrets Management:** Secure storage for environment variables (consider cloud secrets manager) 99 100> **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. 101 102--- 103 104## 2. Quick Start 105 106Follow these steps for a minimal working deployment. Detailed explanations follow in later sections. 107 108### Step 1: Pull the Docker Image 109 110```bash 111# Pull latest stable version 112docker pull ghcr.io/malpercio-dev/atbb:latest 113 114# Or pin to a specific version (recommended for production) 115docker pull ghcr.io/malpercio-dev/atbb:v1.0.0 116``` 117 118Expected output: 119``` 120latest: Pulling from malpercio-dev/atbb 121e7c96db7181b: Pull complete 122... 123Status: Downloaded newer image for ghcr.io/malpercio-dev/atbb:latest 124``` 125 126### Step 2: Create Environment File 127 128```bash 129# Copy the template 130curl -o .env.production https://raw.githubusercontent.com/malpercio-dev/atbb-monorepo/main/.env.production.example 131 132# Generate a strong session secret 133openssl rand -hex 32 134# Output: a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 135``` 136 137**Edit `.env.production` and fill in these REQUIRED values:** 138 139```bash 140# Database connection (from your PostgreSQL provider) 141DATABASE_URL=postgresql://atbb_user:YOUR_DB_PASSWORD@db.example.com:5432/atbb_prod?sslmode=require 142 143# AT Protocol credentials (from Prerequisites step) 144FORUM_DID=did:plc:YOUR_FORUM_DID 145PDS_URL=https://bsky.social 146FORUM_HANDLE=forum.example.com 147FORUM_PASSWORD=YOUR_FORUM_PASSWORD 148 149# OAuth configuration (your public domain) 150OAUTH_PUBLIC_URL=https://forum.example.com 151 152# Session security (use the openssl output from above) 153SESSION_SECRET=a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 154``` 155 156**Secure the file:** 157```bash 158chmod 600 .env.production 159``` 160 161### Step 3: Run Database Migrations 162 163**CRITICAL:** Run migrations BEFORE starting the application. This creates the database schema. 164 165```bash 166docker run --rm \ 167 --env-file .env.production \ 168 ghcr.io/malpercio-dev/atbb:latest \ 169 pnpm --filter @atbb/appview db:migrate 170``` 171 172Expected output: 173``` 174> @atbb/db@0.1.0 db:migrate 175> drizzle-kit migrate 176 177Reading migrations from migrations/ 178Applying migration: 0000_initial_schema.sql 179Migration applied successfully 180``` 181 182**If this fails, DO NOT proceed.** See [Section 4: Database Setup](#4-database-setup) for troubleshooting. 183 184### Step 4: Start the Container 185 186```bash 187docker run -d \ 188 --name atbb \ 189 --restart unless-stopped \ 190 -p 8080:80 \ 191 --env-file .env.production \ 192 ghcr.io/malpercio-dev/atbb:latest 193``` 194 195Options explained: 196- `-d` — Run in background (detached mode) 197- `--name atbb` — Name the container for easy management 198- `--restart unless-stopped` — Auto-restart on crashes or server reboot 199- `-p 8080:80` — Map host port 8080 to container port 80 200- `--env-file .env.production` — Load environment variables 201 202**Verify the container is running:** 203```bash 204docker ps | grep atbb 205# Expected: Container with STATUS "Up X seconds" 206 207docker logs atbb 208# Expected: No errors, services starting 209``` 210 211**Test the application:** 212```bash 213curl http://localhost:8080/api/healthz 214# Expected: {"status":"ok"} 215``` 216 217### Step 5: Configure Reverse Proxy 218 219**The container is now running on port 8080, but NOT accessible publicly yet.** You need a reverse proxy to: 220- Terminate TLS/SSL (HTTPS) 221- Forward traffic from your domain to the container 222- Handle automatic certificate renewal 223 224**Recommended setup with Caddy (automatic HTTPS):** 225 226Install Caddy: 227```bash 228# Ubuntu/Debian 229sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https 230curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg 231curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list 232sudo apt update 233sudo apt install caddy 234``` 235 236Edit `/etc/caddy/Caddyfile`: 237``` 238forum.example.com { 239 reverse_proxy localhost:8080 240} 241``` 242 243Reload Caddy: 244```bash 245sudo systemctl reload caddy 246``` 247 248**Caddy will automatically obtain a Let's Encrypt certificate and enable HTTPS.** 249 250### Step 6: Verify Deployment 251 252Visit your forum: **https://forum.example.com** 253 254Expected: atBB home page loads with no errors. 255 256**If you see errors, proceed to [Section 9: Troubleshooting](#9-troubleshooting).** 257 258--- 259 260## 3. Environment Configuration 261 262Complete reference for all environment variables. See `.env.production.example` for detailed comments. 263 264### Required Variables 265 266| Variable | Description | Example | 267|----------|-------------|---------| 268| `DATABASE_URL` | Database connection string (PostgreSQL or SQLite) | PostgreSQL: `postgresql://user:pass@host:5432/dbname?sslmode=require`; SQLite: `file:./atbb.db` | 269| `FORUM_DID` | Forum's AT Protocol DID | `did:plc:abcdef1234567890` | 270| `PDS_URL` | Personal Data Server URL | `https://bsky.social` | 271| `FORUM_HANDLE` | Forum's AT Protocol handle | `forum.example.com` | 272| `FORUM_PASSWORD` | Forum account password | (minimum 16 characters, alphanumeric + symbols) | 273| `OAUTH_PUBLIC_URL` | Public URL for OAuth redirects | `https://forum.example.com` (MUST be HTTPS in production) | 274| `SESSION_SECRET` | Session encryption key | Generate with: `openssl rand -hex 32` | 275 276### Optional Variables 277 278| Variable | Default | Description | 279|----------|---------|-------------| 280| `PORT` | `3000` | AppView API port (internal) | 281| `WEB_PORT` | `3001` | Web UI port (internal) | 282| `APPVIEW_URL` | `http://localhost:3000` | Internal API URL (keep as localhost for single container) | 283| `JETSTREAM_URL` | `wss://jetstream2.us-east.bsky.network/subscribe` | AT Protocol firehose URL | 284| `SESSION_TTL_DAYS` | `30` | Session lifetime in days (1-90 range) | 285| `REDIS_URL` | (none) | Redis connection string (future: multi-instance deployments) | 286 287### Security Best Practices 288 289**SESSION_SECRET Generation:** 290```bash 291# CRITICAL: Never use a predictable value or leave blank 292openssl rand -hex 32 293 294# Use different secrets for dev/staging/production 295# Rotating the secret invalidates all active sessions 296``` 297 298**Password Requirements:** 299- Minimum 16 characters 300- Mix of uppercase, lowercase, numbers, symbols 301- Unique per environment (never reuse) 302- Store in password manager or secrets vault 303 304**Connection String Security:** 305```bash 306# Good: SSL/TLS enforced 307DATABASE_URL=postgresql://user:pass@host:5432/db?sslmode=require 308 309# Bad: Plain text connection (vulnerable to MITM) 310DATABASE_URL=postgresql://user:pass@host:5432/db 311``` 312 313**File Permissions:** 314```bash 315# Protect your environment file 316chmod 600 .env.production 317 318# Verify permissions 319ls -la .env.production 320# Expected: -rw------- (read/write for owner only) 321``` 322 323### Environment Loading Methods 324 325**Docker CLI:** 326```bash 327# Recommended: Load from file with --init for better signal handling 328docker run --init --env-file .env.production ghcr.io/malpercio-dev/atbb:latest 329 330# Alternative: Individual variables (for orchestrators) 331docker run --init \ 332 -e DATABASE_URL="postgresql://..." \ 333 -e FORUM_DID="did:plc:..." \ 334 -e SESSION_SECRET="..." \ 335 ghcr.io/malpercio-dev/atbb:latest 336``` 337 338**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. 339 340**Docker Compose:** 341```yaml 342services: 343 atbb: 344 image: ghcr.io/malpercio-dev/atbb:latest 345 env_file: 346 - .env.production 347``` 348 349**Kubernetes:** 350```yaml 351# Use Secrets (NOT ConfigMaps for sensitive data) 352apiVersion: v1 353kind: Secret 354metadata: 355 name: atbb-secrets 356type: Opaque 357stringData: 358 DATABASE_URL: "postgresql://..." 359 SESSION_SECRET: "..." 360--- 361apiVersion: apps/v1 362kind: Deployment 363spec: 364 template: 365 spec: 366 containers: 367 - name: atbb 368 envFrom: 369 - secretRef: 370 name: atbb-secrets 371``` 372 373--- 374 375## 4. Database Setup 376 377atBB supports two database backends: 378 379- **PostgreSQL** (recommended for production) — full-featured, suitable for multi-user/multi-server deployments 380- **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). 381 382The rest of this section covers PostgreSQL provisioning. SQLite requires no separate server setup — just point `DATABASE_URL` at a file path. 383 384### PostgreSQL Provisioning 385 386#### Option 1: Managed Database (Recommended) 387 388**AWS RDS:** 3891. Navigate to RDS Console → Create Database 3902. Choose PostgreSQL 14+ (latest stable version) 3913. Select appropriate instance size: 392 - Small forum (<1000 users): `db.t3.micro` or `db.t4g.micro` 393 - Medium forum (1000-10000 users): `db.t3.small` or `db.t4g.small` 394 - Large forum (10000+ users): `db.t3.medium` or higher 3954. Enable "Storage Auto Scaling" (start with 20GB) 3965. Enable "Automated Backups" (7-30 day retention) 3976. Enable "Publicly Accessible" only if container is in different VPC 3987. Security group: Allow PostgreSQL (5432) from container's IP/VPC 3998. Create database: `atbb_prod` 4009. Create user: `atbb_user` with generated password 401 402Connection string format: 403``` 404postgresql://atbb_user:PASSWORD@instance-name.region.rds.amazonaws.com:5432/atbb_prod?sslmode=require 405``` 406 407**DigitalOcean Managed Database:** 4081. Navigate to Databases → Create → PostgreSQL 4092. Choose datacenter closest to your Droplet/container 4103. Select plan (Basic $15/mo sufficient for small forums) 4114. Create database: `atbb_prod` 4125. Create user: `atbb_user` with generated password 4136. Add trusted source: Your Droplet's IP or "All" for simplicity 4147. Download CA certificate (optional, for certificate validation) 415 416Connection string provided in dashboard (copy and use directly). 417 418**Azure Database for PostgreSQL:** 4191. Navigate to Azure Database for PostgreSQL → Create 4202. Choose "Flexible Server" (simpler, cheaper) 4213. Select region and compute tier (Burstable B1ms sufficient for small forums) 4224. Enable "High Availability" for production (optional) 4235. Configure firewall: Add your container's public IP 4246. Create database: `atbb_prod` 425 426Connection string format: 427``` 428postgresql://atbb_user@servername:PASSWORD@servername.postgres.database.azure.com:5432/atbb_prod?sslmode=require 429``` 430 431#### Option 2: Self-Hosted PostgreSQL 432 433**Installation (Ubuntu/Debian):** 434```bash 435# Install PostgreSQL 436sudo apt update 437sudo apt install -y postgresql postgresql-contrib 438 439# Start and enable service 440sudo systemctl enable postgresql 441sudo systemctl start postgresql 442``` 443 444**Create database and user:** 445```bash 446sudo -u postgres psql 447 448-- In psql prompt: 449CREATE DATABASE atbb_prod; 450CREATE USER atbb_user WITH PASSWORD 'YOUR_STRONG_PASSWORD'; 451GRANT ALL PRIVILEGES ON DATABASE atbb_prod TO atbb_user; 452\q 453``` 454 455**Enable remote connections (if container is on different host):** 456 457Edit `/etc/postgresql/14/main/postgresql.conf`: 458``` 459listen_addresses = '*' # Or specific IP 460``` 461 462Edit `/etc/postgresql/14/main/pg_hba.conf`: 463``` 464# Add this line (replace 0.0.0.0/0 with specific IP range in production) 465host atbb_prod atbb_user 0.0.0.0/0 scram-sha-256 466``` 467 468Restart PostgreSQL: 469```bash 470sudo systemctl restart postgresql 471``` 472 473Connection string: 474``` 475postgresql://atbb_user:YOUR_STRONG_PASSWORD@your-server-ip:5432/atbb_prod 476``` 477 478### Running Database Migrations 479 480Migrations create the database schema (tables, indexes, constraints). 481 482**First-time setup:** 483```bash 484docker run --rm \ 485 --env-file .env.production \ 486 ghcr.io/malpercio-dev/atbb:latest \ 487 pnpm --filter @atbb/appview db:migrate 488``` 489 490Options explained: 491- `--rm` — Remove container after migration completes 492- `--env-file .env.production` — Load database connection string 493- `pnpm --filter @atbb/appview db:migrate` — Run Drizzle migrations 494 495**Expected output (success):** 496``` 497Reading migrations from /app/packages/db/migrations 498Applying migration: 0000_initial_schema.sql 499Applying migration: 0001_add_deleted_flag.sql 500All migrations applied successfully 501``` 502 503**Verify migrations:** 504```bash 505# Connect to your database 506psql "postgresql://atbb_user:PASSWORD@host:5432/atbb_prod?sslmode=require" 507 508# List tables 509\dt 510 511# Expected output (12 tables): 512# Schema | Name | Type | Owner 513# --------+-----------------------+-------+----------- 514# public | backfill_errors | table | atbb_user 515# public | backfill_progress | table | atbb_user 516# public | boards | table | atbb_user 517# public | categories | table | atbb_user 518# public | firehose_cursor | table | atbb_user 519# public | forums | table | atbb_user 520# public | memberships | table | atbb_user 521# public | mod_actions | table | atbb_user 522# public | posts | table | atbb_user 523# public | role_permissions | table | atbb_user 524# public | roles | table | atbb_user 525# public | users | table | atbb_user 526``` 527 528### Migration Troubleshooting 529 530**Error: "database does not exist"** 531``` 532FATAL: database "atbb_prod" does not exist 533``` 534 535Solution: Create the database first (see self-hosted instructions above, or create via cloud console). 536 537**Error: "password authentication failed"** 538``` 539FATAL: password authentication failed for user "atbb_user" 540``` 541 542Solution: Verify credentials in `DATABASE_URL` match database user. 543 544**Error: "connection refused"** 545``` 546Error: connect ECONNREFUSED 547``` 548 549Solution: 550- Check database host/port are correct 551- Verify firewall allows connections from container's IP 552- For cloud databases, ensure "trusted sources" includes your IP 553 554**Error: "SSL connection required"** 555``` 556FATAL: no pg_hba.conf entry for host, SSL off 557``` 558 559Solution: Add `?sslmode=require` to connection string. 560 561**Error: "permission denied for schema public"** 562``` 563ERROR: permission denied for schema public 564``` 565 566Solution: Grant schema permissions: 567```sql 568GRANT USAGE ON SCHEMA public TO atbb_user; 569GRANT CREATE ON SCHEMA public TO atbb_user; 570``` 571 572--- 573 574## 5. Running the Container 575 576### Basic Deployment 577 578**Production command (recommended):** 579```bash 580docker run -d \ 581 --name atbb \ 582 --restart unless-stopped \ 583 -p 8080:80 \ 584 --env-file .env.production \ 585 ghcr.io/malpercio-dev/atbb:latest 586``` 587 588**Pin to specific version (recommended for stability):** 589```bash 590docker run -d \ 591 --name atbb \ 592 --restart unless-stopped \ 593 -p 8080:80 \ 594 --env-file .env.production \ 595 ghcr.io/malpercio-dev/atbb:v1.0.0 596``` 597 598**Pin to specific commit SHA (for rollback/testing):** 599```bash 600docker run -d \ 601 --name atbb \ 602 --restart unless-stopped \ 603 -p 8080:80 \ 604 --env-file .env.production \ 605 ghcr.io/malpercio-dev/atbb:main-a1b2c3d 606``` 607 608### Advanced Options 609 610**Custom port mapping:** 611```bash 612# Expose on different host port 613docker run -d \ 614 --name atbb \ 615 -p 3000:80 \ 616 --env-file .env.production \ 617 ghcr.io/malpercio-dev/atbb:latest 618 619# Bind to specific interface (localhost only) 620docker run -d \ 621 --name atbb \ 622 -p 127.0.0.1:8080:80 \ 623 --env-file .env.production \ 624 ghcr.io/malpercio-dev/atbb:latest 625``` 626 627**Resource limits:** 628```bash 629docker run -d \ 630 --name atbb \ 631 --restart unless-stopped \ 632 -p 8080:80 \ 633 --memory="1g" \ 634 --cpus="1.0" \ 635 --env-file .env.production \ 636 ghcr.io/malpercio-dev/atbb:latest 637``` 638 639**Custom network:** 640```bash 641# Create network 642docker network create atbb-network 643 644# Run with network 645docker run -d \ 646 --name atbb \ 647 --network atbb-network \ 648 -p 8080:80 \ 649 --env-file .env.production \ 650 ghcr.io/malpercio-dev/atbb:latest 651``` 652 653### Container Management 654 655**View logs:** 656```bash 657# All logs 658docker logs atbb 659 660# Follow logs (live) 661docker logs -f atbb 662 663# Last 100 lines 664docker logs --tail 100 atbb 665 666# Logs since timestamp 667docker logs --since 2026-02-12T10:00:00 atbb 668``` 669 670**Stop container:** 671```bash 672docker stop atbb 673``` 674 675**Start stopped container:** 676```bash 677docker start atbb 678``` 679 680**Restart container:** 681```bash 682docker restart atbb 683``` 684 685**Remove container:** 686```bash 687# Stop first 688docker stop atbb 689 690# Remove 691docker rm atbb 692``` 693 694**Execute commands inside container (debugging):** 695```bash 696# Interactive shell 697docker exec -it atbb sh 698 699# Run single command 700docker exec atbb ps aux 701docker exec atbb df -h 702docker exec atbb cat /etc/nginx/nginx.conf 703``` 704 705### Health Checks 706 707The container exposes a health endpoint: 708 709**Check via curl:** 710```bash 711curl http://localhost:8080/api/healthz 712``` 713 714**Expected response:** 715```json 716{"status":"ok"} 717``` 718 719**Check via Docker:** 720```bash 721docker inspect atbb | grep -A 5 Health 722``` 723 724**Use in monitoring scripts:** 725```bash 726#!/bin/bash 727# health-check.sh 728 729HEALTH=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/healthz) 730 731if [ "$HEALTH" != "200" ]; then 732 echo "ALERT: atBB health check failed (HTTP $HEALTH)" 733 # Send alert (email, Slack, PagerDuty, etc.) 734 exit 1 735fi 736 737echo "OK: atBB is healthy" 738exit 0 739``` 740 741**Run as cron job:** 742```bash 743# Check every 5 minutes 744*/5 * * * * /path/to/health-check.sh >> /var/log/atbb-health.log 2>&1 745``` 746 747--- 748 749## 6. Reverse Proxy Setup 750 751The container exposes HTTP on port 80. In production, you need a reverse proxy to: 752- Terminate TLS/SSL (enable HTTPS) 753- Manage domain routing 754- Handle certificate renewal 755- Provide additional security headers 756 757### Caddy (Recommended) 758 759**Why Caddy:** 760- Automatic HTTPS with Let's Encrypt (zero configuration) 761- Simple configuration syntax 762- Auto-renewal of certificates 763- Modern defaults (HTTP/2, security headers) 764 765**Installation:** 766 767Ubuntu/Debian: 768```bash 769sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https 770curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg 771curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list 772sudo apt update 773sudo apt install caddy 774``` 775 776CentOS/RHEL: 777```bash 778dnf install 'dnf-command(copr)' 779dnf copr enable @caddy/caddy 780dnf install caddy 781``` 782 783**Basic Configuration:** 784 785Edit `/etc/caddy/Caddyfile`: 786``` 787forum.example.com { 788 reverse_proxy localhost:8080 789} 790``` 791 792**Advanced Configuration (with security headers):** 793 794``` 795forum.example.com { 796 # Reverse proxy to atBB container 797 reverse_proxy localhost:8080 798 799 # Security headers 800 header { 801 # Enable HSTS (force HTTPS) 802 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" 803 804 # Prevent clickjacking 805 X-Frame-Options "SAMEORIGIN" 806 807 # Prevent MIME sniffing 808 X-Content-Type-Options "nosniff" 809 810 # XSS protection 811 X-XSS-Protection "1; mode=block" 812 813 # Referrer policy 814 Referrer-Policy "strict-origin-when-cross-origin" 815 816 # Content Security Policy (adjust as needed) 817 Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" 818 } 819 820 # Access logs 821 log { 822 output file /var/log/caddy/atbb-access.log 823 format json 824 } 825} 826``` 827 828**Apply configuration:** 829```bash 830# Validate configuration 831sudo caddy validate --config /etc/caddy/Caddyfile 832 833# Reload Caddy (no downtime) 834sudo systemctl reload caddy 835 836# Check status 837sudo systemctl status caddy 838``` 839 840**Verify HTTPS:** 841```bash 842curl -I https://forum.example.com 843# Expected: HTTP/2 200 with security headers 844``` 845 846### nginx 847 848**Installation:** 849```bash 850sudo apt install -y nginx 851``` 852 853**Configuration:** 854 855Create `/etc/nginx/sites-available/atbb`: 856```nginx 857# HTTP -> HTTPS redirect 858server { 859 listen 80; 860 listen [::]:80; 861 server_name forum.example.com; 862 return 301 https://$server_name$request_uri; 863} 864 865# HTTPS server 866server { 867 listen 443 ssl http2; 868 listen [::]:443 ssl http2; 869 server_name forum.example.com; 870 871 # SSL certificates (obtain via certbot) 872 ssl_certificate /etc/letsencrypt/live/forum.example.com/fullchain.pem; 873 ssl_certificate_key /etc/letsencrypt/live/forum.example.com/privkey.pem; 874 ssl_trusted_certificate /etc/letsencrypt/live/forum.example.com/chain.pem; 875 876 # SSL settings (Mozilla Modern configuration) 877 ssl_protocols TLSv1.3; 878 ssl_prefer_server_ciphers off; 879 ssl_session_timeout 1d; 880 ssl_session_cache shared:SSL:10m; 881 ssl_session_tickets off; 882 883 # Security headers 884 add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; 885 add_header X-Frame-Options "SAMEORIGIN" always; 886 add_header X-Content-Type-Options "nosniff" always; 887 add_header X-XSS-Protection "1; mode=block" always; 888 889 # Proxy to atBB container 890 location / { 891 proxy_pass http://127.0.0.1:8080; 892 proxy_http_version 1.1; 893 proxy_set_header Host $host; 894 proxy_set_header X-Real-IP $remote_addr; 895 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 896 proxy_set_header X-Forwarded-Proto $scheme; 897 898 # WebSocket support (for future features) 899 proxy_set_header Upgrade $http_upgrade; 900 proxy_set_header Connection "upgrade"; 901 } 902 903 # Access logs 904 access_log /var/log/nginx/atbb-access.log combined; 905 error_log /var/log/nginx/atbb-error.log; 906} 907``` 908 909**Obtain SSL certificate with Certbot:** 910```bash 911# Install Certbot 912sudo apt install -y certbot python3-certbot-nginx 913 914# Obtain certificate (interactive) 915sudo certbot --nginx -d forum.example.com 916 917# Certbot will automatically: 918# - Validate domain ownership 919# - Obtain certificate from Let's Encrypt 920# - Update nginx configuration 921# - Set up auto-renewal 922``` 923 924**Enable site:** 925```bash 926sudo ln -s /etc/nginx/sites-available/atbb /etc/nginx/sites-enabled/ 927sudo nginx -t # Test configuration 928sudo systemctl reload nginx 929``` 930 931### Traefik 932 933**docker-compose.yml with Traefik:** 934```yaml 935version: '3.8' 936 937services: 938 traefik: 939 image: traefik:v2.11 940 command: 941 - "--providers.docker=true" 942 - "--entrypoints.web.address=:80" 943 - "--entrypoints.websecure.address=:443" 944 - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" 945 - "--certificatesresolvers.letsencrypt.acme.email=admin@example.com" 946 - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" 947 ports: 948 - "80:80" 949 - "443:443" 950 volumes: 951 - "/var/run/docker.sock:/var/run/docker.sock:ro" 952 - "./letsencrypt:/letsencrypt" 953 954 atbb: 955 image: ghcr.io/malpercio-dev/atbb:latest 956 env_file: 957 - .env.production 958 labels: 959 - "traefik.enable=true" 960 - "traefik.http.routers.atbb.rule=Host(`forum.example.com`)" 961 - "traefik.http.routers.atbb.entrypoints=websecure" 962 - "traefik.http.routers.atbb.tls.certresolver=letsencrypt" 963 - "traefik.http.services.atbb.loadbalancer.server.port=80" 964``` 965 966Start with: 967```bash 968docker-compose up -d 969``` 970 971--- 972 973## 7. Monitoring & Logs 974 975### Container Logs 976 977**View logs:** 978```bash 979# All logs 980docker logs atbb 981 982# Follow logs (real-time) 983docker logs -f atbb 984 985# Filter by timestamp 986docker logs --since 2026-02-12T10:00:00 atbb 987docker logs --until 2026-02-12T12:00:00 atbb 988``` 989 990**Log format:** JSON structured logs 991 992Example log entry: 993```json 994{ 995 "level": "info", 996 "time": "2026-02-12T14:30:00.000Z", 997 "service": "appview", 998 "msg": "HTTP request", 999 "method": "GET", 1000 "path": "/api/forum", 1001 "status": 200, 1002 "duration": 15 1003} 1004``` 1005 1006**Parse logs with jq:** 1007```bash 1008# Filter by level 1009docker logs atbb | grep '^{' | jq 'select(.level == "error")' 1010 1011# Extract errors from last hour 1012docker logs --since 1h atbb | grep '^{' | jq 'select(.level == "error")' 1013 1014# Count requests by path 1015docker logs atbb | grep '^{' | jq -r '.path' | sort | uniq -c | sort -nr 1016``` 1017 1018### Log Persistence 1019 1020**Forward to log aggregator:** 1021 1022Using Docker logging driver (syslog): 1023```bash 1024docker run -d \ 1025 --name atbb \ 1026 --log-driver syslog \ 1027 --log-opt syslog-address=udp://logserver:514 \ 1028 --log-opt tag="atbb" \ 1029 -p 8080:80 \ 1030 --env-file .env.production \ 1031 ghcr.io/malpercio-dev/atbb:latest 1032``` 1033 1034Using Docker logging driver (json-file with rotation): 1035```bash 1036docker run -d \ 1037 --name atbb \ 1038 --log-driver json-file \ 1039 --log-opt max-size=10m \ 1040 --log-opt max-file=3 \ 1041 -p 8080:80 \ 1042 --env-file .env.production \ 1043 ghcr.io/malpercio-dev/atbb:latest 1044``` 1045 1046### Health Monitoring 1047 1048**Health endpoint:** `GET /api/healthz` 1049 1050Example monitoring script (save as `/usr/local/bin/atbb-health-check`): 1051```bash 1052#!/bin/bash 1053# atbb-health-check - Monitor atBB health and restart if needed 1054 1055CONTAINER_NAME="atbb" 1056HEALTH_URL="http://localhost:8080/api/healthz" 1057MAX_FAILURES=3 1058 1059FAILURES=0 1060 1061while true; do 1062 # Check health endpoint 1063 HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL") 1064 1065 if [ "$HTTP_CODE" != "200" ]; then 1066 FAILURES=$((FAILURES + 1)) 1067 echo "$(date): Health check failed (HTTP $HTTP_CODE), failures: $FAILURES/$MAX_FAILURES" 1068 1069 if [ "$FAILURES" -ge "$MAX_FAILURES" ]; then 1070 echo "$(date): Max failures reached, restarting container" 1071 docker restart "$CONTAINER_NAME" 1072 FAILURES=0 1073 sleep 60 # Wait for restart 1074 fi 1075 else 1076 # Reset failure counter on success 1077 if [ "$FAILURES" -gt 0 ]; then 1078 echo "$(date): Health check recovered" 1079 fi 1080 FAILURES=0 1081 fi 1082 1083 sleep 60 # Check every minute 1084done 1085``` 1086 1087Run as systemd service: 1088```bash 1089sudo chmod +x /usr/local/bin/atbb-health-check 1090 1091cat <<EOF | sudo tee /etc/systemd/system/atbb-health-check.service 1092[Unit] 1093Description=atBB Health Check Monitor 1094After=docker.service 1095Requires=docker.service 1096 1097[Service] 1098Type=simple 1099ExecStart=/usr/local/bin/atbb-health-check 1100Restart=always 1101StandardOutput=append:/var/log/atbb-health-check.log 1102StandardError=append:/var/log/atbb-health-check.log 1103 1104[Install] 1105WantedBy=multi-user.target 1106EOF 1107 1108sudo systemctl daemon-reload 1109sudo systemctl enable atbb-health-check 1110sudo systemctl start atbb-health-check 1111``` 1112 1113### Resource Monitoring 1114 1115**Monitor container resource usage:** 1116```bash 1117# Real-time stats 1118docker stats atbb 1119 1120# Example output: 1121# CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O 1122# atbb 2.5% 256MiB / 1GiB 25% 1.2MB/5MB 0B/0B 1123``` 1124 1125**Set up alerts for resource limits:** 1126```bash 1127#!/bin/bash 1128# atbb-resource-alert - Alert on high resource usage 1129 1130CONTAINER="atbb" 1131CPU_THRESHOLD=80 1132MEM_THRESHOLD=80 1133 1134STATS=$(docker stats --no-stream --format "{{.CPUPerc}},{{.MemPerc}}" "$CONTAINER") 1135CPU=$(echo "$STATS" | cut -d',' -f1 | tr -d '%') 1136MEM=$(echo "$STATS" | cut -d',' -f2 | tr -d '%') 1137 1138if [ "$(echo "$CPU > $CPU_THRESHOLD" | bc)" -eq 1 ]; then 1139 echo "ALERT: CPU usage is ${CPU}% (threshold: ${CPU_THRESHOLD}%)" 1140 # Send notification (email, Slack, etc.) 1141fi 1142 1143if [ "$(echo "$MEM > $MEM_THRESHOLD" | bc)" -eq 1 ]; then 1144 echo "ALERT: Memory usage is ${MEM}% (threshold: ${MEM_THRESHOLD}%)" 1145 # Send notification 1146fi 1147``` 1148 1149### Future: Observability 1150 1151Planned enhancements (not yet implemented): 1152- Prometheus metrics endpoint (`/api/metrics`) 1153- OpenTelemetry tracing 1154- Grafana dashboard templates 1155- Alert manager integration 1156 1157--- 1158 1159## 8. Upgrading 1160 1161### Upgrade Process 1162 1163**IMPORTANT:** Upgrading will cause brief downtime (sessions are stored in memory and will be lost). 1164 1165**Step 1: Check release notes** 1166```bash 1167# View releases on GitHub 1168# https://github.com/malpercio-dev/atbb-monorepo/releases 1169 1170# Look for: 1171# - Breaking changes 1172# - Database migration requirements 1173# - New environment variables 1174``` 1175 1176**Step 2: Backup database** 1177```bash 1178# Backup current database (critical!) 1179pg_dump "postgresql://atbb_user:PASSWORD@host:5432/atbb_prod?sslmode=require" \ 1180 > atbb_backup_$(date +%Y%m%d_%H%M%S).sql 1181 1182# Verify backup 1183ls -lh atbb_backup_*.sql 1184``` 1185 1186**Step 3: Pull new image** 1187```bash 1188# Pull specific version 1189docker pull ghcr.io/malpercio-dev/atbb:v1.1.0 1190 1191# Or pull latest 1192docker pull ghcr.io/malpercio-dev/atbb:latest 1193``` 1194 1195**Step 4: Run migrations (if required)** 1196```bash 1197# Check release notes for migration requirements 1198# If migrations are needed: 1199docker run --rm \ 1200 --env-file .env.production \ 1201 ghcr.io/malpercio-dev/atbb:v1.1.0 \ 1202 pnpm --filter @atbb/appview db:migrate 1203``` 1204 1205**Step 5: Stop old container** 1206```bash 1207docker stop atbb 1208docker rm atbb 1209``` 1210 1211**Step 6: Start new container** 1212```bash 1213docker run -d \ 1214 --name atbb \ 1215 --restart unless-stopped \ 1216 -p 8080:80 \ 1217 --env-file .env.production \ 1218 ghcr.io/malpercio-dev/atbb:v1.1.0 1219``` 1220 1221**Step 7: Verify upgrade** 1222```bash 1223# Check logs for errors 1224docker logs atbb 1225 1226# Test health endpoint 1227curl http://localhost:8080/api/healthz 1228 1229# Visit forum in browser 1230# Test key functionality (login, post, etc.) 1231``` 1232 1233### Rollback Procedure 1234 1235If upgrade fails, rollback to previous version: 1236 1237**Step 1: Stop broken container** 1238```bash 1239docker stop atbb 1240docker rm atbb 1241``` 1242 1243**Step 2: Restore database (if migrations were run)** 1244```bash 1245# Connect to database 1246psql "postgresql://atbb_user:PASSWORD@host:5432/atbb_prod?sslmode=require" 1247 1248# Drop all tables 1249DROP SCHEMA public CASCADE; 1250CREATE SCHEMA public; 1251 1252# Restore from backup 1253psql "postgresql://atbb_user:PASSWORD@host:5432/atbb_prod?sslmode=require" \ 1254 < atbb_backup_20260212_140000.sql 1255``` 1256 1257**Step 3: Start old version** 1258```bash 1259docker run -d \ 1260 --name atbb \ 1261 --restart unless-stopped \ 1262 -p 8080:80 \ 1263 --env-file .env.production \ 1264 ghcr.io/malpercio-dev/atbb:v1.0.0 1265``` 1266 1267### Zero-Downtime Upgrades (Future) 1268 1269Once Redis session storage is implemented, you can upgrade with zero downtime: 1270 12711. Start new container on different port 12722. Test new version 12733. Switch reverse proxy to new port 12744. Stop old container 1275 1276**Not currently supported** because sessions are in-memory. 1277 1278--- 1279 1280## 9. Troubleshooting 1281 1282### Container Won't Start 1283 1284**Symptom:** Container exits immediately after starting 1285 1286**Diagnosis:** 1287```bash 1288docker logs atbb 1289``` 1290 1291**Common causes:** 1292 12931. **Missing environment variables** 1294 ``` 1295 Error: DATABASE_URL is required 1296 ``` 1297 Solution: Verify `.env.production` has all required variables (see Section 3). 1298 12992. **Database connection failed** 1300 ``` 1301 Error: connect ECONNREFUSED 1302 ``` 1303 Solution: 1304 - Verify `DATABASE_URL` is correct 1305 - Check firewall allows connections from container's IP 1306 - Test connection manually: `psql "postgresql://..."` 1307 13083. **Port already in use** 1309 ``` 1310 Error: bind: address already in use 1311 ``` 1312 Solution: Change host port mapping: `-p 8081:80` 1313 13144. **Migrations not run** 1315 ``` 1316 Error: relation "forums" does not exist 1317 ``` 1318 Solution: Run migrations (Section 4). 1319 1320### Database Connection Issues 1321 1322**Symptom:** Application starts but fails on database queries 1323 1324**Error examples:** 1325``` 1326FATAL: password authentication failed for user "atbb_user" 1327FATAL: no pg_hba.conf entry for host, SSL off 1328Error: connect ETIMEDOUT 1329``` 1330 1331**Solutions:** 1332 13331. **Test connection manually:** 1334 ```bash 1335 psql "postgresql://atbb_user:PASSWORD@host:5432/atbb_prod?sslmode=require" 1336 ``` 1337 If this fails, the issue is NOT with atBB (fix database access first). 1338 13392. **Check credentials:** 1340 - Verify username/password in `DATABASE_URL` 1341 - Ensure user has been created in database 1342 13433. **Check SSL settings:** 1344 ```bash 1345 # If database requires SSL, ensure connection string includes: 1346 DATABASE_URL=postgresql://...?sslmode=require 1347 ``` 1348 13494. **Check network/firewall:** 1350 - Verify container can reach database host 1351 - Test from within container: `docker exec atbb ping db.example.com` 1352 - Check cloud provider security groups/firewall rules 1353 1354### OAuth Redirect URI Mismatch 1355 1356**Symptom:** Login fails with "redirect URI mismatch" error 1357 1358**Cause:** `OAUTH_PUBLIC_URL` doesn't match the actual domain users access 1359 1360**Solution:** 1361 13621. Verify `OAUTH_PUBLIC_URL` in `.env.production`: 1363 ```bash 1364 OAUTH_PUBLIC_URL=https://forum.example.com # Must match actual domain 1365 ``` 1366 13672. Common mistakes: 1368 - ❌ `http://` instead of `https://` (use HTTPS in production) 1369 - ❌ Trailing slash: `https://forum.example.com/` (remove trailing slash) 1370 - ❌ Wrong subdomain: `https://www.forum.example.com` vs `https://forum.example.com` 1371 13723. Restart container after fixing: 1373 ```bash 1374 docker restart atbb 1375 ``` 1376 1377### PDS Connectivity Problems 1378 1379**Symptom:** Cannot create posts, forum metadata not syncing 1380 1381**Error in logs:** 1382``` 1383Error: Failed to connect to PDS: ENOTFOUND 1384Error: Invalid credentials for FORUM_HANDLE 1385``` 1386 1387**Solutions:** 1388 13891. **Verify PDS URL:** 1390 ```bash 1391 curl https://bsky.social/xrpc/_health 1392 # Should return: {"version":"0.x.x"} 1393 ``` 1394 13952. **Test forum credentials:** 1396 ```bash 1397 # Use atproto CLI or curl to test auth 1398 curl -X POST https://bsky.social/xrpc/com.atproto.server.createSession \ 1399 -H "Content-Type: application/json" \ 1400 -d '{ 1401 "identifier": "forum.example.com", 1402 "password": "YOUR_FORUM_PASSWORD" 1403 }' 1404 # Should return: {"did":"did:plc:...","accessJwt":"..."} 1405 ``` 1406 14073. **Check environment variables:** 1408 ```bash 1409 docker exec atbb env | grep -E 'FORUM_|PDS_' 1410 # Verify all values are correct 1411 ``` 1412 1413### High Memory Usage 1414 1415**Symptom:** Container using excessive memory (>1GB) 1416 1417**Diagnosis:** 1418```bash 1419docker stats atbb 1420``` 1421 1422**Solutions:** 1423 14241. **Set memory limit:** 1425 ```bash 1426 docker update --memory="512m" atbb 1427 ``` 1428 14292. **Check for memory leak:** 1430 - Monitor over time: `docker stats atbb` 1431 - If memory grows continuously, report issue with logs 1432 14333. **Increase container memory:** 1434 ```bash 1435 # For large forums, 1-2GB may be normal 1436 docker update --memory="2g" atbb 1437 ``` 1438 1439### Logs Filling Disk 1440 1441**Symptom:** Disk space running out due to large log files 1442 1443**Check log size:** 1444```bash 1445du -sh /var/lib/docker/containers/*/ 1446``` 1447 1448**Solutions:** 1449 14501. **Configure log rotation (recommended):** 1451 ```bash 1452 # Stop container 1453 docker stop atbb 1454 docker rm atbb 1455 1456 # Restart with log rotation 1457 docker run -d \ 1458 --name atbb \ 1459 --log-opt max-size=10m \ 1460 --log-opt max-file=3 \ 1461 -p 8080:80 \ 1462 --env-file .env.production \ 1463 ghcr.io/malpercio-dev/atbb:latest 1464 ``` 1465 14662. **Manually clean logs:** 1467 ```bash 1468 # Truncate logs (preserves container) 1469 truncate -s 0 $(docker inspect --format='{{.LogPath}}' atbb) 1470 ``` 1471 14723. **Use external log aggregator** (syslog, fluentd, etc.) 1473 1474### Container Performance Issues 1475 1476**Symptom:** Slow response times, high CPU usage 1477 1478**Diagnosis:** 1479```bash 1480docker stats atbb 1481docker top atbb 1482``` 1483 1484**Solutions:** 1485 14861. **Check database performance:** 1487 - Slow queries often bottleneck at database 1488 - Monitor database server metrics 1489 - Add indexes if needed (consult forum performance guide) 1490 14912. **Increase resources:** 1492 ```bash 1493 docker update --cpus="2.0" --memory="1g" atbb 1494 ``` 1495 14963. **Check reverse proxy settings:** 1497 - Ensure proxy is not buffering excessively 1498 - Verify HTTP/2 is enabled for better performance 1499 15004. **Monitor specific endpoints:** 1501 ```bash 1502 # Extract slow requests from logs 1503 docker logs atbb | grep '^{' | jq 'select(.duration > 1000)' 1504 ``` 1505 1506### Session Errors / Random Logouts 1507 1508**Symptom:** Users randomly logged out, "session expired" errors 1509 1510**Causes:** 1511 15121. **Container restarted** — Sessions are in-memory, lost on restart 15132. **SESSION_SECRET changed** — Invalidates all sessions 15143. **SESSION_SECRET not set** — Each restart generates new secret 1515 1516**Solutions:** 1517 15181. **Verify SESSION_SECRET is set:** 1519 ```bash 1520 docker exec atbb env | grep SESSION_SECRET 1521 # Should show a 64-character hex string 1522 ``` 1523 15242. **If blank, generate and set:** 1525 ```bash 1526 openssl rand -hex 32 1527 # Add to .env.production 1528 # Restart container 1529 ``` 1530 15313. **Future:** Use Redis for persistent sessions (not yet implemented) 1532 1533### Getting Help 1534 1535If you cannot resolve an issue: 1536 15371. **Collect diagnostics:** 1538 ```bash 1539 # Container logs 1540 docker logs atbb > atbb-logs.txt 1541 1542 # Container info 1543 docker inspect atbb > atbb-inspect.json 1544 1545 # Resource usage 1546 docker stats --no-stream atbb 1547 ``` 1548 15492. **Sanitize sensitive data:** 1550 - Remove passwords from logs 1551 - Remove `SESSION_SECRET` from environment dumps 1552 15533. **Report issue:** 1554 - GitHub Issues: https://github.com/malpercio-dev/atbb-monorepo/issues 1555 - Include: atBB version, error messages, steps to reproduce 1556 - Attach sanitized logs 1557 1558--- 1559 1560## 10. Docker Compose Example 1561 1562For simpler local testing or single-server deployments, use Docker Compose. 1563 1564**File:** `docker-compose.example.yml` (included in repository) 1565 1566### What It Provides 1567 1568- PostgreSQL database (local development) 1569- atBB application container 1570- Automatic dependency management (atBB waits for PostgreSQL) 1571- Volume persistence for database 1572- Health checks 1573 1574### Usage 1575 1576**Step 1: Download files** 1577```bash 1578# Download docker-compose.example.yml 1579curl -O https://raw.githubusercontent.com/malpercio-dev/atbb-monorepo/main/docker-compose.example.yml 1580 1581# Download .env.production.example 1582curl -O https://raw.githubusercontent.com/malpercio-dev/atbb-monorepo/main/.env.production.example 1583 1584# Rename to .env 1585mv .env.production.example .env 1586``` 1587 1588**Step 2: Configure environment** 1589```bash 1590# Generate session secret 1591openssl rand -hex 32 1592 1593# Edit .env and fill in: 1594nano .env 1595``` 1596 1597Required changes in `.env`: 1598```bash 1599# AT Protocol credentials (from Prerequisites) 1600FORUM_DID=did:plc:YOUR_FORUM_DID 1601PDS_URL=https://bsky.social 1602FORUM_HANDLE=forum.example.com 1603FORUM_PASSWORD=YOUR_FORUM_PASSWORD 1604 1605# OAuth (for local testing, use http://localhost) 1606OAUTH_PUBLIC_URL=http://localhost 1607 1608# Session secret (generated above) 1609SESSION_SECRET=a1b2c3d4e5f6... 1610 1611# Database connection will be set by docker-compose 1612# (Uses container name "postgres" as hostname) 1613``` 1614 1615**Step 3: Start services** 1616```bash 1617docker-compose -f docker-compose.example.yml up -d 1618``` 1619 1620Expected output: 1621``` 1622Creating network "atbb_default" with the default driver 1623Creating volume "atbb_postgres_data" with default driver 1624Creating atbb-postgres ... done 1625Creating atbb-app ... done 1626``` 1627 1628**Step 4: Run migrations** 1629```bash 1630docker-compose -f docker-compose.example.yml exec atbb \ 1631 pnpm --filter @atbb/appview db:migrate 1632``` 1633 1634**Step 5: Access forum** 1635 1636Visit: **http://localhost** 1637 1638### Management Commands 1639 1640**View logs:** 1641```bash 1642# All services 1643docker-compose -f docker-compose.example.yml logs -f 1644 1645# Specific service 1646docker-compose -f docker-compose.example.yml logs -f atbb 1647docker-compose -f docker-compose.example.yml logs -f postgres 1648``` 1649 1650**Stop services:** 1651```bash 1652docker-compose -f docker-compose.example.yml down 1653``` 1654 1655**Stop and remove data:** 1656```bash 1657docker-compose -f docker-compose.example.yml down -v 1658# WARNING: This deletes the database volume! 1659``` 1660 1661**Restart services:** 1662```bash 1663docker-compose -f docker-compose.example.yml restart 1664``` 1665 1666**Upgrade to new version:** 1667```bash 1668# Pull new image 1669docker-compose -f docker-compose.example.yml pull atbb 1670 1671# Run migrations (if required by release notes) 1672docker-compose -f docker-compose.example.yml exec atbb \ 1673 pnpm --filter @atbb/appview db:migrate 1674 1675# Restart 1676docker-compose -f docker-compose.example.yml restart atbb 1677``` 1678 1679### Production Considerations 1680 1681**DO NOT use docker-compose.example.yml as-is in production.** 1682 1683Limitations: 1684- Database password is weak (change in compose file) 1685- No TLS/SSL for database 1686- No backups configured 1687- Single-server only 1688 1689**For production:** 16901. Use managed PostgreSQL (AWS RDS, DigitalOcean, etc.) 16912. Run atBB container separately (not with local PostgreSQL) 16923. Set up reverse proxy with HTTPS (Caddy/nginx) 16934. Use strong passwords and secrets 16945. Configure automated backups 16956. Set up monitoring and alerting 1696 1697**Modified compose for production (atBB only, external DB):** 1698```yaml 1699version: '3.8' 1700 1701services: 1702 atbb: 1703 image: ghcr.io/malpercio-dev/atbb:v1.0.0 1704 container_name: atbb 1705 restart: unless-stopped 1706 ports: 1707 - "127.0.0.1:8080:80" # Bind to localhost only 1708 env_file: 1709 - .env.production 1710 healthcheck: 1711 test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/api/healthz"] 1712 interval: 30s 1713 timeout: 3s 1714 retries: 3 1715``` 1716 1717--- 1718 1719## Appendix: Quick Reference 1720 1721### Required Environment Variables 1722 1723```bash 1724# PostgreSQL (production recommended) 1725DATABASE_URL=postgresql://user:pass@host:5432/db?sslmode=require 1726# SQLite (lightweight alternative) 1727# DATABASE_URL=file:./atbb.db 1728FORUM_DID=did:plc:xxxxxxxxxxxxx 1729PDS_URL=https://bsky.social 1730FORUM_HANDLE=forum.example.com 1731FORUM_PASSWORD=strong_password_16+_chars 1732OAUTH_PUBLIC_URL=https://forum.example.com 1733SESSION_SECRET=64_hex_chars_from_openssl_rand 1734``` 1735 1736### Essential Commands 1737 1738```bash 1739# Pull image 1740docker pull ghcr.io/malpercio-dev/atbb:latest 1741 1742# Run migrations 1743docker run --rm --env-file .env.production \ 1744 ghcr.io/malpercio-dev/atbb:latest \ 1745 pnpm --filter @atbb/appview db:migrate 1746 1747# Start container 1748docker run -d --name atbb --restart unless-stopped \ 1749 -p 8080:80 --env-file .env.production \ 1750 ghcr.io/malpercio-dev/atbb:latest 1751 1752# View logs 1753docker logs -f atbb 1754 1755# Stop/restart 1756docker stop atbb 1757docker restart atbb 1758 1759# Health check 1760curl http://localhost:8080/api/healthz 1761``` 1762 1763### Support Resources 1764 1765- **Documentation:** https://github.com/malpercio-dev/atbb-monorepo/tree/main/docs 1766- **Issues:** https://github.com/malpercio-dev/atbb-monorepo/issues 1767- **Releases:** https://github.com/malpercio-dev/atbb-monorepo/releases 1768- **AT Protocol Docs:** https://atproto.com/docs 1769 1770### Security Checklist 1771 1772Before going to production: 1773 1774- [ ] Generated `SESSION_SECRET` with `openssl rand -hex 32` 1775- [ ] Used strong, unique passwords (minimum 16 characters) 1776- [ ] Enabled database SSL/TLS (`?sslmode=require`) 1777- [ ] Set `OAUTH_PUBLIC_URL` to HTTPS domain (not HTTP) 1778- [ ] Set file permissions: `chmod 600 .env.production` 1779- [ ] Never committed `.env.production` to version control 1780- [ ] Configured reverse proxy with HTTPS (Caddy/nginx) 1781- [ ] Set up database backups 1782- [ ] Configured log rotation 1783- [ ] Set up health monitoring 1784- [ ] Restricted firewall to ports 80/443 only 1785- [ ] Tested backup restoration procedure 1786 1787--- 1788 1789## 11. NixOS Deployment 1790 1791The atBB flake provides a NixOS module that manages all services declaratively: 1792 1793- **`atbb-appview`** — Hono API server (systemd service) 1794- **`atbb-web`** — Hono web UI server (systemd service) 1795- **`atbb-migrate`** — One-shot database migration service 1796- **PostgreSQL 17** — Local database with peer authentication (optional) 1797- **nginx** — Reverse proxy with automatic ACME/Let's Encrypt TLS (optional) 1798 1799The module is suitable for single-server deployments. Sections 1–10 of this guide describe Docker-based deployment; this section covers the NixOS path exclusively. 1800 1801### Step 1: Add atBB as a Flake Input 1802 1803In your NixOS system flake: 1804 1805```nix 1806{ 1807 inputs = { 1808 nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 1809 atbb.url = "github:malpercio-dev/atbb-monorepo"; 1810 }; 1811 1812 outputs = { nixpkgs, atbb, ... }: { 1813 nixosConfigurations.my-server = nixpkgs.lib.nixosSystem { 1814 system = "x86_64-linux"; 1815 modules = [ 1816 atbb.nixosModules.default 1817 ./configuration.nix 1818 ]; 1819 }; 1820 }; 1821} 1822``` 1823 1824> **Note:** The module is exported as `nixosModules.default`, not `nixosModules.atbb`. 1825 1826### Step 2: Create the Environment File 1827 1828The 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: 1829 1830```bash 1831sudo mkdir -p /etc/atbb 1832sudo tee /etc/atbb/env > /dev/null <<'EOF' 1833# Database — Unix socket peer auth (matches services.atbb.user = "atbb") 1834DATABASE_URL=postgres:///atbb?host=/run/postgresql 1835# PGHOST makes postgres.js use the Unix socket directory reliably, 1836# since it does not always honour the ?host= query parameter in URLs. 1837PGHOST=/run/postgresql 1838 1839# Session security 1840SESSION_SECRET=<generate with: openssl rand -hex 32> 1841 1842# Forum AT Protocol account credentials 1843FORUM_HANDLE=forum.example.com 1844FORUM_PASSWORD=<your forum account password> 1845EOF 1846 1847sudo chmod 600 /etc/atbb/env 1848``` 1849 1850**Why Unix socket for `DATABASE_URL`?** 1851When `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`). 1852 1853**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. 1854 1855### Step 3: Configure the Module 1856 1857Add to your `configuration.nix`: 1858 1859```nix 1860{ 1861 services.atbb = { 1862 enable = true; 1863 domain = "forum.example.com"; 1864 forumDid = "did:plc:your-forum-did"; 1865 pdsUrl = "https://bsky.social"; 1866 1867 # Path to the environment file created in Step 2 1868 environmentFile = /etc/atbb/env; 1869 1870 # Local PostgreSQL (default: true) 1871 # Set to false to use an external database via DATABASE_URL 1872 database.enable = true; 1873 1874 # Run migrations manually after each deploy (safer) 1875 # Set to true to run automatically on every appview start 1876 autoMigrate = false; 1877 }; 1878 1879 # Required when enableACME = true (the default) 1880 security.acme = { 1881 acceptTerms = true; 1882 defaults.email = "admin@example.com"; 1883 }; 1884} 1885``` 1886 1887**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. 1888 1889#### Key Options Reference 1890 1891| Option | Default | Description | 1892|--------|---------|-------------| 1893| `domain` | *(required)* | Public domain for the forum | 1894| `forumDid` | *(required)* | Forum's AT Protocol DID | 1895| `pdsUrl` | *(required)* | URL of the forum's PDS | 1896| `environmentFile` | *(required)* | Path to secrets file | 1897| `database.enable` | `true` | Provision local PostgreSQL 17 | 1898| `database.name` | `"atbb"` | Database name | 1899| `autoMigrate` | `false` | Run migrations on appview start | 1900| `enableNginx` | `true` | Configure nginx reverse proxy | 1901| `enableACME` | `true` | Enable Let's Encrypt TLS | 1902| `appviewPort` | `3000` | Internal port for appview | 1903| `webPort` | `3001` | Internal port for web UI | 1904| `user` / `group` | `"atbb"` | System user/group for services | 1905 1906### Step 4: Deploy 1907 1908Apply your configuration using your preferred NixOS deployment tool: 1909 1910```bash 1911# Local rebuild 1912sudo nixos-rebuild switch --flake .#my-server 1913 1914# Remote via colmena 1915colmena apply --on my-server 1916 1917# Remote via nixos-rebuild 1918nixos-rebuild switch --flake .#my-server \ 1919 --target-host root@forum.example.com 1920``` 1921 1922### Step 5: Run Database Migrations 1923 1924The `atbb-migrate` service is a one-shot systemd unit — it runs once and exits. Trigger it manually after each deployment: 1925 1926```bash 1927sudo systemctl start atbb-migrate 1928 1929# Check migration output 1930sudo journalctl -u atbb-migrate 1931``` 1932 1933**Expected output:** 1934``` 1935Reading migrations from /nix/store/.../apps/appview/drizzle 1936Applying migration: 0000_initial_schema.sql 1937... 1938All migrations applied successfully 1939``` 1940 1941If 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. 1942 1943### Step 6: Verify Services 1944 1945```bash 1946# Check all atBB services 1947systemctl status atbb-appview atbb-web 1948 1949# View live logs 1950journalctl -fu atbb-appview 1951journalctl -fu atbb-web 1952 1953# Test the API 1954curl http://localhost:3000/api/healthz 1955# Expected: {"status":"ok"} 1956 1957# Verify nginx is routing correctly 1958curl https://forum.example.com/api/healthz 1959``` 1960 1961### Using Caddy Instead of nginx 1962 1963If you prefer Caddy, disable the built-in nginx proxy and configure `services.caddy` yourself: 1964 1965```nix 1966{ 1967 services.atbb = { 1968 # ... other options 1969 enableNginx = false; # disable built-in nginx virtualHost 1970 enableACME = false; # Caddy manages TLS automatically 1971 }; 1972 1973 services.caddy = { 1974 enable = true; 1975 virtualHosts."forum.example.com".extraConfig = '' 1976 # AT Protocol well-known endpoints → appview 1977 # Must reach appview (not web UI) for OAuth to work 1978 handle /.well-known/* { 1979 reverse_proxy localhost:${toString config.services.atbb.appviewPort} 1980 } 1981 1982 # REST API → appview 1983 handle /api/* { 1984 reverse_proxy localhost:${toString config.services.atbb.appviewPort} 1985 } 1986 1987 # Web UI — catch-all 1988 handle { 1989 reverse_proxy localhost:${toString config.services.atbb.webPort} 1990 } 1991 ''; 1992 }; 1993} 1994``` 1995 1996See `nix/Caddyfile.example` in the repository for the equivalent standalone Caddyfile. 1997 1998### Upgrading 1999 2000To upgrade atBB, update the flake input and redeploy: 2001 2002```bash 2003# Update atBB to latest 2004nix flake update atbb 2005 2006# Redeploy 2007sudo nixos-rebuild switch --flake .#my-server 2008 2009# Run migrations for the new version 2010sudo systemctl start atbb-migrate 2011``` 2012 2013NixOS 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. 2014 2015--- 2016 2017**End of Deployment Guide** 2018 2019For questions or issues not covered here, please open an issue at: 2020https://github.com/malpercio-dev/atbb-monorepo/issues