Skip to main content

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

SymptomCauseFix
502 Bad GatewayMinimal not running or wrong portConfirm Minimal is up — systemctl status minimal — and proxy_pass port matches service.port
CORS error in browserOPTIONS preflight not handledEnsure the if ($request_method = 'OPTIONS') block exists and returns 204
Headers missing on error responsesadd_header without alwaysAdd always to every add_header directive
403 on System API from outsidedeny all working correctlySystem API is intentionally restricted to internal IPs only
413 Request Entity Too Largenginx body size limitAdd client_max_body_size 10m; inside the location block
Requests timing outnginx timeout shorter than Minimal'sIncrease proxy_read_timeout to match or exceed timeout.max_elapsed_time in config.yml