diff --git a/DOCKER_DEPLOYMENT.md b/DOCKER_DEPLOYMENT.md index 932418c..16f16f6 100644 --- a/DOCKER_DEPLOYMENT.md +++ b/DOCKER_DEPLOYMENT.md @@ -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 diff --git a/Dockerfile b/Dockerfile index bc6ad66..55f0f71 100644 --- a/Dockerfile +++ b/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 @@ -12,7 +12,7 @@ RUN npm ci --only=production COPY client/ ./ 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 WORKDIR /app/server @@ -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"] \ No newline at end of file diff --git a/build-deploy.sh b/build-deploy.sh new file mode 100755 index 0000000..4ee66d4 --- /dev/null +++ b/build-deploy.sh @@ -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" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index d207dba..2c3bb63 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..99bf264 --- /dev/null +++ b/nginx.conf @@ -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; + } + } +} \ No newline at end of file diff --git a/server/.env.production b/server/.env.production index e575a7f..35fd1d3 100644 --- a/server/.env.production +++ b/server/.env.production @@ -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