Add Nginx reverse proxy for production deployment

Architecture Updates:
- Nginx serves static React files for optimal performance
- Nginx proxies API requests to Node.js backend (port 8080)
- Separation of concerns: static files vs API handling
- Professional production setup with proper caching

Features Added:
- nginx.conf with optimized configuration:
  - Static file serving with long-term caching
  - API reverse proxy with rate limiting
  - Security headers and GZIP compression
  - Health check proxying and SPA routing support
- Updated docker-compose.yml for multi-container setup
- build-deploy.sh script for automated deployment
- Updated environment configuration for container networking

Security & Performance:
- Rate limiting on API and auth endpoints
- Security headers (XSS, CSRF, clickjacking protection)
- GZIP compression for static assets
- Proper cache control headers
- Container-to-container communication

Deployment:
- Single command deployment with ./build-deploy.sh
- Nginx on port 80 (exposed as 3000) serving React app
- API server on internal port 8080 (not exposed)
- Persistent data volume mounting for business files
This commit is contained in:
dlawler489 2026-04-21 06:30:44 +10:00
parent 6038535896
commit b2da6c69ed
6 changed files with 214 additions and 40 deletions

View file

@ -1,6 +1,13 @@
# Docker Deployment Guide for Mac Mini
This guide will help you deploy the Etsy Finance Tracker on your Mac Mini using Docker.
This guide will help you deploy the Etsy Finance Tracker on your Mac Mini using Docker with Nginx.
## Architecture
The application uses a multi-container setup:
- **Nginx**: Serves static React files and proxies API requests
- **Node.js API**: Handles backend logic and API endpoints
- **Persistent Data**: Your business data mounted as volumes
## Prerequisites
@ -38,30 +45,20 @@ mkdir -p data/{csv,pdf,spreadsheets}
# cp ~/Documents/BusinessData/*.xlsx data/spreadsheets/
```
### 3. Configure Environment (Optional)
### 3. Build and Deploy
```bash
# Copy the production environment template
cp server/.env.production server/.env
# Option 1: Use the automated build script (recommended)
./build-deploy.sh
# Edit the environment file if needed
nano server/.env
```
### 4. Build and Run with Docker Compose
```bash
# Build and start the application
# Option 2: Manual deployment
cd client && npm install && npm run build && cd ..
docker-compose up --build -d
# View logs
docker-compose logs -f
# Check status
docker-compose ps
```
### 5. Access Your Application
### 4. Access Your Application
- **Web Interface**: http://localhost:3000
- **API Health Check**: http://localhost:3000/health
- **Direct API**: http://localhost:3000/api/ (proxied through nginx)
## Management Commands

View file

@ -1,4 +1,4 @@
# Multi-stage build for production-ready Etsy Finance Tracker
# Multi-stage build for production-ready Etsy Finance Tracker with Nginx
# Stage 1: Build the React client
FROM node:18-alpine AS client-build
@ -24,7 +24,7 @@ RUN npm ci --only=production
COPY server/ ./
RUN npm run build
# Stage 3: Production runtime
# Stage 3: Production API server (no static files)
FROM node:18-alpine AS production
WORKDIR /app
@ -40,22 +40,19 @@ COPY --from=server-build --chown=nodejs:nodejs /app/server/dist ./server/
COPY --from=server-build --chown=nodejs:nodejs /app/server/node_modules ./server/node_modules/
COPY --from=server-build --chown=nodejs:nodejs /app/server/package*.json ./server/
# Copy built client
COPY --from=client-build --chown=nodejs:nodejs /app/client/dist ./client/dist/
# Create data directory for persistent storage
RUN mkdir -p /app/data && chown nodejs:nodejs /app/data
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 3000
# Expose API port (nginx will handle port 80)
EXPOSE 8080
# Health check
# Health check for API server
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD curl -f http://localhost:8080/health || exit 1
# Start the application
# Start the API server
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "server/index.js"]

29
build-deploy.sh Executable file
View file

@ -0,0 +1,29 @@
#!/bin/bash
# Build script for Docker deployment with nginx
echo "🏗️ Building Etsy Finance Tracker for production..."
# Build the React client
echo "📦 Building React client..."
cd client
npm run build
cd ..
# Ensure client build directory exists and has correct permissions
if [ ! -d "client/dist" ]; then
echo "❌ Client build failed - dist directory not found"
exit 1
fi
echo "📁 Client built successfully in client/dist/"
# Build and start with Docker Compose
echo "🐳 Starting Docker containers..."
docker-compose down
docker-compose up --build -d
echo "✅ Deployment complete!"
echo "🌐 Access your app at: http://localhost:3000"
echo "🔍 Health check at: http://localhost:3000/health"
echo "📊 View logs with: docker-compose logs -f"

View file

@ -1,17 +1,39 @@
version: '3.8'
services:
# Etsy Finance Tracker Application
# Nginx reverse proxy and static file server
nginx:
image: nginx:alpine
container_name: etsy-nginx
ports:
- "3000:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./client/dist:/usr/share/nginx/html:ro
depends_on:
- etsy-tracker
restart: unless-stopped
networks:
- etsy-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# Etsy Finance Tracker API Server
etsy-tracker:
build:
context: .
dockerfile: Dockerfile
container_name: etsy-finance-tracker
ports:
- "3000:3000"
expose:
- "8080"
environment:
- NODE_ENV=production
- PORT=3000
- PORT=8080
- CLIENT_URL=http://nginx
volumes:
# Mount data directory for persistent storage
- ./data:/app/data
@ -21,7 +43,7 @@ services:
networks:
- etsy-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3

129
nginx.conf Normal file
View file

@ -0,0 +1,129 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# Basic settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 20M;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/xml+rss
application/json;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# Upstream backend
upstream etsy_api {
server etsy-tracker:8080;
keepalive 32;
}
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Health check endpoint (proxied to backend)
location /health {
proxy_pass http://etsy_api;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API routes - proxy to backend
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://etsy_api;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Auth routes - with stricter rate limiting
location /auth/ {
limit_req zone=login burst=5 nodelay;
proxy_pass http://etsy_api;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Static assets with caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header X-Content-Type-Options "nosniff" always;
}
# React app - serve index.html for all routes (SPA support)
location / {
try_files $uri $uri/ /index.html;
# Prevent caching of index.html
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
}
# Error pages
error_page 404 /index.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}

View file

@ -3,10 +3,10 @@
# Application
NODE_ENV=production
PORT=3000
PORT=8080
# Client Configuration
CLIENT_URL=http://localhost:3000
# Client Configuration (nginx proxy)
CLIENT_URL=http://nginx
# Database (when using MongoDB)
# MONGODB_URI=mongodb://mongodb:27017/etsy-tracker
@ -16,8 +16,8 @@ CLIENT_URL=http://localhost:3000
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_EXPIRES_IN=7d
# CORS Configuration
ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
# CORS Configuration (nginx handles external requests)
ALLOWED_ORIGINS=http://nginx,http://localhost:3000
# File Upload Limits
MAX_FILE_SIZE=10mb
@ -26,7 +26,7 @@ UPLOAD_PATH=/app/uploads
# Logging
LOG_LEVEL=info
# Rate Limiting
# Rate Limiting (nginx also provides rate limiting)
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100