Deploying a Secure Nginx Website with Traefik and Docker Compose
Table of Contents ๐
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 |
---|---|
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.
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 |
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), andnginx-cors
. Thenginx-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 localhtml
andconf
directories into the container as read-only (ro
) for better security.networks
: The service is attached to the externalproxy
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 |
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.