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

atBB Deployment Guide#

Version: 1.2 Last Updated: 2026-02-26 Audience: System administrators deploying atBB to production

Related Documentation:

Table of Contents#

  1. Prerequisites
  2. Quick Start
  3. Environment Configuration
  4. Database Setup
  5. Running the Container
  6. Reverse Proxy Setup
  7. Monitoring & Logs
  8. Upgrading
  9. Troubleshooting
  10. Docker Compose Example
  11. NixOS Deployment

1. Prerequisites#

Before deploying atBB, ensure you have the following:

Infrastructure Requirements#

  • PostgreSQL 14+

    • Managed service recommended: AWS RDS, DigitalOcean Managed Database, Azure Database for PostgreSQL, or similar
    • Minimum 1GB RAM, 10GB storage (scales with forum size)
    • SSL/TLS support enabled (?sslmode=require)
    • Database user with CREATE/ALTER/SELECT/INSERT/UPDATE/DELETE permissions
  • Domain Name & DNS

    • Registered domain name (e.g., forum.example.com)
    • DNS A/AAAA record pointing to your server's public IP
    • Recommended: wildcard DNS for future subdomains (*.forum.example.com)
  • Container Runtime

    • Docker 20.10+ or Docker Desktop
    • Minimum 512MB RAM allocated to container (1GB+ recommended)
    • 2GB disk space for container image and logs

AT Protocol Requirements#

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.

1. Choose a Personal Data Server (PDS)#

Your forum needs a PDS to store its records (forum metadata, categories, moderation actions). Options:

  • Self-hosted PDS: Run your own PDS instance (advanced, recommended for sovereignty)

  • Hosted PDS: Use Bluesky's PDS (https://bsky.social) or another provider

    • Simpler setup, lower maintenance
    • Suitable for testing and small forums

2. Create Forum Account#

Create an account for your forum on your chosen PDS:

# Example with Bluesky PDS
# Visit https://bsky.app and create account with your forum's handle
# Handle should match your domain: forum.example.com

