Skip to main content

Docker Compose Setup

Run Brainz Lab on a single server with Docker Compose.

Prerequisites

  • Docker 24+
  • Docker Compose 2.20+
  • 4GB RAM minimum
  • 20GB disk space

One-Line Install

The fastest way to get started:
curl -fsSL https://raw.githubusercontent.com/brainz-lab/stack/main/install.sh | bash
This will:
  1. Clone the stack repository
  2. Generate secure keys
  3. Create your .env file
  4. Show next steps

Manual Setup

1. Clone the Stack

git clone https://github.com/brainz-lab/stack.git
cd stack

2. Run Setup

./scripts/setup.sh
This generates all required secrets and creates your .env file.

3. Start the Stack

./scripts/start.sh

4. Access the Services

For subdomain routing to work locally, add these to /etc/hosts:
127.0.0.1 recall.localhost reflex.localhost pulse.localhost

Docker Compose File

docker-compose.yml
services:
  # Reverse Proxy
  traefik:
    image: traefik:v3.0
    container_name: brainzlab-traefik
    restart: unless-stopped
    ports:
      - "80:80"
      - "8080:8080"
    volumes:
      - ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro
      - ./traefik/routes.yml:/etc/traefik/routes.yml:ro

  # Infrastructure
  postgres:
    image: postgres:16-alpine
    container_name: brainzlab-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-brainzlab}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-brainzlab}
      POSTGRES_DB: ${POSTGRES_DB:-brainzlab}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./postgres/init:/docker-entrypoint-initdb.d:ro
    ports:
      - "${POSTGRES_PORT:-5432}:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-brainzlab}"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: brainzlab-redis
    restart: unless-stopped
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    ports:
      - "${REDIS_PORT:-6379}:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Recall - Structured Logging
  recall:
    image: ${RECALL_IMAGE:-brainzllc/recall:latest}
    container_name: brainzlab-recall
    restart: unless-stopped
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER:-brainzlab}:${POSTGRES_PASSWORD:-brainzlab}@postgres:5432/recall
      REDIS_URL: redis://redis:6379/1
      RAILS_ENV: production
      RAILS_MASTER_KEY: ${RECALL_MASTER_KEY}
      SECRET_KEY_BASE: ${RECALL_SECRET_KEY}
      RECALL_INGEST_KEY: ${RECALL_INGEST_KEY}
    ports:
      - "${RECALL_PORT:-3001}:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  # Reflex - Error Tracking
  reflex:
    image: ${REFLEX_IMAGE:-brainzllc/reflex:latest}
    container_name: brainzlab-reflex
    restart: unless-stopped
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER:-brainzlab}:${POSTGRES_PASSWORD:-brainzlab}@postgres:5432/reflex
      REDIS_URL: redis://redis:6379/2
      RAILS_ENV: production
      RAILS_MASTER_KEY: ${REFLEX_MASTER_KEY}
      SECRET_KEY_BASE: ${REFLEX_SECRET_KEY}
      REFLEX_INGEST_KEY: ${REFLEX_INGEST_KEY}
    ports:
      - "${REFLEX_PORT:-3002}:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  # Pulse - APM & Performance
  pulse:
    image: ${PULSE_IMAGE:-brainzllc/pulse:latest}
    container_name: brainzlab-pulse
    restart: unless-stopped
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER:-brainzlab}:${POSTGRES_PASSWORD:-brainzlab}@postgres:5432/pulse
      REDIS_URL: redis://redis:6379/3
      RAILS_ENV: production
      RAILS_MASTER_KEY: ${PULSE_MASTER_KEY}
      SECRET_KEY_BASE: ${PULSE_SECRET_KEY}
      PULSE_INGEST_KEY: ${PULSE_INGEST_KEY}
    ports:
      - "${PULSE_PORT:-3003}:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

volumes:
  postgres_data:
  redis_data:

Traefik Configuration

Static Config

traefik/traefik.yml
api:
  insecure: true
  dashboard: true

entryPoints:
  web:
    address: ":80"

providers:
  file:
    filename: /etc/traefik/routes.yml
    watch: true

log:
  level: INFO

Dynamic Routes

traefik/routes.yml
http:
  routers:
    recall:
      rule: "Host(`recall.localhost`)"
      service: recall
      entryPoints:
        - web

    reflex:
      rule: "Host(`reflex.localhost`)"
      service: reflex
      entryPoints:
        - web

    pulse:
      rule: "Host(`pulse.localhost`)"
      service: pulse
      entryPoints:
        - web

  services:
    recall:
      loadBalancer:
        servers:
          - url: "http://recall:3000"

    reflex:
      loadBalancer:
        servers:
          - url: "http://reflex:3000"

    pulse:
      loadBalancer:
        servers:
          - url: "http://pulse:3000"

Production Setup

Custom Domain

Update traefik/routes.yml for your domain:
http:
  routers:
    recall:
      rule: "Host(`recall.mycompany.com`)"
      service: recall
      entryPoints:
        - websecure
      tls:
        certResolver: letsencrypt

Enable HTTPS

Add to traefik/traefik.yml:
entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

certificatesResolvers:
  letsencrypt:
    acme:
      email: [email protected]
      storage: /etc/traefik/acme.json
      httpChallenge:
        entryPoint: web

Configure DNS

recall.mycompany.com  → Your Server IP
reflex.mycompany.com  → Your Server IP
pulse.mycompany.com   → Your Server IP

Management Scripts

View Logs

./scripts/logs.sh           # All services
./scripts/logs.sh recall    # Specific service

Check Status

docker compose ps

Stop Services

./scripts/stop.sh

Reset (Warning: Deletes Data)

./scripts/reset.sh

Connect Your App

Configure your Rails app to use your self-hosted instance:
config/initializers/brainzlab.rb
BrainzLab.configure do |config|
  config.secret_key = ENV['BRAINZLAB_SECRET_KEY']

  # Point to your self-hosted instance
  config.recall_url = 'http://localhost:3001'  # or https://recall.mycompany.com
  config.reflex_url = 'http://localhost:3002'  # or https://reflex.mycompany.com
  config.pulse_url  = 'http://localhost:3003'  # or https://pulse.mycompany.com
end

Docker Images

Images are available from:
RegistryImage
Docker Hubbrainzllc/recall:latest
Docker Hubbrainzllc/reflex:latest
Docker Hubbrainzllc/pulse:latest
GitHubghcr.io/brainz-lab/recall:latest
GitHubghcr.io/brainz-lab/reflex:latest
GitHubghcr.io/brainz-lab/pulse:latest
Use the *_IMAGE environment variables to switch registries:
RECALL_IMAGE=ghcr.io/brainz-lab/recall:latest
REFLEX_IMAGE=ghcr.io/brainz-lab/reflex:latest
PULSE_IMAGE=ghcr.io/brainz-lab/pulse:latest

Troubleshooting

Services Not Starting

# Check container logs
docker compose logs recall
docker compose logs reflex
docker compose logs pulse

# Check if ports are in use
lsof -i :3001
lsof -i :3002
lsof -i :3003

Database Connection Issues

# Verify PostgreSQL is healthy
docker compose exec postgres pg_isready

# Check database exists
docker compose exec postgres psql -U brainzlab -l

Health Check

Each service exposes a health endpoint:
curl http://localhost:3001/up  # Recall
curl http://localhost:3002/up  # Reflex
curl http://localhost:3003/up  # Pulse

Need Help?

Get support from the Brainz Lab team