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
|
# 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
|
||||||
|
|
||||||
|
|
|
||||||
19
Dockerfile
19
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
|
# 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
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'
|
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
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
|
# 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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue