Self-Hosting
BunBase is designed to run comfortably on a $6/mo VPS. This guide covers bare-metal deployment on Linux with a systemd service and Caddy for automatic HTTPS.
Requirements
Section titled “Requirements”- Linux VPS (Debian/Ubuntu recommended), 1 CPU, 512 MB RAM minimum
- A domain name pointed at the VPS IP
- Bun installed (bun.sh/install)
1. Install Bun
Section titled “1. Install Bun”curl -fsSL https://bun.sh/install | bashsource ~/.bashrcbun --version2. Create a service user (as root)
Section titled “2. Create a service user (as root)”adduser --disabled-password bunbasemkdir -p /opt/bunbase /var/bunbase/datachown -R bunbase:bunbase /opt/bunbase /var/bunbase/data3. Deploy the code (as bunbase)
Section titled “3. Deploy the code (as bunbase)”su - bunbasegit clone https://github.com/palmcode-ae/bunbase /opt/bunbasecd /opt/bunbasebun install --frozen-lockfile --production4. Configure environment
Section titled “4. Configure environment”cp .env.example /etc/bunbase.env# Edit with your values — SECRET_KEY and ADMIN_SECRET are required in production.nano /etc/bunbase.envGenerate strong secrets:
openssl rand -hex 32 # for SECRET_KEYopenssl rand -hex 24 # for ADMIN_SECRET5. systemd service
Section titled “5. systemd service”Create /etc/systemd/system/bunbase.service:
[Unit]Description=BunBase serverAfter=network.targetWants=network-online.target
[Service]Type=simpleUser=bunbaseGroup=bunbaseWorkingDirectory=/opt/bunbaseEnvironmentFile=/etc/bunbase.envExecStart=/home/bunbase/.bun/bin/bun run apps/server/src/index.tsRestart=on-failureRestartSec=5sStandardOutput=journalStandardError=journalSyslogIdentifier=bunbase
# Harden the processNoNewPrivileges=truePrivateTmp=trueProtectSystem=strictReadWritePaths=/var/bunbase/data
[Install]WantedBy=multi-user.targetEnable and start:
systemctl daemon-reloadsystemctl enable --now bunbasesystemctl status bunbasejournalctl -u bunbase -f # follow logs6. HTTPS with Caddy (as root)
Section titled “6. HTTPS with Caddy (as root)”Install Caddy (caddyserver.com) then edit /etc/caddy/Caddyfile.
Single server (recommended)
Section titled “Single server (recommended)”BunBase workers share port 8080 via reusePort — the OS kernel load-balances connections across them. Caddy only needs to proxy to a single address:
your-domain.com { reverse_proxy localhost:8080 { # Pass real client IP for rate limiting and audit logs header_up X-Real-IP {remote_host} header_up X-Forwarded-Proto {scheme}
# Health check — returns 503 when DB is unreachable health_uri /api/v1/admin/health health_interval 10s health_timeout 5s }
# Match BunBase's 1 GB maxRequestBodySize for file uploads request_body { max_size 1GB }}Multi-server (horizontal scaling)
Section titled “Multi-server (horizontal scaling)”When running multiple BunBase instances across machines, configure REDIS_URL on every node (required for distributed rate limiting, session cache, realtime fanout, and presence). Then point Caddy at all nodes:
your-domain.com { reverse_proxy { to node1:8080 node2:8080 node3:8080
lb_policy least_conn health_uri /api/v1/admin/health health_interval 10s health_timeout 5s
header_up X-Real-IP {remote_host} header_up X-Forwarded-Proto {scheme} }
# WebSocket connections use ip_hash so each client stays # on the same node for the duration of the connection. @realtime path /realtime reverse_proxy @realtime { to node1:8080 node2:8080 node3:8080 lb_policy ip_hash }
request_body { max_size 1GB }}Apply the config
Section titled “Apply the config”systemctl restart caddyCaddy automatically provisions and renews a Let’s Encrypt certificate.
7. Set APP_URL
Section titled “7. Set APP_URL”Add your public domain to the environment file so OAuth callbacks, magic links, and storage URLs are correct:
# In /etc/bunbase.envAPP_URL=https://your-domain.comPUBLIC_URL=https://your-domain.com8. Verify
Section titled “8. Verify”curl https://your-domain.com/api/v1/admin/healthExpected response:
{ "status": "ok", "db": "ok", "version": "0.1.0", "uptime_ms": 1234, "memory": { "rss_mb": 42, "heap_used_mb": 28, "heap_total_mb": 64 }, "server": { "pending_requests": 0, "pending_websockets": 0, ... }, "cluster": { "active_workers": 4, ... }}statusis"ok"when everything is healthy,"degraded"when the DB worker is unreachable (returns503).dbis"ok"or"error"— use this for load balancer health checks to automatically remove an instance when its DB connection is broken.
Backups
Section titled “Backups”BunBase stores everything in DATA_DIR. Back it up with:
# SQLite — use the backup API to get a consistent snapshotsqlite3 /var/bunbase/data/bunbase.db ".backup /var/bunbase/data/bunbase.db.bak"
# Then sync to remotersync -az /var/bunbase/data/ backup-host:/backups/bunbase/Schedule via cron:
0 3 * * * /usr/local/bin/bunbase-backup.shDocker alternative
Section titled “Docker alternative”See docker/docker-compose.yml for a ready-to-use Docker Compose setup.
cp .env.example .env# Edit .envdocker compose -f docker/docker-compose.yml up -dOne-click deploys
Section titled “One-click deploys”BunBase ships deploy configs for three managed platforms. All three use the same Docker image (docker/Dockerfile), mount a persistent volume at /data, and expose port 8080.
Note on internal port. The Dockerfile sets
PORT=8080andEXPOSE 8080. Keep this value unless you also change the Dockerfile.
Fly.io
Section titled “Fly.io”Fly.io is the recommended managed option — cheap, fast cold starts, and proper persistent volumes on free and paid plans.
Prerequisites: install flyctl and run fly auth login.
# 1. Clone the repo and cd ingit clone https://github.com/palmcode-ae/bunbase && cd bunbase
# 2. Register the app (fly.toml is already present)fly launch --no-deploy
# 3. Create the persistent volume (1 GB in US East)fly volumes create data --size 1 --region iad
# 4. Set the required secretfly secrets set SECRET_KEY=$(openssl rand -hex 32)
# 5. Set your public URL (replace with your actual app name)fly secrets set PUBLIC_URL=https://bunbase.fly.dev
# 6. Deployfly deployAfter the first deploy, visit https://<your-app>.fly.dev/api/v1/admin/health to confirm it is running.
To use a custom domain: fly certs add your-domain.com.
Cost: the default config runs a single shared-1x-512mb machine that auto-stops when idle, so you only pay for actual compute time.
Railway
Section titled “Railway”Railway auto-detects railway.json and builds from docker/Dockerfile.
Prerequisites: install the Railway CLI or connect via the dashboard.
Via dashboard
Section titled “Via dashboard”- Go to railway.app → New Project → Deploy from GitHub repo.
- Select the BunBase repo. Railway detects
railway.jsonautomatically. - Add environment variables in the Variables tab:
SECRET_KEY— generate withopenssl rand -hex 32PUBLIC_URL— set to your Railway-assigned domain once the service is createdDATA_DIR—/data
- Add a Volume under the service settings, mounted at
/data. Persistent volumes require the Pro plan or the Railway Volumes addon. - Redeploy.
Via CLI
Section titled “Via CLI”railway loginrailway link # link to an existing project, or create onerailway up # deploys from the repo rootSet variables from the CLI:
railway variables set SECRET_KEY=$(openssl rand -hex 32)railway variables set DATA_DIR=/dataNote: Without a persistent volume, the SQLite database and uploaded files are lost on each redeploy. Enable volumes before going to production.
Render
Section titled “Render”Render auto-detects render.yaml when you connect your GitHub repo.
- Go to render.com → New → Blueprint → connect the BunBase repo.
- Render reads
render.yamland creates the web service and the 1 GB persistent disk automatically. - In the service’s Environment tab, add
SECRET_KEYas a secret env var:Terminal window openssl rand -hex 32 # paste the output as the value PUBLIC_URLis forwarded automatically fromRENDER_EXTERNAL_URL— no manual step needed.- Click Apply (or push a commit) to trigger the first deploy.
Verify once live:
curl https://<your-service>.onrender.com/api/v1/admin/healthNote: Persistent disks on Render require a paid plan (Starter or higher). The render.yaml sets plan: starter.
Nginx (alternative to Caddy)
Section titled “Nginx (alternative to Caddy)”server { listen 443 ssl; server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
location / { proxy_pass http://localhost:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }}The Upgrade and Connection headers are required for WebSocket (realtime) support.