Deploying a Secure Nginx Website with Traefik and Docker Compose
| A secure web server with a proper late-night glow. |
Table of Contents ๐
Shared file-defined middleware uses the @file provider suffix. Traefik provider namespaces.
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
| Date | Change |
|---|---|
| 2026-10-11 | Added security fix lines, effective module checks and distribution backport guidance. |
| 2026-10-11 | Fixed middleware scope, browser discovery CORS and the official Nginx image user. |
| 2026-06-12 | Matrix Federation: Corrected the future Matrix .well-known federation port to 8448. |
| 2025-09-17 | Initial 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: |
You will also need:
- Docker and Docker Compose installed on your server.
- A domain name pointed to your serverโs IP address.
sudoor 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 htmlThis 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 |
3.2. Docker Compose File
Choose a patched image before deploying. The example uses the stable 1.30.5 fix line: the official advisories list fixes in 1.30.4 for regex map, slice and SSI issues, and in 1.30.5 for an HTTP/3 buffer overflow. Recheck the Nginx security advisories when installing or extending this configuration.
A compiled module alone does not establish exposure: inspect the effective configuration for its use. This static-site example does not enable SSI, slice, DAV, MP4 processing, OCSP or HTTP/3, or define regex maps. Keep these features in your security review when adding services; their absence does not replace installing security fixes. Nginx security advisories.
For distribution packages, compare the complete package revision with the vendor tracker. Debian can backport a fix while retaining an older upstream version; the upstream number alone cannot establish patch status. Check the release-specific entries in the Debian Nginx security tracker.
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.30.5
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 ---
# Selective CrowdSec blocking is optional; verify all intended clients.
- "traefik.http.routers.nginx.middlewares=security-headers@file"
- "traefik.docker.network=proxy"
# --- Service Definition ---
- "traefik.http.services.nginx.loadbalancer.server.port=80"
networks:
- proxy
networks:
proxy:
external: true
EOFKey Points from this file:
labels: These are instructions for Traefik. We define routing, TLS, and a chain of middlewares.middlewares: The sharedsecurity-headers@filemiddleware applies only to this router. Addcrowdsec-bouncer@fileafter testing the blocking policy. Matrix browser discovery needs CORS on/.well-known/matrix/client; federation is server-to-server and does not require CORS. Keep the CORS header on the client discovery location rather than enabling it on the whole website. Matrix client discovery.volumes: We mount our localhtmlandconfdirectories into the container as read-only (ro) for better security.networks: The service is attached to the externalproxynetwork, 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 nginx;
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:8448"}';
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;
}
}
}
EOFNow, 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 |
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;
}
EOF3.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>
EOF5. 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 -dVerify the running binary and syntax, then inspect the complete configuration locally. -V reports the version and build options; -t validates configuration, while -T also prints all included configuration. Keep that output private if your configuration contains sensitive values. Nginx command-line parameters.
docker compose exec nginx nginx -V
docker compose exec nginx nginx -t
docker compose exec nginx nginx -TYou 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.





