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