Reverse Proxy Configuration¶
Configure a reverse proxy to expose the New Hires Reporting System to the internet with HTTPS support.
Overview¶
In production, Docker containers bind to localhost only for security. A reverse proxy is required to:
- ✅ Provide HTTPS/SSL termination
- ✅ Handle public internet traffic
- ✅ Add security headers
- ✅ Enable rate limiting
- ✅ Support load balancing (if needed)
Architecture¶
Nginx Configuration¶
Nginx is the recommended reverse proxy for production deployments.
Prerequisites¶
# Install Nginx
sudo apt update
sudo apt install nginx -y
# Start and enable Nginx
sudo systemctl start nginx
sudo systemctl enable nginx
# Verify installation
nginx -v
Frontend Configuration¶
Create /etc/nginx/sites-available/newhires-frontend:
server {
listen 80;
listen [::]:80;
server_name your-domain.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your-domain.com;
# SSL Configuration (see SSL Certificates guide)
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# 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 "no-referrer-when-downgrade" always;
# React frontend (served by Nginx container)
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
# Standard proxy headers
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;
# Caching for static assets
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_read_timeout 60s;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
}
# Logging
access_log /var/log/nginx/newhires-frontend-access.log;
error_log /var/log/nginx/newhires-frontend-error.log;
}
Backend Configuration¶
Create /etc/nginx/sites-available/newhires-backend:
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=upload_limit:10m rate=5r/m;
server {
listen 80;
listen [::]:80;
server_name api.your-domain.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name api.your-domain.com;
# SSL Configuration
ssl_certificate /etc/letsencrypt/live/api.your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.your-domain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# 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;
# CORS headers (for frontend access)
add_header Access-Control-Allow-Origin "https://your-domain.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
# FastAPI backend
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
# Standard proxy headers
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;
# Rate limiting (general API)
limit_req zone=api_limit burst=20 nodelay;
# File upload support
client_max_body_size 50M;
# Timeouts (longer for file uploads and AI processing)
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
proxy_send_timeout 300s;
}
# File upload endpoints (stricter rate limit)
location /api/v1/files {
proxy_pass http://127.0.0.1:8000;
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;
# Stricter rate limit for uploads
limit_req zone=upload_limit burst=2 nodelay;
client_max_body_size 50M;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
proxy_send_timeout 300s;
}
# Health check endpoint (no rate limiting, no logging)
location /health {
proxy_pass http://127.0.0.1:8000/health;
proxy_http_version 1.1;
proxy_set_header Host $host;
access_log off;
}
# Logging
access_log /var/log/nginx/newhires-backend-access.log;
error_log /var/log/nginx/newhires-backend-error.log;
}
Enable Sites¶
# Create symbolic links
sudo ln -s /etc/nginx/sites-available/newhires-frontend \
/etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/newhires-backend \
/etc/nginx/sites-enabled/
# Remove default site (optional)
sudo rm /etc/nginx/sites-enabled/default
# Test configuration
sudo nginx -t
# Reload Nginx
sudo systemctl reload nginx
# Check status
sudo systemctl status nginx
Caddy Configuration¶
Caddy automatically handles HTTPS with Let's Encrypt.
Prerequisites¶
# Install Caddy
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
# Verify installation
caddy version
Caddyfile¶
Create /etc/caddy/Caddyfile:
# Frontend
your-domain.com {
# Automatic HTTPS (Let's Encrypt)
reverse_proxy 127.0.0.1:8080
# Security headers
header {
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
X-XSS-Protection "1; mode=block"
Referrer-Policy no-referrer-when-downgrade
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
}
# Access log
log {
output file /var/log/caddy/newhires-frontend.log
}
}
# Backend
api.your-domain.com {
# Automatic HTTPS (Let's Encrypt)
reverse_proxy 127.0.0.1:8000
# CORS
header {
Access-Control-Allow-Origin "https://your-domain.com"
Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Access-Control-Allow-Headers "Content-Type, Authorization"
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
}
# File upload size limit
request_body {
max_size 50MB
}
# Timeouts
transport http {
read_timeout 300s
write_timeout 300s
}
# Access log
log {
output file /var/log/caddy/newhires-backend.log
}
}
Start Caddy¶
# Test configuration
sudo caddy validate --config /etc/caddy/Caddyfile
# Create log directory
sudo mkdir -p /var/log/caddy
sudo chown caddy:caddy /var/log/caddy
# Start Caddy
sudo systemctl enable caddy
sudo systemctl start caddy
# Check status
sudo systemctl status caddy
# View logs
sudo journalctl -u caddy -f
Traefik Configuration¶
Traefik is Docker-native and integrates well with Docker Compose.
Docker Compose with Traefik¶
Modify your docker-compose.prod.yml:
version: '3.8'
services:
traefik:
image: traefik:v2.10
container_name: traefik
restart: unless-stopped
command:
- "--api.insecure=false"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
# HTTP to HTTPS redirect
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
networks:
- newhires-network
backend:
image: 878796852397.dkr.ecr.us-east-1.amazonaws.com/newhires-backend:${IMAGE_TAG}
labels:
- "traefik.enable=true"
- "traefik.http.routers.backend.rule=Host(`api.your-domain.com`)"
- "traefik.http.routers.backend.entrypoints=websecure"
- "traefik.http.routers.backend.tls.certresolver=letsencrypt"
- "traefik.http.services.backend.loadbalancer.server.port=8000"
# CORS middleware
- "traefik.http.middlewares.cors.headers.accesscontrolalloworigin=https://your-domain.com"
- "traefik.http.middlewares.cors.headers.accesscontrolallowmethods=GET,POST,PUT,DELETE,OPTIONS"
- "traefik.http.middlewares.cors.headers.accesscontrolallowheaders=Content-Type,Authorization"
- "traefik.http.routers.backend.middlewares=cors@docker"
networks:
- newhires-network
frontend:
image: 878796852397.dkr.ecr.us-east-1.amazonaws.com/newhires-frontend:${IMAGE_TAG}
labels:
- "traefik.enable=true"
- "traefik.http.routers.frontend.rule=Host(`your-domain.com`)"
- "traefik.http.routers.frontend.entrypoints=websecure"
- "traefik.http.routers.frontend.tls.certresolver=letsencrypt"
- "traefik.http.services.frontend.loadbalancer.server.port=8080"
networks:
- newhires-network
workers:
image: 878796852397.dkr.ecr.us-east-1.amazonaws.com/newhires-workers:${IMAGE_TAG}
# No Traefik labels - workers don't need external access
networks:
- newhires-network
db:
image: postgres:16
# No Traefik labels - database is internal only
networks:
- newhires-network
networks:
newhires-network:
name: newhires-network
Apache Configuration¶
Prerequisites¶
# Install Apache
sudo apt update
sudo apt install apache2 -y
# Enable required modules
sudo a2enmod proxy proxy_http ssl headers rewrite
# Verify modules
apache2ctl -M | grep proxy
Frontend Configuration¶
Create /etc/apache2/sites-available/newhires-frontend.conf:
<VirtualHost *:80>
ServerName your-domain.com
# Redirect to HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>
<VirtualHost *:443>
ServerName your-domain.com
# SSL Configuration
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/your-domain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/your-domain.com/privkey.pem
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
# Security headers
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
# Proxy settings
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8080/
ProxyPassReverse / http://127.0.0.1:8080/
ProxyTimeout 60
# Logging
CustomLog /var/log/apache2/newhires-frontend-access.log combined
ErrorLog /var/log/apache2/newhires-frontend-error.log
</VirtualHost>
Backend Configuration¶
Create /etc/apache2/sites-available/newhires-backend.conf:
<VirtualHost *:80>
ServerName api.your-domain.com
# Redirect to HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>
<VirtualHost *:443>
ServerName api.your-domain.com
# SSL Configuration
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/api.your-domain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/api.your-domain.com/privkey.pem
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
# Security headers
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
# CORS headers
Header always set Access-Control-Allow-Origin "https://your-domain.com"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Content-Type, Authorization"
# Proxy settings
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/
# File upload size
LimitRequestBody 52428800
ProxyTimeout 300
# Logging
CustomLog /var/log/apache2/newhires-backend-access.log combined
ErrorLog /var/log/apache2/newhires-backend-error.log
</VirtualHost>
Enable Sites¶
# Enable sites
sudo a2ensite newhires-frontend
sudo a2ensite newhires-backend
# Disable default site (optional)
sudo a2dissite 000-default
# Test configuration
sudo apache2ctl configtest
# Reload Apache
sudo systemctl reload apache2
# Check status
sudo systemctl status apache2
Testing¶
Verify Frontend¶
# Test HTTP redirect
curl -I http://your-domain.com
# Expected: 301 Moved Permanently → https://your-domain.com
# Test HTTPS
curl -I https://your-domain.com
# Expected: 200 OK
# Test from browser
open https://your-domain.com
Verify Backend¶
# Test health endpoint
curl https://api.your-domain.com/health
# Expected response:
# {"status":"healthy","database":"connected","workers":"operational"}
# Test CORS
curl -H "Origin: https://your-domain.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-X OPTIONS \
https://api.your-domain.com/api/v1/files
# Test from browser console (should work without CORS errors)
fetch('https://api.your-domain.com/health')
.then(r => r.json())
.then(console.log)
Troubleshooting¶
"502 Bad Gateway"¶
Cause: Backend service not running or not accessible
Solutions:
# Check if services are running
docker-compose -f docker-compose.prod.yml ps
# Check backend health directly
curl http://127.0.0.1:8000/health
curl http://127.0.0.1:8080
# Check reverse proxy logs
sudo tail -f /var/log/nginx/error.log # Nginx
sudo journalctl -u caddy -f # Caddy
sudo tail -f /var/log/apache2/error.log # Apache
# Restart Docker services
docker-compose -f docker-compose.prod.yml restart
# Restart reverse proxy
sudo systemctl restart nginx # or caddy/apache2
"504 Gateway Timeout"¶
Cause: Request took too long (AI processing, large file)
Solutions: 1. Increase timeouts in reverse proxy config (already set to 300s) 2. Check worker logs for Bedrock timeouts:
3. Verify AWS Bedrock is responding 4. Check job queue for stuck jobs"CORS Policy Blocked"¶
Cause: CORS headers not configured correctly
Solutions:
1. Verify Access-Control-Allow-Origin matches your frontend domain exactly
2. Ensure CORS headers are in backend proxy config
3. Restart reverse proxy after config changes
4. Clear browser cache or use incognito mode
5. Check browser console for exact CORS error message
SSL Certificate Errors¶
Cause: Invalid, expired, or misconfigured certificate
Solutions:
# Check certificates (Let's Encrypt)
sudo certbot certificates
# Renew if needed
sudo certbot renew
# Test certificate validity
openssl s_client -connect your-domain.com:443 -servername your-domain.com
# Verify certificate paths in config
sudo ls -la /etc/letsencrypt/live/your-domain.com/
See SSL Certificates Guide for detailed setup.
Security Best Practices¶
- ✅ Always redirect HTTP to HTTPS
- ✅ Use TLSv1.2 or TLSv1.3 only (disable older versions)
- ✅ Enable HSTS (Strict-Transport-Security header)
- ✅ Configure security headers (X-Frame-Options, CSP, etc.)
- ✅ Set CORS to specific origin (never use
*in production) - ✅ Enable rate limiting to prevent abuse
- ✅ Limit file upload sizes (50MB default)
- ✅ Set reasonable timeouts
- ✅ Enable access logs for monitoring
- ✅ Keep reverse proxy software updated
- ✅ Use a firewall (UFW, iptables)
- ✅ Disable directory listing
- ✅ Hide server version headers
Firewall Configuration¶
UFW (Uncomplicated Firewall)¶
# Allow SSH (important - don't lock yourself out!)
sudo ufw allow ssh
# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Enable firewall
sudo ufw enable
# Check status
sudo ufw status
Next Steps¶
- SSL Certificates - Set up HTTPS
- Health Monitoring - Monitor your deployment
- Security Hardening - Additional security measures
- Common Issues - Troubleshooting guide