replace nginx with existing traefik, split networks.
This commit is contained in:
@@ -20,14 +20,14 @@ JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
|||||||
AUTHENTIK_CLIENT_ID=your-authentik-client-id
|
AUTHENTIK_CLIENT_ID=your-authentik-client-id
|
||||||
AUTHENTIK_CLIENT_SECRET=your-authentik-client-secret
|
AUTHENTIK_CLIENT_SECRET=your-authentik-client-secret
|
||||||
AUTHENTIK_BASE_URL=https://your-authentik-domain.com
|
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
|
# Weather API Configuration
|
||||||
# Get a free API key from https://openweathermap.org/api
|
# Get a free API key from https://openweathermap.org/api
|
||||||
WEATHER_API_KEY=your-openweathermap-api-key
|
WEATHER_API_KEY=your-openweathermap-api-key
|
||||||
|
|
||||||
# Application URLs
|
# Application URLs (automatically configured for production)
|
||||||
FRONTEND_URL=http://localhost:3000
|
FRONTEND_URL=https://turftracker.kaspers.us
|
||||||
|
|
||||||
# Node Environment
|
# Node Environment
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|||||||
28
README.md
28
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
|
- **Frontend**: React 18, Tailwind CSS, React Router, React Query, Leaflet Maps
|
||||||
- **Backend**: Node.js, Express.js, PostgreSQL
|
- **Backend**: Node.js, Express.js, PostgreSQL
|
||||||
- **Authentication**: JWT, OAuth2 (Authentik)
|
- **Authentication**: JWT, OAuth2 (Authentik)
|
||||||
- **Infrastructure**: Docker, Nginx
|
- **Infrastructure**: Docker, Traefik
|
||||||
- **Maps**: OpenStreetMap via Leaflet, Esri Satellite Imagery
|
- **Maps**: OpenStreetMap via Leaflet, Esri Satellite Imagery
|
||||||
- **APIs**: OpenWeatherMap
|
- **APIs**: OpenWeatherMap
|
||||||
|
|
||||||
@@ -75,8 +75,18 @@ TurfTracker is a comprehensive web application designed for homeowners to track
|
|||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Docker and Docker Compose
|
- Docker and Docker Compose
|
||||||
|
- Traefik reverse proxy running with `proxy` network
|
||||||
|
- DNS pointing `turftracker.kaspers.us` to your server
|
||||||
- Git
|
- 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
|
### Installation
|
||||||
|
|
||||||
1. **Clone the repository**
|
1. **Clone the repository**
|
||||||
@@ -123,14 +133,14 @@ TurfTracker is a comprehensive web application designed for homeowners to track
|
|||||||
```
|
```
|
||||||
|
|
||||||
4. **Access the application**
|
4. **Access the application**
|
||||||
- Frontend: http://localhost:3000
|
- Application: https://turftracker.kaspers.us
|
||||||
- Backend API: http://localhost:5000
|
- API: https://turftracker.kaspers.us/api
|
||||||
- Database: localhost:5432
|
- Database: Internal network only (not exposed)
|
||||||
|
|
||||||
### First Time Setup
|
### First Time Setup
|
||||||
|
|
||||||
1. **Create an admin account**
|
1. **Create an admin account**
|
||||||
- Go to http://localhost:3000/register
|
- Go to https://turftracker.kaspers.us/register
|
||||||
- Register with your email and password
|
- Register with your email and password
|
||||||
- The first user becomes an admin automatically
|
- The first user becomes an admin automatically
|
||||||
|
|
||||||
@@ -212,7 +222,7 @@ If you have an Authentik instance for SSO:
|
|||||||
- Create new "OAuth2/OpenID Provider"
|
- Create new "OAuth2/OpenID Provider"
|
||||||
- Set Authorization grant type: `authorization-code`
|
- Set Authorization grant type: `authorization-code`
|
||||||
- Set Client type: `confidential`
|
- 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
|
- Note the Client ID and Client Secret
|
||||||
|
|
||||||
2. **In your `.env` file:**
|
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_ID=your-client-id-from-authentik
|
||||||
AUTHENTIK_CLIENT_SECRET=your-client-secret-from-authentik
|
AUTHENTIK_CLIENT_SECRET=your-client-secret-from-authentik
|
||||||
AUTHENTIK_BASE_URL=https://your-authentik-domain.com
|
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:**
|
3. **In Authentik Applications:**
|
||||||
@@ -252,9 +262,7 @@ turftracker/
|
|||||||
│ └── package.json
|
│ └── package.json
|
||||||
├── database/ # PostgreSQL schema
|
├── database/ # PostgreSQL schema
|
||||||
│ └── init.sql # Database initialization
|
│ └── init.sql # Database initialization
|
||||||
├── nginx/ # Reverse proxy configuration
|
└── docker-compose.yml # Container orchestration with Traefik
|
||||||
│ └── nginx.conf
|
|
||||||
└── docker-compose.yml # Container orchestration
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage Guide
|
## Usage Guide
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ WORKDIR /app
|
|||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm ci --only=production
|
RUN npm install --only=production
|
||||||
|
|
||||||
# Copy source code
|
# Copy source code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -1,66 +1,80 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
turftracker:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
services:
|
services:
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
environment:
|
environment:
|
||||||
- REACT_APP_API_URL=http://localhost:5000
|
- REACT_APP_API_URL=https://turftracker.kaspers.us
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend:/app
|
- ./frontend:/app
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- 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:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=production
|
||||||
- DB_HOST=db
|
- DB_HOST=db
|
||||||
- DB_PORT=5432
|
- DB_PORT=5432
|
||||||
- DB_NAME=turftracker
|
- DB_NAME=${DB_NAME:-turftracker}
|
||||||
- DB_USER=turftracker
|
- DB_USER=${DB_USER:-turftracker}
|
||||||
- DB_PASSWORD=password123
|
- DB_PASSWORD=${DB_PASSWORD:-password123}
|
||||||
- JWT_SECRET=your-super-secret-jwt-key
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
- AUTHENTIK_CLIENT_ID=${AUTHENTIK_CLIENT_ID:-}
|
- AUTHENTIK_CLIENT_ID=${AUTHENTIK_CLIENT_ID:-}
|
||||||
- AUTHENTIK_CLIENT_SECRET=${AUTHENTIK_CLIENT_SECRET:-}
|
- AUTHENTIK_CLIENT_SECRET=${AUTHENTIK_CLIENT_SECRET:-}
|
||||||
- AUTHENTIK_BASE_URL=${AUTHENTIK_BASE_URL:-}
|
- 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:-}
|
- WEATHER_API_KEY=${WEATHER_API_KEY:-}
|
||||||
|
- FRONTEND_URL=https://turftracker.kaspers.us
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- 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:
|
db:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=turftracker
|
- POSTGRES_USER=${DB_USER:-turftracker}
|
||||||
- POSTGRES_PASSWORD=password123
|
- POSTGRES_PASSWORD=${DB_PASSWORD:-password123}
|
||||||
- POSTGRES_DB=turftracker
|
- POSTGRES_DB=${DB_NAME:-turftracker}
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
- ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
|
- ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
networks:
|
||||||
nginx:
|
- turftracker
|
||||||
image: nginx:alpine
|
# Database should not be exposed to proxy network for security
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
volumes:
|
|
||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
|
||||||
depends_on:
|
|
||||||
- frontend
|
|
||||||
- backend
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
@@ -7,7 +7,7 @@ WORKDIR /app
|
|||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm ci
|
RUN npm install
|
||||||
|
|
||||||
# Copy source code
|
# Copy source code
|
||||||
COPY . .
|
COPY . .
|
||||||
@@ -25,7 +25,7 @@ EXPOSE 3000
|
|||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
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 || exit 1
|
CMD wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1
|
||||||
|
|
||||||
# Start the application
|
# Start the application
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "start"]
|
||||||
151
nginx/nginx.conf
151
nginx/nginx.conf
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user