WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at main 2023 lines 53 kB view raw view rendered
1# atBB Deployment Guide 2 3**Version:** 1.2 4**Last Updated:** 2026-02-26 5**Audience:** System administrators deploying atBB to production 6 7> **Related Documentation:** 8> - [docs/trust-model.md](trust-model.md) — Trust model for self-hosted deployment: what the AppView controls, user data guarantees, and security implications 9> - [docs/plans/complete/2026-02-11-deployment-infrastructure-design.md](plans/complete/2026-02-11-deployment-infrastructure-design.md) — Architectural decisions and design rationale behind this deployment approach 10 11## Table of Contents 12 131. [Prerequisites](#1-prerequisites) 142. [Quick Start](#2-quick-start) 153. [Environment Configuration](#3-environment-configuration) 164. [Database Setup](#4-database-setup) 175. [Running the Container](#5-running-the-container) 186. [Reverse Proxy Setup](#6-reverse-proxy-setup) 197. [Monitoring & Logs](#7-monitoring--logs) 208. [Upgrading](#8-upgrading) 219. [Troubleshooting](#9-troubleshooting) 2210. [Docker Compose Example](#10-docker-compose-example) 2311. [NixOS Deployment](#11-nixos-deployment) 24 25--- 26 27## 1. Prerequisites 28 29Before deploying atBB, ensure you have the following: 30 31### Infrastructure Requirements 32 33- **PostgreSQL 14+** 34 - Managed service recommended: AWS RDS, DigitalOcean Managed Database, Azure Database for PostgreSQL, or similar 35 - Minimum 1GB RAM, 10GB storage (scales with forum size) 36 - SSL/TLS support enabled (`?sslmode=require`) 37 - Database user with CREATE/ALTER/SELECT/INSERT/UPDATE/DELETE permissions 38 39- **Domain Name & DNS** 40 - Registered domain name (e.g., `forum.example.com`) 41 - DNS A/AAAA record pointing to your server's public IP 42 - Recommended: wildcard DNS for future subdomains (`*.forum.example.com`) 43 44- **Container Runtime** 45 - Docker 20.10+ or Docker Desktop 46 - Minimum 512MB RAM allocated to container (1GB+ recommended) 47 - 2GB disk space for container image and logs 48 49### AT Protocol Requirements 50 51**IMPORTANT:** atBB integrates with the AT Protocol network (the decentralized protocol powering Bluesky). You must set up your forum's AT Protocol identity before deployment. 52 53#### 1. Choose a Personal Data Server (PDS) 54 55Your forum needs a PDS to store its records (forum metadata, categories, moderation actions). Options: 56 57- **Self-hosted PDS:** Run your own PDS instance (advanced, recommended for sovereignty) 58 - Guide: https://github.com/bluesky-social/pds 59 - Requires separate server and domain 60 - Full control over data and federation 61 62- **Hosted PDS:** Use Bluesky's PDS (`https://bsky.social`) or another provider 63 - Simpler setup, lower maintenance 64 - Suitable for testing and small forums 65 66#### 2. Create Forum Account 67 68Create an account for your forum on your chosen PDS: 69 70```bash 71# Example with Bluesky PDS 72# Visit https://bsky.app and create account with your forum's handle 73# Handle should match your domain: forum.example.com 74``` 75 76**Record these values (you'll need them later):** 77- Forum Handle: `forum.example.com` 78- Forum Password: (choose a strong password, minimum 16 characters) 79- Forum DID: `did:plc:xxxxxxxxxxxxx` (found in account settings or PDS admin interface) 80- PDS URL: `https://bsky.social` (or your PDS URL) 81 82#### 3. Understand Lexicon Namespace 83 84atBB uses the `space.atbb.*` lexicon namespace for its records: 85- `space.atbb.forum.forum` — Forum metadata (name, description, rules) 86- `space.atbb.forum.category` — Forum categories 87- `space.atbb.post` — User posts and replies 88- `space.atbb.membership` — User membership records 89- `space.atbb.modAction` — Moderation actions 90 91Your forum's DID will own the forum-level records, while users' DIDs own their posts and memberships. 92 93### Security Requirements 94 95- **TLS/SSL Certificate:** Let's Encrypt (free) or commercial certificate 96- **Firewall:** Restrict inbound ports to 80/443 only 97- **SSH Access:** Key-based authentication (disable password auth) 98- **Secrets Management:** Secure storage for environment variables (consider cloud secrets manager) 99 100> **Before deploying:** Read [docs/trust-model.md](trust-model.md). It explains what the AppView controls (the Forum DID's credentials and write access), what your users can count on, and the security implications of a compromised server. 101 102--- 103 104## 2. Quick Start 105 106Follow these steps for a minimal working deployment. Detailed explanations follow in later sections. 107 108### Step 1: Pull the Docker Image 109 110```bash 111# Pull latest stable version 112docker pull ghcr.io/malpercio-dev/atbb:latest 113 114# Or pin to a specific version (recommended for production) 115docker pull ghcr.io/malpercio-dev/atbb:v1.0.0 116``` 117 118Expected output: 119``` 120latest: Pulling from malpercio-dev/atbb 121e7c96db7181b: Pull complete 122... 123Status: Downloaded newer image for ghcr.io/malpercio-dev/atbb:latest 124``` 125 126### Step 2: Create Environment File 127 128```bash 129# Copy the template 130curl -o .env.production https://raw.githubusercontent.com/malpercio-dev/atbb-monorepo/main/.env.production.example 131 132# Generate a strong session secret 133openssl rand -hex 32 134# Output: a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 135``` 136 137**Edit `.env.production` and fill in these REQUIRED values:** 138 139```bash 140# Database connection (from your PostgreSQL provider) 141DATABASE_URL=postgresql://atbb_user:YOUR_DB_PASSWORD@db.example.com:5432/atbb_prod?sslmode=require 142 143# AT Protocol credentials (from Prerequisites step) 144FORUM_DID=did:plc:YOUR_FORUM_DID 145PDS_URL=https://bsky.social 146FORUM_HANDLE=forum.example.com 147FORUM_PASSWORD=YOUR_FORUM_PASSWORD 148 149# OAuth configuration (your public domain) 150OAUTH_PUBLIC_URL=https://forum.example.com 151 152# Session security (use the openssl output from above) 153SESSION_SECRET=a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 154``` 155 156**Secure the file:** 157```bash 158chmod 600 .env.production 159``` 160 161### Step 3: Run Database Migrations 162 163**CRITICAL:** Run migrations BEFORE starting the application. This creates the database schema. 164 165```bash 166docker run --rm \ 167 --env-file .env.production \ 168 ghcr.io/malpercio-dev/atbb:latest \ 169 pnpm --filter @atbb/appview db:migrate 170``` 171 172Expected output: 173``` 174> @atbb/db@0.1.0 db:migrate 175> drizzle-kit migrate 176 177Reading migrations from migrations/ 178Applying migration: 0000_initial_schema.sql 179Migration applied successfully 180``` 181 182**If this fails, DO NOT proceed.** See [Section 4: Database Setup](#4-database-setup) for troubleshooting. 183 184### Step 4: Start the Container 185 186```bash 187docker run -d \ 188 --name atbb \ 189 --restart unless-stopped \ 190 -p 8080:80 \ 191 --env-file .env.production \ 192 ghcr.io/malpercio-dev/atbb:latest 193``` 194 195Options explained: 196- `-d` — Run in background (detached mode) 197- `--name atbb` — Name the container for easy management 198- `--restart unless-stopped` — Auto-restart on crashes or server reboot 199- `-p 8080:80` — Map host port 8080 to container port 80 200- `--env-file .env.production` — Load environment variables 201 202**Verify the container is running:** 203```bash 204docker ps | grep atbb 205# Expected: Container with STATUS "Up X seconds" 206 207docker logs atbb 208# Expected: No errors, services starting 209``` 210 211**Test the application:** 212```bash 213curl http://localhost:8080/api/healthz 214# Expected: {"status":"ok"} 215``` 216 217### Step 5: Configure Reverse Proxy 218 219**The container is now running on port 8080, but NOT accessible publicly yet.** You need a reverse proxy to: 220- Terminate TLS/SSL (HTTPS) 221- Forward traffic from your domain to the container 222- Handle automatic certificate renewal 223 224**Recommended setup with Caddy (automatic HTTPS):** 225 226Install Caddy: 227```bash 228# Ubuntu/Debian 229sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https 230curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg 231curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list 232sudo apt update 233sudo apt install caddy 234``` 235 236Edit `/etc/caddy/Caddyfile`: 237``` 238forum.example.com { 239 reverse_proxy localhost:8080 240} 241``` 242 243Reload Caddy: 244```bash 245sudo systemctl reload caddy 246``` 247 248**Caddy will automatically obtain a Let's Encrypt certificate and enable HTTPS.** 249 250### Step 6: Verify Deployment 251 252Visit your forum: **https://forum.example.com** 253 254Expected: atBB home page loads with no errors. 255 256**If you see errors, proceed to [Section 9: Troubleshooting](#9-troubleshooting).** 257 258--- 259 260## 3. Environment Configuration 261 262Complete reference for all environment variables. See `.env.production.example` for detailed comments. 263 264### Required Variables 265 266| Variable | Description | Example | 267|----------|-------------|---------| 268| `DATABASE_URL` | Database connection string (PostgreSQL or SQLite) | PostgreSQL: `postgresql://user:pass@host:5432/dbname?sslmode=require`; SQLite: `file:./atbb.db` | 269| `FORUM_DID` | Forum's AT Protocol DID | `did:plc:abcdef1234567890` | 270| `PDS_URL` | Personal Data Server URL | `https://bsky.social` | 271| `FORUM_HANDLE` | Forum's AT Protocol handle | `forum.example.com` | 272| `FORUM_PASSWORD` | Forum account password | (minimum 16 characters, alphanumeric + symbols) | 273| `OAUTH_PUBLIC_URL` | Public URL for OAuth redirects | `https://forum.example.com` (MUST be HTTPS in production) | 274| `SESSION_SECRET` | Session encryption key | Generate with: `openssl rand -hex 32` | 275 276### Optional Variables 277 278| Variable | Default | Description | 279|----------|---------|-------------| 280| `PORT` | `3000` | AppView API port (internal) | 281| `WEB_PORT` | `3001` | Web UI port (internal) | 282| `APPVIEW_URL` | `http://localhost:3000` | Internal API URL (keep as localhost for single container) | 283| `JETSTREAM_URL` | `wss://jetstream2.us-east.bsky.network/subscribe` | AT Protocol firehose URL | 284| `SESSION_TTL_DAYS` | `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