Record these values (you'll need them later):

  • Forum Handle: forum.example.com
  • Forum Password: (choose a strong password, minimum 16 characters)
  • Forum DID: did:plc:xxxxxxxxxxxxx (found in account settings or PDS admin interface)
  • PDS URL: https://bsky.social (or your PDS URL)

3. Understand Lexicon Namespace#

atBB uses the space.atbb.* lexicon namespace for its records:

  • space.atbb.forum.forum — Forum metadata (name, description, rules)
  • space.atbb.forum.category — Forum categories
  • space.atbb.post — User posts and replies
  • space.atbb.membership — User membership records
  • space.atbb.modAction — Moderation actions

Your forum's DID will own the forum-level records, while users' DIDs own their posts and memberships.

Security Requirements#

  • TLS/SSL Certificate: Let's Encrypt (free) or commercial certificate
  • Firewall: Restrict inbound ports to 80/443 only
  • SSH Access: Key-based authentication (disable password auth)
  • Secrets Management: Secure storage for environment variables (consider cloud secrets manager)

Before deploying: Read docs/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.


2. Quick Start#

Follow these steps for a minimal working deployment. Detailed explanations follow in later sections.

Step 1: Pull the Docker Image#

# Pull latest stable version
docker pull ghcr.io/malpercio-dev/atbb:latest

# Or pin to a specific version (recommended for production)
docker pull ghcr.io/malpercio-dev/atbb:v1.0.0

Expected output:

latest: Pulling from malpercio-dev/atbb
e7c96db7181b: Pull complete
...
Status: Downloaded newer image for ghcr.io/malpercio-dev/atbb:latest

Step 2: Create Environment File#

# Copy the template
curl -o .env.production https://raw.githubusercontent.com/malpercio-dev/atbb-monorepo/main/.env.production.example

# Generate a strong session secret
openssl rand -hex 32
# Output: a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456

Edit .env.production and fill in these REQUIRED values:

# Database connection (from your PostgreSQL provider)
DATABASE_URL=postgresql://atbb_user:YOUR_DB_PASSWORD@db.example.com:5432/atbb_prod?sslmode=require

# AT Protocol credentials (from Prerequisites step)
FORUM_DID=did:plc:YOUR_FORUM_DID
PDS_URL=https://bsky.social
FORUM_HANDLE=forum.example.com
FORUM_PASSWORD=YOUR_FORUM_PASSWORD

# OAuth configuration (your public domain)
OAUTH_PUBLIC_URL=https://forum.example.com

# Session security (use the openssl output from above)
SESSION_SECRET=a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456

Secure the file:

chmod 600 .env.production

Step 3: Run Database Migrations#

CRITICAL: Run migrations BEFORE starting the application. This creates the database schema.

docker run --rm \
  --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:latest \
  pnpm --filter @atbb/appview db:migrate

Expected output:

> @atbb/db@0.1.0 db:migrate
> drizzle-kit migrate

Reading migrations from migrations/
Applying migration: 0000_initial_schema.sql
Migration applied successfully

If this fails, DO NOT proceed. See Section 4: Database Setup for troubleshooting.

Step 4: Start the Container#

docker run -d \
  --name atbb \
  --restart unless-stopped \
  -p 8080:80 \
  --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:latest

Options explained:

  • -d — Run in background (detached mode)
  • --name atbb — Name the container for easy management
  • --restart unless-stopped — Auto-restart on crashes or server reboot
  • -p 8080:80 — Map host port 8080 to container port 80
  • --env-file .env.production — Load environment variables

Verify the container is running:

docker ps | grep atbb
# Expected: Container with STATUS "Up X seconds"

docker logs atbb
# Expected: No errors, services starting

Test the application:

curl http://localhost:8080/api/healthz
# Expected: {"status":"ok"}

Step 5: Configure Reverse Proxy#

The container is now running on port 8080, but NOT accessible publicly yet. You need a reverse proxy to:

  • Terminate TLS/SSL (HTTPS)
  • Forward traffic from your domain to the container
  • Handle automatic certificate renewal

Recommended setup with Caddy (automatic HTTPS):

Install Caddy:

# Ubuntu/Debian
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

Edit /etc/caddy/Caddyfile:

forum.example.com {
    reverse_proxy localhost:8080
}

Reload Caddy:

sudo systemctl reload caddy

Caddy will automatically obtain a Let's Encrypt certificate and enable HTTPS.

Step 6: Verify Deployment#

Visit your forum: https://forum.example.com

Expected: atBB home page loads with no errors.

If you see errors, proceed to Section 9: Troubleshooting.


3. Environment Configuration#

Complete reference for all environment variables. See .env.production.example for detailed comments.

Required Variables#

Variable Description Example
DATABASE_URL Database connection string (PostgreSQL or SQLite) PostgreSQL: postgresql://user:pass@host:5432/dbname?sslmode=require; SQLite: file:./atbb.db
FORUM_DID Forum's AT Protocol DID did:plc:abcdef1234567890
PDS_URL Personal Data Server URL https://bsky.social
FORUM_HANDLE Forum's AT Protocol handle forum.example.com
FORUM_PASSWORD Forum account password (minimum 16 characters, alphanumeric + symbols)
OAUTH_PUBLIC_URL Public URL for OAuth redirects https://forum.example.com (MUST be HTTPS in production)
SESSION_SECRET Session encryption key Generate with: openssl rand -hex 32

Optional Variables#

Variable Default Description
PORT 3000 AppView API port (internal)
WEB_PORT 3001 Web UI port (internal)
APPVIEW_URL http://localhost:3000 Internal API URL (keep as localhost for single container)
JETSTREAM_URL wss://jetstream2.us-east.bsky.network/subscribe AT Protocol firehose URL
SESSION_TTL_DAYS 30 Session lifetime in days (1-90 range)
REDIS_URL (none) Redis connection string (future: multi-instance deployments)

Security Best Practices#

SESSION_SECRET Generation:

# CRITICAL: Never use a predictable value or leave blank
openssl rand -hex 32

# Use different secrets for dev/staging/production
# Rotating the secret invalidates all active sessions

Password Requirements:

  • Minimum 16 characters
  • Mix of uppercase, lowercase, numbers, symbols
  • Unique per environment (never reuse)
  • Store in password manager or secrets vault

Connection String Security:

# Good: SSL/TLS enforced
DATABASE_URL=postgresql://user:pass@host:5432/db?sslmode=require

# Bad: Plain text connection (vulnerable to MITM)
DATABASE_URL=postgresql://user:pass@host:5432/db

File Permissions:

# Protect your environment file
chmod 600 .env.production

# Verify permissions
ls -la .env.production
# Expected: -rw------- (read/write for owner only)

Environment Loading Methods#

Docker CLI:

# Recommended: Load from file with --init for better signal handling
docker run --init --env-file .env.production ghcr.io/malpercio-dev/atbb:latest

# Alternative: Individual variables (for orchestrators)
docker run --init \
  -e DATABASE_URL="postgresql://..." \
  -e FORUM_DID="did:plc:..." \
  -e SESSION_SECRET="..." \
  ghcr.io/malpercio-dev/atbb:latest

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.

Docker Compose:

services:
  atbb:
    image: ghcr.io/malpercio-dev/atbb:latest
    env_file:
      - .env.production

Kubernetes:

# Use Secrets (NOT ConfigMaps for sensitive data)
apiVersion: v1
kind: Secret
metadata:
  name: atbb-secrets
type: Opaque
stringData:
  DATABASE_URL: "postgresql://..."
  SESSION_SECRET: "..."
---
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: atbb
        envFrom:
        - secretRef:
            name: atbb-secrets

4. Database Setup#

atBB supports two database backends:

  • PostgreSQL (recommended for production) — full-featured, suitable for multi-user/multi-server deployments
  • 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).

The rest of this section covers PostgreSQL provisioning. SQLite requires no separate server setup — just point DATABASE_URL at a file path.

PostgreSQL Provisioning#

AWS RDS:

  1. Navigate to RDS Console → Create Database
  2. Choose PostgreSQL 14+ (latest stable version)
  3. Select appropriate instance size:
    • Small forum (<1000 users): db.t3.micro or db.t4g.micro
    • Medium forum (1000-10000 users): db.t3.small or db.t4g.small
    • Large forum (10000+ users): db.t3.medium or higher
  4. Enable "Storage Auto Scaling" (start with 20GB)
  5. Enable "Automated Backups" (7-30 day retention)
  6. Enable "Publicly Accessible" only if container is in different VPC
  7. Security group: Allow PostgreSQL (5432) from container's IP/VPC
  8. Create database: atbb_prod
  9. Create user: atbb_user with generated password

Connection string format:

postgresql://atbb_user:PASSWORD@instance-name.region.rds.amazonaws.com:5432/atbb_prod?sslmode=require

DigitalOcean Managed Database:

  1. Navigate to Databases → Create → PostgreSQL
  2. Choose datacenter closest to your Droplet/container
  3. Select plan (Basic $15/mo sufficient for small forums)
  4. Create database: atbb_prod
  5. Create user: atbb_user with generated password
  6. Add trusted source: Your Droplet's IP or "All" for simplicity
  7. Download CA certificate (optional, for certificate validation)

Connection string provided in dashboard (copy and use directly).

