Skip to content
Self-Hosted Deployment

Run on your infrastructure

Deploy the BuildBase server on your own infrastructure. Your end users' data stays in your own database, with the same dashboard and SDK. One Docker command to start.

Why self-host

Your database and network policies, with the same dashboard and SDK.

Your data, your database

Your end users' accounts and data stay in your own MongoDB. The install stays connected to BuildBase for console sign-in, scheduling and status.

Your security policies

Use your own firewall rules, VPN, and network policies. No shared infrastructure with other tenants.

Your database

Run your own MongoDB instance. Control backups, replication, disaster recovery, and data retention.

Connected to central

Client, server, and auth portal run on your infrastructure. The BuildBase central server signs your team in to the console, manages your organizations and licensing, and receives a status check-in every five minutes.

One command setup

Public Docker image on Docker Hub. Copy the compose file, run one command, and the installation checks in with the dashboard.

Run anywhere

AWS, GCP, Azure, DigitalOcean, Hetzner, on-premises, bare metal - anywhere Docker runs. No vendor lock-in.

Architecture

Split architecture. End-user data stays on your servers. The dashboard connects over HTTPS.

Your Infrastructure

Hosted by you - full control

• Tenant Server - users, emails, workflows, billing

• Client App - web dashboard for your team

• Auth Portal - login, signup, password reset

• MongoDB - your data, your backups

• Redis - sessions, cache, queues

Central Server Only

Console sign-in, org management and licensing

• Org Management - create and manage organizations

• Admin Auth - Google OAuth for org admins

• ES256 Keys - secure token signing

• Installation Keys - per-instance API keys

• Licensing - installation verification

Your end users' accounts and application data stay in your own MongoDB. The installation stays connected to BuildBase: the central server signs your team in to the console, manages your organizations, receives a status check-in every five minutes and is told about plan changes. Visitor IP addresses are sent to IPinfo for location lookups unless IP_INFO_MODE is set to disabled, and uploaded files go to a Google Cloud Storage bucket you own.

All modules included

The same modules, SDK, API and dashboard. Some need an account of your own with a vendor.

Authentication
Billing & Subscriptions
Email Campaigns
Workflow Automation
Multi-Tenant Workspaces
SDK & API
User & Audience Management
Access Control (RBAC)
Credits & Usage Metering
Feature Flags
Push Notifications
Forms & Data Collection
Content Management
Analytics & Reporting
Tracking & Tags
Webhooks & Events
Short Links
Collections & Custom Data
Slack & Team Notifications
Assets & Media

File storage needs a Google Cloud Storage bucket of your own, the only storage the server supports.

Deploy in three steps

Create an installation, start it with Docker, and add your organization.

1

Create an installation

An installation is one server running the platform. Create it in the dashboard and you get its ID, an API key shown once, the compose file and an env template. Self-host is priced per installation, and is arranged by talking to us.

2

Deploy with Docker

Copy the compose file and .env template to your server and run docker compose up -d. That starts the tenant server, client app, auth portal, MongoDB and Redis, and the installation checks in with the dashboard.

3

Add your organization

Once the installation has checked in, create a self-hosted organization on it, or attach one you already have. Manage users, billing, emails and workflows from the central dashboard.

Licensed per installation

An installation is one server running the platform. Run as many organizations on it as your plan allows, and manage them all from the dashboard.

Counted per installation

Your allowance belongs to your account, not to one organization. The Installations page shows how many you run, how many you're allowed, and how many organizations each one can serve. Self-host is priced per installation, and is arranged by talking to us.

Several organizations on one server

When your plan allows more than one, several organizations share one installation, with its MongoDB and Redis. Each still keeps its data in a database of its own.

Move an organization

Put an organization on an installation, take it off, or put it back later. Moving between installations that share the same MongoDB and Redis needs no data copied across.

Rotate the key without downtime

Make a new API key for an installation, and keep the old one working for up to an hour while you update the server. Nothing goes offline.

See what it runs

Each installation shows whether it is connected and which version it last reported. Test connection checks the server address from your browser.

Your users never pay for a lapse

If your self-hosting plan lapses, your team is asked to renew before it can manage the organization again. Your users notice nothing: sign-in, the SDK and the server API keep working the whole time.

Quick Start

One file. One command.

Server, client, auth, MongoDB, and Redis - all included. Copy, paste, run.

docker-compose.selfhost.yml
# Self-Hosted: MongoDB + Redis + Server + Client + Auth
# Save as docker-compose.selfhost.yml
# Usage: docker compose -f docker-compose.selfhost.yml --env-file .env.selfhost up -d

