Self-Hosting
Run SonicJS on your own infrastructure — Docker, Node.js, or any VPS — with SQLite persistence and no Cloudflare account required.
Overview
SonicJS is Cloudflare-native by default, but ships full self-host support for teams that need data sovereignty, private networks, or a simpler single-server deployment.
In self-host mode, Cloudflare platform services are replaced with portable equivalents:
| Cloudflare | Self-Host |
|---|---|
| D1 (SQLite at the edge) | better-sqlite3 (local SQLite file) |
| R2 (object storage) | Local filesystem (./data/media/) |
| KV (key-value cache) | In-memory LRU cache |
| Workers runtime | Node.js via @hono/node-server |
The application code is identical — SonicJS detects the runtime and switches adapters automatically at startup.
Quick Start
Three commands — build, run, seed:
# 1. Build the image
docker build -t sonicjs .
# 2. Run (replace secrets with real values)
docker run -d \
--name sonicjs \
-p 3000:3000 \
-v $(pwd)/data:/app/data \
-e JWT_SECRET=$(openssl rand -base64 32) \
-e BETTER_AUTH_SECRET=$(openssl rand -base64 32) \
sonicjs
# 3. Seed the first admin user
docker exec sonicjs npm run reset
Visit http://localhost:3000/admin and sign in with admin@sonicjs.com / sonicjs!.
Change the default password immediately after first login.
Docker
Build
docker build -t sonicjs .
The Dockerfile uses a two-stage build:
- Builder — installs all dependencies, compiles TypeScript with esbuild
- Runtime — Alpine Node.js image with only production artifacts (~120 MB final image)
The runtime stage runs as the node user (non-root) for security.
Run
docker run -d \
--name sonicjs \
--restart unless-stopped \
-p 3000:3000 \
-v sonicjs_data:/app/data \
-e JWT_SECRET="$(openssl rand -base64 32)" \
-e BETTER_AUTH_SECRET="$(openssl rand -base64 32)" \
sonicjs
| Flag | Purpose |
|---|---|
-v sonicjs_data:/app/data | Persists the SQLite database across restarts |
-e JWT_SECRET | Signs all session tokens — must be secret |
-e BETTER_AUTH_SECRET | Better Auth internal key — must be secret |
--restart unless-stopped | Auto-restart on crash or host reboot |
First-Boot Seed
After the container starts, run the seed script once to create the admin user and initialize RBAC:
docker exec sonicjs npm run reset
The script is idempotent — if an admin already exists, it exits silently.
To use custom credentials:
docker exec \
-e SONICJS_ADMIN_EMAIL=you@yourdomain.com \
-e SONICJS_ADMIN_PASSWORD=strongpassword \
sonicjs npm run reset
Health Check
curl http://localhost:3000/health
# {"status":"ok"}
Docker Compose
For a version-controlled, reproducible setup:
# docker-compose.yml
version: "3.9"
services:
sonicjs:
build: .
container_name: sonicjs
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- sonicjs_data:/app/data
environment:
JWT_SECRET: "${JWT_SECRET}"
BETTER_AUTH_SECRET: "${BETTER_AUTH_SECRET}"
SONICJS_ADMIN_EMAIL: "${SONICJS_ADMIN_EMAIL:-admin@sonicjs.com}"
SONICJS_ADMIN_PASSWORD: "${SONICJS_ADMIN_PASSWORD:-sonicjs!}"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
volumes:
sonicjs_data:
Create a .env file (never commit to git):
JWT_SECRET=$(openssl rand -base64 32)
BETTER_AUTH_SECRET=$(openssl rand -base64 32)
SONICJS_ADMIN_EMAIL=admin@yourdomain.com
SONICJS_ADMIN_PASSWORD=a-strong-password
Start and seed:
docker compose up -d
docker compose exec sonicjs npm run reset
Node.js
Run directly without Docker — useful for PaaS deployments (Railway, Render, Fly.io) or bare-metal:
Start Server
# From repo root
npm install
npm run build:core
# Start the self-host server
cd my-sonicjs-app && npm run self-host
Seed the admin:
cd my-sonicjs-app && npm run reset
Set PORT to change the listen port (default 3000).
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
JWT_SECRET | Yes | — | Signs session JWTs. Use openssl rand -base64 32. |
BETTER_AUTH_SECRET | Yes | — | Better Auth signing key. Same generation. |
PORT | No | 3000 | HTTP listen port. |
SONICJS_DB_PATH | No | ./data/sonicjs.db | Path to SQLite database file. |
SONICJS_ADMIN_EMAIL | No | admin@sonicjs.com | Admin email for npm run reset. |
SONICJS_ADMIN_PASSWORD | No | sonicjs! | Admin password for npm run reset. |
Security rule: JWT_SECRET and BETTER_AUTH_SECRET must be unique, random 256-bit values in production. Never share them between environments.
Database & Persistence
SQLite File
The database lives at SONICJS_DB_PATH (./data/sonicjs.db by default). Migrations run automatically on startup — no manual migration step.
In Docker, always mount /app/data:
# Named volume (recommended)
-v sonicjs_data:/app/data
# Bind mount (easier to inspect/backup)
-v $(pwd)/data:/app/data
Without a mount, the database is lost when the container stops.
Backups
SQLite supports live consistent backups via the .backup command or simple file copy (WAL mode ensures consistency):
Backup
# Copy from host-side bind mount
cp data/sonicjs.db "backups/sonicjs-$(date +%Y%m%d-%H%M%S).db"
For continuous replication, Litestream streams WAL frames to S3, R2, or GCS in real time — a good fit for production self-hosted installs.
Production Hardening
Reverse Proxy with HTTPS
Never expose port 3000 directly to the internet. Use a reverse proxy that handles TLS.
Reverse Proxy
# Caddyfile — automatic HTTPS via Let's Encrypt
cms.yourdomain.com {
reverse_proxy sonicjs:3000
}
Production Checklist
-
JWT_SECRETis a random 256-bit value, not the default -
BETTER_AUTH_SECRETis a random 256-bit value, not the default - Admin password changed from
sonicjs! - HTTPS enforced via reverse proxy (Caddy / nginx)
- SQLite volume on a persistent disk
- Automated backup strategy in place (file copy + offsite, or Litestream)
- Container restarts automatically (
--restart unless-stopped) -
.envfile is in.gitignore, not committed
Updating
Pull the latest code and rebuild:
Update
git pull origin main
docker build -t sonicjs .
docker stop sonicjs && docker rm sonicjs
docker run -d \
--name sonicjs \
-p 3000:3000 \
-v sonicjs_data:/app/data \
-e JWT_SECRET="$JWT_SECRET" \
-e BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
sonicjs
New database migrations run automatically on startup. No manual migration step.
Troubleshooting
[seed] Database not found
The server hasn't started yet or the volume isn't mounted. Check logs:
docker logs sonicjs
Wait for Server listening on :3000, then re-run docker exec sonicjs npm run reset.
/admin returns 403 after seeding
RBAC grants weren't applied. This usually means the seed ran before the DB was fully migrated. Fix:
docker stop sonicjs && docker rm sonicjs
rm data/sonicjs.db # or docker volume rm sonicjs_data
# restart + re-seed
Port already in use
Change the host-side port:
-p 8080:3000 # SonicJS on port 8080
SQLITE_BUSY under heavy write load
SQLite is single-writer. It handles most self-hosted workloads fine. Under sustained heavy concurrency, consider:
- Switching to the Cloudflare deployment for distributed writes
- Adding Litestream for streaming replication to a replica
Media uploads not persisting
Ensure the volume covers the media/ subdirectory: the volume must be mounted at /app/data, not /app/data/media.
Next Steps
- Blog: Self-Hosting SonicJS with Docker — deeper walkthrough with Docker Compose and production tips
- Production Deployment on Cloudflare — global edge distribution with D1, R2, KV
- Security — CORS, CSRF, rate limiting
- Configuration — all env vars and plugin options