Nginx Config File Structure Explained

9 min read·Matthieu|

A complete walkthrough of how Nginx configuration files are organized on disk, how contexts nest inside each other, how include directives pull in files, and how directive inheritance actually works.

You installed Nginx. You opened /etc/nginx/nginx.conf and found a file that includes other files, which include more files. Some directives sit inside curly braces, others float at the top level. Before you start configuring server blocks, SSL, or reverse proxies, you need the map.

This article gives you that map. No recipes. Just the mental model for how Nginx configuration works, so you can read and modify any config you encounter.

[-> install-nginx-debian-ubuntu]

Where are Nginx configuration files stored?

On Debian 12 and Ubuntu 24.04, the apt package installs everything under /etc/nginx/. Here is what that directory looks like on a fresh install:

/etc/nginx/
├── nginx.conf              # Main config entry point
├── mime.types              # Maps file extensions to MIME types
├── conf.d/                 # Drop-in config files (*.conf auto-included)
├── sites-available/        # All virtual host config files
│   └── default             # Default server block
├── sites-enabled/          # Symlinks to active virtual hosts
│   └── default -> ../sites-available/default
├── snippets/               # Reusable config fragments
│   └── fastcgi-php.conf
├── modules-available/      # Available dynamic module configs
├── modules-enabled/        # Symlinks to loaded modules
├── fastcgi.conf            # FastCGI directive defaults
├── fastcgi_params          # FastCGI parameter mappings
├── proxy_params            # Proxy header defaults
├── scgi_params             # SCGI parameter mappings
├── uwsgi_params            # uWSGI parameter mappings
├── koi-utf                 # Character set mapping files
├── koi-win
└── win-utf

You can verify this on your own server:

ls -la /etc/nginx/

The file you edit most often is not nginx.conf itself. It is usually a file inside sites-available/ or conf.d/. The main nginx.conf sets global defaults and pulls in those files via include directives.

How are Nginx config files organized?

Nginx configuration uses a tree of nested contexts. Each context is a block directive wrapped in curly braces. Directives placed inside a context only apply within that scope.

The hierarchy looks like this:

main (top level, outside any braces)
├── events { }
└── http { }
    └── server { }
        └── location { }

Every directive in the config lives at one of these levels. The main context is the file itself. Everything else nests inside it.

What does the main context control?

The main context is everything outside of any block directive in nginx.conf. It controls process-level settings that affect the entire Nginx instance.

Key directives at this level:

