Internal tools are the backbone of every operations team, yet most organizations either buy expensive SaaS platforms that charge per-user premiums, or they assign engineering sprints to build bespoke admin panels from scratch. Budibase is an open-source low-code platform that lets non-developers design data-driven applications with a visual builder while giving engineers full control over hosting, authentication, and data custody. It connects to PostgreSQL, MySQL, MongoDB, REST APIs, and Google Sheets, then generates reactive web apps with role-based access control, automation triggers, and customizable themes.
This guide deploys Budibase on Ubuntu with Docker Compose, Caddy for automatic HTTPS, CouchDB for application definitions and user metadata, Redis for caching and job queuing, and MinIO for object storage. By the end, you will have a production-ready low-code platform accessible at a public domain, with persistent data volumes, health checks, secure secrets handling, and a backup strategy that covers both the document store and the object storage layer.
Running Budibase on your own infrastructure means you retain full ownership of application schemas, user directories, audit logs, and file attachments. You avoid per-seat licensing, can enforce network-level isolation, and integrate directly with your existing identity provider without routing credentials through a third-party cloud.
Architecture and flow overview
Caddy sits at the edge and terminates TLS with automatically managed Let's Encrypt certificates. It reverse-proxies HTTPS traffic to the Budibase app container on its internal HTTP port. The app container serves the React frontend and the REST API, persists application definitions, screen layouts, datasource configurations, and user accounts in CouchDB, and enqueues background automation jobs in Redis. The worker container polls Redis for pending jobs, executes automation steps, handles data source queries, and writes results back to CouchDB. MinIO stores file attachments, CSV imports, template exports, and generated PDFs.
- Edge: Caddy with automatic TLS and HTTP/2
- App: Budibase apps container (frontend + API)
- Worker: Budibase worker container (background jobs)
- State: CouchDB for documents, Redis for queues, MinIO for objects
- Control: Docker Compose with restart policies and health checks
- Ops: Backup workflow covering CouchDB dumps and MinIO buckets
All services except Caddy remain inside an isolated Docker bridge network. CouchDB and Redis are unreachable from the public internet because they do not publish host ports. The app and worker containers share an internal network segment with the database and object storage, while Caddy is the only service bound to the host's public interfaces.
Prerequisites
- Linux VM or server with Ubuntu 22.04/24.04 and sudo access
- Docker Engine 24.x or newer with the Compose plugin
- A DNS A record pointing your Budibase domain to the server IP
- Firewall rules allowing TCP 22 (SSH) and TCP 443 (HTTPS)
- Password manager entry for the initial admin credentials
- Named owner assigned for platform operations and upgrades
Step-by-step deployment
1) Create directories and permissions
Use a fixed directory layout so runbooks, backup scripts, and monitoring checks reference the same paths every time.
sudo mkdir -p /opt/budibase/{compose,backups,plugins}
sudo mkdir -p /var/lib/budibase-couchdb
sudo mkdir -p /var/lib/budibase-minio
sudo mkdir -p /var/lib/budibase-redis
sudo chown -R 1000:1000 /var/lib/budibase-couchdb
sudo chown -R 1000:1000 /var/lib/budibase-minio
sudo chown -R 999:999 /var/lib/budibase-redis
sudo chown -R $USER:$USER /opt/budibaseIf the copy button is unavailable in your browser/editor, manually copy the command block above.
2) Store secrets in an environment file
Keep credentials out of the compose file and out of version control. Restrict the file to root read access.
cat > /opt/budibase/.env.prod <<'EOF'
BUDIBASE_ENVIRONMENT=production
MAIN_PORT=10000
COUCH_DB_USER=budibase_couch
COUCH_DB_PASSWORD=$(openssl rand -base64 32)
REDIS_PASSWORD=$(openssl rand -base64 32)
MINIO_ACCESS_KEY=$(openssl rand -base64 24)
MINIO_SECRET_KEY=$(openssl rand -base64 32)
API_ENCRYPTION_KEY=$(openssl rand -base64 32)
JWT_SECRET=$(openssl rand -base64 48)
INTERNAL_API_KEY=$(openssl rand -base64 24)
[email protected]
BB_ADMIN_USER_PASSWORD=$(openssl rand -base64 24)
PLUGINS_DIR=/plugins
OFFLINE_MODE=
EOF
chmod 600 /opt/budibase/.env.prodIf the copy button is unavailable in your browser/editor, manually copy the command block above.
3) Configure the Caddy reverse proxy
Caddy handles TLS automatically and forwards traffic to the Budibase app container. Place the Caddyfile outside the compose directory so it can be updated independently.
cat > /opt/budibase/Caddyfile <<'EOF'
budibase.example.com {
reverse_proxy app-service:4002
header {
X-Frame-Options DENY
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
}
encode gzip zstd
}
EOFIf the copy button is unavailable in your browser/editor, manually copy the command block above.
4) Define the Docker Compose services
This compose file defines the core Budibase services. We omit the optional LiteLLM service to keep the stack focused and production-stable.
cat > /opt/budibase/compose/docker-compose.yml <<'EOF'
services:
app-service:
restart: unless-stopped
image: budibase/apps:latest
container_name: bbapps
env_file:
- /opt/budibase/.env.prod
environment:
SELF_HOSTED: 1
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
WORKER_URL: http://worker-service:4003
MINIO_URL: http://minio-service:9000
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
PORT: 4002
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
JWT_SECRET: ${JWT_SECRET}
LOG_LEVEL: info
ENABLE_ANALYTICS: "true"
REDIS_URL: redis-service:6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
REDIS_USERNAME: ${REDIS_USERNAME:-}
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
PLUGINS_DIR: ${PLUGINS_DIR}
OFFLINE_MODE: ${OFFLINE_MODE:-}
depends_on:
- worker-service
- redis-service
- couchdb-service
- minio-service
volumes:
- /opt/budibase/plugins:/plugins
networks:
- budibase
worker-service:
restart: unless-stopped
image: budibase/worker:latest
container_name: bbworker
env_file:
- /opt/budibase/.env.prod
environment:
SELF_HOSTED: 1
PORT: 4003
CLUSTER_PORT: ${MAIN_PORT}
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
JWT_SECRET: ${JWT_SECRET}
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
MINIO_URL: http://minio-service:9000
APPS_URL: http://app-service:4002
COUCH_DB_USERNAME: ${COUCH_DB_USER}
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
REDIS_URL: redis-service:6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
REDIS_USERNAME: ${REDIS_USERNAME:-}
OFFLINE_MODE: ${OFFLINE_MODE:-}
depends_on:
- redis-service
- minio-service
- couchdb-service
networks:
- budibase
couchdb-service:
restart: unless-stopped
image: budibase/database:2.1.0
container_name: bbcouch
env_file:
- /opt/budibase/.env.prod
environment:
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
- COUCHDB_USER=${COUCH_DB_USER}
- TARGETBUILD=docker-compose
- DATA_DIR=/data
volumes:
- /var/lib/budibase-couchdb:/data
networks:
- budibase
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:5984/_up"]
interval: 30s
timeout: 10s
retries: 5
redis-service:
restart: unless-stopped
image: redis:7-alpine
container_name: bbredis
env_file:
- /opt/budibase/.env.prod
command: redis-server --requirepass "${REDIS_PASSWORD}" --appendonly yes
volumes:
- /var/lib/budibase-redis:/data
networks:
- budibase
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 30s
timeout: 10s
retries: 5
minio-service:
restart: unless-stopped
image: minio/minio:latest
container_name: bbminio
env_file:
- /opt/budibase/.env.prod
environment:
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
MINIO_BROWSER: "off"
command: server /data --console-address ":9001"
volumes:
- /var/lib/budibase-minio:/data
networks:
- budibase
healthcheck:
test: ["CMD", "sh", "-c", "curl -f http://127.0.0.1:9000/minio/health/live || exit 1"]
interval: 30s
timeout: 20s
retries: 3
caddy:
restart: unless-stopped
image: caddy:2-alpine
container_name: bbcaddy
ports:
- "443:443"
- "80:80"
volumes:
- /opt/budibase/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- budibase
networks:
budibase:
driver: bridge
volumes:
caddy_data:
caddy_config:
EOFIf the copy button is unavailable in your browser/editor, manually copy the command block above.
5) Start the stack and verify health
Pull images, create the network, and bring up services in dependency order. Wait for CouchDB and Redis health checks before trusting the app.
cd /opt/budibase/compose
docker compose pull
docker compose up -d
sleep 15
docker compose ps
docker compose logs --tail=50 app-serviceIf the copy button is unavailable in your browser/editor, manually copy the command block above.
6) Create the MinIO bucket for attachments
Budibase expects a bucket named budibase to exist in MinIO. Create it after the stack is healthy.
docker compose exec minio-service mc alias set local http://127.0.0.1:9000 "$MINIO_ACCESS_KEY" "$MINIO_SECRET_KEY"
docker compose exec minio-service mc mb local/budibase --ignore-existing
docker compose exec minio-service mc anonymous set download local/budibaseIf the copy button is unavailable in your browser/editor, manually copy the command block above.
7) Configure automated backups
Back up CouchDB with its native dump tool and mirror the MinIO data directory. Store archives on separate storage or a remote target.
cat > /opt/budibase/backups/backup-budibase.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
TS=$(date +%F-%H%M)
mkdir -p /opt/budibase/backups/archive
tar -czf /opt/budibase/backups/archive/budibase-couchdb-$TS.tgz -C /var/lib/budibase-couchdb .
tar -czf /opt/budibase/backups/archive/budibase-minio-$TS.tgz -C /var/lib/budibase-minio .
find /opt/budibase/backups/archive -type f -mtime +14 -delete
echo "Backup completed: budibase-couchdb-$TS.tgz"
EOF
chmod +x /opt/budibase/backups/backup-budibase.shIf the copy button is unavailable in your browser/editor, manually copy the command block above.
Configuration and secrets handling
Use least privilege from day one. Keep administrative access limited to named team members and remove shared credentials. Rotate the initial admin password after first login and track rotation dates in your security calendar. If your organization requires SSO, plan it as an explicit follow-up change with rollback steps.
For secrets, prefer an external secret manager when available. If not, use encrypted disk plus strict file permissions and treat environment files as sensitive assets in backups. Never place API tokens directly into application exports or version control. If automation pipelines render provisioning files, redact secrets from CI logs and artifact retention.
To reduce drift, keep Caddyfile and Compose definitions under version control. Use pull requests for changes, include screenshots in reviews, and require a named approver from operations. This creates a paper trail for incident retrospectives and compliance checks.
Also define capacity guardrails early: target query latency for large datasources, max concurrent automation runs, and plugin approval process. These controls prevent slow degradation that only appears under load. Add lightweight dashboards that observe the low-code platform itself so failures are visible before users complain.
Verification
Verification should be more than "the page loads." Confirm service state, health endpoint output, certificate validity, and backup artifact creation. Keep expected outputs in your runbook and capture evidence after each change.
- All containers show healthy status in
docker compose ps - CouchDB responds with
200 OKon/_up - Redis accepts authenticated
PING - Budibase web UI loads at the HTTPS domain without certificate warnings
- MinIO bucket
budibaseexists and is writable - Backup script produces non-empty archives with current timestamps
If any check fails, stop rollout and execute rollback before proceeding to broader changes.
Common issues and fixes
App container exits on startup
Review environment variable syntax and verify CouchDB is healthy before the app starts. Check docker compose logs app-service for connection refused errors.
Login works but apps do not save
Usually indicates write-permission mismatch in CouchDB or MinIO storage. Reapply ownership and restart the affected service.
File uploads fail silently
Confirm the MinIO bucket exists and the app service environment variables point to the correct MinIO URL and credentials.
Worker job queue grows without processing
Check Redis memory and authentication. Verify the worker container can reach Redis and CouchDB on the internal Docker network.
Backups exist but restore fails
Most restore failures come from undocumented file paths or permission mismatches. Include a monthly restore drill and verify ownership after extraction.
Service not available after reboot
Validate Docker restart policies. Ensure the data mount paths exist before Compose tries to attach volumes.
FAQ
Is Budibase suitable for production internal tools?
Yes for many teams, provided you add health checks, backups, and operational monitoring. The self-hosted edition is designed for production use with proper infrastructure.
Why CouchDB instead of PostgreSQL?
Budibase uses CouchDB for document-oriented app metadata and revision history. This is by design and should not be swapped without understanding the migration path.
Should MinIO be exposed publicly?
No. Keep MinIO on the internal Docker network and let the app service communicate directly. Only Caddy should listen on public ports.
What is a safe upgrade process?
Pin image tags, back up before upgrading, test in staging, and verify health endpoints after each change.
Can we start with .env files and migrate to a secret manager later?
Yes. Begin with strict permissions, then migrate to a dedicated secret manager as operational maturity increases.
How many admins should the platform have?
Keep it minimal: typically two named maintainers and one break-glass account controlled by policy.
What should a handoff runbook include?
Startup commands, verification checklist, backup and restore procedures, escalation contacts, and rollback steps.
Can I use an external S3-compatible store instead of MinIO?
Yes. Update the MinIO URL and credentials environment variables to point at your external endpoint, ensuring the bucket and permissions are pre-configured.
Internal links
- Production Guide: Deploy n8n with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
- Production Guide: Deploy Vaultwarden with Docker Compose + Caddy on Ubuntu
- Production Guide: Deploy Ghost with Docker Compose + Caddy + MySQL on Ubuntu
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.