Azure Database for PostgreSQL:

  1. Navigate to Azure Database for PostgreSQL → Create
  2. Choose "Flexible Server" (simpler, cheaper)
  3. Select region and compute tier (Burstable B1ms sufficient for small forums)
  4. Enable "High Availability" for production (optional)
  5. Configure firewall: Add your container's public IP
  6. Create database: atbb_prod

Connection string format:

postgresql://atbb_user@servername:PASSWORD@servername.postgres.database.azure.com:5432/atbb_prod?sslmode=require

Option 2: Self-Hosted PostgreSQL#

Installation (Ubuntu/Debian):

# Install PostgreSQL
sudo apt update
sudo apt install -y postgresql postgresql-contrib

# Start and enable service
sudo systemctl enable postgresql
sudo systemctl start postgresql

Create database and user:

sudo -u postgres psql

-- In psql prompt:
CREATE DATABASE atbb_prod;
CREATE USER atbb_user WITH PASSWORD 'YOUR_STRONG_PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE atbb_prod TO atbb_user;
\q

Enable remote connections (if container is on different host):

Edit /etc/postgresql/14/main/postgresql.conf:

listen_addresses = '*'  # Or specific IP

Edit /etc/postgresql/14/main/pg_hba.conf:

# Add this line (replace 0.0.0.0/0 with specific IP range in production)
host    atbb_prod    atbb_user    0.0.0.0/0    scram-sha-256

Restart PostgreSQL:

sudo systemctl restart postgresql

Connection string:

postgresql://atbb_user:YOUR_STRONG_PASSWORD@your-server-ip:5432/atbb_prod

Running Database Migrations#

Migrations create the database schema (tables, indexes, constraints).

First-time setup:

docker run --rm \
  --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:latest \
  pnpm --filter @atbb/appview db:migrate

Options explained:

  • --rm — Remove container after migration completes
  • --env-file .env.production — Load database connection string
  • pnpm --filter @atbb/appview db:migrate — Run Drizzle migrations

Expected output (success):

Reading migrations from /app/packages/db/migrations
Applying migration: 0000_initial_schema.sql
Applying migration: 0001_add_deleted_flag.sql
All migrations applied successfully

Verify migrations:

# Connect to your database
psql "postgresql://atbb_user:PASSWORD@host:5432/atbb_prod?sslmode=require"

# List tables
\dt

# Expected output (12 tables):
#  Schema |         Name          | Type  |   Owner
# --------+-----------------------+-------+-----------
#  public | backfill_errors       | table | atbb_user
#  public | backfill_progress     | table | atbb_user
#  public | boards                | table | atbb_user
#  public | categories            | table | atbb_user
#  public | firehose_cursor       | table | atbb_user
#  public | forums                | table | atbb_user
#  public | memberships           | table | atbb_user
#  public | mod_actions           | table | atbb_user
#  public | posts                 | table | atbb_user
#  public | role_permissions      | table | atbb_user
#  public | roles                 | table | atbb_user
#  public | users                 | table | atbb_user

Migration Troubleshooting#

Error: "database does not exist"

FATAL: database "atbb_prod" does not exist

Solution: Create the database first (see self-hosted instructions above, or create via cloud console).

Error: "password authentication failed"

FATAL: password authentication failed for user "atbb_user"

Solution: Verify credentials in DATABASE_URL match database user.

Error: "connection refused"

Error: connect ECONNREFUSED

Solution:

  • Check database host/port are correct
  • Verify firewall allows connections from container's IP
  • For cloud databases, ensure "trusted sources" includes your IP

Error: "SSL connection required"

FATAL: no pg_hba.conf entry for host, SSL off

Solution: Add ?sslmode=require to connection string.

Error: "permission denied for schema public"

ERROR: permission denied for schema public

Solution: Grant schema permissions:

GRANT USAGE ON SCHEMA public TO atbb_user;
GRANT CREATE ON SCHEMA public TO atbb_user;

5. Running the Container#

Basic Deployment#

Production command (recommended):

docker run -d \
  --name atbb \
  --restart unless-stopped \
  -p 8080:80 \
  --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:latest

Pin to specific version (recommended for stability):

docker run -d \
  --name atbb \
  --restart unless-stopped \
  -p 8080:80 \
  --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:v1.0.0

Pin to specific commit SHA (for rollback/testing):

docker run -d \
  --name atbb \
  --restart unless-stopped \
  -p 8080:80 \
  --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:main-a1b2c3d

Advanced Options#

Custom port mapping:

# Expose on different host port
docker run -d \
  --name atbb \
  -p 3000:80 \
  --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:latest

# Bind to specific interface (localhost only)
docker run -d \
  --name atbb \
  -p 127.0.0.1:8080:80 \
  --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:latest

Resource limits:

docker run -d \
  --name atbb \
  --restart unless-stopped \
  -p 8080:80 \
  --memory="1g" \
  --cpus="1.0" \
  --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:latest

Custom network:

# Create network
docker network create atbb-network

# Run with network
docker run -d \
  --name atbb \
  --network atbb-network \
  -p 8080:80 \
  --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:latest

Container Management#

View logs:

# All logs
docker logs atbb

# Follow logs (live)
docker logs -f atbb

# Last 100 lines
docker logs --tail 100 atbb

# Logs since timestamp
docker logs --since 2026-02-12T10:00:00 atbb

Stop container:

docker stop atbb

Start stopped container:

docker start atbb

Restart container:

docker restart atbb

Remove container:

# Stop first
docker stop atbb

# Remove
docker rm atbb

Execute commands inside container (debugging):

# Interactive shell
docker exec -it atbb sh

# Run single command
docker exec atbb ps aux
docker exec atbb df -h
docker exec atbb cat /etc/nginx/nginx.conf

Health Checks#

