From 3840b627dc5706fa336c09fd4c8f9d934586e521 Mon Sep 17 00:00:00 2001 From: Jake Kasper Date: Thu, 21 Aug 2025 07:23:55 -0500 Subject: [PATCH] replace nginx with existing traefik, split networks. --- .env.example | 6 +- README.md | 28 +++++--- backend/Dockerfile | 2 +- docker-compose.yml | 66 +++++++++++-------- frontend/Dockerfile | 4 +- nginx/nginx.conf | 151 -------------------------------------------- 6 files changed, 64 insertions(+), 193 deletions(-) delete mode 100644 nginx/nginx.conf diff --git a/.env.example b/.env.example index c168c14..fa4f8b0 100644 --- a/.env.example +++ b/.env.example @@ -20,14 +20,14 @@ JWT_SECRET=your-super-secret-jwt-key-change-this-in-production AUTHENTIK_CLIENT_ID=your-authentik-client-id AUTHENTIK_CLIENT_SECRET=your-authentik-client-secret AUTHENTIK_BASE_URL=https://your-authentik-domain.com -AUTHENTIK_CALLBACK_URL=http://localhost:5000/api/auth/authentik/callback +AUTHENTIK_CALLBACK_URL=https://turftracker.kaspers.us/api/auth/authentik/callback # Weather API Configuration # Get a free API key from https://openweathermap.org/api WEATHER_API_KEY=your-openweathermap-api-key -# Application URLs -FRONTEND_URL=http://localhost:3000 +# Application URLs (automatically configured for production) +FRONTEND_URL=https://turftracker.kaspers.us # Node Environment NODE_ENV=development diff --git a/README.md b/README.md index 03c708f..e839a48 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ TurfTracker is a comprehensive web application designed for homeowners to track - **Frontend**: React 18, Tailwind CSS, React Router, React Query, Leaflet Maps - **Backend**: Node.js, Express.js, PostgreSQL - **Authentication**: JWT, OAuth2 (Authentik) -- **Infrastructure**: Docker, Nginx +- **Infrastructure**: Docker, Traefik - **Maps**: OpenStreetMap via Leaflet, Esri Satellite Imagery - **APIs**: OpenWeatherMap @@ -75,8 +75,18 @@ TurfTracker is a comprehensive web application designed for homeowners to track ### Prerequisites - Docker and Docker Compose +- Traefik reverse proxy running with `proxy` network +- DNS pointing `turftracker.kaspers.us` to your server - Git +### Traefik Configuration Notes + +This application is pre-configured for deployment behind Traefik with: +- **Networks**: `proxy` (external) for Traefik, `turftracker` (internal) for service communication +- **SSL/TLS**: Automatic Let's Encrypt certificates via Traefik +- **Security**: Database isolated from public network +- **Routing**: Frontend serves on root, API on `/api` path + ### Installation 1. **Clone the repository** @@ -123,14 +133,14 @@ TurfTracker is a comprehensive web application designed for homeowners to track ``` 4. **Access the application** - - Frontend: http://localhost:3000 - - Backend API: http://localhost:5000 - - Database: localhost:5432 + - Application: https://turftracker.kaspers.us + - API: https://turftracker.kaspers.us/api + - Database: Internal network only (not exposed) ### First Time Setup 1. **Create an admin account** - - Go to http://localhost:3000/register + - Go to https://turftracker.kaspers.us/register - Register with your email and password - The first user becomes an admin automatically @@ -212,7 +222,7 @@ If you have an Authentik instance for SSO: - Create new "OAuth2/OpenID Provider" - Set Authorization grant type: `authorization-code` - Set Client type: `confidential` - - Set Redirect URIs: `http://localhost:5000/api/auth/authentik/callback` + - Set Redirect URIs: `https://turftracker.kaspers.us/api/auth/authentik/callback` - Note the Client ID and Client Secret 2. **In your `.env` file:** @@ -220,7 +230,7 @@ If you have an Authentik instance for SSO: AUTHENTIK_CLIENT_ID=your-client-id-from-authentik AUTHENTIK_CLIENT_SECRET=your-client-secret-from-authentik AUTHENTIK_BASE_URL=https://your-authentik-domain.com - AUTHENTIK_CALLBACK_URL=http://localhost:5000/api/auth/authentik/callback + AUTHENTIK_CALLBACK_URL=https://turftracker.kaspers.us/api/auth/authentik/callback ``` 3. **In Authentik Applications:** @@ -252,9 +262,7 @@ turftracker/ │ └── package.json ├── database/ # PostgreSQL schema │ └── init.sql # Database initialization -├── nginx/ # Reverse proxy configuration -│ └── nginx.conf -└── docker-compose.yml # Container orchestration +└── docker-compose.yml # Container orchestration with Traefik ``` ## Usage Guide diff --git a/backend/Dockerfile b/backend/Dockerfile index 06cd254..4161fde 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -7,7 +7,7 @@ WORKDIR /app COPY package*.json ./ # Install dependencies -RUN npm ci --only=production +RUN npm install --only=production # Copy source code COPY . . diff --git a/docker-compose.yml b/docker-compose.yml index be23e6b..32178a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,66 +1,80 @@ version: '3.8' +networks: + proxy: + external: true + turftracker: + driver: bridge + services: frontend: build: context: ./frontend dockerfile: Dockerfile - ports: - - "3000:3000" environment: - - REACT_APP_API_URL=http://localhost:5000 + - REACT_APP_API_URL=https://turftracker.kaspers.us volumes: - ./frontend:/app - /app/node_modules depends_on: - backend + networks: + - proxy + - turftracker + labels: + - "traefik.enable=true" + - "traefik.http.routers.turftracker-frontend.rule=Host(`turftracker.kaspers.us`)" + - "traefik.http.routers.turftracker-frontend.entrypoints=websecure" + - "traefik.http.routers.turftracker-frontend.tls.certresolver=letsencrypt" + - "traefik.http.services.turftracker-frontend.loadbalancer.server.port=3000" + - "traefik.docker.network=proxy" backend: build: context: ./backend dockerfile: Dockerfile - ports: - - "5000:5000" environment: - - NODE_ENV=development + - NODE_ENV=production - DB_HOST=db - DB_PORT=5432 - - DB_NAME=turftracker - - DB_USER=turftracker - - DB_PASSWORD=password123 - - JWT_SECRET=your-super-secret-jwt-key + - DB_NAME=${DB_NAME:-turftracker} + - DB_USER=${DB_USER:-turftracker} + - DB_PASSWORD=${DB_PASSWORD:-password123} + - JWT_SECRET=${JWT_SECRET} - AUTHENTIK_CLIENT_ID=${AUTHENTIK_CLIENT_ID:-} - AUTHENTIK_CLIENT_SECRET=${AUTHENTIK_CLIENT_SECRET:-} - AUTHENTIK_BASE_URL=${AUTHENTIK_BASE_URL:-} - - AUTHENTIK_CALLBACK_URL=${AUTHENTIK_CALLBACK_URL:-} + - AUTHENTIK_CALLBACK_URL=${AUTHENTIK_CALLBACK_URL:-https://turftracker.kaspers.us/api/auth/authentik/callback} - WEATHER_API_KEY=${WEATHER_API_KEY:-} + - FRONTEND_URL=https://turftracker.kaspers.us volumes: - ./backend:/app - /app/node_modules depends_on: - db + networks: + - proxy + - turftracker + labels: + - "traefik.enable=true" + - "traefik.http.routers.turftracker-backend.rule=Host(`turftracker.kaspers.us`) && PathPrefix(`/api`)" + - "traefik.http.routers.turftracker-backend.entrypoints=websecure" + - "traefik.http.routers.turftracker-backend.tls.certresolver=letsencrypt" + - "traefik.http.services.turftracker-backend.loadbalancer.server.port=5000" + - "traefik.docker.network=proxy" db: image: postgres:15-alpine environment: - - POSTGRES_USER=turftracker - - POSTGRES_PASSWORD=password123 - - POSTGRES_DB=turftracker - ports: - - "5432:5432" + - POSTGRES_USER=${DB_USER:-turftracker} + - POSTGRES_PASSWORD=${DB_PASSWORD:-password123} + - POSTGRES_DB=${DB_NAME:-turftracker} volumes: - postgres_data:/var/lib/postgresql/data - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql - - nginx: - image: nginx:alpine - ports: - - "80:80" - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf - depends_on: - - frontend - - backend + networks: + - turftracker + # Database should not be exposed to proxy network for security volumes: postgres_data: \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 7808036..452fd30 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -7,7 +7,7 @@ WORKDIR /app COPY package*.json ./ # Install dependencies -RUN npm ci +RUN npm install # Copy source code COPY . . @@ -25,7 +25,7 @@ EXPOSE 3000 # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:3000 || exit 1 + CMD wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1 # Start the application CMD ["npm", "start"] \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf deleted file mode 100644 index cb8cb74..0000000 --- a/nginx/nginx.conf +++ /dev/null @@ -1,151 +0,0 @@ -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; - - # Gzip compression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types - application/atom+xml - application/geo+json - application/javascript - application/x-javascript - application/json - application/ld+json - application/manifest+json - application/rdf+xml - application/rss+xml - application/xhtml+xml - application/xml - font/eot - font/otf - font/ttf - image/svg+xml - text/css - text/javascript - text/plain - text/xml; - - # Rate limiting - limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; - limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m; - - # Security headers - add_header X-Content-Type-Options nosniff; - add_header X-Frame-Options DENY; - add_header X-XSS-Protection "1; mode=block"; - add_header Referrer-Policy "strict-origin-when-cross-origin"; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://*.openstreetmap.org https://server.arcgisonline.com; connect-src 'self' https://api.openweathermap.org https://*.openstreetmap.org https://server.arcgisonline.com;"; - - upstream backend { - server backend:5000; - } - - upstream frontend { - server frontend:3000; - } - - server { - listen 80; - server_name localhost; - - # API routes - location /api/ { - limit_req zone=api burst=20 nodelay; - - proxy_pass http://backend; - 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; - - # CORS headers for API - add_header Access-Control-Allow-Origin $http_origin; - add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"; - add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization"; - add_header Access-Control-Allow-Credentials true; - - # Handle preflight requests - if ($request_method = 'OPTIONS') { - add_header Access-Control-Allow-Origin $http_origin; - add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"; - add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization"; - add_header Access-Control-Allow-Credentials true; - add_header Content-Length 0; - add_header Content-Type text/plain; - return 204; - } - } - - # Auth routes with stricter rate limiting - location /api/auth/ { - limit_req zone=auth burst=5 nodelay; - - proxy_pass http://backend; - 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; - } - - # Health check endpoint - location /health { - proxy_pass http://backend; - access_log off; - } - - # Static files and React app - location / { - proxy_pass http://frontend; - 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; - - # Handle React Router - try_files $uri $uri/ @fallback; - } - - # Fallback for React Router (SPA) - location @fallback { - proxy_pass http://frontend; - 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; - } - - # Cache static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - proxy_pass http://frontend; - expires 1y; - add_header Cache-Control "public, immutable"; - } - - # Security - deny access to sensitive files - location ~ /\. { - deny all; - access_log off; - log_not_found off; - } - - location ~ ~$ { - deny all; - access_log off; - log_not_found off; - } - } -} \ No newline at end of file