Directive Purpose Typical value
user OS user the worker processes run as www-data
worker_processes Number of worker processes auto (matches CPU cores)
pid Path to the PID file /run/nginx.pid
error_log Global error log path and level /var/log/nginx/error.log
include Load module configs /etc/nginx/modules-enabled/*.conf

Here is the main context from the default Debian 12 nginx.conf:

user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;

These five lines run before anything else. They set who Nginx runs as, how many workers it spawns, and where it writes errors.

What goes in the events block?

The events block configures how Nginx handles connections at the OS level. It sits inside the main context.

events {
    worker_connections 768;
    # multi_accept on;
}

worker_connections sets the maximum number of simultaneous connections per worker process. With worker_processes auto on a 4-core machine, you get 4 x 768 = 3,072 concurrent connections. The default from the Nginx source is 512, but Debian sets it to 768.

What is the http context for?

The http block contains all configuration related to handling HTTP traffic. Every server block lives inside it. Directives you place here apply to all virtual hosts unless a server or location block overrides them.

http {
    sendfile on;
    tcp_nopush on;
    types_hash_max_size 2048;
    server_tokens off;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    access_log /var/log/nginx/access.log;

    gzip on;

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Notice server_tokens off; near the top. This hides the Nginx version number from response headers. Version disclosure helps attackers target known vulnerabilities. The Debian default does not include this line. Add it.

The two include lines at the bottom are how Nginx pulls in your actual site configurations. We will cover include in detail below.

How do server blocks work?

A server block defines a virtual host. It lives inside the http context. Each server block listens on an address/port combination and matches requests by the Host header.

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    root /var/www/example.com/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

When a request arrives, Nginx picks the server block by matching server_name against the Host header. If no block matches, Nginx uses the default_server:

listen 80 default_server;

You typically create one file per domain in sites-available/, each containing one server block (or two, if you have both HTTP and HTTPS).

How does Nginx match location blocks?

A location block defines how Nginx handles requests for specific URI patterns. It lives inside a server block (or inside another location).

Nginx evaluates location blocks in a specific order:

Modifier Type Example Behavior
= Exact match location = / Matches only /. Stops searching immediately.
^~ Priority prefix location ^~ /images/ Matches prefix. Skips regex checks.
~ Regex (case-sensitive) location ~ \.php$ First regex match wins.
~* Regex (case-insensitive) location ~* \.(jpg|png)$ First regex match wins.
(none) Prefix location /api/ Longest prefix wins, but regex can override.

The matching algorithm:

  1. Nginx checks all prefix locations and remembers the longest match.
  2. If that match uses = or ^~, stop. Use it.
  3. Otherwise, check regex locations in the order they appear in the config file.
  4. First regex match wins. If no regex matches, use the longest prefix from step 1.

This means the order of prefix locations in your config file does not matter. But regex order does.

How does the include directive work?

The include directive inserts the contents of another file (or files matching a glob pattern) into the current position in the config. Nginx resolves it at config load time, before the config is parsed as a whole.

include /etc/nginx/mime.types;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;

The glob *.conf matches all files ending in .conf in that directory. The glob * matches everything. Both are common patterns.

The include directive works in any context. You can use it inside http, server, or location blocks:

server {
    listen 443 ssl;
    include snippets/ssl-params.conf;
}

This is how snippets/ works. You write a reusable fragment once and include it wherever you need it.

One thing to watch: Nginx fails to start if an include glob matches zero files and the path is not a glob. If you write include /etc/nginx/conf.d/*.conf; and the directory is empty, Nginx starts fine (globs are allowed to match nothing). But include /etc/nginx/ssl.conf; will fail if that file does not exist.

What is the difference between sites-available, sites-enabled, and conf.d?

These three directories serve different purposes. Debian and Ubuntu use all three.

Directory Purpose How Nginx reads it When to use
sites-available/ Stores all virtual host configs Not read directly Always. One file per domain.
sites-enabled/ Contains symlinks to active configs Included via include /etc/nginx/sites-enabled/*; Symlink here to activate a site.
conf.d/ Drop-in config fragments Included via include /etc/nginx/conf.d/*.conf; Global http-level settings, upstream blocks, maps.

The sites-available / sites-enabled pattern lets you disable a site without deleting its config. To enable a site:

ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/

To disable it:

rm /etc/nginx/sites-enabled/example.com

Then reload:

sudo nginx -t && sudo systemctl reload nginx

Always run nginx -t before reloading. It validates the config without affecting running traffic.

The conf.d/ directory is simpler. Every .conf file in it is automatically loaded. No symlinks needed. Some administrators prefer conf.d/ for its simplicity and skip sites-available/sites-enabled entirely. Both approaches work. Pick one and stay consistent.

One gotcha: if you use both conf.d/ and sites-enabled/, make sure you do not define conflicting server blocks in both places. Nginx will load both, and the result depends on directive merging rules and server_name matching.

How does Nginx directive inheritance work?

Nginx passes directives from parent contexts down to child contexts. This is the part most people get wrong, and the source of subtle production bugs.

There are three types of directives, and each inherits differently.

Normal directives

Normal directives hold a single value. If a child context defines the same directive, it completely replaces the parent value. If the child does not define it, the parent value is inherited.

Example: root is a normal directive.

http {
    root /var/www/default;

    server {
        server_name example.com;
        # root is inherited: /var/www/default

        location /app {
            root /var/www/app;
            # root is overridden: /var/www/app
        }

        location /blog {
            # root is inherited: /var/www/default
        }
    }
}

Other normal directives: index, access_log (when used once), error_log, client_max_body_size.

Array directives

Array directives can appear multiple times in the same context to accumulate values. But when a child context defines even one instance, it replaces all parent values. Not merges. Replaces.

add_header is the most common array directive. This behavior causes a well-known production bug.

What happens when you add headers in both http and location blocks?

If you define add_header in the http context and then define a different add_header in a location block, the location block wipes all headers from the http context. Not just the one you are overriding. All of them.

http {
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    server {
        server_name example.com;

        location /api/ {
            add_header X-Custom "api-response";
            # X-Frame-Options is GONE
            # X-Content-Type-Options is GONE
            # Only X-Custom is sent
        }
    }
}

To verify this is happening on your server:

curl -I https://example.com/api/

Check the response headers. If your security headers are missing from /api/ responses but present on other paths, this is why.

The fix: repeat all headers in the child context.

location /api/ {
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-Custom "api-response";
}

Or use a snippet:

# /etc/nginx/snippets/security-headers.conf
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=63072000" always;
location /api/ {
    include snippets/security-headers.conf;
    add_header X-Custom "api-response";
}

Note: Nginx 1.29.3+ introduced add_header_inherit merge; which changes this behavior. With merge, child contexts append to parent headers instead of replacing them. If you are running Nginx 1.28.x (the current stable as of March 2026), you do not have this yet.

The same wipe behavior applies to proxy_set_header. If you define any proxy_set_header in a location block, all proxy_set_header directives from the server or http context are lost. The official docs state it explicitly: "These directives are inherited from the previous configuration level if and only if there are no proxy_set_header directives defined on the current level."

server {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    location /api/ {
        proxy_set_header X-Request-ID $request_id;
        # Host, X-Real-IP, X-Forwarded-For are all GONE
        proxy_pass http://backend;
    }
}

Fix it the same way: repeat all headers in the location block, or use an include snippet.

Action directives

Action directives like rewrite and return do not inherit into nested contexts. They execute only in the context where they are defined.

server {
    rewrite ^/old/(.*)$ /new/$1 permanent;

    location /app {
        # The rewrite above does NOT apply here
        # Requests matching /app are handled by this location
    }
}

The try_files directive has a related gotcha. When placed in the server context, Nginx creates an implicit pseudo-location with the lowest possible priority. If any regular location block matches the request, try_files at the server level never runs:

server {
    try_files $uri /index.php;   # Never runs for /app/* requests
    location /app { }            # This catches them first
}

Always place try_files inside a specific location block.

How do you inspect the effective Nginx configuration?

Two commands help you understand what Nginx actually sees after resolving all include directives.

Test the config for syntax errors:

sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Always run this before reloading. If the test fails, Nginx tells you the file and line number.

Dump the full merged config:

sudo nginx -T

This outputs the entire effective configuration with all includes resolved and expanded inline. Pipe it to less for readability:

sudo nginx -T | less

Or search for a specific directive:

sudo nginx -T | grep -n "add_header"

This shows every add_header directive across all included files, with line numbers in the merged output. Use this when debugging inheritance issues. If a header appears in the http block but not in a location block, and you see no add_header in that location, the header is inherited. If you see a different add_header in the location, all parent headers are wiped.

Use nginx -T whenever config changes do not behave as expected.

Quick reference: the full picture

/etc/nginx/nginx.conf
│
├─ main context (process-level: user, worker_processes, pid)
│
├─ events { }  (connection handling: worker_connections)
│
└─ http { }    (all HTTP config)
   │
   ├─ include conf.d/*.conf      (global HTTP settings, upstreams, maps)
   ├─ include sites-enabled/*    (virtual host configs)
   │
   └─ server { }                 (one per domain/port)
      │
      ├─ listen, server_name     (routing)
      ├─ root, index             (defaults for this host)
      │
      └─ location { }           (URI pattern matching)
         ├─ try_files, root      (per-path behavior)
         └─ proxy_pass           (reverse proxy target)

Inheritance: parent -> child (down only)
Normal directives: child overrides parent
Array directives: child REPLACES ALL parent values
Action directives: no inheritance

[-> nginx-administration-vps]


Copyright 2026 Virtua.Cloud. All rights reserved. This content is original work by the Virtua.Cloud team. Reproduction, republication, or redistribution without written permission is prohibited.

Ready to try it yourself?

Deploy your own server in seconds. Linux, Windows, or FreeBSD.

See VPS Plans