diff --git a/dockerize.md b/dockerize.md new file mode 100644 index 0000000..5502890 --- /dev/null +++ b/dockerize.md @@ -0,0 +1,738 @@ +# Docker Production Build Guide + +## Goal +Build a production-ready Docker image containing both the frontend (React/Vite) and backend (Express/Node.js) that connects to an external MySQL database. The container will serve the frontend on port 5173 and proxy API requests to the backend running on localhost:3000 inside the container. + +**SUCCESS CRITERIA**: Docker image is built successfully and ready for deployment. + +## Architecture +- **Single Docker Container**: Contains both FE and BE +- **External Database**: MySQL server (not in container) +- **Port Exposure**: Only port 5173 (frontend) exposed +- **Internal Communication**: FE → BE via localhost:3000 inside container +- **Reverse Proxy**: Caddy handles SSL/domain externally (Unraid setup) + +--- + +## Task 1: Create Dockerfile + +**File**: `Dockerfile` (in project root) + +### Requirements: +1. **Multi-stage build** for optimal image size +2. **Stage 1 - Build Frontend**: + - Use `node:20-alpine` as base + - Set working directory to `/app` + - Copy `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml` + - Copy `packages/frontend/package.json` + - Install pnpm globally: `npm install -g pnpm` + - Install dependencies: `pnpm install --frozen-lockfile` + - Copy entire `packages/frontend` directory + - Build frontend: `cd packages/frontend && pnpm build` + - Output will be in `packages/frontend/dist` + +3. **Stage 2 - Build Backend**: + - Use `node:20-alpine` as base + - Set working directory to `/app` + - Copy workspace files (same as stage 1) + - Install pnpm globally + - Install dependencies: `pnpm install --frozen-lockfile` + - Copy entire `packages/backend` directory + - Generate Prisma client: `cd packages/backend && npx prisma generate` + - Build backend: `cd packages/backend && pnpm build` + - Output will be in `packages/backend/dist` + +4. **Stage 3 - Production Runtime**: + - Use `node:20-alpine` as base + - Install pnpm globally and `serve` package globally: `npm install -g pnpm serve` + - Set working directory to `/app` + - Copy production dependencies setup: + - Copy `package.json`, `pnpm-workspace.yaml`, `pnpm-lock.yaml` + - Copy `packages/backend/package.json` to `packages/backend/` + - Copy `packages/frontend/package.json` to `packages/frontend/` + - Install ONLY production dependencies: `pnpm install --prod --frozen-lockfile` + - Copy backend build artifacts: + - From stage 2: `packages/backend/dist` → `/app/packages/backend/dist` + - From stage 2: `packages/backend/prisma` → `/app/packages/backend/prisma` + - From stage 2: `packages/backend/node_modules/.prisma` → `/app/packages/backend/node_modules/.prisma` + - Copy frontend build artifacts: + - From stage 1: `packages/frontend/dist` → `/app/packages/frontend/dist` + - Create uploads directory: `mkdir -p /app/packages/backend/uploads` + - Expose port 5173 + - Create startup script (see Task 2) + - Set CMD to run startup script + +### Important Notes: +- Use `.dockerignore` to exclude `node_modules`, `dist`, `.env` files +- Ensure Prisma client is generated and copied correctly +- Backend needs access to Prisma schema for migrations + +--- + +## Task 2: Create Startup Script + +**File**: `docker-entrypoint.sh` (in project root) + +### Requirements: +1. **Bash script** with proper shebang: `#!/bin/sh` +2. **Set error handling**: `set -e` +3. **Environment validation**: + - Check required env vars: `DATABASE_URL`, `SESSION_SECRET` + - Exit with error message if missing +4. **Database migration**: + - Navigate to backend: `cd /app/packages/backend` + - Run Prisma migrations: `npx prisma migrate deploy` + - Optional: Run seed if `RUN_SEED=true` env var is set +5. **Start backend**: + - Start backend in background: `node dist/server.js &` + - Store PID: `BACKEND_PID=$!` +6. **Wait for backend**: + - Add 5-second sleep or health check loop + - Ensure backend is ready before starting frontend +7. **Start frontend**: + - Serve frontend on port 5173: `serve -s /app/packages/frontend/dist -l 5173` +8. **Signal handling**: + - Trap SIGTERM/SIGINT to gracefully shutdown both processes + - Kill backend PID on exit + +### Script Template: +```bash +#!/bin/sh +set -e + +echo "🚀 Starting Znakovni.hr Production Container..." + +# Validate environment +if [ -z "$DATABASE_URL" ]; then + echo "❌ ERROR: DATABASE_URL is required" + exit 1 +fi + +# Run migrations +cd /app/packages/backend +echo "📦 Running database migrations..." +npx prisma migrate deploy + +# Start backend +echo "🔧 Starting backend server..." +cd /app/packages/backend +node dist/server.js & +BACKEND_PID=$! + +# Wait for backend +sleep 5 + +# Start frontend +echo "🎨 Starting frontend server..." +cd /app +serve -s packages/frontend/dist -l 5173 -n & +FRONTEND_PID=$! + +# Trap signals +trap "kill $BACKEND_PID $FRONTEND_PID" SIGTERM SIGINT + +# Wait for processes +wait $BACKEND_PID $FRONTEND_PID +``` + +--- + +## Task 3: Create .dockerignore + +**File**: `.dockerignore` (in project root) + +### Exclude: +``` +node_modules +dist +.env +.env.local +.env.*.local +*.log +.git +.gitignore +.vscode +.idea +*.md +!README.md +packages/*/node_modules +packages/*/dist +packages/backend/uploads/* +!packages/backend/uploads/.gitkeep +.DS_Store +``` + +--- + +## Task 4: Update Frontend Build Configuration + +**File**: `packages/frontend/vite.config.ts` + +### Changes Required: +1. **Remove hardcoded host**: Change `host: '192.168.1.238'` to `host: '0.0.0.0'` or remove entirely +2. **Production build**: Ensure build outputs to `dist` directory +3. **API proxy**: Remove proxy config (not needed in production, handled by env var) + +### Updated Config: +```typescript +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + host: '0.0.0.0', + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + }, + }, +}); +``` + +--- + +## Task 5: Update Frontend API Configuration + +**File**: `packages/frontend/.env.production` (create new file) + +### Content: +```env +VITE_API_URL=http://localhost:3000 +``` + +**Note**: In production Docker container, frontend connects to backend via localhost since they're in the same container. + +--- + +## Task 6: Update Backend Server Configuration + +**File**: `packages/backend/src/server.ts` + +### Verify/Update: +1. **HOST binding**: Ensure `HOST = process.env.HOST || '0.0.0.0'` (line 85) +2. **PORT**: Ensure `PORT = parseInt(process.env.PORT || '3000', 10)` (line 16) +3. **CORS origin**: Should accept `FRONTEND_URL` from env (line 24) +4. **Static uploads**: Ensure uploads path is correct (line 57-58) + +### No changes needed if these are already correct (they are based on codebase retrieval). + +--- + +## Task 7: Create Docker Compose Example + +**File**: `docker-compose.yml` (in project root) + +### Purpose: +Provide example for running the container with proper environment variables. + +### Content: +```yaml +version: '3.8' + +services: + znakovni: + build: + context: . + dockerfile: Dockerfile + image: znakovni:latest + container_name: znakovni-app + restart: unless-stopped + ports: + - "5173:5173" + environment: + # Database Configuration + DATABASE_URL: "mysql://user:password@192.168.1.74:3306/znakovni" + # Optional: Shadow database for Prisma migrations + # SHADOW_DATABASE_URL: "mysql://user:password@192.168.1.74:3306/znakovni_shadow" + + # Server Configuration + NODE_ENV: production + PORT: 3000 + HOST: 0.0.0.0 + FRONTEND_URL: http://localhost:5173 + + # Session Secret (CHANGE THIS!) + SESSION_SECRET: your-super-secret-session-key-change-in-production-min-32-chars + + # OAuth - Google (optional, configure if using) + # GOOGLE_CLIENT_ID: your-google-client-id + # GOOGLE_CLIENT_SECRET: your-google-client-secret + # GOOGLE_CALLBACK_URL: https://yourdomain.com/api/auth/google/callback + + # OAuth - Microsoft (optional, configure if using) + # MICROSOFT_CLIENT_ID: your-microsoft-client-id + # MICROSOFT_CLIENT_SECRET: your-microsoft-client-secret + # MICROSOFT_CALLBACK_URL: https://yourdomain.com/api/auth/microsoft/callback + + # File Upload + UPLOAD_DIR: ./uploads + MAX_FILE_SIZE: 104857600 + + # Optional: Run database seed on startup + # RUN_SEED: "false" + + volumes: + # Persist uploaded files + - ./uploads:/app/packages/backend/uploads + + networks: + - znakovni-network + + # Health check + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +networks: + znakovni-network: + driver: bridge +``` + +--- + +## Task 8: Create Production Environment Template + +**File**: `.env.production.example` (in project root) + +### Content: +```env +# =========================================== +# ZNAKOVNI.HR PRODUCTION CONFIGURATION +# =========================================== + +# Database Configuration (REQUIRED) +# Point to your external MySQL server +DATABASE_URL="mysql://username:password@192.168.1.74:3306/znakovni" + +# Optional: Shadow database for Prisma migrations +# SHADOW_DATABASE_URL="mysql://username:password@192.168.1.74:3306/znakovni_shadow" + +# Server Configuration +NODE_ENV=production +PORT=3000 +HOST=0.0.0.0 +FRONTEND_URL=http://localhost:5173 + +# Session Secret (REQUIRED - CHANGE THIS!) +# Generate with: openssl rand -base64 32 +SESSION_SECRET=CHANGE-THIS-TO-A-RANDOM-STRING-MIN-32-CHARACTERS-LONG + +# OAuth - Google (Optional) +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_CALLBACK_URL=https://yourdomain.com/api/auth/google/callback + +# OAuth - Microsoft (Optional) +MICROSOFT_CLIENT_ID= +MICROSOFT_CLIENT_SECRET= +MICROSOFT_CALLBACK_URL=https://yourdomain.com/api/auth/microsoft/callback + +# File Upload Configuration +UPLOAD_DIR=./uploads +MAX_FILE_SIZE=104857600 + +# Database Seeding (Optional) +# Set to "true" to run seed on container startup +RUN_SEED=false +``` + +--- + +## Task 9: Build Docker Image + +### Build Command: +```bash +# From project root +docker build -t znakovni:latest . +``` + +### Verify Build Success: +```bash +# Check image exists +docker images | grep znakovni + +# Should show: +# znakovni latest