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.0 Last Updated: 2026-02-12 Audience: System administrators deploying atBB to production

Related Documentation: See docs/plans/2026-02-11-deployment-infrastructure-design.md for architectural decisions and design rationale behind this deployment approach.

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

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)

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 PostgreSQL connection string postgresql://user:pass@host:5432/dbname?sslmode=require
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#

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:
#  Schema |       Name        | Type  |   Owner
# --------+-------------------+-------+-----------
#  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 | 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#

DATABASE_URL=postgresql://user:pass@host:5432/db?sslmode=require
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

End of Deployment Guide

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