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