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.
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.
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.
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.
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.
One file. One command.
Server, client, auth, MongoDB, and Redis - all included. Copy, paste, run.
# 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: bridgeShared Cloud vs Self-Hosted
Same SDK and dashboard. Different hosting.
| Feature | Shared Cloud | Self-Hosted |
|---|---|---|
| All platform modules | ||
| User & audience data | Isolated per org | Your servers |
| Database | Per-org database | Your MongoDB |
| Redis / caching | Pre-configured | Your Redis |
| Dashboard UI | Pre-configured | Hosted by you |
| SDK & API | Same SDK | Same SDK |
| Authentication tokens | Central server | ES256 signed, verified locally |
| Emails & campaigns | Pre-configured | Your infrastructure |
| Workflows & jobs | Pre-configured | Your workers |
| File storage | Cloud storage | Your Google Cloud Storage bucket |
| SSL & networking | Pre-configured | You control |
| Updates | Automatic | docker compose pull |
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.
| Image | Description | Port |
|---|---|---|
| buildbaseapp/tenant-server | Backend API server | 3000 |
| buildbaseapp/client | Web dashboard (Next.js) | 3000 |
| buildbaseapp/auth | Auth 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.
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.aiFrequently 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.