On this page

Skip to content

Setting up and Configuring Nginx with Docker Compose

I have often heard of Nginx, knowing primarily that it is used for reverse proxying. Previously, at work, I saw a Team Leader ask the operations team to set it up; when they were busy, the Team Leader handled it himself. It seems like a fundamental skill for senior engineers, so I wanted to study it a bit. As for the .NET query part of Elasticsearch, I'll start that in December.

As usual, I don't want to install it locally, so I'm using Docker to build the environment for easier testing and learning. However, after looking into it, I found that Nginx configuration is quite complex, so this note focuses mainly on the Docker Compose part, while also testing the Docker Compose V2 syntax.

Introduction to Nginx

Nginx (pronounced "engine-x") is a Web Server primarily used for:

  1. Serving static files: HTML, CSS, JavaScript, images, etc.
  2. Reverse Proxy: Forwarding requests to backend applications.

Basic Nginx Configuration

The main configuration file for Nginx is /etc/nginx/nginx.conf. When the container starts, it reads this file to determine Nginx's behavior.

Nginx Architecture Hierarchy

The Nginx configuration file consists of multiple contexts. Each context corresponds to a block, which can be divided into the following levels from outer to inner:

Main Context (Outermost)

Located at the outermost level of the configuration file, it defines global settings.

main context (outermost)
├── user nginx;
├── worker_processes auto;
├── error_log /var/log/nginx/error.log;
├── pid /run/nginx.pid;

├── events { }     # Event processing settings
├── http { }       # HTTP-related settings
├── mail { }       # Mail proxy settings (optional)
└── stream { }     # TCP/UDP proxy settings (optional)

Purpose: To set execution parameters for the Nginx process, such as the number of workers, user permissions, and PID file location.

Events Context

Defines how Nginx handles connections.

Purpose: To set connection handling parameters, such as the number of connections each worker can handle and the event processing model.

HTTP Context

Defines global settings for the HTTP server.

http {
    # HTTP global settings

    ├── map { }         # Variable mapping (can have multiple)

    ├── server { }      # Virtual host (can have multiple)
    ├── server { }

    ├── upstream { }    # Backend server group (can have multiple)
    └── upstream { }
}

Purpose: To set global parameters related to HTTP, such as MIME types, log formats, and gzip compression. Settings defined at this level apply to all virtual hosts.

Common Blocks and Directives

Server Block

The server block defines a virtual host used to handle requests for specific domains or ports.

Placement: Can only be placed within the http block; multiple server blocks can be defined.

Basic Structure:

nginx
server {
    listen 80;                    # Port to listen on
    server_name example.com;      # Domain name

    location / {                  # Path matching rules
        # Handling method
    }
}

Within the server block, you primarily use the location directive to define how different paths are handled.

Location Block

Matching Logic

location / uses prefix matching and will match all paths starting with /. Since all URL paths start with /, location / effectively matches all requests and is usually used as a fallback rule.

Basic Prefix Matching:

Nginx selects the most specific (longest) matching rule.

nginx
server {
    listen 80;
    server_name localhost;

    location / {
        # Lower priority, matches all paths
        return 200 "Root path\n";
    }

    location /api/ {
        # Higher priority, /api/ is more specific than /
        return 200 "API path\n";
    }
}

Actual Operation:

Request: http://example.com/
→ Matches location /

Request: http://example.com/about.html
→ Matches location /

Request: http://example.com/api/users
→ Matches location /api/ (more specific)

Full Matching Rules and Priority

Location supports various matching modifiers, with the following priority:

  1. Exact match =: Matches only if identical (highest priority)
  2. Prefix strong match ^~: Stops searching for regular expressions after a successful prefix match
  3. Regular expression (case-sensitive) ~: Used in the order defined
  4. Regular expression (case-insensitive) ~*: Used in the order defined
  5. Normal prefix match: Longest one takes priority
  6. General match /: Default rule (lowest priority)

Test Example:

nginx
server {
    listen 80;
    server_name localhost;

    # Set default Content-Type
    default_type "text/plain; charset=utf-8";

    # 1. exact match (highest priority)
    location = /test123 {
        return 200 "exact_match\n";
    }

    # 2. prefix strong (stops regex search after match)
    location ^~ /test999 {
        return 200 "prefix_strong\n";
    }

    # 3. regex case-sensitive
    location ~ ^/test[0-9]+$ {
        return 200 "regex_sensitive\n";
    }

    # 4. regex case-insensitive
    location ~* ^/TEST[0-9]+$ {
        return 200 "regex_insensitive\n";
    }

    # 5. normal prefix
    location /test {
        return 200 "prefix_normal\n";
    }

    # 6. fallback (default rule)
    location / {
        return 200 "root\n";
    }
}

Test Results:

bash
# Exact match (highest priority)
curl http://localhost/test123
 "exact_match"

# Regex (case-sensitive)
curl http://localhost/test456
 "regex_sensitive"

# Regex (case-insensitive)
curl http://localhost/TEST456
 "regex_insensitive"

curl http://localhost/TEST789
 "regex_insensitive"

# Prefix strong (stops regex search)
curl http://localhost/test999
 "prefix_strong"

# Normal prefix
curl http://localhost/test
 "prefix_normal"

curl http://localhost/testaaa
 "prefix_normal"

# Default rule
curl http://localhost/other
 "root"

Testing matching rules:

bash
# View Nginx loaded configuration and filter for location rules
nginx -T | grep -A5 "/test"

WARNING

  1. If you define both location /test and location ^~ /test, Nginx will throw an error due to rule conflict; you should choose one.
  2. It is recommended to use the curl command for testing, as some browsers automatically convert URL case based on history. For example, if you access http://localhost/test123 first, the browser might automatically convert http://localhost/TEST123 to lowercase.

Handling Methods

1. Serving static files

Read files directly from the file system and respond:

nginx
server {
    listen 80;
    server_name example.com;

    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
        try_files $uri $uri/ =404;
    }
}

Directive Explanation:

  • root: Specifies the root directory for static files.
  • index: Specifies the default index file; when a directory is requested, it searches for these files in order.
  • try_files: Attempts to find files or directories in order; returns a 404 error if none exist.

Operation Example:

Assuming the root directory structure is as follows:

/usr/share/nginx/html/
├── index.html
├── about.html
└── docs/
    └── index.html

Request processing flow:

Request: http://example.com/
→ Reads /usr/share/nginx/html/index.html

Request: http://example.com/about.html
→ Reads /usr/share/nginx/html/about.html

Request: http://example.com/docs/
→ Reads /usr/share/nginx/html/docs/index.html

Request: http://example.com/notfound.html
→ Returns 404 error

2. Reverse Proxy

Forward requests to a backend application using the proxy_pass directive:

nginx
server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://backend:8080;
    }
}

Proxy Pass Directive

proxy_pass is the core directive for Nginx as a reverse proxy, used to forward requests to a backend server.

Placement: Can only be placed within location, if in location, or limit_except blocks.

Basic Usage:

nginx
location / {
    proxy_pass http://backend:8080;
}

Impact of Trailing Slash:

Whether the proxy_pass URI contains a trailing slash affects forwarding behavior.

Case 1: proxy_pass has no URI (no trailing slash or path)

nginx
location /app/ {
    # No path, full forwarding
    proxy_pass http://backend;
}

The complete original URI is passed to the backend:

Request: http://example.com/app/test
Forwarded: http://backend/app/test

Case 2: proxy_pass has a URI (contains a path, even if just /)

nginx
location /app/ {
    # Contains path /, performs replacement
    proxy_pass http://backend/;
}

The part matched by location is replaced by the proxy_pass URI:

Request: http://example.com/app/test
Forwarded: http://backend/test      (/app/ replaced by /)

Map Directive

The map directive is used to create custom variables, setting output variables based on the values of input variables.

map variables defined in the http block are global and can be used by all server blocks. If multiple map blocks are defined with the same output variable name, Nginx uses the last loaded definition, and previous ones are discarded.

Placement: Can only be placed within the http block.

Basic Syntax:

nginx
map $input_variable $output_variable {
    value1  result1;
    value2  result2;
    default default_result;
}

