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:
@@ -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
1
.gitignore
vendored
@@ -6,3 +6,4 @@ npm-debug.log
|
||||
.saac/
|
||||
client/dist/
|
||||
client/node_modules/
|
||||
*.tsbuildinfo
|
||||
|
||||
25
Dockerfile
25
Dockerfile
@@ -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
|
||||
|
||||
|
||||
19
README.md
19
README.md
@@ -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)
|
||||
|
||||
60
client/package-lock.json
generated
60
client/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"}
|
||||
@@ -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
1837
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user