The container exposes a health endpoint:

Check via curl:

curl http://localhost:8080/api/healthz

Expected response:

{"status":"ok"}

Check via Docker:

docker inspect atbb | grep -A 5 Health

Use in monitoring scripts:

#!/bin/bash
# health-check.sh

HEALTH=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/healthz)

if [ "$HEALTH" != "200" ]; then
  echo "ALERT: atBB health check failed (HTTP $HEALTH)"
  # Send alert (email, Slack, PagerDuty, etc.)
  exit 1
fi

echo "OK: atBB is healthy"
exit 0

Run as cron job:

# Check every 5 minutes
*/5 * * * * /path/to/health-check.sh >> /var/log/atbb-health.log 2>&1

6. Reverse Proxy Setup#

The container exposes HTTP on port 80. In production, you need a reverse proxy to:

  • Terminate TLS/SSL (enable HTTPS)
  • Manage domain routing
  • Handle certificate renewal
  • Provide additional security headers

Why Caddy:

  • Automatic HTTPS with Let's Encrypt (zero configuration)
  • Simple configuration syntax
  • Auto-renewal of certificates
  • Modern defaults (HTTP/2, security headers)

Installation:

Ubuntu/Debian:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

CentOS/RHEL:

dnf install 'dnf-command(copr)'
dnf copr enable @caddy/caddy
dnf install caddy

Basic Configuration:

Edit /etc/caddy/Caddyfile:

forum.example.com {
    reverse_proxy localhost:8080
}

Advanced Configuration (with security headers):

forum.example.com {
    # Reverse proxy to atBB container
    reverse_proxy localhost:8080

    # Security headers
    header {
        # Enable HSTS (force HTTPS)
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

        # Prevent clickjacking
        X-Frame-Options "SAMEORIGIN"

        # Prevent MIME sniffing
        X-Content-Type-Options "nosniff"

        # XSS protection
        X-XSS-Protection "1; mode=block"

        # Referrer policy
        Referrer-Policy "strict-origin-when-cross-origin"

        # Content Security Policy (adjust as needed)
        Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
    }

    # Access logs
    log {
        output file /var/log/caddy/atbb-access.log
        format json
    }
}

Apply configuration:

# Validate configuration
sudo caddy validate --config /etc/caddy/Caddyfile

# Reload Caddy (no downtime)
sudo systemctl reload caddy

# Check status
sudo systemctl status caddy

Verify HTTPS:

curl -I https://forum.example.com
# Expected: HTTP/2 200 with security headers

nginx#

Installation:

sudo apt install -y nginx

Configuration:

Create /etc/nginx/sites-available/atbb:

# HTTP -> HTTPS redirect
server {
    listen 80;
    listen [::]:80;
    server_name forum.example.com;
    return 301 https://$server_name$request_uri;
}

# HTTPS server
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name forum.example.com;

    # SSL certificates (obtain via certbot)
    ssl_certificate /etc/letsencrypt/live/forum.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/forum.example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/forum.example.com/chain.pem;

    # SSL settings (Mozilla Modern configuration)
    ssl_protocols TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Proxy to atBB container
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket support (for future features)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Access logs
    access_log /var/log/nginx/atbb-access.log combined;
    error_log /var/log/nginx/atbb-error.log;
}

Obtain SSL certificate with Certbot:

# Install Certbot
sudo apt install -y certbot python3-certbot-nginx

# Obtain certificate (interactive)
sudo certbot --nginx -d forum.example.com

# Certbot will automatically:
# - Validate domain ownership
# - Obtain certificate from Let's Encrypt
# - Update nginx configuration
# - Set up auto-renewal

Enable site:

sudo ln -s /etc/nginx/sites-available/atbb /etc/nginx/sites-enabled/
sudo nginx -t  # Test configuration
sudo systemctl reload nginx

Traefik#

docker-compose.yml with Traefik:

version: '3.8'

services:
  traefik:
    image: traefik:v2.11
    command:
      - "--providers.docker=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.email=admin@example.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./letsencrypt:/letsencrypt"

  atbb:
    image: ghcr.io/malpercio-dev/atbb:latest
    env_file:
      - .env.production
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.atbb.rule=Host(`forum.example.com`)"
      - "traefik.http.routers.atbb.entrypoints=websecure"
      - "traefik.http.routers.atbb.tls.certresolver=letsencrypt"
      - "traefik.http.services.atbb.loadbalancer.server.port=80"

Start with:

docker-compose up -d

7. Monitoring & Logs#

Container Logs#

View logs:

# All logs
docker logs atbb

# Follow logs (real-time)
docker logs -f atbb

# Filter by timestamp
docker logs --since 2026-02-12T10:00:00 atbb
docker logs --until 2026-02-12T12:00:00 atbb

Log format: JSON structured logs

Example log entry:

{
  "level": "info",
  "time": "2026-02-12T14:30:00.000Z",
  "service": "appview",
  "msg": "HTTP request",
  "method": "GET",
  "path": "/api/forum",
  "status": 200,
  "duration": 15
}

Parse logs with jq:

# Filter by level
docker logs atbb | grep '^{' | jq 'select(.level == "error")'

# Extract errors from last hour
docker logs --since 1h atbb | grep '^{' | jq 'select(.level == "error")'

# Count requests by path
docker logs atbb | grep '^{' | jq -r '.path' | sort | uniq -c | sort -nr

Log Persistence#

Forward to log aggregator:

Using Docker logging driver (syslog):

docker run -d \
  --name atbb \
  --log-driver syslog \
  --log-opt syslog-address=udp://logserver:514 \
  --log-opt tag="atbb" \
  -p 8080:80 \
  --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:latest

Using Docker logging driver (json-file with rotation):

docker run -d \
  --name atbb \
  --log-driver json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  -p 8080:80 \
  --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:latest

Health Monitoring#

Health endpoint: GET /api/healthz

Example monitoring script (save as /usr/local/bin/atbb-health-check):

#!/bin/bash
# atbb-health-check - Monitor atBB health and restart if needed

CONTAINER_NAME="atbb"
HEALTH_URL="http://localhost:8080/api/healthz"
MAX_FAILURES=3

FAILURES=0

while true; do
  # Check health endpoint
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL")

  if [ "$HTTP_CODE" != "200" ]; then
    FAILURES=$((FAILURES + 1))
    echo "$(date): Health check failed (HTTP $HTTP_CODE), failures: $FAILURES/$MAX_FAILURES"

    if [ "$FAILURES" -ge "$MAX_FAILURES" ]; then
      echo "$(date): Max failures reached, restarting container"
      docker restart "$CONTAINER_NAME"
      FAILURES=0
      sleep 60  # Wait for restart
    fi
  else
    # Reset failure counter on success
    if [ "$FAILURES" -gt 0 ]; then
      echo "$(date): Health check recovered"
    fi
    FAILURES=0
  fi

  sleep 60  # Check every minute
done

Run as systemd service:

sudo chmod +x /usr/local/bin/atbb-health-check

cat <<EOF | sudo tee /etc/systemd/system/atbb-health-check.service
[Unit]
Description=atBB Health Check Monitor
After=docker.service
Requires=docker.service

[Service]
Type=simple
ExecStart=/usr/local/bin/atbb-health-check
Restart=always
StandardOutput=append:/var/log/atbb-health-check.log
StandardError=append:/var/log/atbb-health-check.log

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable atbb-health-check
sudo systemctl start atbb-health-check

Resource Monitoring#

Monitor container resource usage:

# Real-time stats
docker stats atbb

# Example output:
# CONTAINER   CPU %   MEM USAGE / LIMIT   MEM %   NET I/O     BLOCK I/O
# atbb        2.5%    256MiB / 1GiB       25%     1.2MB/5MB   0B/0B

Set up alerts for resource limits:

#!/bin/bash
# atbb-resource-alert - Alert on high resource usage

CONTAINER="atbb"
CPU_THRESHOLD=80
MEM_THRESHOLD=80

STATS=$(docker stats --no-stream --format "{{.CPUPerc}},{{.MemPerc}}" "$CONTAINER")
CPU=$(echo "$STATS" | cut -d',' -f1 | tr -d '%')
MEM=$(echo "$STATS" | cut -d',' -f2 | tr -d '%')

if [ "$(echo "$CPU > $CPU_THRESHOLD" | bc)" -eq 1 ]; then
  echo "ALERT: CPU usage is ${CPU}% (threshold: ${CPU_THRESHOLD}%)"
  # Send notification (email, Slack, etc.)
fi

if [ "$(echo "$MEM > $MEM_THRESHOLD" | bc)" -eq 1 ]; then
  echo "ALERT: Memory usage is ${MEM}% (threshold: ${MEM_THRESHOLD}%)"
  # Send notification
fi

Future: Observability#

Planned enhancements (not yet implemented):

  • Prometheus metrics endpoint (/api/metrics)
  • OpenTelemetry tracing
  • Grafana dashboard templates
  • Alert manager integration

8. Upgrading#

Upgrade Process#

IMPORTANT: Upgrading will cause brief downtime (sessions are stored in memory and will be lost).

Step 1: Check release notes

# View releases on GitHub
# https://github.com/malpercio-dev/atbb-monorepo/releases

# Look for:
# - Breaking changes
# - Database migration requirements
# - New environment variables

Step 2: Backup database

# Backup current database (critical!)
pg_dump "postgresql://atbb_user:PASSWORD@host:5432/atbb_prod?sslmode=require" \
  > atbb_backup_$(date +%Y%m%d_%H%M%S).sql

# Verify backup
ls -lh atbb_backup_*.sql

Step 3: Pull new image

# Pull specific version
docker pull ghcr.io/malpercio-dev/atbb:v1.1.0

# Or pull latest
docker pull ghcr.io/malpercio-dev/atbb:latest

Step 4: Run migrations (if required)

# Check release notes for migration requirements
# If migrations are needed:
docker run --rm \
  --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:v1.1.0 \
  pnpm --filter @atbb/appview db:migrate

Step 5: Stop old container

docker stop atbb
docker rm atbb

Step 6: Start new container

docker run -d \
  --name atbb \
  --restart unless-stopped \
  -p 8080:80 \
  --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:v1.1.0

Step 7: Verify upgrade

# Check logs for errors
docker logs atbb

# Test health endpoint
curl http://localhost:8080/api/healthz

# Visit forum in browser
# Test key functionality (login, post, etc.)

Rollback Procedure#

If upgrade fails, rollback to previous version:

Step 1: Stop broken container

docker stop atbb
docker rm atbb

Step 2: Restore database (if migrations were run)

# Connect to database
psql "postgresql://atbb_user:PASSWORD@host:5432/atbb_prod?sslmode=require"

# Drop all tables
DROP SCHEMA public CASCADE;
CREATE SCHEMA public;

# Restore from backup
psql "postgresql://atbb_user:PASSWORD@host:5432/atbb_prod?sslmode=require" \
  < atbb_backup_20260212_140000.sql

Step 3: Start old version

docker run -d \
  --name atbb \
  --restart unless-stopped \
  -p 8080:80 \
  --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:v1.0.0

Zero-Downtime Upgrades (Future)#

Once Redis session storage is implemented, you can upgrade with zero downtime:

  1. Start new container on different port
  2. Test new version
  3. Switch reverse proxy to new port
  4. Stop old container

