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:
parent
6038535896
commit
b2da6c69ed
6 changed files with 214 additions and 40 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
17
Dockerfile
17
Dockerfile
|
|
@ -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
29
build-deploy.sh
Executable 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"
|
||||
|
|
@ -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
129
nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue