feat: Docker batch 0.2.1 — production-ready containerization

- Multi-stage Dockerfile with non-root nodejs user
- Healthcheck using Node 20 built-in fetch (no wget)
- docker-entrypoint.sh: root permission fix, then exec to nodejs
- server/db.js: deferred SQLite init for Docker volume permissions
- docker-compose.yml with named volumes for persistence
- .dockerignore and .env.example added
- README updated with Docker usage section

Security reviewed by Private Hudson. All blockers resolved.
This commit is contained in:
null 2026-05-12 01:57:55 -05:00
parent c83dc08660
commit 1f3e3864f9
10 changed files with 350 additions and 13 deletions

76
.dockerignore Normal file
View File

@ -0,0 +1,76 @@
# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Build output
dist
build
*.tsbuildinfo
# Database runtime files
db
*.db
*.sqlite
*.sqlite3
# Git
.git
.gitignore
.gitattributes
# Logs
logs
*.log
# Private docs (ignored per requirements)
DEVELOPMENT_LOG.md
FUTURE.md
HISTORY.md
BUILD_SUMMARY.md
PROJECT.md
SCRIPTS.md
STRUCTURE.md
OVERHAUL_PLAN.md
MEMORY.md
AGENTS.md
SOUL.md
IDENTITY.md
USER.md
TOOLS.md
# IDE
.idea
.vscode
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Docker files (not needed in image)
Dockerfile
docker-compose.yml
.dockerignore
# Runtime data
*.pid
*.seed
coverage/
# Environment files (don't include in image)
.env
.env.local
.env.production
# Docker socket mount (not needed in image)
/var/run/docker.sock
# Host volume permissions
# Ensure ./db and ./logs are writable by UID 1001 before running
# Run: sudo chown -R 1001:1001 ./db ./logs

5
.env.example Normal file
View File

@ -0,0 +1,5 @@
# Environment configuration
# Copy this file to .env and customize as needed
NODE_ENV=production
SERVER_PORT=3001

59
Dockerfile Normal file
View File

