# 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