services:
  # ── Infrastructure ──────────────────────────────────────────
  mongodb:
    image: mongo:7.0
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - SETUID
      - SETGID
      - DAC_OVERRIDE
      # The healthcheck runs mongosh as root, which leaves root-owned files
      # in /data/db/.mongodb; without CHOWN the entrypoint cannot hand them
      # back to mongodb and every restart after the first crash-loops.
      - CHOWN
    volumes:
      - mongodb_data:/data/db
    networks:
      - db
    healthcheck:
      # mongosh's own cold start can take most of 5s on a first boot, which
      # failed a first `up` with "container mongodb is unhealthy" while
      # mongod was fine. Same probe as the repo's docker-compose.selfhost.yml.
      test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"]
      interval: 10s
      timeout: 15s
      start_period: 60s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 1024M
          cpus: "1.0"
          pids: 256
    logging:
      driver: json-file
      options:
        max-size: "100m"
        max-file: "3"

  redis:
    image: redis:7.4-bookworm
    restart: unless-stopped
    read_only: true
    user: "999:999"
    cap_drop:
      - ALL
    # maxmemory stays well under the 256M limit below: Redis needs memory
    # beyond its data (allocator overhead, fragmentation, the AOF rewrite's
    # fork), and a Redis that reaches the limit is killed instead of refusing
    # writes. To give Redis more, raise both numbers together.
    command: redis-server --appendonly yes --maxmemory-policy noeviction --maxmemory 150mb --requirepass ${REDIS_PASSWORD:?REDIS_PASSWORD is required — run the generate-secrets command from the quick start}
    tmpfs:
      - /tmp
    volumes:
      - redis_data:/data
    networks:
      - db
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:?REDIS_PASSWORD is required — run the generate-secrets command from the quick start}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: "0.5"
          pids: 64
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "3"

  # ── Backend ─────────────────────────────────────────────────
  tenant-server:
    image: buildbaseapp/tenant-server:latest
    restart: unless-stopped
    # The workers run in this process. Compose's default of 10 s would kill
    # a running job on every upgrade or restart; the server exits by itself
    # within 150 s, and long jobs hand themselves back as it closes.
    stop_grace_period: 180s
    read_only: true
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    tmpfs:
      - /tmp
      - /var/log
    ports:
      - "${TENANT_SERVER_PORT:-4101}:3000"
    environment:
      - NODE_ENV=production
      - PORT=3000
      # Keep the Node heap below the 1024M container limit — the image
      # default is sized for larger hosts and would get OOM-killed here.
      - NODE_OPTIONS=--max-old-space-size=768
      - SERVER_URL=${TENANT_SERVER_URL:-http://localhost:4101}
      - APPLICATION_URL=${CLIENT_URL:-http://localhost:4100}
      - AUTH_SERVER_URL=${AUTH_URL:-http://localhost:4103}
      - MONGO_CONNECTION_URL=mongodb://mongodb:27017/
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_PASSWORD=${REDIS_PASSWORD:?REDIS_PASSWORD is required — run the generate-secrets command from the quick start}
      - INSTALLATION_API_KEY=${INSTALLATION_API_KEY:-}
      - INSTALLATION_ID=${INSTALLATION_ID:-}
      - JWT_PASS=${JWT_PASS:?JWT_PASS is required — run the generate-secrets command from the quick start}
      - OAUTH2_SECRET=${OAUTH2_SECRET:?OAUTH2_SECRET is required — run the generate-secrets command from the quick start}
      - DB_ENCRYPTION_KEY=${DB_ENCRYPTION_KEY:?DB_ENCRYPTION_KEY is required (64 hex chars) — run the generate-secrets command from the quick start}
      - SECRET_KEY=${SECRET_KEY:?SECRET_KEY is required — run the generate-secrets command from the quick start}
      - GOOGLE_AUTH_CLIENT_ID=${GOOGLE_AUTH_CLIENT_ID:-}
      - GOOGLE_AUTH_CLIENT_SECRET=${GOOGLE_AUTH_CLIENT_SECRET:-}
      - GOOGLE_STORAGE_ASSETS_BUCKET_NAME=${GOOGLE_STORAGE_ASSETS_BUCKET_NAME:-}
      # Direct uploads (up to 100 MB, form file fields, SDK uploads). Empty
      # keeps every upload on the 5 MB server route.
      - GOOGLE_STORAGE_STAGING_BUCKET_NAME=${GOOGLE_STORAGE_STAGING_BUCKET_NAME:-}
      - GOOGLE_STORAGE_FINALIZE_TOKEN=${GOOGLE_STORAGE_FINALIZE_TOKEN:-}
      - GOOGLE_STORAGE_USAGE_LOG_BUCKET=${GOOGLE_STORAGE_USAGE_LOG_BUCKET:-}
      - GOOGLE_STORAGE_USAGE_LOG_PREFIX=${GOOGLE_STORAGE_USAGE_LOG_PREFIX:-}
      # Owner data exports. Empty keeps them off (the routes answer 503).
      - EXPORTS_BUCKET=${EXPORTS_BUCKET:-}
      # Storage switches. Empty keeps each one's code default.
      - TENANT_STORAGE_GATE=${TENANT_STORAGE_GATE:-}
      - TENANT_STORAGE_BILLING=${TENANT_STORAGE_BILLING:-}
      - TENANT_STORAGE_INTEGRITY=${TENANT_STORAGE_INTEGRITY:-}
      - TENANT_STORAGE_PURGE=${TENANT_STORAGE_PURGE:-}
      # URL safety. Unset means no reputation checking and links keep working;
      # each tuning var below falls back to its code default when empty.
      - GOOGLE_WEB_RISK_API_KEY=${GOOGLE_WEB_RISK_API_KEY:-}
      - URL_SAFETY_ENABLED=${URL_SAFETY_ENABLED:-}
      - URL_SAFETY_LOG_ONLY=${URL_SAFETY_LOG_ONLY:-}
      - URL_SAFETY_CLEAN_TTL_SECONDS=${URL_SAFETY_CLEAN_TTL_SECONDS:-}
      - WEB_RISK_TIMEOUT_MS=${WEB_RISK_TIMEOUT_MS:-}
      # Breached-password check. Unset means on; off for an install that
      # cannot reach api.pwnedpasswords.com.
      - BREACHED_PASSWORD_CHECK=${BREACHED_PASSWORD_CHECK:-}
      # Bot protection (Cloudflare Turnstile) on this install's sign-in
      # domain. Unset means no challenge unless an org saves its own keys.
      - TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY:-}
      - TURNSTILE_SECRET_KEY=${TURNSTILE_SECRET_KEY:-}
      # Phone sign-in code limits per number and per IP. Unset means the
      # defaults (5 and 10 per number, 20 and 50 per IP, an hour and a day).
      - SMS_NUMBER_HOURLY_MAX=${SMS_NUMBER_HOURLY_MAX:-}
      - SMS_NUMBER_DAILY_MAX=${SMS_NUMBER_DAILY_MAX:-}
      - SMS_IP_HOURLY_MAX=${SMS_IP_HOURLY_MAX:-}
      - SMS_IP_DAILY_MAX=${SMS_IP_DAILY_MAX:-}
      # Commerce (selling through Stripe Connect). All off or empty by
      # default: unset keeps selling, PayPal payouts and label providers
      # closed, and file items unavailable.
      - TENANT_MARKETPLACE=${TENANT_MARKETPLACE:-}
      - AFFILIATES_PAYPAL=${AFFILIATES_PAYPAL:-}
      - GOOGLE_STORAGE_DELIVERABLES_BUCKET_NAME=${GOOGLE_STORAGE_DELIVERABLES_BUCKET_NAME:-}
      - CLAMAV_HOST=${CLAMAV_HOST:-}
      - CLAMAV_PORT=${CLAMAV_PORT:-}
      - SHIPPING_EASYPOST=${SHIPPING_EASYPOST:-}
      - SHIPPING_SHIPPO=${SHIPPING_SHIPPO:-}
      - CORS_WHITELISTED_DOMAINS=${CLIENT_URL:-http://localhost:4100},${AUTH_URL:-http://localhost:4103}
      # Optional: point APM/log telemetry at YOUR OWN New Relic account.
      # Leave unset (the default) and no telemetry is collected or sent.
      - NEW_RELIC_LICENSE_KEY=${NEW_RELIC_LICENSE_KEY:-}
      - NEW_RELIC_APP_NAME=${NEW_RELIC_APP_NAME:-}
      - LOG_LEVEL=${LOG_LEVEL:-}
      # The job dashboard's password. Unset, it falls back to the
      # installation API key, and the server warns at every boot.
      - BULLMQ_DASHBOARD_PASSWORD=${BULLMQ_DASHBOARD_PASSWORD:-}
    depends_on:
      mongodb:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - db
      - app
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/api/ready"]
      interval: 15s
      timeout: 5s
      start_period: 45s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 1024M
          cpus: "1.0"
          pids: 256
    logging:
      driver: json-file
      options:
        max-size: "100m"
        max-file: "5"

  # ── Frontend ────────────────────────────────────────────────
  client:
    image: buildbaseapp/client:latest
    restart: unless-stopped
    # NOTE: read_only must NOT be set on client or auth. Their entrypoints
    # rewrite __NEXT_PUBLIC_*__ URL placeholders in the JS bundles at
    # startup; a read-only filesystem makes that rewrite fail silently and
    # the app calls the literal placeholder string instead of your URL.
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    tmpfs:
      - /tmp
      - /var/cache/nginx
      - /var/run
    ports:
      - "${CLIENT_PORT:-4100}:3000"
    environment:
      - NEXT_PUBLIC_SERVER_URL=${TENANT_SERVER_URL:-http://localhost:4101}
      - NEXT_PUBLIC_DEFAULT_TENANT_SERVER_URL=${TENANT_SERVER_URL:-http://localhost:4101}
      - NEXT_PUBLIC_INSTALLATION_ID=${INSTALLATION_ID:-}
      # Optional: browser monitoring against YOUR OWN New Relic account.
      # Account ID and browser key are shared; the app ID is per app.
      - NEXT_PUBLIC_NEW_RELIC_ACCOUNT_ID=${NEW_RELIC_BROWSER_ACCOUNT_ID:-}
      - NEXT_PUBLIC_NEW_RELIC_BROWSER_LICENSE_KEY=${NEW_RELIC_BROWSER_LICENSE_KEY:-}
      - NEXT_PUBLIC_NEW_RELIC_APP_ID=${NEW_RELIC_BROWSER_APP_ID_CLIENT:-}
    networks:
      - app
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/"]
      interval: 15s
      timeout: 5s
      start_period: 30s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: "0.5"
          pids: 128
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "3"

  auth:
    image: buildbaseapp/auth:latest
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    tmpfs:
      - /tmp
      - /var/cache/nginx:uid=1001,gid=1001
      - /var/run:uid=1001,gid=1001
    ports:
      - "${AUTH_PORT:-4103}:3000"
    environment:
      - NEXT_PUBLIC_SERVER_URL=${TENANT_SERVER_URL:-http://localhost:4101}
      # Optional: browser monitoring against YOUR OWN New Relic account.
      - NEXT_PUBLIC_NEW_RELIC_ACCOUNT_ID=${NEW_RELIC_BROWSER_ACCOUNT_ID:-}
      - NEXT_PUBLIC_NEW_RELIC_BROWSER_LICENSE_KEY=${NEW_RELIC_BROWSER_LICENSE_KEY:-}
      - NEXT_PUBLIC_NEW_RELIC_APP_ID=${NEW_RELIC_BROWSER_APP_ID_AUTH:-}
    networks:
      - app
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
      interval: 15s
      timeout: 5s
      start_period: 30s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: "0.5"
          pids: 128
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "3"

  # Optional virus scanner for the files sellers sell. Off unless you start
  # the "scanner" profile:
  #   docker compose ... --profile scanner up -d
  # and set CLAMAV_HOST=clamav in .env.selfhost. It needs about 3 GB of
  # memory (clamd keeps its signatures in memory) and outbound internet,
  # since freshclam downloads the signatures; the first start takes a few
  # minutes. Port 3310 is reachable inside the app network only.
  clamav:
    image: clamav/clamav:1.4.6
    profiles: ["scanner"]
    restart: unless-stopped
    volumes:
      - clamav_db:/var/lib/clamav
    networks:
      - app
    healthcheck:
      test: ["CMD", "/usr/local/bin/clamdcheck.sh"]
      interval: 60s
      timeout: 15s
      start_period: 600s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 3072M
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "3"

volumes:
  mongodb_data:
  redis_data:
  clamav_db:

# Network segmentation: databases isolated from frontends.
# Only tenant-server bridges both networks.
networks:
  db:
    driver: bridge
    internal: true
  app:
    driver: bridge

Shared Cloud vs Self-Hosted

Same SDK and dashboard. Different hosting.

Feature
Shared Cloud
Self-Hosted
All platform modules
User & audience dataIsolated per orgYour servers
DatabasePer-org databaseYour MongoDB
Redis / cachingPre-configuredYour Redis
Dashboard UIPre-configuredHosted by you
SDK & APISame SDKSame SDK
Authentication tokensCentral serverES256 signed, verified locally
Emails & campaignsPre-configuredYour infrastructure
Workflows & jobsPre-configuredYour workers
File storageCloud storageYour Google Cloud Storage bucket
SSL & networkingPre-configuredYou control
UpdatesAutomaticdocker compose pull
Security

Security

Built for compliance-sensitive deployments.

  • ES256 (ECDSA P-256) asymmetric token signing - private key never leaves central server
  • Per-organization key pairs with key rotation support
  • Audience-locked tokens with 2-minute expiry and replay protection (jti)
  • Per-installation API keys with HMAC-SHA256 request signing
  • Installation-scoped access - each instance only sees its assigned orgs
  • All sensitive database fields encrypted at rest (AES-256-CBC)
  • No end-user passwords or end-user data flow through the central server; it holds your team's console sign-in
  • Central server URL pinned via read-only lock file in Docker images
  • Read-only tenant-server filesystem prevents runtime code tampering
  • Brute-force protection with rate limiting on all auth endpoints
  • SSRF protection on outbound requests (blocks private IPs, cloud metadata)
  • Non-root Docker container with dumb-init signal handling
  • Network segmentation - databases on internal-only network, unreachable from frontends
  • All capabilities dropped (cap_drop: ALL) with minimal cap_add where required
  • PID limits on all containers to prevent fork bomb attacks
  • CPU and memory limits on all containers to prevent resource exhaustion
  • Docker socket mounted read-only for autoheal (production template)
  • Privilege escalation blocked (no-new-privileges) on every container

Docker images

Three official images on Docker Hub. No source code, no secrets, no source maps.

ImageDescriptionPort
buildbaseapp/tenant-serverBackend API server3000
buildbaseapp/clientWeb dashboard (Next.js)3000
buildbaseapp/authAuth portal (Next.js)3000

Platform: linux/amd64, linux/arm64 · Base: Node.js 22 Alpine · Non-root user · Graceful shutdown via dumb-init

Requirements

Minimal requirements. The quick-start compose includes MongoDB and Redis.

Server- Linux (Ubuntu 20.04+ recommended)
Memory- 2 GB RAM minimum
CPU- 2 vCPU minimum
Docker- Docker Engine 20+ and Docker Compose v2
Database- MongoDB 7+ (included in quick start)
Cache- Redis 7+ (included in quick start)
Network- Public IP or domain (for production). Localhost works for testing.

Clone it, or let Claude run it

The self-hosted starter is the Compose stack in a repo. The self-hosting Claude skill walks through deploying, hardening and upgrading it.

Self-host BuildBase with Claude

buildbase-selfhost@buildbase-skills

Deploys and operates the self-hosted stack: the architecture, Docker Compose and Nginx configs, environment variables, production hardening and upgrades.

In Claude Code:

Any agent (Cursor, Codex, Copilot and others), from a shell:

Then ask Claude: “Deploy BuildBase self-hosted with Docker Compose.”

Other ways to install, including claude.ai

Frequently Asked Questions

What are the requirements to self-host?

A Linux server with 2 GB RAM, Docker, and a public IP or domain. MongoDB and Redis are included in the quick-start compose file.

Does the dashboard still work?

Yes. The dashboard at console.buildbase.app connects to your self-hosted server over HTTPS. Your end users' accounts and data stay in your own MongoDB. The install stays connected to BuildBase for console sign-in, scheduling and status.

How does authentication work between central and my server?

The central server signs an ES256 token with your org's private key. Your server verifies it using the public key. No secrets travel over the network.

Can I use localhost for testing?

Yes. The setup wizard checks the connection from your browser, so http://localhost:4101, 192.168.x.x and other LAN addresses work. Use a public domain with HTTPS for production.

How do I update to a new version?

Run `docker compose pull`, then `docker compose up -d`. The production compose file runs 2 API replicas behind Nginx with auto-heal.

Can I run multiple organizations on one server?

On Self-host, yes: one installation can run several organizations. They share the server, the MongoDB instance and Redis, and each organization’s data sits in its own database. Self-host is priced per installation, and is arranged by talking to us.

Is the Docker image open source?

No. The image is published on Docker Hub at buildbaseapp/tenant-server and contains compiled JavaScript only - no source code or source maps.

How do backups work?

You manage your own MongoDB backups. Use mongodump, MongoDB Atlas automated backups, or any preferred tool. Daily backups with point-in-time recovery are recommended.

Can I switch from cloud to self-hosted?

You can add a self-hosted organization to the same account, but there is no built-in tool to move a cloud organization’s data. Workflows export and import as JSON, and contacts export and import as CSV.

Is there an SLA for self-hosted?

Self-hosted uptime depends on your infrastructure. For the hosted dashboard: High availability is a priority. Uptime targets vary by plan; no guarantee of 100%. For production, use the production compose file with 2 API replicas, Nginx, and auto-heal.

Run it on your own servers

Create a self-hosted organization, copy the compose file, and bring it up with Docker. Your end users' data stays in your own database.

Free plan7-day free trial on paid plansNo credit card required