Not currently supported because sessions are in-memory.


9. Troubleshooting#

Container Won't Start#

Symptom: Container exits immediately after starting

Diagnosis:

docker logs atbb

Common causes:

  1. Missing environment variables

    Error: DATABASE_URL is required
    

    Solution: Verify .env.production has all required variables (see Section 3).

  2. Database connection failed

    Error: connect ECONNREFUSED
    

    Solution:

    • Verify DATABASE_URL is correct
    • Check firewall allows connections from container's IP
    • Test connection manually: psql "postgresql://..."
  3. Port already in use

    Error: bind: address already in use
    

    Solution: Change host port mapping: -p 8081:80

  4. Migrations not run

    Error: relation "forums" does not exist
    

    Solution: Run migrations (Section 4).

Database Connection Issues#

Symptom: Application starts but fails on database queries

Error examples:

FATAL: password authentication failed for user "atbb_user"
FATAL: no pg_hba.conf entry for host, SSL off
Error: connect ETIMEDOUT

Solutions:

  1. Test connection manually:

    psql "postgresql://atbb_user:PASSWORD@host:5432/atbb_prod?sslmode=require"
    

    If this fails, the issue is NOT with atBB (fix database access first).

  2. Check credentials:

    • Verify username/password in DATABASE_URL
    • Ensure user has been created in database
  3. Check SSL settings:

    # If database requires SSL, ensure connection string includes:
    DATABASE_URL=postgresql://...?sslmode=require
    
  4. Check network/firewall:

    • Verify container can reach database host
    • Test from within container: docker exec atbb ping db.example.com
    • Check cloud provider security groups/firewall rules

OAuth Redirect URI Mismatch#

Symptom: Login fails with "redirect URI mismatch" error

Cause: OAUTH_PUBLIC_URL doesn't match the actual domain users access

Solution:

  1. Verify OAUTH_PUBLIC_URL in .env.production:

    OAUTH_PUBLIC_URL=https://forum.example.com  # Must match actual domain
    
  2. Common mistakes:

    • http:// instead of https:// (use HTTPS in production)
    • ❌ Trailing slash: https://forum.example.com/ (remove trailing slash)
    • ❌ Wrong subdomain: https://www.forum.example.com vs https://forum.example.com
  3. Restart container after fixing:

    docker restart atbb
    

PDS Connectivity Problems#

Symptom: Cannot create posts, forum metadata not syncing

Error in logs:

Error: Failed to connect to PDS: ENOTFOUND
Error: Invalid credentials for FORUM_HANDLE

Solutions:

  1. Verify PDS URL:

    curl https://bsky.social/xrpc/_health
    # Should return: {"version":"0.x.x"}
    
  2. Test forum credentials:

    # Use atproto CLI or curl to test auth
    curl -X POST https://bsky.social/xrpc/com.atproto.server.createSession \
      -H "Content-Type: application/json" \
      -d '{
        "identifier": "forum.example.com",
        "password": "YOUR_FORUM_PASSWORD"
      }'
    # Should return: {"did":"did:plc:...","accessJwt":"..."}
    
  3. Check environment variables:

    docker exec atbb env | grep -E 'FORUM_|PDS_'
    # Verify all values are correct
    

High Memory Usage#

Symptom: Container using excessive memory (>1GB)

Diagnosis:

docker stats atbb

Solutions:

  1. Set memory limit:

    docker update --memory="512m" atbb
    
  2. Check for memory leak:

    • Monitor over time: docker stats atbb
    • If memory grows continuously, report issue with logs
  3. Increase container memory:

    # For large forums, 1-2GB may be normal
    docker update --memory="2g" atbb
    

Logs Filling Disk#

Symptom: Disk space running out due to large log files

Check log size:

