Skip to content

Commit c804741

Browse files
lane711claude
andcommitted
fix(self-host): add USER node + HEALTHCHECK to Dockerfile; add E2E spec
- Dockerfile: run as non-root node user (security hardening) - Dockerfile: add HEALTHCHECK via fetch to /health - README: Docker self-hosting quick-start section - tests/e2e/94-self-host-smoke.spec.ts: smoke tests for self-hosted instance Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a478752 commit c804741

3 files changed

Lines changed: 120 additions & 0 deletions

File tree

Dockerfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,12 @@ ENV SONICJS_STORAGE_PATH=/app/data/media
6767
ENV SONICJS_KV_PATH=/app/data/kv.json
6868
ENV PORT=3000
6969

70+
# Run as non-root for security.
71+
RUN chown -R node:node /app
72+
USER node
73+
74+
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
75+
CMD node -e "fetch('http://localhost:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
76+
7077
# Run via Node with native ESM + TypeScript stripping (Node 22+).
7178
CMD ["node", "--experimental-strip-types", "my-sonicjs-app/src/self-host.ts"]

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,25 @@ npx create-sonicjs@latest my-app
2929

3030
> **⚠️ Note:** This repository is for **developing the SonicJS core package**. To build an application with SonicJS, use the command above to create a new project.
3131
32+
## 🐳 Self-Hosting with Docker
33+
34+
No Cloudflare account? Run SonicJS on any server with Docker and SQLite:
35+
36+
```bash
37+
docker build -t sonicjs .
38+
docker run -d --name sonicjs -p 3000:3000 \
39+
-v $(pwd)/data:/app/data \
40+
-e JWT_SECRET=$(openssl rand -base64 32) \
41+
-e BETTER_AUTH_SECRET=$(openssl rand -base64 32) \
42+
sonicjs
43+
44+
# Create the first admin user
45+
docker exec sonicjs npm run reset
46+
# → admin@sonicjs.com / sonicjs! (change after first login)
47+
```
48+
49+
See the [Self-Hosting guide](https://sonicjs.com/self-hosting) for Docker Compose, Node.js, backup strategy, and production hardening.
50+
3251
## 🚀 Features
3352

3453
### Core Platform
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
/**
4+
* Self-Host Smoke Tests
5+
*
6+
* Validates the Docker / Node.js self-hosted server (non-Cloudflare deployment).
7+
* These tests run against a self-hosted instance started with `npm run self-host`.
8+
*
9+
* Set BASE_URL=http://localhost:3000 when running against a self-hosted instance.
10+
*
11+
* Covers:
12+
* - Health endpoint
13+
* - Sign-up and sign-in via better-auth credential flow
14+
* - Admin panel access after seeding
15+
* - Content and media routes reachable
16+
*/
17+
18+
const SELF_HOST_EMAIL = process.env.SONICJS_ADMIN_EMAIL ?? 'admin@sonicjs.com'
19+
const SELF_HOST_PASSWORD = process.env.SONICJS_ADMIN_PASSWORD ?? 'sonicjs!'
20+
21+
test.describe('Self-Host Smoke Tests', () => {
22+
test('health endpoint returns ok', async ({ request }) => {
23+
const res = await request.get('/health')
24+
expect(res.ok()).toBeTruthy()
25+
const body = await res.json()
26+
// Self-host health check returns { status: 'ok' } (minimal)
27+
expect(body).toHaveProperty('status')
28+
expect(['ok', 'running']).toContain(body.status)
29+
})
30+
31+
test('unauthenticated /admin redirects to login', async ({ page }) => {
32+
await page.goto('/admin')
33+
await page.waitForURL(/\/(admin\/login|auth\/login)/)
34+
expect(page.url()).toMatch(/login/)
35+
})
36+
37+
test('sign-in with seeded admin credentials', async ({ page }) => {
38+
await page.goto('/admin')
39+
await page.waitForURL(/login/)
40+
41+
// Fill credentials
42+
const emailField = page.locator('input[type="email"], input[name="email"]').first()
43+
const passwordField = page.locator('input[type="password"], input[name="password"]').first()
44+
await emailField.fill(SELF_HOST_EMAIL)
45+
await passwordField.fill(SELF_HOST_PASSWORD)
46+
47+
await page.locator('button[type="submit"], input[type="submit"]').first().click()
48+
49+
// Should land on admin dashboard
50+
await page.waitForURL(/\/admin/, { timeout: 10_000 })
51+
expect(page.url()).toContain('/admin')
52+
53+
// Should NOT still be on login
54+
expect(page.url()).not.toMatch(/login/)
55+
})
56+
57+
test('admin content route accessible after sign-in', async ({ page, request }) => {
58+
// Sign in via API
59+
const signIn = await request.post('/auth/sign-in/email', {
60+
data: { email: SELF_HOST_EMAIL, password: SELF_HOST_PASSWORD },
61+
})
62+
expect(signIn.ok()).toBeTruthy()
63+
64+
const cookies = signIn.headers()['set-cookie']
65+
expect(cookies).toBeTruthy()
66+
67+
// Admin content page should return 200
68+
const content = await request.get('/admin/content')
69+
expect(content.ok()).toBeTruthy()
70+
})
71+
72+
test('admin media route accessible after sign-in', async ({ request }) => {
73+
const signIn = await request.post('/auth/sign-in/email', {
74+
data: { email: SELF_HOST_EMAIL, password: SELF_HOST_PASSWORD },
75+
})
76+
expect(signIn.ok()).toBeTruthy()
77+
78+
const media = await request.get('/admin/media')
79+
expect(media.ok()).toBeTruthy()
80+
})
81+
82+
test('sign-up creates a new user (self-host allows registration)', async ({ request }) => {
83+
const unique = `e2e-${Date.now()}@test.sonicjs.local`
84+
const res = await request.post('/auth/sign-up/email', {
85+
data: {
86+
email: unique,
87+
password: 'TestPassword1!',
88+
name: 'E2E Test User',
89+
},
90+
})
91+
// 200 = success, 409 = already exists (idempotent re-run)
92+
expect([200, 409]).toContain(res.status())
93+
})
94+
})

0 commit comments

Comments
 (0)