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:
parent
c83dc08660
commit
1f3e3864f9
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Environment configuration
|
||||||
|
# Copy this file to .env and customize as needed
|
||||||
|
|
||||||
|
NODE_ENV=production
|
||||||
|
SERVER_PORT=3001
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -818,7 +818,7 @@ Version numbers must correlate directly to the active overhaul phase.
|
||||||
- **Phase 4** uses `0.4.x`
|
- **Phase 4** uses `0.4.x`
|
||||||
- **Phase 5** uses `0.5.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
|
## 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
|
```txt
|
||||||
Design system first → scaffold/build → polish → verify
|
Design system first → scaffold/build → polish → verify
|
||||||
```
|
```
|
||||||
|
|
|
||||||
100
README.md
100
README.md
|
|
@ -129,7 +129,7 @@ Version numbers correlate directly to the active phase:
|
||||||
- Documentation and release cleanup
|
- Documentation and release cleanup
|
||||||
- Final push to `dev` for the completed phase
|
- 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
|
## 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.
|
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
|
## Design Source of Truth
|
||||||
|
|
||||||
See [OVERHAUL_PLAN.md](./OVERHAUL_PLAN.md) for the full rebuild plan and Scarlett's design implementation brief.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
@ -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
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
{
|
{
|
||||||
"name": "queuenorth-website",
|
"name": "queuenorth-website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"vite\" \"node server/index.js\"",
|
"dev": "concurrently \"vite\" \"node server/index.js\"",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"start": "node server/index.js",
|
"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": {
|
"dependencies": {
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import { existsSync, mkdirSync } from 'fs'
|
import { existsSync, mkdirSync, chmodSync } from 'fs'
|
||||||
import sqlite3 from 'better-sqlite3'
|
import sqlite3 from 'better-sqlite3'
|
||||||
import z from 'zod'
|
import z from 'zod'
|
||||||
|
|
||||||
|
|
@ -10,10 +10,13 @@ const __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = path.dirname(__filename)
|
const __dirname = path.dirname(__filename)
|
||||||
const app = express()
|
const app = express()
|
||||||
const dbPath = path.join(__dirname, '../db/queuenorth.db')
|
const dbPath = path.join(__dirname, '../db/queuenorth.db')
|
||||||
|
const dbDir = path.dirname(dbPath)
|
||||||
|
|
||||||
// Create db directory if it doesn't exist
|
// Create db directory if it doesn't exist
|
||||||
if (!existsSync(path.dirname(dbPath))) {
|
if (!existsSync(dbDir)) {
|
||||||
mkdirSync(path.dirname(dbPath), { recursive: true })
|
mkdirSync(dbDir, { recursive: true })
|
||||||
|
// Try to set writable permissions, ignore if running as non-root
|
||||||
|
try { chmodSync(dbDir, 0o755) } catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue