Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 104 additions & 32 deletions apps/web/nginx.conf
Original file line number Diff line number Diff line change
@@ -1,45 +1,26 @@
# Server block - defines how to handle requests
# HTTP Server - Works without SSL certificates
server {
listen 80;
listen [::]:80;

# ( _ for default/catch-all)
server_name _;

# Root directory where static files are located
# This is where we copied our React build output
root /usr/share/nginx/html;

# Default file to serve
index index.html;

location / {
# try_files directive - tries files in order:
# 1. $uri - Try the exact URI (e.g., /logo.png)
# 2. $uri/ - Try as a directory with index.html
# 3. /index.html - Fallback to index.html for all other routes
#
# This is CRITICAL for React Router:
# - /dashboard → index.html (React Router handles it)
# - /projects/123 → index.html (React Router handles it)
# - /static/logo.png → serves the actual file
try_files $uri $uri/ /index.html;

# Add headers for browser caching
add_header Cache-Control "no-cache, must-revalidate";
}

# Cache static assets for 1 year (they have content hashes)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";

# Allow files to be served from this location
try_files $uri =404;
}


# This file is generated at container startup
# Contains runtime environment variables
# Runtime environment variables
location = /env-config.js {
add_header Cache-Control "no-store, no-cache, must-revalidate";
try_files $uri =404;
Expand Down Expand Up @@ -68,20 +49,12 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
}


# Security Headers

# Prevent clickjacking attacks
add_header X-Frame-Options "SAMEORIGIN" always;

# Prevent MIME type sniffing
add_header X-Content-Type-Options "nosniff" always;

# Enable XSS protection
add_header X-XSS-Protection "1; mode=block" always;

# Compression
# Enable gzip compression for better performance
gzip on;
gzip_vary on;
gzip_min_length 1024;
Expand All @@ -100,8 +73,108 @@ server {
image/svg+xml;

# Error Pages
error_page 404 /index.html;

# Don't log access to favicon (reduces log noise)
location = /favicon.ico {
log_not_found off;
access_log off;
}

location = /robots.txt {
log_not_found off;
access_log off;
}
}

# HTTPS Server - Only active if SSL certificates are mounted
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;

server_name _;

# Custom error page (could create 404.html in build)
# SSL Configuration
# These paths should match where certificates are mounted in docker-compose
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;

# SSL Settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

root /usr/share/nginx/html;
index index.html;

location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, must-revalidate";
}

# Cache static assets for 1 year (they have content hashes)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}

# Runtime environment variables
location = /env-config.js {
add_header Cache-Control "no-store, no-cache, must-revalidate";
try_files $uri =404;
}

# API Proxy - Routes API requests to backend
location /api {
proxy_pass http://backend:3002;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
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 proxy
location /health {
proxy_pass http://backend:3002/health;
proxy_http_version 1.1;
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;
}

# Security Headers (with HSTS for HTTPS)
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 Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# Compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/rss+xml
font/truetype
font/opentype
application/vnd.ms-fontobject
image/svg+xml;

# Error Pages
error_page 404 /index.html;

# Don't log access to favicon (reduces log noise)
Expand All @@ -110,7 +183,6 @@ server {
access_log off;
}

# Don't log access to robots.txt
location = /robots.txt {
log_not_found off;
access_log off;
Expand Down