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
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