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 # 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 ## Prerequisites
@ -38,30 +45,20 @@ mkdir -p data/{csv,pdf,spreadsheets}
# cp ~/Documents/BusinessData/*.xlsx data/spreadsheets/ # cp ~/Documents/BusinessData/*.xlsx data/spreadsheets/
``` ```
### 3. Configure Environment (Optional) ### 3. Build and Deploy
```bash ```bash
# Copy the production environment template # Option 1: Use the automated build script (recommended)
cp server/.env.production server/.env ./build-deploy.sh
# Edit the environment file if needed # Option 2: Manual deployment
nano server/.env cd client && npm install && npm run build && cd ..
```
### 4. Build and Run with Docker Compose
```bash
# Build and start the application
docker-compose up --build -d 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 - **Web Interface**: http://localhost:3000
- **API Health Check**: http://localhost:3000/health - **API Health Check**: http://localhost:3000/health
- **Direct API**: http://localhost:3000/api/ (proxied through nginx)
## Management Commands ## 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 # Stage 1: Build the React client
FROM node:18-alpine AS client-build FROM node:18-alpine AS client-build
@ -12,7 +12,7 @@ RUN npm ci --only=production
COPY client/ ./ COPY client/ ./
RUN npm run build RUN npm run build
# Stage 2: Build the Node.js server # Stage 2: Build the Node.js server
FROM node:18-alpine AS server-build FROM node:18-alpine AS server-build
WORKDIR /app/server WORKDIR /app/server
@ -24,7 +24,7 @@ RUN npm ci --only=production
COPY server/ ./ COPY server/ ./
RUN npm run build RUN npm run build
# Stage 3: Production runtime # Stage 3: Production API server (no static files)
FROM node:18-alpine AS production FROM node:18-alpine AS production
WORKDIR /app 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/node_modules ./server/node_modules/
COPY --from=server-build --chown=nodejs:nodejs /app/server/package*.json ./server/ 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 # Create data directory for persistent storage
RUN mkdir -p /app/data && chown nodejs:nodejs /app/data RUN mkdir -p /app/data && chown nodejs:nodejs /app/data
# Switch to non-root user # Switch to non-root user
USER nodejs USER nodejs
# Expose port # Expose API port (nginx will handle port 80)
EXPOSE 3000 EXPOSE 8080
# Health check # Health check for API server
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ 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", "--"] ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "server/index.js"] 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' version: '3.8'
services: 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: etsy-tracker:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: etsy-finance-tracker container_name: etsy-finance-tracker
ports: expose:
- "3000:3000" - "8080"
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- PORT=3000 - PORT=8080
- CLIENT_URL=http://nginx
volumes: volumes:
# Mount data directory for persistent storage # Mount data directory for persistent storage
- ./data:/app/data - ./data:/app/data
@ -21,7 +43,7 @@ services:
networks: networks:
- etsy-network - etsy-network
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"] test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 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 # Application
NODE_ENV=production NODE_ENV=production
PORT=3000 PORT=8080
# Client Configuration # Client Configuration (nginx proxy)
CLIENT_URL=http://localhost:3000 CLIENT_URL=http://nginx
# Database (when using MongoDB) # Database (when using MongoDB)
# MONGODB_URI=mongodb://mongodb:27017/etsy-tracker # 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_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_EXPIRES_IN=7d JWT_EXPIRES_IN=7d
# CORS Configuration # CORS Configuration (nginx handles external requests)
ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 ALLOWED_ORIGINS=http://nginx,http://localhost:3000
# File Upload Limits # File Upload Limits
MAX_FILE_SIZE=10mb MAX_FILE_SIZE=10mb
@ -26,7 +26,7 @@ UPLOAD_PATH=/app/uploads
# Logging # Logging
LOG_LEVEL=info LOG_LEVEL=info
# Rate Limiting # Rate Limiting (nginx also provides rate limiting)
RATE_LIMIT_WINDOW_MS=900000 RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100 RATE_LIMIT_MAX_REQUESTS=100