Skip to content

NGINX CORS Configuration — Adding Cross-Origin Headers at the Reverse Proxy Layer

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about NGINX CORS Configuration. We cover key concepts, practical examples, and best practices to help you master this topic.

NGINX can add CORS headers at the reverse proxy layer, handling preflight requests and injecting Access-Control-* headers before forwarding to backend servers, enabling centralized CORS management.

What You'll Learn

  • Adding CORS headers in NGINX location blocks
  • Handling OPTIONS preflight requests
  • Dynamic origin configuration with maps

Why It Matters

Reverse proxy CORS configuration centralizes cross-origin policy management without modifying backend code. DodaTech's API Gateway uses NGINX to apply consistent CORS policies across Microservices.

flowchart LR
    A["Browser"] --> B["NGINX Proxy"]
    B --> C{"Request method?"}
    C -->|"OPTIONS"| D["NGINX responds with CORS headers"]
    C -->|"Other"| E["Forward to backend"]
    E --> F["Backend responds"]
    F --> G["NGINX adds CORS headers"]
    G --> H["Response to browser"]
    style D fill:#fef08a,stroke:#ca8a04
    style B fill:#dbeafe,stroke:#2563eb

Code Examples

# Basic CORS configuration in NGINX
server {
    listen 443 ssl;
    server_name api.example.com;

    location /api/ {
        # Handle preflight
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
            add_header 'Access-Control-Max-Age' 86400;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        # Add CORS headers to actual responses
        add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        add_header 'Access-Control-Expose-Headers' 'X-RateLimit-Remaining' always;

        proxy_pass http://backend:3000;
    }
}
# Dynamic CORS origins with map
map $http_origin $cors_origin {
    default "";
    "~^https://app\.example\.com$" "$http_origin";
    "~^https://[a-z]+\.admin\.example\.com$" "$http_origin";
    "~^https://dashboard\.example\.com$" "$http_origin";
}

map $request_method $cors_methods {
    default "GET, POST, PUT, DELETE, OPTIONS";
    OPTIONS "GET, POST, PUT, DELETE, OPTIONS";
}

server {
    location /api/ {
        if ($cors_origin = "") {
            # Origin not allowed, do not add CORS headers
        }

        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' $cors_origin;
            add_header 'Access-Control-Allow-Methods' $cors_methods;
            add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Max-Age' 86400;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        add_header 'Access-Control-Allow-Origin' $cors_origin always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        proxy_pass http://backend:3000;
    }
}
# CORS for multiple subdomains with regex
map $http_origin $allow_origin {
    default "false";
    "~^https://(www\.)?example\.com$" "true";
    "~^https://[a-z0-9-]+\.example\.com$" "true";
}

server {
    location /api/ {
        if ($allow_origin = "true") {
            add_header 'Access-Control-Allow-Origin' $http_origin always;
        }

        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' $http_origin;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'DNT, User-Agent, X-Requested-With, Content-Type, Authorization';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain; charset=utf-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        proxy_pass http://backend:3000;
    }
}
# Test NGINX CORS headers
curl -I -H "Origin: https://app.example.com" \
  https://api.example.com/api/data

# Test preflight
curl -X OPTIONS -I \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  https://api.example.com/api/data

Common Mistakes

1. Using if Blocks Incorrectly

NGINX if statements can cause unexpected behavior. Use maps for origin validation instead.

2. Not Using the Always Parameter

Without always, add_header is only applied for 2xx and 3xx responses. Use always for all responses.

3. Forgetting to Handle OPTIONS for Preflight

Without OPTIONS handling, preflight requests are forwarded to the backend, which may block them.

4. Conflicting Headers with Backend

If the backend also sets CORS headers, they can duplicate or conflict. Remove backend CORS when using NGINX.

5. Not Validating Dynamic Origins

Using $http_origin directly without validation allows any origin. Always validate with a map.

Practice Questions

  1. How do you add CORS headers in NGINX?
  2. How does NGINX handle OPTIONS preflight requests?
  3. What is the map directive used for in CORS configuration?
  4. Why use the always parameter with add_header?
  5. How do you handle multiple allowed origins?

Answers:

  1. Use the add_header directive with Access-Control-* headers.
  2. Use an if ($request_method = 'OPTIONS') block to return appropriate headers.
  3. The map directive evaluates the Origin header against patterns and returns the allowed origin or empty string.
  4. The always parameter ensures headers are added to all responses, including errors.
  5. Use the map directive with regex patterns to validate and return the matching origin.

Challenge: Set up NGINX as a reverse proxy for a Microservices Architecture with three backend services, each with different CORS requirements. Implement a unified CORS configuration with dynamic origins, proper preflight handling, and centralized logging of CORS rejections.

FAQ

Does NGINX CORS configuration affect performance?

Minimal. Header injection happens at the proxy layer before forwarding to the backend. The overhead is negligible compared to request processing.

Can I use NGINX to override backend CORS headers?

Yes. Use proxy_hide_header to remove backend CORS headers, then add your own headers with add_header. This gives you full control at the proxy level.

How do I handle CORS for WebSocket connections in NGINX?

WebSocket connections have different headers. Use proxy_set_header Upgrade and proxy_set_header Connection in addition to CORS headers for WebSocket endpoints.

What is the difference between add_header and set_header in NGINX?

add_header adds a header to the response. set_header modifies a request header before forwarding to the backend. CORS uses add_header for response headers.

How do I debug CORS issues in NGINX?

Add the error_log and access_log directives with debug level. Check the NGINX error log for CORS-related issues and use curl to test header responses.

Mini Project

Build an NGINX-based API gateway with dynamic CORS support for multiple backend services. Implement origin validation with maps, preflight handling, credential support, and CORS header stripping from backend responses. Add a monitoring endpoint that reports CORS rejection statistics.

What's Next

Learn Apache CORS configuration using mod_headers, then explore CORS error handling patterns for debugging.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro