Harden template for production: add comments, fix bugs, add lock file

- Add inline comments to docker-compose.yml explaining the 5 key rules
  (expose vs ports, DB host, DB name persistence, no Traefik labels)
- Add comments to Dockerfile explaining multi-stage build, layer caching,
  and why .dockerignore excludes client/dist
- Add comments to .dockerignore explaining each exclusion
- Fix dev script: use nodemon (auto-restart) instead of node for server.js
- Add postinstall script to auto-install client deps (cd client && npm install)
- Fix SPA fallback: bare return → next() to prevent hanging requests
- Add root package-lock.json for deterministic server dependency installs
- Remove committed tsconfig.tsbuildinfo build artifact, add *.tsbuildinfo to .gitignore
- Update README: simpler install (npm install handles everything), reference SAAC_DEPLOYMENT.md,
  use npx instead of pnpm dlx for shadcn components
This commit is contained in:
SAAC Daemon
2026-02-18 16:36:33 +01:00
parent 0e21d26c05
commit 5b37f88477
10 changed files with 1962 additions and 17 deletions

View File

@@ -1,6 +1,13 @@
# Dependencies — installed inside the container via npm install
node_modules
client/node_modules
# Built React output — the Dockerfile builds this fresh inside the container
# using a multi-stage build (COPY --from=builder). Excluding it here prevents
# stale local builds from leaking into the Docker build context.
client/dist
# Files not needed in the Docker image
npm-debug.log
.git
.gitignore

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ npm-debug.log
.saac/
client/dist/
client/node_modules/
*.tsbuildinfo

View File

@@ -1,15 +1,27 @@
# Multi-stage Dockerfile for React + Express application
# See SAAC_DEPLOYMENT.md for deployment rules and common mistakes.
#
# Stage 1 (builder): Installs client deps and builds the React app with Vite.
# Stage 2 (production): Installs server deps only, copies built React output.
#
# The .dockerignore excludes client/dist from the build context — this is
# intentional. The builder stage creates a fresh dist/ inside the container,
# and COPY --from=builder copies it between stages (bypasses .dockerignore).
# --- Stage 1: Build React frontend ---
FROM node:20-alpine AS builder
WORKDIR /app/client
# Install client dependencies and build
# Install client dependencies first (layer caching — only re-runs if package*.json change)
COPY client/package*.json ./
RUN npm install
# Copy client source and build
COPY client/ ./
RUN npm run build
# --- Production stage ---
# --- Stage 2: Production server ---
FROM node:20-alpine
ARG SOURCE_COMMIT=unknown
@@ -17,23 +29,28 @@ ARG APP_VERSION=1.0.0
WORKDIR /app
# curl is required for the Docker healthcheck below
RUN apk add --no-cache curl
# Install server dependencies
# Install server dependencies only (not devDependencies — they're not needed in production).
# If your app needs devDependencies for a build step (e.g. TypeScript), install ALL deps
# first, run the build, then prune: RUN npm install && npm run build && npm prune --production
COPY package*.json ./
RUN npm install --production
# Copy server code
COPY server.js ./
# Copy built React app from builder
# Copy built React app from the builder stage (not from host — .dockerignore doesn't apply)
COPY --from=builder /app/client/dist ./client/dist
ENV GIT_COMMIT=${SOURCE_COMMIT} \
APP_VERSION=${APP_VERSION}
# Use "expose" in docker-compose.yml, not "ports". This EXPOSE is just documentation.
EXPOSE 3000
# Healthcheck — must match the healthcheck in docker-compose.yml
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=2 \
CMD curl -f http://localhost:3000/health || exit 1

View File

