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'
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

View file

@ -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('/');
const upstream = await fetch(`${backendUrl}/uploads/${filePath}`, {
next: { revalidate: 86400 },
});
// 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 });
if (!upstream.ok) {
return new NextResponse('File not found', { status: upstream.status });
}
// Read the file
const fileBuffer = fs.readFileSync(absolutePath);
const buffer = await upstream.arrayBuffer();
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();
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, {
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 });
}
}