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:

CloudflareSelf-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 runtimeNode.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:

  1. Builder — installs all dependencies, compiles TypeScript with esbuild
  2. 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
FlagPurpose
-v sonicjs_data:/app/dataPersists the SQLite database across restarts
-e JWT_SECRETSigns all session tokens — must be secret
-e BETTER_AUTH_SECRETBetter Auth internal key — must be secret
--restart unless-stoppedAuto-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

VariableRequiredDefaultDescription
JWT_SECRETYesSigns session JWTs. Use openssl rand -base64 32.
BETTER_AUTH_SECRETYesBetter Auth signing key. Same generation.
PORTNo3000HTTP listen port.
SONICJS_DB_PATHNo./data/sonicjs.dbPath to SQLite database file.
SONICJS_ADMIN_EMAILNoadmin@sonicjs.comAdmin email for npm run reset.
SONICJS_ADMIN_PASSWORDNosonicjs!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_SECRET is a random 256-bit value, not the default
  • BETTER_AUTH_SECRET is 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)
  • .env file 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

Was this page helpful?