@@ -24,22 +24,23 @@ React + shadcn/ui + Express + PostgreSQL + Redis scaffold for SAAC deployment.
│ ├── components.json # shadcn/ui config
│ ├── vite.config.ts # Vite config with path aliases
│ └── package.json # Client dependencies
├── docker-compose.yml # PostgreSQL + Redis + App
├── docker-compose.yml # PostgreSQL + Redis + App (see comments inside)
├── Dockerfile # Multi-stage: build React → serve with Express
├── SAAC_DEPLOYMENT.md # Deployment rules — READ THIS FIRST
└── package.json # Server dependencies
```
## Development
```bash
# Install all dependencies
npm install && cd client && npm install && cd ..
# Install all dependencies (postinstall auto-installs client deps)
npm install
# Run backend + frontend concurrently
# Run backend + frontend concurrently (nodemon + Vite HMR)
npm run dev
# Or run separately:
npm run dev:server # Express on :3000
npm run dev:server # Express on :3000 (with nodemon auto-restart)
npm run dev:client # Vite on :5173 (proxies /api to :3000)
```
@@ -47,9 +48,9 @@ npm run dev:client # Vite on :5173 (proxies /api to :3000)
```bash
cd client
pnpm dlx shadcn@latest add dialog # Add Dialog component
pnpm dlx shadcn@latest add table # Add Table component
pnpm dlx shadcn@latest add select # Add Select component
npx shadcn@latest add dialog # Add Dialog component
npx shadcn@latest add table # Add Table component
npx shadcn@latest add select # Add Select component
```
Import: `import { Button } from "@/components/ui/button"`
@@ -60,6 +61,8 @@ Import: `import { Button } from "@/components/ui/button"`
saac deploy
```
See `SAAC_DEPLOYMENT.md` for deployment rules, common mistakes, and debugging commands.
## API Routes
- `GET /health` — Health check (PostgreSQL + Redis)

View File

@@ -2109,6 +2109,66 @@
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.7.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.7.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@tybys/wasm-util": "^0.10.1"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.1",
"dev": true,
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",

View File

@@ -1 +0,0 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/input.tsx","./src/components/ui/separator.tsx","./src/lib/utils.ts","./src/pages/Home.tsx"],"version":"5.7.3"}

View File

@@ -1,3 +1,13 @@
# SAAC Application Stack
# See SAAC_DEPLOYMENT.md for full deployment rules and common mistakes.
#
# Key rules:
# 1. Use "expose", NEVER "ports" — Traefik handles routing
# 2. Database host = service name ("postgres"), NEVER "localhost"
# 3. Do NOT change POSTGRES_DB after first deploy — Docker volume keeps the old name
# 4. Do NOT add Traefik labels — SAAC uses file provider, labels are ignored
# 5. Keep it simple — get this working first, then iterate
services:
app:
build:
@@ -9,10 +19,16 @@ services:
environment:
- NODE_ENV=production
- PORT=3000
# Database host MUST be the service name ("postgres"), not "localhost".
# In Docker, localhost = inside this container. "postgres" resolves to
# the postgres service below via Docker DNS.
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
# WARNING: Do NOT change POSTGRES_DB after first deploy!
# The Docker volume persists the original database name.
# If you change this, the new database won't exist and your app will crash.
- POSTGRES_DB=postgres
- REDIS_HOST=redis
- REDIS_PORT=6379
@@ -22,6 +38,9 @@ services:
condition: service_healthy
redis:
condition: service_healthy
# Use "expose" to make the port available to Traefik on the Docker network.
# NEVER use "ports" (e.g. "3000:3000") — it binds to the host and conflicts
# with other apps. Traefik handles all external routing automatically.
expose:
- "3000"
healthcheck:
@@ -37,6 +56,7 @@ services:
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
# This creates the database on first start. Must match POSTGRES_DB above.
- POSTGRES_DB=postgres
volumes:
- postgres-data:/var/lib/postgresql/data

1837
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,9 @@
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "concurrently \"node server.js\" \"cd client && npm run dev\"",
"dev": "concurrently \"nodemon server.js\" \"cd client && npm run dev\"",
"build": "cd client && npm run build",
"postinstall": "[ -d client ] && cd client && npm install || true",
"dev:server": "nodemon server.js",
"dev:client": "cd client && npm run dev"
},

View File

@@ -73,9 +73,9 @@ app.get('/api/status', async (req, res) => {
const clientDist = path.join(__dirname, 'client', 'dist');
app.use(express.static(clientDist));
// SPA fallback — all non-API routes serve index.html
app.get('*', (req, res) => {
if (req.path.startsWith('/api/') || req.path === '/health') return;
// SPA fallback — all non-API routes serve index.html so React Router works
app.get('*', (req, res, next) => {
if (req.path.startsWith('/api/') || req.path === '/health') return next();
res.sendFile(path.join(clientDist, 'index.html'));
});