Skip to content

Commit 638de93

Browse files
lane711claude
andauthored
docs(www): self-hosting docs page and Docker deployment blog post (#992)
* feat(self-host): Tier 1 Docker / Node self-hosted deployment Adds a self-hosting adapter layer so SonicJS runs outside Cloudflare Workers with no code changes to the core CMS. - **sqlite-driver.ts** — D1Database-compatible SQLite driver via better-sqlite3; auto-migrates fresh databases; handles tsup's flat-dist layout by walking up directories to find migrations/ - **filesystem-driver.ts** — R2Bucket-compatible local storage adapter - **memory-kv-driver.ts** — KVNamespace adapter with optional JSON persistence across restarts - **sync-queue-driver.ts** — MessageQueue adapter (synchronous, in-process) - **node-server.ts** — wraps a SonicJS Hono app in a fresh Hono wrapper so DB/storage/KV bindings are injected BEFORE bootstrap middleware runs (ordering fix: Hono appends middleware in registration order) - **my-sonicjs-app/src/self-host.ts** — Node/Bun entry point; reads env vars (SONICJS_DB_PATH, SONICJS_STORAGE_PATH, etc.) and serves via @hono/node-server - **Dockerfile** — multi-stage build (builder compiles native modules; runtime Alpine copies node_modules + built dist) - **docker-compose.yml** — persistent volume for /app/data - Added missing `password_reset_token`, `password_reset_expires`, and `two_factor_enabled` columns to auth_user (were in schema.ts / Drizzle model but missing from 0001_core.sql migration) - Unit tests for all four adapters (sqlite-driver, filesystem, kv, queue) using the real better-sqlite3 harness per R10 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(self-host): add npm run reset seed script for Docker first boot Creates initial admin user (admin@sonicjs.com / sonicjs!) on first boot using @better-auth/utils/password for hash compatibility. Idempotent — skips silently if any user already exists. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(self-host): seed RBAC grants so admin can access /admin Admin route is gated by requireRbac('portal','access') — seeding auth_user alone isn't enough. Call bootstrapDocumentTypes + ensureSystemRbacSeed + addUserRoleByName via the D1-compatible SqliteDriver after creating the auth_user/auth_account rows. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(self-host): add better-sqlite3 to lock file; rebase onto main - Rebase onto origin/main (resolved migrations-bundle conflict by regenerating from our updated 0001_core.sql) - Move better-sqlite3 to root optionalDependencies so npm ci in CI can install the native module for sqlite-driver tests - Inject better-sqlite3 lock file entry (npm skips it locally because the package is already resolved via parent node_modules in the conductor worktree — CI needs an explicit entry) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(self-host): regenerate migrations bundle post-rebase * docs(www): add self-hosting docs page and Docker deployment blog post - New /self-hosting doc page: Docker, Docker Compose, Node.js, env vars, backup, production hardening, troubleshooting - New blog post: self-host-sonicjs-with-docker.mdx covering full Docker workflow, Compose, Node.js/Bun, security checklist, and Cloudflare comparison - Navigation: add Self-Hosting under Deployment group - /deployment page: link to self-hosting guide in overview and next-steps Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 35e4ca3 commit 638de93

5 files changed

Lines changed: 807 additions & 0 deletions

File tree

Lines changed: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
1+
---
2+
title: "Self-Hosting SonicJS with Docker: A Complete Guide"
3+
description: "Run SonicJS on your own server, VPS, or private cloud using Docker. No Cloudflare account required — just Docker, a volume, and two environment variables."
4+
keywords:
5+
- "self-host sonicjs"
6+
- "sonicjs docker"
7+
- "sonicjs vps deployment"
8+
- "sonicjs node.js self-host"
9+
- "headless cms self-hosted"
10+
- "docker cms deployment"
11+
category: "tutorials"
12+
tags:
13+
- "deployment"
14+
- "docker"
15+
- "self-hosting"
16+
- "node.js"
17+
- "tutorial"
18+
publishedAt: "2026-07-01"
19+
author:
20+
name: "SonicJS Team"
21+
twitter: "sonicjs"
22+
github: "lane711/sonicjs"
23+
featured: false
24+
featuredImage:
25+
url: "/images/blog/self-host-sonicjs-with-docker/hero.png"
26+
alt: "Isometric illustration of a server rack with a Docker whale logo serving SonicJS content to browsers"
27+
width: 1792
28+
height: 1024
29+
openGraph:
30+
type: "article"
31+
title: "Self-Hosting SonicJS with Docker: A Complete Guide"
32+
description: "Run SonicJS on your own infrastructure — Docker Compose, SQLite persistence, first-boot seed, and production hardening in one guide."
33+
---
34+
35+
# Self-Hosting SonicJS with Docker: A Complete Guide
36+
37+
<TLDRBox>
38+
**TL;DR** — SonicJS ships a production-ready Docker image. Three commands get you running:
39+
40+
```bash
41+
docker build -t sonicjs .
42+
docker run -d -p 3000:3000 -v $(pwd)/data:/app/data \
43+
-e JWT_SECRET=$(openssl rand -base64 32) \
44+
-e BETTER_AUTH_SECRET=$(openssl rand -base64 32) \
45+
sonicjs
46+
docker exec sonicjs npm run reset
47+
```
48+
49+
Default admin: `admin@sonicjs.com` / `sonicjs!`**change it after first login**.
50+
51+
**Key facts:**
52+
- Single SQLite file in a Docker volume — no external database required
53+
- `npm run reset` seeds admin + RBAC on first boot; safe to run again (idempotent)
54+
- Supports Node.js 20+, Bun, and Docker
55+
- Full self-host — zero dependency on Cloudflare
56+
</TLDRBox>
57+
58+
SonicJS was built for Cloudflare's global edge, but sometimes the right deployment is a VPS you control. Maybe you're on a private network, you need data sovereignty, or you simply prefer owning the full stack. SonicJS supports all of that through its **self-host mode** — the same headless CMS, running on any Node.js server or Docker container.
59+
60+
This guide walks through running SonicJS on your own infrastructure from first container to production-hardened deployment.
61+
62+
## How Self-Hosting Works
63+
64+
The Cloudflare deployment binds to platform APIs: D1 for the database, R2 for media, KV for caching. The self-host mode replaces each of those with portable equivalents:
65+
66+
| Cloudflare | Self-Host |
67+
|---|---|
68+
| D1 (SQLite at the edge) | `better-sqlite3` (local SQLite file) |
69+
| R2 (object storage) | Local filesystem (`./data/media/`) |
70+
| KV (key-value cache) | In-memory LRU cache |
71+
| Workers runtime | Node.js `@hono/node-server` |
72+
73+
The SQLite file lives in a Docker volume (`/app/data/sonicjs.db`), so it persists across container restarts. The application code is identical — SonicJS detects the runtime and switches the adapters automatically.
74+
75+
## Prerequisites
76+
77+
- **Docker 20+** (or Node.js 20+ if running without Docker)
78+
- The SonicJS source repo — `git clone https://github.com/lane711/sonicjs.git`
79+
- `openssl` (for generating secrets — `brew install openssl` on macOS, available by default on Linux)
80+
81+
## Option A: Docker (Recommended)
82+
83+
### Step 1: Build the Image
84+
85+
From the repo root:
86+
87+
```bash
88+
docker build -t sonicjs .
89+
```
90+
91+
The multi-stage Dockerfile builds the TypeScript and packages only the runtime artifacts into a minimal Alpine image. Build time is typically 60–90 seconds on first run (npm install + esbuild compile); subsequent builds are fast thanks to layer caching.
92+
93+
### Step 2: Run the Container
94+
95+
Create a `data/` directory for the SQLite volume, then start the container:
96+
97+
```bash
98+
mkdir -p data
99+
100+
docker run -d \
101+
--name sonicjs \
102+
-p 3000:3000 \
103+
-v $(pwd)/data:/app/data \
104+
-e JWT_SECRET=$(openssl rand -base64 32) \
105+
-e BETTER_AUTH_SECRET=$(openssl rand -base64 32) \
106+
sonicjs
107+
```
108+
109+
The container is healthy when logs show `SonicJS self-host listening on :3000`.
110+
111+
```bash
112+
docker logs sonicjs
113+
# Server listening on http://0.0.0.0:3000
114+
```
115+
116+
### Step 3: Seed the First Admin
117+
118+
The database is auto-migrated at startup, but the admin account doesn't exist yet. Run the seed script inside the container:
119+
120+
```bash
121+
docker exec sonicjs npm run reset
122+
```
123+
124+
Output:
125+
126+
```
127+
[seed] Admin user created:
128+
Email: admin@sonicjs.com
129+
Password: sonicjs!
130+
⚠ Change this password after first login!
131+
```
132+
133+
The `reset` command is idempotent — run it as many times as you like. If an admin already exists, it exits silently.
134+
135+
### Step 4: Verify
136+
137+
```bash
138+
# Health endpoint
139+
curl http://localhost:3000/health
140+
# → {"status":"ok"}
141+
142+
# Open the admin UI
143+
open http://localhost:3000/admin
144+
```
145+
146+
Sign in with `admin@sonicjs.com` / `sonicjs!` and change the password immediately.
147+
148+
---
149+
150+
## Option B: Docker Compose
151+
152+
For a reproducible, version-controlled setup, use Docker Compose:
153+
154+
```yaml
155+
# docker-compose.yml
156+
version: "3.9"
157+
158+
services:
159+
sonicjs:
160+
build: .
161+
container_name: sonicjs
162+
restart: unless-stopped
163+
ports:
164+
- "3000:3000"
165+
volumes:
166+
- sonicjs_data:/app/data
167+
environment:
168+
JWT_SECRET: "${JWT_SECRET}"
169+
BETTER_AUTH_SECRET: "${BETTER_AUTH_SECRET}"
170+
SONICJS_ADMIN_EMAIL: "${SONICJS_ADMIN_EMAIL:-admin@sonicjs.com}"
171+
SONICJS_ADMIN_PASSWORD: "${SONICJS_ADMIN_PASSWORD:-sonicjs!}"
172+
healthcheck:
173+
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
174+
interval: 30s
175+
timeout: 10s
176+
retries: 3
177+
start_period: 10s
178+
179+
volumes:
180+
sonicjs_data:
181+
```
182+
183+
Create a `.env` file (never commit this):
184+
185+
```bash
186+
JWT_SECRET=$(openssl rand -base64 32)
187+
BETTER_AUTH_SECRET=$(openssl rand -base64 32)
188+
SONICJS_ADMIN_EMAIL=admin@yourdomain.com
189+
SONICJS_ADMIN_PASSWORD=a-strong-password-here
190+
```
191+
192+
Start and seed:
193+
194+
```bash
195+
docker compose up -d
196+
docker compose exec sonicjs npm run reset
197+
```
198+
199+
---
200+
201+
## Option C: Node.js Directly
202+
203+
If you prefer running without Docker — bare metal, a VM, or a PaaS like Railway or Render:
204+
205+
```bash
206+
# Install dependencies
207+
npm install
208+
209+
# Build the core package
210+
npm run build:core
211+
212+
# Start the self-host server
213+
cd my-sonicjs-app && npm run self-host
214+
```
215+
216+
Then seed:
217+
218+
```bash
219+
cd my-sonicjs-app && npm run reset
220+
```
221+
222+
The `self-host` script starts `@hono/node-server` on port 3000. Set `PORT` to override.
223+
224+
For Bun:
225+
226+
```bash
227+
cd my-sonicjs-app && npm run self-host:bun
228+
```
229+
230+
---
231+
232+
## Environment Variables
233+
234+
| Variable | Required | Default | Description |
235+
|---|---|---|---|
236+
| `JWT_SECRET` | Yes | — | Signs session JWTs. Generate with `openssl rand -base64 32`. |
237+
| `BETTER_AUTH_SECRET` | Yes | — | Better Auth internal signing key. Same generation. |
238+
| `PORT` | No | `3000` | HTTP port to listen on. |
239+
| `SONICJS_DB_PATH` | No | `./data/sonicjs.db` | Path to the SQLite database file. |
240+
| `SONICJS_ADMIN_EMAIL` | No | `admin@sonicjs.com` | Admin email used by `npm run reset`. |
241+
| `SONICJS_ADMIN_PASSWORD` | No | `sonicjs!` | Admin password used by `npm run reset`. |
242+
243+
**Never use the default `JWT_SECRET` or `BETTER_AUTH_SECRET` in production.** These protect every session token in your system.
244+
245+
---
246+
247+
## Database Persistence
248+
249+
The SQLite database lives at `SONICJS_DB_PATH` (default `./data/sonicjs.db`). In Docker, always mount `/app/data` as a named volume or bind mount:
250+
251+
```bash
252+
# Named volume (recommended — managed by Docker)
253+
-v sonicjs_data:/app/data
254+
255+
# Bind mount (easier to inspect/backup)
256+
-v $(pwd)/data:/app/data
257+
```
258+
259+
Without a volume, the database is lost when the container stops.
260+
261+
### Backups
262+
263+
Backing up SQLite is as simple as copying the file:
264+
265+
```bash
266+
# From host (bind mount)
267+
cp data/sonicjs.db "backups/sonicjs-$(date +%Y%m%d-%H%M%S).db"
268+
269+
# From inside the container
270+
docker exec sonicjs sqlite3 /app/data/sonicjs.db ".backup '/app/data/backup.db'"
271+
docker cp sonicjs:/app/data/backup.db ./backups/
272+
```
273+
274+
SQLite's WAL mode means live backups are consistent — no need to stop the server.
275+
276+
---
277+
278+
## Production Hardening
279+
280+
### Reverse Proxy with HTTPS
281+
282+
Never expose port 3000 directly. Put SonicJS behind nginx or Caddy:
283+
284+
**Caddy** (automatic HTTPS via Let's Encrypt — the simplest option):
285+
286+
```
287+
# Caddyfile
288+
cms.yourdomain.com {
289+
reverse_proxy sonicjs:3000
290+
}
291+
```
292+
293+
**nginx:**
294+
295+
```nginx
296+
server {
297+
listen 443 ssl;
298+
server_name cms.yourdomain.com;
299+
300+
ssl_certificate /etc/ssl/certs/sonicjs.crt;
301+
ssl_certificate_key /etc/ssl/private/sonicjs.key;
302+
303+
location / {
304+
proxy_pass http://localhost:3000;
305+
proxy_set_header Host $host;
306+
proxy_set_header X-Real-IP $remote_addr;
307+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
308+
proxy_set_header X-Forwarded-Proto $scheme;
309+
}
310+
}
311+
```
312+
313+
### Security Checklist
314+
315+
- [ ] `JWT_SECRET` and `BETTER_AUTH_SECRET` are random 256-bit values — not defaults
316+
- [ ] Admin password changed from `sonicjs!`
317+
- [ ] HTTPS enforced — container not exposed directly on port 80/443
318+
- [ ] SQLite volume is on a persistent disk with regular backups
319+
- [ ] Container running as non-root (the Dockerfile already does this via `USER node`)
320+
- [ ] `SONICJS_ADMIN_PASSWORD` env var is in `.env`, not in `docker-compose.yml` committed to git
321+
322+
### Container Hardening
323+
324+
The production Dockerfile already:
325+
- Uses a multi-stage build (no build tools in the final image)
326+
- Runs as `node` user (non-root)
327+
- Produces a minimal Alpine image (~120 MB)
328+
329+
---
330+
331+
## Updating SonicJS
332+
333+
```bash
334+
git pull origin main
335+
docker build -t sonicjs .
336+
docker stop sonicjs && docker rm sonicjs
337+
338+
docker run -d \
339+
--name sonicjs \
340+
-p 3000:3000 \
341+
-v $(pwd)/data:/app/data \
342+
-e JWT_SECRET="$JWT_SECRET" \
343+
-e BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
344+
sonicjs
345+
```
346+
347+
New migrations run automatically at startup. No manual migration step needed.
348+
349+
With Docker Compose:
350+
351+
```bash
352+
git pull origin main
353+
docker compose build
354+
docker compose up -d
355+
```
356+
357+
---
358+
359+
## Troubleshooting
360+
361+
**`[seed] Database not found`** — The container hasn't started yet, or the volume isn't mounted. Wait for `Server listening on :3000` in the logs, then re-run `docker exec sonicjs npm run reset`.
362+
363+
**`/admin` returns 403 after seeding** — The RBAC grants weren't applied. This usually means `npm run reset` ran before the DB was migrated. Stop the container, delete `data/sonicjs.db`, restart, and re-run `npm run reset`.
364+
365+
**Port 3000 already in use** — Change the host port: `-p 8080:3000`.
366+
367+
**SQLite `SQLITE_BUSY` errors under load** — SQLite handles concurrent writes with WAL mode and is fine for most self-hosted workloads. If you're seeing lock contention under heavy write load, consider running multiple read replicas via Litestream or switching to the Cloudflare deployment for horizontal scale.
368+
369+
**Media uploads not persisting** — Ensure the volume includes the `media/` subdirectory: `-v $(pwd)/data:/app/data`. Uploads land in `/app/data/media/`.
370+
371+
---
372+
373+
## When to Use Self-Hosting vs. Cloudflare
374+
375+
| | Self-Hosting | Cloudflare |
376+
|---|---|---|
377+
| **Data sovereignty** | Full control | Data on Cloudflare's network |
378+
| **Global latency** | Depends on host location | 300+ edge locations, sub-50 ms globally |
379+
| **Scale** | Limited by single server | Effectively unlimited |
380+
| **Cost at scale** | Predictable VPS cost | Pay-per-request |
381+
| **Setup complexity** | Docker + reverse proxy | Wrangler config |
382+
| **Cold starts** | None (persistent process) | None (Workers are always warm) |
383+
| **Backups** | Manual (SQLite file copy) | D1 automated backups |
384+
385+
For most teams with data residency requirements, a compliance mandate, or a private intranet deployment, self-hosting is the right call. For public-facing, globally distributed content at scale, the [Cloudflare deployment](/deployment) is the better fit.
386+
387+
---
388+
389+
## Next Steps
390+
391+
- **[Self-Hosting reference docs](/self-hosting)** — full configuration reference, all env vars, adapter internals
392+
- **[Production Deployment on Cloudflare](/deployment)** — if you want global edge distribution instead
393+
- **[Configuration guide](/configuration)** — all env vars and plugin options
394+
- **[Security overview](/security)** — CORS, CSRF, rate limiting, and password hashing
395+
396+
Need help? Join the [SonicJS Discord](https://discord.gg/sonicjs) or open an issue on [GitHub](https://github.com/lane711/sonicjs).

0 commit comments

Comments
 (0)