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