NGINX Reverse Proxy Deployment¶
📋 Overview¶
ARCP uses NGINX as a reverse proxy to provide:
- HTTPS Termination: Handles SSL/TLS encryption
- HTTP/2 Support: Modern protocol with multiplexing
- Load Balancing: Connection pooling to backend
- Security Headers: Automatic injection of security headers
- Rate Limiting: Built-in DDoS protection
- mTLS Support: Client certificate validation for enhanced security
- WebSocket Upgrade: Seamless WebSocket proxying
Architecture¶
HTTPS mode (ARCP_TLS_ENABLED=true, started with --profile tls):
flowchart LR
Client[Client/Agent] -->|HTTPS :443| NGINX[NGINX Container]
Client -->|HTTP :80| NGINX
NGINX -->|Redirect to HTTPS| NGINX
NGINX -->|HTTP :8001| ARCP[ARCP Backend]
NGINX -->|mTLS Headers| ARCP
ARCP -->|Response| NGINX
NGINX -->|HTTPS Response| Client HTTP mode (ARCP_TLS_ENABLED=false, NGINX not started):
flowchart LR
Client[Client/Agent] -->|HTTP :8001| ARCP[ARCP Backend]
ARCP -->|Response| Client Key Points: - Set ARCP_TLS_ENABLED=true and use --profile tls to start NGINX with HTTPS on ports 80/443 - Set ARCP_TLS_ENABLED=false to skip NGINX entirely — ARCP is directly accessible on http://localhost:8001 - When NGINX is active, it terminates SSL/TLS and forwards HTTP internally to the ARCP backend - Client certificates are validated by NGINX and forwarded to ARCP via headers
🚀 Quick Start¶
1. Generate SSL Certificates¶
Option A: Self-Signed (Development)
# Create certs directory
mkdir -p certs
# Generate self-signed certificate
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout certs/server.key \
-out certs/server.crt \
-subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"
Option B: Let's Encrypt (Production)
# Install certbot
apt-get update && apt-get install certbot
# Generate certificate
certbot certonly --standalone -d your-domain.com
# Copy certificates to certs directory
cp /etc/letsencrypt/live/your-domain.com/fullchain.pem certs/server.crt
cp /etc/letsencrypt/live/your-domain.com/privkey.pem certs/server.key
2. Configure Environment¶
Edit deployment/docker/.env:
# Set true to enable HTTPS via NGINX, false for HTTP-only (no NGINX)
ARCP_TLS_ENABLED=true
# COMPOSE_PROFILES controls which Docker Compose profiles are activated.
# It is read automatically by docker compose — no --profile flag needed.
# Must match ARCP_TLS_ENABLED:
# ARCP_TLS_ENABLED=true -> COMPOSE_PROFILES=tls
# ARCP_TLS_ENABLED=false -> COMPOSE_PROFILES=
COMPOSE_PROFILES=tls
# Certificate files (mounted to NGINX container)
ARCP_TLS_CERT_FILENAME=server.crt
ARCP_TLS_KEY_FILENAME=server.key
# Trust NGINX as reverse proxy
TRUSTED_HOSTS=localhost,127.0.0.1,nginx,arcp,redis,prometheus,grafana,jaeger,172.20.0.0/16
3. Start Services¶
Always use the same command — COMPOSE_PROFILES in .env handles everything:
COMPOSE_PROFILES=tls→ NGINX starts, HTTPS on 443COMPOSE_PROFILES=(empty) → NGINX skipped, HTTP only on 8001
4. Verify NGINX¶
# Check NGINX is running
docker ps | grep nginx
# Test HTTPS endpoint
curl -k https://localhost/health
# View NGINX logs
docker logs arcp-nginx
# Follow logs in real-time
docker logs -f arcp-nginx
📁 File Structure¶
project_root/
├── certs/
│ ├── server.crt # SSL certificate (required)
│ ├── server.key # SSL private key (required)
│ └── ca-bundle.crt # CA bundle for mTLS (optional)
├── deployment/
│ ├── nginx/
│ │ └── nginx.conf # NGINX configuration
│ └── docker/
│ └── docker-compose.yml
└── .env # Environment configuration
⚙️ Configuration¶
NGINX Container (docker-compose.yml)¶
The nginx service is gated behind the tls profile. COMPOSE_PROFILES in your .env activates it automatically — no --profile flag needed on the command line:
nginx:
image: nginx:alpine
container_name: arcp-nginx
restart: unless-stopped
profiles:
- tls # activated when COMPOSE_PROFILES=tls in .env
ports:
- "80:80" # HTTP (redirects to HTTPS)
- "443:443" # HTTPS
volumes:
- ../nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ../../certs:/etc/nginx/certs:ro
- nginx_logs:/var/log/nginx
depends_on:
arcp:
condition: service_healthy
networks:
- arcp_network
Tip: Set
COMPOSE_PROFILES=tls(andARCP_TLS_ENABLED=true) in.envto enable NGINX, orCOMPOSE_PROFILES=(empty, andARCP_TLS_ENABLED=false) to disable it. Always rundocker compose up -d --build— the.envfile controls everything.
Key Features (nginx.conf)¶
1. HTTP to HTTPS Redirect
2. HTTPS Server with mTLS
server {
listen 443 ssl;
http2 on;
# SSL certificates
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
# mTLS (optional client certificate)
ssl_verify_client optional_no_ca;
ssl_verify_depth 2;
# Modern TLS configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:...';
}
3. Backend Proxy
location / {
proxy_pass http://arcp:8001;
# Forward client information
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;
# mTLS: Forward client certificate
proxy_set_header X-Client-Cert $ssl_client_escaped_cert;
proxy_set_header X-Client-Verify $ssl_client_verify;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
4. Rate Limiting
# Define rate limit zones
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/m;
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=10r/m;
# Apply to locations
location / {
limit_req zone=api_limit burst=20 nodelay;
}
location ~ ^/(api/auth|api/login) {
limit_req zone=auth_limit burst=5 nodelay;
}
5. Security Headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
🔒 mTLS Configuration¶
Enable Client Certificate Validation¶
1. Prepare CA Bundle
2. Update nginx.conf
server {
listen 443 ssl;
# SSL certificate
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
# mTLS: Require client certificate
ssl_client_certificate /etc/nginx/certs/ca-bundle.crt;
ssl_verify_client on; # or 'optional' for mixed mode
ssl_verify_depth 2;
location / {
# Forward client cert to backend
proxy_set_header X-Client-Cert $ssl_client_escaped_cert;
proxy_set_header X-Client-Verify $ssl_client_verify;
proxy_set_header X-Client-S-DN $ssl_client_s_dn;
proxy_pass http://arcp:8001;
}
}
3. Enable in ARCP
Edit .env:
4. Test with Client Certificate
🔧 Customization¶
Adjust Rate Limits¶
Edit deployment/nginx/nginx.conf:
# More restrictive
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=50r/m;
# More permissive
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=200r/m;
Custom SSL Ciphers¶
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
Add Custom Headers¶
Increase Upload Limit¶
🐛 Troubleshooting¶
Certificate Errors¶
Problem: "SSL certificate problem: self signed certificate"
Solution:
# Development: Use -k flag to bypass verification
curl -k https://localhost/health
# Production: Use valid CA-signed certificate
# or add self-signed cert to trust store
502 Bad Gateway¶
Problem: NGINX can't reach backend
Solution:
# Check ARCP container is running
docker ps | grep arcp
# Check ARCP health
docker exec arcp curl http://localhost:8001/health
# Verify network connectivity
docker exec arcp-nginx ping arcp
# Check NGINX logs
docker logs arcp-nginx
WebSocket Connection Failed¶
Problem: WebSocket upgrade not working
Solution: Ensure these headers are set in nginx.conf:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
Rate Limit Too Restrictive¶
Problem: "429 Too Many Requests"
Solution:
# Increase burst size
location / {
limit_req zone=api_limit burst=50 nodelay;
}
# Or increase rate limit
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=200r/m;
Reload Configuration¶
After modifying nginx.conf:
# Test configuration
docker exec arcp-nginx nginx -t
# Reload (graceful)
docker exec arcp-nginx nginx -s reload
# Or restart container
docker restart arcp-nginx
📊 Monitoring¶
View NGINX Logs¶
# Access logs
docker exec arcp-nginx tail -f /var/log/nginx/access.log
# Error logs
docker exec arcp-nginx tail -f /var/log/nginx/error.log
# Logs on host (if volume mounted)
tail -f /var/lib/docker/volumes/arcp_nginx_logs/_data/access.log
Log Format¶
Access logs include timing information:
127.0.0.1 - - [16/Feb/2026:10:30:45 +0000] "GET /api/agents HTTP/2.0" 200 1234
rt=0.050 uct="0.001" uht="0.005" urt="0.044"
Where: - rt: Total request time - uct: Upstream connect time - uht: Upstream header time - urt: Upstream response time
Health Check¶
# NGINX health
curl http://localhost/health
# Backend health (via NGINX)
curl -k https://localhost/health
🔐 Security Best Practices¶
✅ Do: - Use valid SSL certificates in production - Enable HSTS (Strict-Transport-Security) - Keep NGINX updated (use latest alpine image) - Restrict TLS to 1.2+ only - Enable rate limiting on all endpoints - Use mTLS for agent authentication - Monitor NGINX logs for suspicious activity
❌ Don't: - Use self-signed certificates in production - Expose ARCP backend directly (always use NGINX) - Allow weak TLS ciphers (SSL 3.0, TLS 1.0, TLS 1.1) - Set rate limits too high - Ignore certificate expiration dates - Allow unlimited request sizes