From beaaf1b99ad1e399c7f352e3c46137072b2329ab Mon Sep 17 00:00:00 2001 From: Chris Haulmark Date: Mon, 25 May 2026 08:21:45 -0600 Subject: [PATCH] Rewrite docker-compose for staging; fix uploads proxy to use BACKEND_URL HTTP --- docker-compose.yml | 96 +++--------------- .../src/app/api/uploads/[...path]/route.ts | 98 ++++--------------- 2 files changed, 36 insertions(+), 158 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a9637e5..9afa7a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,67 +1,53 @@ version: '3.8' services: - # Frontend service frontend: build: context: ./frontend dockerfile: Dockerfile restart: unless-stopped - user: node ports: - "3000:3000" depends_on: - - backend - env_file: ./frontend/.env - volumes: - - ./logs:/app/logs:rw + backend: + condition: service_healthy + environment: + - BACKEND_URL=http://backend:4000 + - NODE_ENV=production networks: - app-network healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + test: ["CMD", "wget", "-qO-", "http://localhost:3000"] interval: 30s timeout: 10s retries: 3 - deploy: - resources: - limits: - memory: 512M - cpus: '0.5' - # Backend API service backend: build: context: ./backend dockerfile: Dockerfile restart: unless-stopped - user: node ports: - "4000:4000" depends_on: - - mongodb - - redis + mongodb: + condition: service_healthy + redis: + condition: service_healthy env_file: ./backend/.env volumes: - ./uploads:/app/uploads:rw - - ./logs:/app/logs:rw networks: - app-network healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:4000/health"] + test: ["CMD", "wget", "-qO-", "http://localhost:4000/health"] interval: 30s timeout: 10s retries: 3 - deploy: - resources: - limits: - memory: 512M - cpus: '0.5' - # MongoDB service mongodb: image: mongo:7.0 restart: unless-stopped - user: mongodb volumes: - mongo-data:/data/db:rw - ./mongo-init:/docker-entrypoint-initdb.d:ro @@ -71,84 +57,32 @@ services: environment: - MONGO_INITDB_ROOT_USERNAME=${MONGO_USER} - MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD} - command: ["--auth", "--bind_ip_all", "--tlsMode", "preferTLS"] + command: ["--auth", "--bind_ip_all"] healthcheck: test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017 --quiet interval: 30s timeout: 10s - retries: 3 - deploy: - resources: - limits: - memory: 1G - cpus: '1.0' + retries: 5 - # Redis service redis: image: redis:alpine restart: unless-stopped command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"] + env_file: ./backend/.env volumes: - redis-data:/data:rw networks: - app-network healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 30s timeout: 10s retries: 3 - deploy: - resources: - limits: - memory: 256M - cpus: '0.2' - # Nginx service for production - nginx: - image: nginx:alpine - restart: unless-stopped - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx/conf:/etc/nginx/conf.d:ro - - ./nginx/ssl:/etc/nginx/ssl:ro - - ./frontend/public:/var/www/html:ro - depends_on: - - frontend - - backend - networks: - - app-network - healthcheck: - test: ["CMD", "curl", "-f", "https://localhost"] - interval: 30s - timeout: 10s - retries: 3 - deploy: - resources: - limits: - memory: 128M - cpus: '0.1' - - # Security scanner - security_scanner: - image: aquasec/trivy - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./security-reports:/reports - command: ["image", "--format", "table", "--output", "/reports/scan-$(date +%Y%m%d).txt", "lad-website-frontend:latest", "lad-website-backend:latest"] - profiles: - - security - -# Networks with isolation networks: app-network: driver: bridge - ipam: - config: - - subnet: 172.20.0.0/24 -# Volumes with backup capability volumes: mongo-data: name: lad-mongo-data diff --git a/frontend/src/app/api/uploads/[...path]/route.ts b/frontend/src/app/api/uploads/[...path]/route.ts index fb221ed..ded06e7 100644 --- a/frontend/src/app/api/uploads/[...path]/route.ts +++ b/frontend/src/app/api/uploads/[...path]/route.ts @@ -1,91 +1,35 @@ import { NextRequest, NextResponse } from 'next/server'; import path from 'path'; -import fs from 'fs'; -/** - * API route to serve uploaded files - * This allows us to serve files from the backend uploads directory - */ export async function GET( request: NextRequest, { params }: { params: Promise<{ path: string[] }> } ) { + const { path: pathSegments } = await params; + const filePath = pathSegments.join('/').replace(/^uploads\//, ''); + const backendUrl = process.env.BACKEND_URL ?? 'http://localhost:4000'; + try { - const { path: pathSegments } = await params; - const filePath = pathSegments.join('/'); - - // Construct the absolute path to the file - // This assumes the backend is in the same directory as the frontend - // Check if filePath already starts with 'uploads/' - const absolutePath = filePath.startsWith('uploads/') - ? path.join(process.cwd(), '..', 'backend', filePath) - : path.join(process.cwd(), '..', 'backend', 'uploads', filePath); - - console.log('Requested file path:', filePath); - console.log('Absolute path:', absolutePath); - - // Check if the file exists - if (!fs.existsSync(absolutePath)) { - return new NextResponse('File not found', { status: 404 }); + const upstream = await fetch(`${backendUrl}/uploads/${filePath}`, { + next: { revalidate: 86400 }, + }); + + if (!upstream.ok) { + return new NextResponse('File not found', { status: upstream.status }); } - - // Read the file - const fileBuffer = fs.readFileSync(absolutePath); - - // Determine content type based on file extension - const ext = path.extname(filePath).toLowerCase(); - let contentType = 'application/octet-stream'; // Default content type - - // Set content type based on file extension - switch (ext) { - case '.mp4': - contentType = 'video/mp4'; - break; - case '.webm': - contentType = 'video/webm'; - break; - case '.jpg': - case '.jpeg': - contentType = 'image/jpeg'; - break; - case '.png': - contentType = 'image/png'; - break; - case '.gif': - contentType = 'image/gif'; - break; - case '.vtt': - contentType = 'text/vtt'; - break; - case '.srt': - contentType = 'text/plain'; - break; - case '.txt': - contentType = 'text/plain'; - break; - case '.pdf': - contentType = 'application/pdf'; - break; - case '.doc': - case '.docx': - contentType = 'application/msword'; - break; - case '.ppt': - case '.pptx': - contentType = 'application/vnd.ms-powerpoint'; - break; - } - - // Return the file with appropriate headers - return new NextResponse(fileBuffer, { + + const buffer = await upstream.arrayBuffer(); + const contentType = upstream.headers.get('content-type') ?? 'application/octet-stream'; + const filename = path.basename(filePath); + + return new NextResponse(buffer, { headers: { 'Content-Type': contentType, - 'Content-Disposition': `inline; filename="${path.basename(filePath)}"`, - 'Cache-Control': 'public, max-age=86400' // Cache for 1 day - } + 'Content-Disposition': `inline; filename="${filename}"`, + 'Cache-Control': 'public, max-age=86400', + }, }); - } catch (error) { - console.error('Error serving file:', error); - return new NextResponse('Internal Server Error', { status: 500 }); + } catch { + return new NextResponse('Upstream unavailable', { status: 502 }); } }