This tutorial explains how to deploy a simple and secure Nginx web server using Docker Compose. This setup is designed to run behind an existing Traefik reverse proxy, providing a robust and easily manageable solution for hosting a static website.

Changelog

DateChange
2025-09-17Initial Version: Article created to demonstrate a secure Nginx deployment behind a Traefik v3 stack.

1. Prerequisites

This guide builds directly upon a previously established secure Docker environment. Before you begin, you must have a fully functional Traefik v3 and CrowdSec stack.

โš ๏ธ CRUCIAL PREREQUISITE

This tutorial is part of a series and assumes you have a fully functional Docker environment with Traefik running. You must have already completed the setup described in our previous tutorial:

If you havenโ€™t already created the external network for Traefik, do so now:

docker network create proxy

You will also need:

  • Docker and Docker Compose installed on your server.
  • A domain name pointed to your serverโ€™s IP address.
  • sudo or root access.

2. Directory Structure

To keep our project organized, weโ€™ll create a dedicated directory for the Nginx service. All subsequent file paths in this guide are relative to this base directory.

# Create the main directory for your Nginx site
sudo mkdir -p /opt/containers/nginx
cd /opt/containers/nginx

# Create subdirectories for configuration and website files
sudo mkdir conf html

This structure separates your Nginx configuration from your actual website content.

3. Configuration Files

Next, we will create the necessary configuration files for Nginx and Docker Compose.

3.1. Environment File (.env)

First, create a .env file to store your domain name. This makes the configuration cleaner and easier to update.

sudo tee .env > /dev/null << 'EOF'
DOMAIN_NAME=your-domain.com
EOF
โš ๏ธ IMPORTANT

Replace your-domain.com with your actual domain name.

3.2. Docker Compose File

Now, create the docker-compose.yml file. It will read the DOMAIN_NAME variable from your .env file.

sudo tee docker-compose.yml > /dev/null << 'EOF'
services:
  nginx:
    image: nginx:1.27.1
    container_name: nginx
    restart: unless-stopped
    volumes:
      - ./html:/usr/share/nginx/html:ro
      - ./conf/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./conf/mime.types:/etc/nginx/mime.types:ro
    labels:
      - "traefik.enable=true"
      # --- Routing ---
      - "traefik.http.routers.nginx.rule=Host(`${DOMAIN_NAME}`) || Host(`www.${DOMAIN_NAME}`)"
      - "traefik.http.routers.nginx.entrypoints=websecure"
      - "traefik.http.routers.nginx.tls=true"
      - "traefik.http.routers.nginx.tls.certresolver=tls_resolver"

      # --- Middlewares ---
      # 1. CORS headers for Matrix federation (defined on-the-fly)
      - "traefik.http.middlewares.nginx-cors.headers.accessControlAllowMethods=GET,OPTIONS,PUT,POST"
      - "traefik.http.middlewares.nginx-cors.headers.accessControlAllowHeaders=*"
      - "traefik.http.middlewares.nginx-cors.headers.accessControlAllowOriginList=*"
      
      # 2. Assignment of all middlewares
      - "traefik.http.routers.nginx.middlewares=security-headers@file,crowdsec-bouncer@docker,nginx-cors@docker"

      # --- Service Definition ---
      - "traefik.http.services.nginx.loadbalancer.server.port=80"
    networks:
      - proxy

networks:
  proxy:
    external: true
EOF

Key Points from this file:

  • labels: These are instructions for Traefik. We define routing, TLS, and a chain of middlewares.
  • middlewares: We apply three middlewares: security-headers (from a file), crowdsec-bouncer (from the Traefik serviceโ€™s own labels), and nginx-cors. The nginx-cors middleware is defined on-the-fly here and is critical for Matrix federation, as it allows other servers to access the .well-known delegation files. For a standard website without Matrix integration, this middleware and its assignment can be omitted.
  • volumes: We mount our local html and conf directories into the container as read-only (ro) for better security.
  • networks: The service is attached to the external proxy network, allowing it to communicate with the Traefik container.

3.3. Nginx Configuration

Next, create the main Nginx configuration file at /opt/containers/nginx/conf/nginx.conf. This file controls the behavior of the web server and is optimized for serving a static site with Gzip compression.

sudo tee conf/nginx.conf > /dev/null << 'EOF'
worker_processes  2;
user              www-data;

events {
    use           epoll;
    worker_connections  128;
}

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

    # --- Basic Settings ---
    sendfile        on;
    tcp_nopush      on;
    tcp_nodelay     on;
    keepalive_timeout  65;
    types_hash_max_size 2048;

    # --- Gzip Settings ---
    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    # --- Main Server Block ---
    server {
        listen 80;
        server_name _;

        root /usr/share/nginx/html;
        index index.html;

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

        # --- Future-Proofing for Matrix Synapse ---
        location /.well-known/matrix/server {
            return 200 '{"m.server": "matrix.YOUR_DOMAIN_COM_PLACEHOLDER:443"}';
            add_header "Content-Type" "application/json";
        }
        location /.well-known/matrix/client {
             return 200 '{"m.homeserver": {"base_url": "https://matrix.YOUR_DOMAIN_COM_PLACEHOLDER"}, "m.identity_server": {"base_url": "https://vector.im"}}';
             add_header "Content-Type" "application/json; charset=utf-8";
             add_header "Access-Control-Allow-Origin" "*";
        }

        # --- Error Pages ---
        error_page 404 /404.html;
        location = /404.html {
            internal;
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            internal;
        }
    }
}
EOF

Now, read the .env file and use sed to replace the placeholders:

export $(grep -v '^#' .env | xargs)
sudo sed -i "s/YOUR_DOMAIN_COM_PLACEHOLDER/${DOMAIN_NAME}/g" conf/nginx.conf
โ„น๏ธ PREPARING FOR MATRIX SYNAPSE

The location /.well-known/matrix blocks are included as a forward-thinking measure. They are required for setting up a federated Matrix Synapse server on your domain. By including them now, you wonโ€™t need to reconfigure this Nginx service when you decide to deploy Matrix later. These directives tell Matrix clients and other servers where to find your chat homeserver.

3.4. Mime Types

Create a mime.types file at /opt/containers/nginx/conf/mime.types. This file ensures that Nginx sends the correct Content-Type header for various file formats, which is particularly important for web fonts and modern web assets.

sudo tee conf/mime.types > /dev/null << 'EOF'
types {
    text/html                                        html htm shtml;
    text/css                                         css;
    text/xml                                         xml;
    image/gif                                        gif;
    image/jpeg                                       jpeg jpg;
    application/javascript                           js;
    application/atom+xml                             atom;
    application/rss+xml                              rss;

    # Fonts
    application/vnd.ms-fontobject                    eot;
    font/truetype                                    ttf;
    font/opentype                                    otf;
    font/woff                                        woff;
    font/woff2                                       woff2;

    image/svg+xml                                    svg svgz;
    image/png                                        png;
    image/x-icon                                     ico;
}
EOF

3.5. Website Content

Before launching the server, place your website files into the /opt/containers/nginx/html directory. For a quick test, create a simple index.html file within it.

# Create a simple index.html for testing
sudo tee html/index.html > /dev/null << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Welcome to My Nginx Site</title>
    <style>
        body { font-family: sans-serif; background-color: #f0f0f0; text-align: center; padding: 50px; }
        h1 { color: #333; }
    </style>
</head>
<body>
    <h1>Success!</h1>
    <p>Your Nginx website is running securely behind Traefik.</p>
</body>
</html>
EOF

# Create custom error pages
sudo tee html/404.html > /dev/null << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>404 Not Found</title>
</head>
<body>
    <h1>404 - Page Not Found</h1>
    <p>The page you are looking for does not exist.</p>
</body>
</html>
EOF

sudo tee html/50x.html > /dev/null << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Server Error</title>
</head>
<body>
    <h1>50x - Server Error</h1>
    <p>An internal server error occurred.</p>
</body>
</html>
EOF

5. Launch and Verify

With all the files in place, you can start your Nginx container. Use docker compose for consistency with the main Traefik stack.

# Navigate to your project directory if you aren't already there
cd /opt/containers/nginx

# Start the service in the background
docker compose up -d

You can check the status of your new container with docker compose ps. It should show a state of Up.

Now, open a web browser and navigate to https://your-domain.com. You should see the โ€œSuccess!โ€ message from your index.html file, served over a secure HTTPS connection managed by Traefik.

6. Conclusion

You have successfully deployed a secure, containerized Nginx web server behind your Traefik and CrowdSec stack. This setup not only serves your static content efficiently but also benefits from centralized TLS management, security headers, and threat protection. Furthermore, it is already prepared for future expansion, such as the addition of a Matrix Synapse server.