nginx Reference
Minimal should never be exposed directly to the internet. nginx sits in front as a reverse proxy, handling TLS termination, CORS, and request forwarding.
Minimal Architecture with nginx
Client → nginx (443) → Minimal (3045) → Database
nginx is responsible for:
- TLS termination
- CORS preflight responses
- Forwarding
X-Forwarded-*headers - Optionally injecting identity headers from your auth system
Minimal is responsible for:
- Processing the request
- Querying the database
- Returning the JSON response
Base Configuration
server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /etc/ssl/certs/your-cert.pem;
ssl_certificate_key /etc/ssl/private/your-key.pem;
# ── Auto REST API ──
location /minimal/api/ {
# Handle CORS preflight
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'X-Org-Id, X-Project-Id, X-Space-Id, X-User-Id, X-User-Roles, Content-Type' always;
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
return 204;
}
# CORS headers on all responses
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'X-Org-Id, X-Project-Id, X-Space-Id, X-User-Id, X-User-Roles, Content-Type' always;
proxy_pass http://127.0.0.1:3045/;
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;
proxy_read_timeout 30s;
proxy_connect_timeout 10s;
proxy_send_timeout 30s;
}
# ── System API ──
# Restrict org/project creation to internal network only
location /minimal/system/ {
# Block all external access — allow only internal IPs
allow 127.0.0.1;
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
proxy_pass http://127.0.0.1:3045/;
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;
}
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name your-domain.com;
return 301 https://$host$request_uri;
}
CORS Configuration
Allow all origins (development)
add_header 'Access-Control-Allow-Origin' '*' always;
Restrict to specific origins (production)
map $http_origin $cors_origin {
default '';
'https://app.your-domain.com' $http_origin;
'https://docs.your-domain.com' $http_origin;
'http://localhost:3000' $http_origin;
'http://localhost:3001' $http_origin;
}
server {
...
location /minimal/api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'X-Org-Id, X-Project-Id, X-Space-Id, X-User-Id, X-User-Roles, Content-Type' always;
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'X-Org-Id, X-Project-Id, X-Space-Id, X-User-Id, X-User-Roles, Content-Type' always;
proxy_pass http://127.0.0.1:3045/;
}
}
Injecting Identity Headers
If your auth system validates tokens at the nginx layer (e.g. using auth_request), you can inject identity headers directly in nginx before forwarding to Minimal:
location /minimal/api/ {
# Your auth subrequest — validates token and sets $user_id, $user_roles etc.
auth_request /auth/validate;
auth_request_set $user_id $upstream_http_x_user_id;
auth_request_set $user_roles $upstream_http_x_user_roles;
auth_request_set $org_id $upstream_http_x_org_id;
auth_request_set $project_id $upstream_http_x_project_id;
auth_request_set $space_id $upstream_http_x_space_id;
# Inject into the upstream request to Minimal
proxy_set_header X-User-Id $user_id;
proxy_set_header X-User-Roles $user_roles;
proxy_set_header X-Org-Id $org_id;
proxy_set_header X-Project-Id $project_id;
proxy_set_header X-Space-Id $space_id;
proxy_pass http://127.0.0.1:3045/;
}
# Internal auth validation endpoint
location = /auth/validate {
internal;
proxy_pass http://127.0.0.1:4000/validate;
proxy_pass_request_body off;
proxy_set_header Content-Length '';
proxy_set_header X-Original-URI $request_uri;
}
Rate Limiting
Protect Minimal from excessive requests:
# Define rate limit zones — add above the server block
limit_req_zone $binary_remote_addr zone=minimal_api:10m rate=100r/s;
limit_req_zone $binary_remote_addr zone=minimal_system:10m rate=5r/m;
server {
...
location /minimal/api/ {
# Allow bursts of 20 requests, then enforce 100 req/s
limit_req zone=minimal_api burst=20 nodelay;
...
}
location /minimal/system/ {
# System API — very low rate, internal only
limit_req zone=minimal_system burst=2 nodelay;
...
}
}
Timeouts
Match nginx timeouts to Minimal's configured timeout values:
location /minimal/api/ {
proxy_connect_timeout 10s; # matches timeout.timeout in config.yml
proxy_read_timeout 30s; # allow time for slow queries
proxy_send_timeout 30s;
keepalive_timeout 65s;
}
Gzip Compression
Compress JSON responses — especially useful for large list results:
gzip on;
gzip_types application/json;
gzip_min_length 1024;
gzip_proxied any;
gzip_vary on;
Testing and Reloading
# Test config syntax
nginx -t
# Reload without downtime
nginx -s reload
# Full restart
systemctl restart nginx
# Check nginx error log
tail -f /var/log/nginx/error.log
# Test CORS preflight manually
curl -X OPTIONS https://your-domain.com/minimal/api/rest/auto/v1/ms/demo_db/users \
-H "Origin: http://localhost:3001" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: X-Org-Id,X-Project-Id,X-Space-Id,X-User-Id,X-User-Roles" \
-v 2>&1 | grep -E "< HTTP|Access-Control"
Common nginx Issues
| Symptom | Cause | Fix |
|---|---|---|
502 Bad Gateway | Minimal not running or wrong port | Confirm Minimal is up — systemctl status minimal — and proxy_pass port matches service.port |
| CORS error in browser | OPTIONS preflight not handled | Ensure the if ($request_method = 'OPTIONS') block exists and returns 204 |
| Headers missing on error responses | add_header without always | Add always to every add_header directive |
403 on System API from outside | deny all working correctly | System API is intentionally restricted to internal IPs only |
413 Request Entity Too Large | nginx body size limit | Add client_max_body_size 10m; inside the location block |
| Requests timing out | nginx timeout shorter than Minimal's | Increase proxy_read_timeout to match or exceed timeout.max_elapsed_time in config.yml |