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