Skip to content

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

[Internet] → [Reverse Proxy :443] → [Frontend :8080]  (React + Nginx)
                                  ↘ [Backend  :8000]  (FastAPI)

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:

docker logs newhires-workers --tail=50
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