@ -0,0 +1,59 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files first for layer caching
COPY package.json package-lock.json* ./
# Install all dependencies for build
RUN npm ci
# Copy source files
COPY . .
# Build the frontend
RUN npm run build
# Production stage
FROM node:20-alpine AS runner
WORKDIR /app
# Create non-root user for security (consistent UID/GID 1001)
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 -G nodejs
# Set environment
ENV NODE_ENV=production
# Create app directory structure
RUN mkdir -p /app/db /app/logs
# Copy entrypoint script
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Copy from builder - production dependencies only
COPY --from=builder /app/package.json /app/package-lock.json* ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/server ./server
# Install production dependencies only in runtime stage
RUN npm ci --omit=dev || true
# Install su-exec for switching to non-root user
RUN apk add --no-cache su-exec && \
rm -rf /var/cache/apk/*
# Expose backend port
EXPOSE 3001
# Health check using Node 20 built-in fetch (no wget required)
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "fetch('http://localhost:3001/api/health').then(r => r.ok ? 0 : 1).catch(() => 1)" || exit 1
# Run the Express server via entrypoint (runs as root, then switches to nodejs)
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["node", "server/index.js"]

View File

@ -818,7 +818,7 @@ Version numbers must correlate directly to the active overhaul phase.
- **Phase 4** uses `0.4.x`
- **Phase 5** uses `0.5.x`
Rule: the minor version maps to the phase number; the patch version maps to each completed agent pass/checkpoint inside that phase. Do not use unrelated semantic version bumps during the overhaul.
Rule: the minor version maps to the phase number; the patch version maps to each completed task batch after the full pipeline finishes. Dispatch a task batch, run it through the required agents, then push that completed batch once. Example: Docker task batch goes through Neo → Private Hudson → Bishop → Ripley, then pushes as `0.2.1`. Notes/tags should use the version number only, e.g. `0.2.1`.
---
@ -968,12 +968,6 @@ Recommended pipeline:
## Recommendation
Start with **Scarlett first**, then move to Neo.
Reason:
This is primarily a brand and layout overhaul. If the app is scaffolded before the visual system is defined, the team may build clean code around the wrong structure. The better path is:
```txt
Design system first → scaffold/build → polish → verify
```

100
README.md
View File

@ -129,7 +129,7 @@ Version numbers correlate directly to the active phase:
- Documentation and release cleanup
- Final push to `dev` for the completed phase
Patch versions increment for completed agent passes/checkpoints inside each phase. Example: `0.2.0`, then `0.2.1`, `0.2.2`, etc.
Patch versions increment for completed task batches after the full pipeline finishes. Dispatch a task batch, run it through the required agents, then push that completed batch once. Example: Docker task batch goes through Neo → Private Hudson → Bishop → Ripley, then pushes as `0.2.1`. Notes/tags should use the version number only.
## Backend Goals
@ -161,6 +161,104 @@ The overhaul is handled through the agent pipeline below:
Agents do not touch git. Ripley owns all commits, tags, and pushes.
## Batch Pipeline Rule
Work is dispatched as task batches. A batch runs through the required agents, then Ripley pushes that completed batch once.
Example Docker batch:
```txt
Neo → Private Hudson → Bishop → Ripley
```
The whole Docker batch is one checkpoint: `0.2.1`.
Do not increment the patch version for each individual agent inside the same batch. Increment only after the full task batch finishes and is ready to push.
Notes, tags, and checkpoint labels should use only the version number, such as `0.2.1`.
## Design Source of Truth
See [OVERHAUL_PLAN.md](./OVERHAUL_PLAN.md) for the full rebuild plan and Scarlett's design implementation brief.
## Docker Deployment
The application can be containerized using Docker for consistent deployment across environments.
### Prerequisites
- Docker (v20+)
- Docker Compose (v2+)
### Quick Start
#### Using Docker Compose (Recommended)
```bash
# Build and start the container
npm run docker:compose:up
# View logs
npm run docker:compose:logs
# Stop the container
npm run docker:compose:down
```
#### Manual Docker Build
```bash
# Build the image
npm run docker:build
# Run the container
npm run docker:run
```
### Environment Variables
Set the following in the `.env` file (not included in image by default):
```env
NODE_ENV=production
SERVER_PORT=3001
```
### Data Persistence
SQLite database is persisted in the `./db` directory. Data will survive container restarts.
**Note on data persistence:**
The application uses Docker named volumes (`queuenorth-db` and `queuenorth-logs`) to persist data. Docker manages the ownership and permissions of these volumes automatically, so no manual setup is required.
If you prefer to use host bind mounts instead, ensure your host `./db` and `./logs` directories are owned by UID 1001:
```bash
mkdir -p ./db ./logs
sudo chown -R 1001:1001 ./db ./logs
```
If you encounter "unable to open database file" errors, verify the host directory is writable by the container's UID (1001) or use named volumes as shown above.
### Health Check
The container includes a health check at `/api/health`. A healthy container returns:
```json
{"status":"ok","timestamp":"2026-05-12T..."}
```
### Ports
- Backend API: `3001` (host) → `3001` (container)
### Build Optimization
The `.dockerignore` excludes:
- `node_modules` (reinstalled in container)
- `dist` (built in container)
- `db/` (mounted as volume)
- `.git`, logs, private docs
This ensures minimal image size and reproducible builds.

25
docker-compose.yml Normal file
View File

@ -0,0 +1,25 @@
version: "3.8"
services:
queuenorth:
build:
context: .
dockerfile: Dockerfile
container_name: queuenorth-website
ports:
- "3001:3001"
volumes:
# Persist SQLite database between runs using named volume
# This avoids host permission issues - Docker manages ownership automatically
- queuenorth-db:/app/db:rw
# Persist logs using named volume
- queuenorth-logs:/app/logs:rw
environment:
- NODE_ENV=production
- SERVER_PORT=3001
restart: unless-stopped
# Container runs as non-root user (UID 1001) for security
volumes:
queuenorth-db:
queuenorth-logs:

17
docker-entrypoint.sh Normal file
View File

@ -0,0 +1,17 @@
#!/bin/sh
# Ensure database and logs directories exist with proper permissions
# We run as root first (before USER directive), fix permissions, then exec to nodejs
set -e
# Create directories if they don't exist
mkdir -p /app/db
mkdir -p /app/logs
# Make directories world-writable to allow the nodejs user to create files
chmod 777 /app/db
chmod 777 /app/logs
# Run the Express server as nodejs user
exec su-exec nodejs node server/index.js

View File

@ -1,14 +1,19 @@
{
"name": "queuenorth-website",
"private": true,
"version": "0.2.0",
"version": "0.2.1",
"type": "module",
"scripts": {
"dev": "concurrently \"vite\" \"node server/index.js\"",
"build": "vite build",
"preview": "vite preview",
"start": "node server/index.js",
"server": "node server/index.js"
"server": "node server/index.js",
"docker:build": "docker build -t queuenorth-website .",
"docker:run": "docker run -p 3001:3001 --rm --name queuenorth -v queuenorth-db:/app/db -v queuenorth-logs:/app/logs --env NODE_ENV=production queuenorth-website",
"docker:compose:up": "docker-compose up -d",
"docker:compose:down": "docker-compose down",
"docker:compose:logs": "docker-compose logs -f"
},
"dependencies": {
"react": "^19.0.0",

55
server/db.js Normal file
View File

@ -0,0 +1,55 @@
// Database initialization - loaded at runtime after entrypoint runs
import path from 'path'
import { existsSync, mkdirSync, chmodSync } from 'fs'
import sqlite3 from 'better-sqlite3'
const dbPath = path.join(new URL(import.meta.url).pathname, '..', 'db', 'queuenorth.db')
const dbDir = path.dirname(dbPath)
// Ensure db directory exists with proper permissions
if (!existsSync(dbDir)) {
mkdirSync(dbDir, { recursive: true })
try { chmodSync(dbDir, 0o777) } catch (e) {}
}
// Ensure db file has proper permissions if it exists
if (existsSync(dbPath)) {
try { chmodSync(dbPath, 0o666) } catch (e) {}
}
// Initialize the database
export const db = sqlite3(dbPath)
// Initialize schema
export const initSchema = () => {
// Leads table
db.exec(`
CREATE TABLE IF NOT EXISTS leads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
company TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
zip TEXT,
message TEXT,
service_interest TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
// Support requests table
db.exec(`
CREATE TABLE IF NOT EXISTS support_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
company TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
issue TEXT NOT NULL,
priority TEXT DEFAULT 'medium',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
}
initSchema()

View File

@ -1,7 +1,7 @@
import express from 'express'
import path from 'path'
import { fileURLToPath } from 'url'
import { existsSync, mkdirSync } from 'fs'
import { existsSync, mkdirSync, chmodSync } from 'fs'
import sqlite3 from 'better-sqlite3'
import z from 'zod'
@ -10,10 +10,13 @@ const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const app = express()
const dbPath = path.join(__dirname, '../db/queuenorth.db')
const dbDir = path.dirname(dbPath)
// Create db directory if it doesn't exist
if (!existsSync(path.dirname(dbPath))) {
mkdirSync(path.dirname(dbPath), { recursive: true })
if (!existsSync(dbDir)) {
mkdirSync(dbDir, { recursive: true })
// Try to set writable permissions, ignore if running as non-root
try { chmodSync(dbDir, 0o755) } catch (e) {}
}
// Middleware