Simple Example:

nginx
http {
    # Determine variable value based on request method
    map $request_method $is_post {
        POST    "yes";
        default "no";
    }
}

Using Regular Expressions:

nginx
map $http_user_agent $browser_type {
    ~Chrome         "chrome";      # Case-sensitive, matches only Chrome
    ~*firefox       "firefox";     # Case-insensitive, matches Firefox, firefox, FIREFOX
    ~*mobile        "mobile";      # Case-insensitive, matches Mobile, mobile
    default         "default";
}

Matching order is top-to-bottom; it stops at the first successful match.

About Variables:

In addition to using map to create custom variables, Nginx has many built-in variables, such as $remote_addr (client IP), $host (hostname), and $request_uri (request URI). For a complete list of variables, refer to the Alphabetical index of variables.

Default nginx.conf

You can use the following command to view the default nginx.conf:

bash
docker run --rm nginx cat /etc/nginx/nginx.conf

Below is the content of the default configuration file for the nginx:1.29.3 Docker image; other versions may vary slightly:

nginx
user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /run/nginx.pid;


events {
    worker_connections  1024;
}


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

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

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

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

Note the last line include /etc/nginx/conf.d/*.conf;, which means all .conf files placed in the /etc/nginx/conf.d/ directory will be loaded into the http block.

Actual Effect:

nginx
http {
    # HTTP settings from nginx.conf
    include /etc/nginx/mime.types;
    access_log /var/log/nginx/access.log;

    # --- Content of conf.d/default.conf is inserted here ---
    server {
        listen 80;
        server_name localhost;
        # ...
    }
    # --- Insertion ends ---
}

Therefore, you only need to create website configuration files (e.g., default.conf) in the conf.d directory, and these settings will be automatically loaded into the http block. The default main context and events settings are usually sufficient.

Scenarios for Modifying nginx.conf

In most cases, you do not need to modify nginx.conf, unless you need to adjust the following settings:

Main Context Settings:

  • worker_processes: Number of worker processes.
  • worker_rlimit_nofile: Number of files each worker can open.
  • user: User executing the worker.

Events Block Settings:

  • worker_connections: Number of connections per worker.
  • use: Event processing model (e.g., epoll).

If you need to modify these settings, you can use the following command to copy out the default nginx.conf:

bash
docker run --rm nginx cat /etc/nginx/nginx.conf > volumes/config/nginx.conf

Using a Configuration Generator

If you are unsure how to write the configuration file, you can use DigitalOcean's Nginx Config Generator to help generate a basic configuration file. This tool provides a graphical interface where you can select different configuration options based on your needs, such as:

  • Static website or Reverse Proxy.
  • SSL/HTTPS settings.
  • PHP support.
  • Compression and caching settings.

The generated configuration file can be copied and used directly, then fine-tuned according to actual requirements.

Creating Website Configuration in conf.d

When the Nginx Docker image starts, it has a built-in default.conf file in the /etc/nginx/conf.d/ directory as the default configuration. Below is the content of the default default.conf:

nginx
server {
    listen       80;
    server_name  localhost;

    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

In practice, you will create your own configuration files to override or replace the default settings.

Directive Inheritance and Overriding

Many Nginx directives can be set at different levels, and child-level settings override parent-level settings. Taking log settings as an example:

access_log and error_log can be set at multiple levels:

DirectiveAvailable Locations
error_logmain, http, mail, stream, server, location
access_loghttp, server, location, if in location, limit_except

If you set the log path in server or location, it will override the upper-level settings. If not set, it inherits the settings from the upper level (http or main).

Example:

nginx
# nginx.conf
http {
    access_log /var/log/nginx/access.log;  # Default path

    # conf.d/site-a.conf
    server {
        server_name site-a.com;
        access_log /var/log/nginx/site-a.log;  # Override, use independent log
    }

    # conf.d/site-b.conf
    server {
        server_name site-b.com;
        # Not set, inherits /var/log/nginx/access.log from http
    }
}

Cross-file Sharing of Map Variables

map variables defined in the http block are global and can be used by all server blocks. Since Nginx loads all files in conf.d/*.conf, map variables defined in different conf files are shared in the same namespace.

Nginx loads conf files in alphabetical order of filenames. Since duplicate definitions of the same output variable will be overwritten, it is recommended to centralize all map definitions in a single independent conf file to avoid unexpected overwriting issues.

File Structure Example:

volumes/config/conf.d/
├── maps.conf        # All map definitions
├── api.conf         # API service settings
└── default.conf     # Default website settings

Implementation Examples

Static Website Example

Create Data Directories

First, create the necessary data directories to store Nginx configuration files, website files, and log files.

bash
# Create data directories
mkdir -p volumes/config/conf.d
mkdir -p volumes/html
mkdir -p volumes/logs

Basic Docker Compose Configuration

Create a compose.yaml file:

yaml
services:
  nginx:
    image: nginx:latest
    container_name: nginx
    restart: always
    ports:
      - "80:80"
    volumes:
      # Uncomment the line below if you need to override main context settings
      # - ./volumes/config/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./volumes/config/conf.d:/etc/nginx/conf.d:ro
      - ./volumes/html:/usr/share/nginx/html:ro
      - ./volumes/logs:/var/log/nginx
    environment:
      - TZ=Asia/Taipei

Volumes Mount Explanation

yaml
volumes:
  - ./volumes/config/conf.d:/etc/nginx/conf.d:ro      # Configuration files (read-only)
  - ./volumes/html:/usr/share/nginx/html:ro           # Static files (read-only)
  - ./volumes/logs:/var/log/nginx                     # Log directory (needs write access)

Use of :ro (read-only):

Why add :ro?

  • Prevents processes inside the container from accidentally modifying host files.
  • Protects configuration files from tampering.
  • Clearly expresses that these directories are for reading only.

For example, when the container starts, Nginx checks if default.conf is the official default configuration file; if so, it attempts to add listen [::]:80; to support IPv6. Adding :ro prevents this modification.

What should not have :ro?

  • Log directory: Nginx must be able to write logs.
  • Upload directory: If the website allows users to upload files.
  • Cache directory: Nginx needs to write cache data.

Create Website Configuration File

Create default.conf in the volumes/config/conf.d directory:

nginx
server {
    listen 80;        # Listen on IPv4
    listen [::]:80;   # Listen on IPv6
    server_name localhost;

    # Set root directory
    root /usr/share/nginx/html;
    index index.html index.htm;

    # Character encoding
    charset utf-8;

    # Log paths
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # Main location settings
    location / {
        try_files $uri $uri/ =404;
    }

    # Deny access to hidden files
    location ~ /\. {
        deny all;
    }
}

Create Test Web Page

Create index.html in the volumes/html directory:

html
<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Nginx Test Page</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            background: white;
            padding: 40px;
            border-radius: 8px;
            text-align: center;
        }
        h1 {
            color: #2c3e50;
            margin-bottom: 20px;
        }
        p {
            color: #555;
            line-height: 1.6;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>✓ Nginx Running Successfully</h1>
        <p>This is an Nginx test environment set up using Docker Compose</p>
    </div>
</body>
</html>

Verify Web Page

Run the following command to start the container:

bash
docker compose up -d

Enter http://localhost/ in your browser to see the test page.

Testing and Reloading Configuration

After modifying the configuration file, you can use the following commands in the running container to check the syntax, avoiding service failure due to configuration errors.

bash
# Test configuration file syntax
docker compose exec nginx nginx -t

# Reload configuration file (without interrupting service)
docker compose exec nginx nginx -s reload

It is recommended to test the syntax with nginx -t first, and then execute nginx -s reload to reload after confirming it is correct.

Reverse Proxy Example

Forward requests to a backend application. For official Reverse Proxy example documentation, refer to NGINX Reverse Proxy and WebSocket proxying.

Below is an integrated reference example:

volumes/config/conf.d/maps.conf:

nginx
# WebSocket support: Define Connection upgrade variable
map $http_upgrade $connection_upgrade {
    ''      close;
    default upgrade;
}

volumes/config/conf.d/default.conf:

nginx
server {
    listen 80;
    listen [::]:80;
    server_name localhost;

    location / {
        # Forward to the application in the web container
        proxy_pass http://web:8080/;

        # === Basic Headers (Required) ===
        # Pass original hostname
        proxy_set_header Host $host;

        # Pass real client IP
        proxy_set_header X-Real-IP $remote_addr;

        # Pass client IP chain (if passing through multiple proxies)
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # Pass original protocol (http or https)
        proxy_set_header X-Forwarded-Proto $scheme;

        # === Full Forwarding Information (Optional) ===
        # Pass original hostname (specifically for X-Forwarded series)
        # proxy_set_header X-Forwarded-Host $host;

        # Pass original port
        # Note: $server_port is the port Nginx is listening on
        # proxy_set_header X-Forwarded-Port $server_port;

        # === WebSocket Support (Optional) ===
        # WebSocket requires HTTP/1.1 protocol
        # proxy_http_version 1.1;

        # Pass Upgrade Header
        # proxy_set_header Upgrade $http_upgrade;

        # Set Connection based on whether Upgrade Header exists
        # proxy_set_header Connection $connection_upgrade;

        # WebSocket long connection timeout (default 60 seconds, set to 24 hours here)
        # proxy_read_timeout 86400s;

        # === Request Tracking (Optional) ===
        # Pass unique request ID for log tracking and troubleshooting
        # Requires Nginx 1.11.0+
        # proxy_set_header X-Request-ID $request_id;

        # === Timeout Settings (Optional) ===
        # Timeout for connecting to the application (default 60 seconds)
        # May need adjustment if the application starts slowly or the network is unstable
        # proxy_connect_timeout 60s;

        # Timeout for sending requests to the application (default 60 seconds)
        # Need to increase this value if uploading large files
        # proxy_send_timeout 60s;

        # Timeout for reading responses from the application (default 60 seconds)
        # Need to adjust this value if the application takes longer to process (report generation, data export, etc.)
        # Note: If WebSocket is enabled, use the proxy_read_timeout 86400s above
        # proxy_read_timeout 60s;
    }
}

Docker Compose with Web Service

yaml
services:
  nginx:
    image: nginx:latest
    container_name: nginx
    ports:
      - "80:80"
    volumes:
      - ./volumes/config/conf.d:/etc/nginx/conf.d:ro
    depends_on:
      - web
    environment:
      - TZ=Asia/Taipei

  web:
    image: mcr.microsoft.com/dotnet/samples:aspnetapp
    container_name: web
    environment:
      - TZ=Asia/Taipei
    # Do not expose ports, only accessible internally by Nginx

Here, the sample image provided by .NET is used directly. For details, refer to the official GitHub .NET container samples.

This image is a Razor Pages web application. After the container starts, enter http://localhost/ in your browser to view the web page content.

Template and Environment Variable Example

The official Nginx image has supported template functionality since version 1.19. For detailed usage, refer to the Official Nginx Docker Hub.

This feature allows users to focus on a few settings by defining template files and using environment variables in Docker Compose.

Modify Docker Compose

yaml
services:
  nginx:
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - ./volumes/templates:/etc/nginx/templates
      - ./volumes/config/templates.d:/etc/nginx/conf.d  # Used to view generated conf files
    environment:
      - TZ=Asia/Taipei
      - NGINX_HOST=localhost
      - NGINX_PORT=80

Create Template File

Create default.conf.template in the volumes/templates directory:

nginx
server {
    listen       ${NGINX_PORT};
    server_name  ${NGINX_HOST};

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}

When the container starts, it automatically replaces environment variables in the configuration file, resulting in the following:

nginx
server {
    listen       80;
    server_name  localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}

Precautions

When using the template method, environment variable replacement only occurs when the container starts. Therefore:

  • ✅ After modifying environment variables, you need to restart the container: docker compose up -d
  • ❌ You cannot use docker compose exec nginx nginx -s reload to reload the configuration.

If you need to adjust settings frequently, it is recommended to use the direct mounting of conf files.

Reference Resources

Change Log

  • 2025-11-27 Initial version created.