Rewrite docker-compose for staging; fix uploads proxy to use BACKEND_URL HTTP

This commit is contained in:
Chris Haulmark 2026-05-25 08:21:45 -06:00
parent 50a54255a8
commit beaaf1b99a
2 changed files with 36 additions and 158 deletions

View file

@ -1,67 +1,53 @@
version: '3.8' version: '3.8'
services: services:
# Frontend service
frontend: frontend:
build: build:
context: ./frontend context: ./frontend
dockerfile: Dockerfile dockerfile: Dockerfile
restart: unless-stopped restart: unless-stopped
user: node
ports: ports:
- "3000:3000" - "3000:3000"
depends_on: depends_on:
- backend backend:
env_file: ./frontend/.env condition: service_healthy
volumes: environment:
- ./logs:/app/logs:rw - BACKEND_URL=http://backend:4000
- NODE_ENV=production
networks: networks:
- app-network - app-network
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"] test: ["CMD", "wget", "-qO-", "http://localhost:3000"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
# Backend API service
backend: backend:
build: build:
context: ./backend context: ./backend
dockerfile: Dockerfile dockerfile: Dockerfile
restart: unless-stopped restart: unless-stopped
user: node
ports: ports:
- "4000:4000" - "4000:4000"
depends_on: depends_on:
- mongodb mongodb:
- redis condition: service_healthy
redis:
condition: service_healthy
env_file: ./backend/.env env_file: ./backend/.env
volumes: volumes:
- ./uploads:/app/uploads:rw - ./uploads:/app/uploads:rw
- ./logs:/app/logs:rw
networks: networks:
- app-network - app-network
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4000/health"] test: ["CMD", "wget", "-qO-", "http://localhost:4000/health"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
# MongoDB service
mongodb: mongodb:
image: mongo:7.0 image: mongo:7.0
restart: unless-stopped restart: unless-stopped
user: mongodb
volumes: volumes:
- mongo-data:/data/db:rw - mongo-data:/data/db:rw
- ./mongo-init:/docker-entrypoint-initdb.d:ro - ./mongo-init:/docker-entrypoint-initdb.d:ro
@ -71,84 +57,32 @@ services:
environment: environment:
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USER} - MONGO_INITDB_ROOT_USERNAME=${MONGO_USER}
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD} - MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
command: ["--auth", "--bind_ip_all", "--tlsMode", "preferTLS"] command: ["--auth", "--bind_ip_all"]
healthcheck: healthcheck:
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017 --quiet test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017 --quiet
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 5
deploy:
resources:
limits:
memory: 1G
cpus: '1.0'
# Redis service
redis: redis:
image: redis:alpine image: redis:alpine
restart: unless-stopped restart: unless-stopped
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"] command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
env_file: ./backend/.env
volumes: volumes:
- redis-data:/data:rw - redis-data:/data:rw
networks: networks:
- app-network - app-network
healthcheck: healthcheck:
test: ["CMD", "redis-cli", "ping"] test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 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: networks:
app-network: app-network:
driver: bridge driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24
# Volumes with backup capability
volumes: volumes:
mongo-data: mongo-data:
name: lad-mongo-data name: lad-mongo-data

View file

@ -1,91 +1,35 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import path from 'path'; 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( export async function GET(
request: NextRequest, request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> } { 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 { try {
const { path: pathSegments } = await params; const upstream = await fetch(`${backendUrl}/uploads/${filePath}`, {
const filePath = pathSegments.join('/'); next: { revalidate: 86400 },
});
// Construct the absolute path to the file
// This assumes the backend is in the same directory as the frontend if (!upstream.ok) {
// Check if filePath already starts with 'uploads/' return new NextResponse('File not found', { status: upstream.status });
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 });
} }
// Read the file const buffer = await upstream.arrayBuffer();
const fileBuffer = fs.readFileSync(absolutePath); const contentType = upstream.headers.get('content-type') ?? 'application/octet-stream';
const filename = path.basename(filePath);
// Determine content type based on file extension
const ext = path.extname(filePath).toLowerCase(); return new NextResponse(buffer, {
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, {
headers: { headers: {
'Content-Type': contentType, 'Content-Type': contentType,
'Content-Disposition': `inline; filename="${path.basename(filePath)}"`, 'Content-Disposition': `inline; filename="${filename}"`,
'Cache-Control': 'public, max-age=86400' // Cache for 1 day 'Cache-Control': 'public, max-age=86400',
} },
}); });
} catch (error) { } catch {
console.error('Error serving file:', error); return new NextResponse('Upstream unavailable', { status: 502 });
return new NextResponse('Internal Server Error', { status: 500 });
} }
} }