du -sh /var/lib/docker/containers/*/

Solutions:

  1. Configure log rotation (recommended):

    # Stop container
    docker stop atbb
    docker rm atbb
    
    # Restart with log rotation
    docker run -d \
      --name atbb \
      --log-opt max-size=10m \
      --log-opt max-file=3 \
      -p 8080:80 \
      --env-file .env.production \
      ghcr.io/malpercio-dev/atbb:latest
    
  2. Manually clean logs:

    # Truncate logs (preserves container)
    truncate -s 0 $(docker inspect --format='{{.LogPath}}' atbb)
    
  3. Use external log aggregator (syslog, fluentd, etc.)

Container Performance Issues#

Symptom: Slow response times, high CPU usage

Diagnosis:

docker stats atbb
docker top atbb

Solutions:

  1. Check database performance:

    • Slow queries often bottleneck at database
    • Monitor database server metrics
    • Add indexes if needed (consult forum performance guide)
  2. Increase resources:

    docker update --cpus="2.0" --memory="1g" atbb
    
  3. Check reverse proxy settings:

    • Ensure proxy is not buffering excessively
    • Verify HTTP/2 is enabled for better performance
  4. Monitor specific endpoints:

    # Extract slow requests from logs
    docker logs atbb | grep '^{' | jq 'select(.duration > 1000)'
    

Session Errors / Random Logouts#

Symptom: Users randomly logged out, "session expired" errors

Causes:

  1. Container restarted — Sessions are in-memory, lost on restart
  2. SESSION_SECRET changed — Invalidates all sessions
  3. SESSION_SECRET not set — Each restart generates new secret

Solutions:

  1. Verify SESSION_SECRET is set:

    docker exec atbb env | grep SESSION_SECRET
    # Should show a 64-character hex string
    
  2. If blank, generate and set:

    openssl rand -hex 32
    # Add to .env.production
    # Restart container
    
  3. Future: Use Redis for persistent sessions (not yet implemented)

Getting Help#

If you cannot resolve an issue:

  1. Collect diagnostics:

    # Container logs
    docker logs atbb > atbb-logs.txt
    
    # Container info
    docker inspect atbb > atbb-inspect.json
    
    # Resource usage
    docker stats --no-stream atbb
    
  2. Sanitize sensitive data:

    • Remove passwords from logs
    • Remove SESSION_SECRET from environment dumps
  3. Report issue:


10. Docker Compose Example#

For simpler local testing or single-server deployments, use Docker Compose.

File: docker-compose.example.yml (included in repository)

What It Provides#

  • PostgreSQL database (local development)
  • atBB application container
  • Automatic dependency management (atBB waits for PostgreSQL)
  • Volume persistence for database
  • Health checks

Usage#

Step 1: Download files

# Download docker-compose.example.yml
curl -O https://raw.githubusercontent.com/malpercio-dev/atbb-monorepo/main/docker-compose.example.yml

# Download .env.production.example
curl -O https://raw.githubusercontent.com/malpercio-dev/atbb-monorepo/main/.env.production.example

# Rename to .env
mv .env.production.example .env

Step 2: Configure environment

# Generate session secret
openssl rand -hex 32

# Edit .env and fill in:
nano .env

Required changes in .env:

# AT Protocol credentials (from Prerequisites)
FORUM_DID=did:plc:YOUR_FORUM_DID
PDS_URL=https://bsky.social
FORUM_HANDLE=forum.example.com
FORUM_PASSWORD=YOUR_FORUM_PASSWORD

# OAuth (for local testing, use http://localhost)
OAUTH_PUBLIC_URL=http://localhost

# Session secret (generated above)
SESSION_SECRET=a1b2c3d4e5f6...

# Database connection will be set by docker-compose
# (Uses container name "postgres" as hostname)

Step 3: Start services

docker-compose -f docker-compose.example.yml up -d

Expected output:

Creating network "atbb_default" with the default driver
Creating volume "atbb_postgres_data" with default driver
Creating atbb-postgres ... done
Creating atbb-app ... done

Step 4: Run migrations

docker-compose -f docker-compose.example.yml exec atbb \
  pnpm --filter @atbb/appview db:migrate

Step 5: Access forum

Visit: http://localhost

Management Commands#

View logs:

# All services
docker-compose -f docker-compose.example.yml logs -f

# Specific service
docker-compose -f docker-compose.example.yml logs -f atbb
docker-compose -f docker-compose.example.yml logs -f postgres

Stop services:

docker-compose -f docker-compose.example.yml down

Stop and remove data:

docker-compose -f docker-compose.example.yml down -v
# WARNING: This deletes the database volume!

Restart services:

docker-compose -f docker-compose.example.yml restart

Upgrade to new version:

# Pull new image
docker-compose -f docker-compose.example.yml pull atbb

# Run migrations (if required by release notes)
docker-compose -f docker-compose.example.yml exec atbb \
  pnpm --filter @atbb/appview db:migrate

# Restart
docker-compose -f docker-compose.example.yml restart atbb

Production Considerations#

DO NOT use docker-compose.example.yml as-is in production.

Limitations:

  • Database password is weak (change in compose file)
  • No TLS/SSL for database
  • No backups configured
  • Single-server only

For production:

  1. Use managed PostgreSQL (AWS RDS, DigitalOcean, etc.)
  2. Run atBB container separately (not with local PostgreSQL)
  3. Set up reverse proxy with HTTPS (Caddy/nginx)
  4. Use strong passwords and secrets
  5. Configure automated backups
  6. Set up monitoring and alerting

Modified compose for production (atBB only, external DB):

version: '3.8'

services:
  atbb:
    image: ghcr.io/malpercio-dev/atbb:v1.0.0
    container_name: atbb
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"  # Bind to localhost only
    env_file:
      - .env.production
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/api/healthz"]
      interval: 30s
      timeout: 3s
      retries: 3

Appendix: Quick Reference#

Required Environment Variables#

# PostgreSQL (production recommended)
DATABASE_URL=postgresql://user:pass@host:5432/db?sslmode=require
# SQLite (lightweight alternative)
# DATABASE_URL=file:./atbb.db
FORUM_DID=did:plc:xxxxxxxxxxxxx
PDS_URL=https://bsky.social
FORUM_HANDLE=forum.example.com
FORUM_PASSWORD=strong_password_16+_chars
OAUTH_PUBLIC_URL=https://forum.example.com
SESSION_SECRET=64_hex_chars_from_openssl_rand

Essential Commands#

# Pull image
docker pull ghcr.io/malpercio-dev/atbb:latest

# Run migrations
docker run --rm --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:latest \
  pnpm --filter @atbb/appview db:migrate

# Start container
docker run -d --name atbb --restart unless-stopped \
  -p 8080:80 --env-file .env.production \
  ghcr.io/malpercio-dev/atbb:latest

# View logs
docker logs -f atbb

# Stop/restart
docker stop atbb
docker restart atbb

# Health check
curl http://localhost:8080/api/healthz

Support Resources#

Security Checklist#

Before going to production:

  • Generated SESSION_SECRET with openssl rand -hex 32
  • Used strong, unique passwords (minimum 16 characters)
  • Enabled database SSL/TLS (?sslmode=require)
  • Set OAUTH_PUBLIC_URL to HTTPS domain (not HTTP)
  • Set file permissions: chmod 600 .env.production
  • Never committed .env.production to version control
  • Configured reverse proxy with HTTPS (Caddy/nginx)
  • Set up database backups
  • Configured log rotation
  • Set up health monitoring
  • Restricted firewall to ports 80/443 only
  • Tested backup restoration procedure

11. NixOS Deployment#

The atBB flake provides a NixOS module that manages all services declaratively:

  • atbb-appview — Hono API server (systemd service)
  • atbb-web — Hono web UI server (systemd service)
  • atbb-migrate — One-shot database migration service
  • PostgreSQL 17 — Local database with peer authentication (optional)
  • nginx — Reverse proxy with automatic ACME/Let's Encrypt TLS (optional)

The module is suitable for single-server deployments. Sections 1–10 of this guide describe Docker-based deployment; this section covers the NixOS path exclusively.

Step 1: Add atBB as a Flake Input#

In your NixOS system flake:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    atbb.url    = "github:malpercio-dev/atbb-monorepo";
  };

  outputs = { nixpkgs, atbb, ... }: {
    nixosConfigurations.my-server = nixpkgs.lib.nixosSystem {
      system  = "x86_64-linux";
      modules = [
        atbb.nixosModules.default
        ./configuration.nix
      ];
    };
  };
}

Note: The module is exported as nixosModules.default, not nixosModules.atbb.

Step 2: Create the Environment File#

The 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:

sudo mkdir -p /etc/atbb
sudo tee /etc/atbb/env > /dev/null <<'EOF'
# Database — Unix socket peer auth (matches services.atbb.user = "atbb")
DATABASE_URL=postgres:///atbb?host=/run/postgresql
# PGHOST makes postgres.js use the Unix socket directory reliably,
# since it does not always honour the ?host= query parameter in URLs.
PGHOST=/run/postgresql

# Session security
SESSION_SECRET=<generate with: openssl rand -hex 32>

# Forum AT Protocol account credentials
FORUM_HANDLE=forum.example.com
FORUM_PASSWORD=<your forum account password>
EOF

sudo chmod 600 /etc/atbb/env

Why Unix socket for DATABASE_URL? When 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).

Secrets management: For automated deployments, consider sops-nix or agenix to provision /etc/atbb/env as an encrypted secret rather than managing it manually.

Step 3: Configure the Module#

Add to your configuration.nix:

{
  services.atbb = {
    enable  = true;
    domain  = "forum.example.com";
    forumDid = "did:plc:your-forum-did";
    pdsUrl  = "https://bsky.social";

    # Path to the environment file created in Step 2
    environmentFile = /etc/atbb/env;

    # Local PostgreSQL (default: true)
    # Set to false to use an external database via DATABASE_URL
    database.enable = true;

    # Run migrations manually after each deploy (safer)
    # Set to true to run automatically on every appview start
    autoMigrate = false;
  };

  # Required when enableACME = true (the default)
  security.acme = {
    acceptTerms = true;
    defaults.email = "admin@example.com";
  };
}

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.

Key Options Reference#

Option Default Description
domain (required) Public domain for the forum
forumDid (required) Forum's AT Protocol DID
pdsUrl (required) URL of the forum's PDS
environmentFile (required) Path to secrets file
database.enable true Provision local PostgreSQL 17
database.name "atbb" Database name
autoMigrate false Run migrations on appview start
enableNginx true Configure nginx reverse proxy
enableACME true Enable Let's Encrypt TLS
appviewPort 3000 Internal port for appview
webPort 3001 Internal port for web UI
user / group "atbb" System user/group for services

Step 4: Deploy#

Apply your configuration using your preferred NixOS deployment tool:

# Local rebuild
sudo nixos-rebuild switch --flake .#my-server

# Remote via colmena
colmena apply --on my-server

# Remote via nixos-rebuild
nixos-rebuild switch --flake .#my-server \
  --target-host root@forum.example.com

Step 5: Run Database Migrations#

The atbb-migrate service is a one-shot systemd unit — it runs once and exits. Trigger it manually after each deployment:

sudo systemctl start atbb-migrate

# Check migration output
sudo journalctl -u atbb-migrate

Expected output:

Reading migrations from /nix/store/.../apps/appview/drizzle
Applying migration: 0000_initial_schema.sql
...
All migrations applied successfully

If 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.

Step 6: Verify Services#

# Check all atBB services
systemctl status atbb-appview atbb-web

# View live logs
journalctl -fu atbb-appview
journalctl -fu atbb-web

# Test the API
curl http://localhost:3000/api/healthz
# Expected: {"status":"ok"}

# Verify nginx is routing correctly
curl https://forum.example.com/api/healthz

Using Caddy Instead of nginx#

If you prefer Caddy, disable the built-in nginx proxy and configure services.caddy yourself:

{
  services.atbb = {
    # ... other options
    enableNginx = false;   # disable built-in nginx virtualHost
    enableACME  = false;   # Caddy manages TLS automatically
  };

  services.caddy = {
    enable = true;
    virtualHosts."forum.example.com".extraConfig = ''
      # AT Protocol well-known endpoints → appview
      # Must reach appview (not web UI) for OAuth to work
      handle /.well-known/* {
        reverse_proxy localhost:${toString config.services.atbb.appviewPort}
      }

      # REST API → appview
      handle /api/* {
        reverse_proxy localhost:${toString config.services.atbb.appviewPort}
      }

      # Web UI — catch-all
      handle {
        reverse_proxy localhost:${toString config.services.atbb.webPort}
      }
    '';
  };
}

See nix/Caddyfile.example in the repository for the equivalent standalone Caddyfile.

Upgrading#

To upgrade atBB, update the flake input and redeploy:

# Update atBB to latest
nix flake update atbb

# Redeploy
sudo nixos-rebuild switch --flake .#my-server

# Run migrations for the new version
sudo systemctl start atbb-migrate

NixOS 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.


End of Deployment Guide

For questions or issues not covered here, please open an issue at: https://github.com/malpercio-dev/atbb-monorepo/issues