Secure Docker containers with Traefik reverse proxy and Authelia authentication

Advanced 45 min Apr 19, 2026 712 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Set up a production-grade security stack using Traefik v3 reverse proxy with SSL automation and Authelia for multi-factor authentication. This tutorial covers Docker hardening, LDAP integration, and container security monitoring.

Prerequisites

  • Root access to server
  • Domain name with DNS control
  • Basic Docker knowledge
  • Email account for SSL certificates

What this solves

This tutorial creates a secure container environment where Traefik handles SSL termination and routing while Authelia provides authentication with support for LDAP and OIDC providers. You get automatic SSL certificates, multi-factor authentication, and hardened Docker containers suitable for production workloads.

Step-by-step installation

Update system and install Docker with security hardening

Start by updating your system and installing Docker with security-focused configuration.

sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo dnf update -y
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Configure Docker daemon with security hardening

Create a secure Docker daemon configuration that enables user namespaces, restricts capabilities, and sets security options.

{
  "userns-remap": "default",
  "live-restore": true,
  "userland-proxy": false,
  "no-new-privileges": true,
  "seccomp-profile": "/etc/docker/seccomp.json",
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 64000,
      "Soft": 64000
    }
  },
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
sudo systemctl enable docker
sudo systemctl start docker
sudo usermod -aG docker $USER
newgrp docker

Create project directory structure

Set up the directory structure for your secure container stack with proper permissions.

mkdir -p ~/secure-stack/{traefik,authelia,config,data,logs}
cd ~/secure-stack
chmod 755 ~/secure-stack
chmod 750 ~/secure-stack/{config,data,logs}

Create Docker network for secure communication

Create a custom Docker network that isolates your secure services and enables encrypted communication.

docker network create --driver bridge \
  --subnet=172.20.0.0/16 \
  --opt encrypted=true \
  --opt com.docker.network.bridge.name=secure-br0 \
  secure-network

Configure Traefik v3 with SSL automation

Set up Traefik as a reverse proxy with automatic SSL certificate generation and security headers.

global:
  checkNewVersion: false
  sendAnonymousUsage: false

api:
  dashboard: true
  debug: false

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entrypoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"
    http:
      middlewares:
        - security-headers@file
      tls:
        certResolver: letsencrypt

serversTransport:
  insecureSkipVerify: false

certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@example.com
      storage: /certificates/acme.json
      httpChallenge:
        entryPoint: web

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: secure-network
  file:
    filename: /config/middlewares.yml
    watch: true

log:
  level: INFO
  filePath: "/logs/traefik.log"
  format: json

accessLog:
  filePath: "/logs/access.log"
  format: json

Create Traefik security middlewares

Define security middlewares for headers, rate limiting, and authentication integration.

http:
  middlewares:
    security-headers:
      headers:
        accessControlAllowMethods:
          - GET
          - OPTIONS
          - PUT
          - POST
          - DELETE
        accessControlMaxAge: 100
        hostsProxyHeaders:
          - "X-Forwarded-Host"
        referrerPolicy: "same-origin"
        customRequestHeaders:
          X-Forwarded-Proto: "https"
        customResponseHeaders:
          X-Robots-Tag: "noindex,nofollow,nosnippet,noarchive"
        sslRedirect: true
        sslTemporaryRedirect: true
        sslHost: "example.com"
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000
        forceSTSHeader: true
        frameDeny: true
        contentTypeNosniff: true
        browserXssFilter: true
        permissionsPolicy: "camera=(), microphone=(), geolocation=()"
    
    rate-limit:
      rateLimit:
        average: 100
        period: 1m
        burst: 50
    
    authelia:
      forwardAuth:
        address: "http://authelia:9091/api/verify?rd=https://auth.example.com"
        trustForwardHeader: true
        authResponseHeaders:
          - "Remote-User"
          - "Remote-Groups"
          - "Remote-Name"
          - "Remote-Email"

Generate Authelia configuration

Create Authelia configuration with LDAP support, session security, and multi-factor authentication.

openssl rand -base64 32 > config/session_secret
openssl rand -base64 32 > config/storage_secret
openssl rand -base64 32 > config/jwt_secret
theme: auto
default_redirection_url: https://example.com
server:
  host: 0.0.0.0
  port: 9091
  asset_path: /config/assets/
  headers:
    csp_template: ""
  buffers:
    read: 4096
    write: 4096
  timeouts:
    read: 6s
    write: 6s
    idle: 30s

log:
  level: info
  format: text
  file_path: /logs/authelia.log
  keep_stdout: true

telemetry:
  metrics:
    enabled: true
    address: tcp://0.0.0.0:9959

totp:
  disable: false
  issuer: authelia.com
  algorithm: sha1
  digits: 6
  period: 30
  skew: 1
  secret_size: 32

webauthn:
  disable: false
  display_name: Authelia
  attestation_conveyance_preference: indirect
  user_verification: preferred
  timeout: 60s

ntp:
  address: "time.cloudflare.com:123"
  version: 3
  max_desync: 3s
  disable_startup_check: false
  disable_failure: false

authentication_backend:
  password_reset:
    disable: false
  refresh_interval: 5m
  ldap:
    implementation: custom
    url: ldap://openldap:389
    timeout: 5s
    start_tls: false
    tls:
      skip_verify: false
      minimum_version: TLS1.2
    base_dn: dc=example,dc=com
    username_attribute: uid
    additional_users_dn: ou=users
    users_filter: (&({username_attribute}={input})(objectClass=person))
    additional_groups_dn: ou=groups
    groups_filter: (&(member={dn})(objectClass=groupOfNames))
    group_name_attribute: cn
    mail_attribute: mail
    display_name_attribute: displayName
    user: cn=admin,dc=example,dc=com
    password: changeme123

session:
  name: authelia_session
  domain: example.com
  same_site: lax
  secret: file:///config/session_secret
  expiration: 1h
  inactivity: 5m
  remember_me_duration: 1M
  redis:
    host: redis
    port: 6379
    password: ""
    database_index: 0
    maximum_active_connections: 8
    minimum_idle_connections: 0

regulation:
  max_retries: 3
  find_time: 2m
  ban_time: 5m

storage:
  encryption_key: file:///config/storage_secret
  local:
    path: /data/db.sqlite3

notifier:
  disable_startup_check: false
  smtp:
    host: smtp.gmail.com
    port: 587
    timeout: 5s
    username: admin@example.com
    password: changeme123
    sender: admin@example.com
    identifier: localhost
    subject: "[Authelia] {title}"
    startup_check_address: admin@example.com
    disable_require_tls: false
    disable_html_emails: false
    tls:
      skip_verify: false
      minimum_version: TLS1.2

identity_providers:
  oidc:
    hmac_secret: file:///config/jwt_secret
    issuer_private_key: |
      -----BEGIN RSA PRIVATE KEY-----
      # Generate with: openssl genrsa -out private.pem 4096
      -----END RSA PRIVATE KEY-----
    access_token_lifespan: 1h
    authorize_code_lifespan: 1m
    id_token_lifespan: 1h
    refresh_token_lifespan: 90m
    enable_client_debug_messages: false
    clients:
      - id: example-app
        description: Example Application
        secret: $pbkdf2-sha512$310000$example-hash
        public: false
        authorization_policy: two_factor
        redirect_uris:
          - https://app.example.com/oauth/callback
        scopes:
          - openid
          - profile
          - email
          - groups
        response_types:
          - code
        grant_types:
          - authorization_code
        response_modes:
          - form_post
          - query
          - fragment

access_control:
  default_policy: deny
  networks:
    - name: internal
      networks:
        - 10.0.0.0/8
        - 172.16.0.0/12
        - 192.168.0.0/16
  rules:
    - domain: auth.example.com
      policy: bypass
    - domain: traefik.example.com
      policy: two_factor
    - domain: "*.example.com"
      policy: two_factor

Create Redis configuration for session storage

Configure Redis with security settings for storing Authelia sessions.

bind 127.0.0.1
port 6379
protected-mode yes
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
databases 16
always-show-logo no
set-proc-title yes
proc-title-template "{title} {listen-addr} {server-mode}"
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
rdb-del-sync-files no
dir ./
maxmemory 256mb
maxmemory-policy allkeys-lru

Create Docker Compose configuration

Define the complete secure stack with proper networking, volumes, and security constraints.

version: '3.8'

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - secure-network
    ports:
      - "80:80"
      - "443:443"
    environment:
      - TRAEFIK_LOG_LEVEL=INFO
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik/traefik.yml:/traefik.yml:ro
      - ./traefik/middlewares.yml:/config/middlewares.yml:ro
      - ./data/certificates:/certificates
      - ./logs:/logs
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.routers.traefik.rule=Host(`traefik.example.com`)"
      - "traefik.http.routers.traefik.middlewares=authelia@file"
      - "traefik.http.routers.traefik.tls=true"
      - "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
      - "traefik.http.routers.traefik.service=api@internal"
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

  authelia:
    image: authelia/authelia:latest
    container_name: authelia
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - secure-network
    expose:
      - 9091
    environment:
      - TZ=UTC
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./authelia/configuration.yml:/config/configuration.yml:ro
      - ./config:/config:ro
      - ./data/authelia:/data
      - ./logs:/logs
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.authelia.entrypoints=websecure"
      - "traefik.http.routers.authelia.rule=Host(`auth.example.com`)"
      - "traefik.http.routers.authelia.tls=true"
      - "traefik.http.routers.authelia.tls.certresolver=letsencrypt"
      - "traefik.http.routers.authelia.service=authelia"
      - "traefik.http.services.authelia.loadbalancer.server.port=9091"
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    depends_on:
      - redis

  redis:
    image: redis:7-alpine
    container_name: redis
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - secure-network
    expose:
      - 6379
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./config/redis.conf:/usr/local/etc/redis/redis.conf:ro
      - ./data/redis:/data
    command: redis-server /usr/local/etc/redis/redis.conf
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    cap_add:
      - SETGID
      - SETUID
      - DAC_OVERRIDE

  example-app:
    image: nginx:alpine
    container_name: example-app
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - secure-network
    expose:
      - 80
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./data/www:/usr/share/nginx/html:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.example-app.entrypoints=websecure"
      - "traefik.http.routers.example-app.rule=Host(`app.example.com`)"
      - "traefik.http.routers.example-app.middlewares=authelia@file"
      - "traefik.http.routers.example-app.tls=true"
      - "traefik.http.routers.example-app.tls.certresolver=letsencrypt"
    read_only: true
    tmpfs:
      - /var/cache/nginx
      - /var/run
      - /tmp
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - DAC_OVERRIDE
      - SETGID
      - SETUID
      - NET_BIND_SERVICE

networks:
  secure-network:
    external: true

volumes:
  certificates:
  authelia-data:
  redis-data:

Set proper file permissions and ownership

Configure secure file permissions for certificates and configuration files.

sudo chown -R $(id -u):$(id -g) ~/secure-stack
chmod 600 ~/secure-stack/config/*_secret
chmod 644 ~/secure-stack/traefik/traefik.yml
chmod 644 ~/secure-stack/authelia/configuration.yml
touch ~/secure-stack/data/certificates/acme.json
chmod 600 ~/secure-stack/data/certificates/acme.json
Never use chmod 777. It gives every user on the system full access to your files. Instead, use specific permissions: 600 for secrets, 644 for config files, and 755 for directories.

Create sample application content

Add a simple test page to verify the authentication flow works correctly.

mkdir -p data/www



    
    
    
    


    <div class="container">
        <h1>🔒 Secure Application</h1>
        <div class="status">
            ✅ Authentication successful - You are accessing a protected resource
        </div>
        <p>This page is protected by Authelia authentication and served through Traefik reverse proxy with SSL.</p>
        <h3>Security Features Active:</h3>
        <ul>
            <li>SSL/TLS encryption via Let's Encrypt</li>
            <li>Multi-factor authentication</li>
            <li>Security headers protection</li>
            <li>Rate limiting</li>
            <li>Container security hardening</li>
        </ul>
        <p><strong>Timestamp:</strong> <span></span></p>
    </div>
    

Start the secure container stack

Launch all services and verify they start correctly with proper networking.

docker compose up -d
docker compose logs -f --tail=20

Configure DNS and SSL certificates

Set up DNS records and verify SSL certificate generation for your domains.

Note: Replace example.com with your actual domain and ensure DNS A records point to your server's IP address before proceeding.
# Check certificate generation
docker compose logs traefik | grep -i certificate

# Verify ACME challenge
curl -I http://auth.example.com/.well-known/acme-challenge/test

# Test SSL endpoint
curl -I https://auth.example.com

Set up container security monitoring

Configure monitoring for container security events and performance metrics.

#!/bin/bash

# Container security monitoring script
LOG_FILE="/var/log/container-security.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')

# Check for privileged containers
echo "[$DATE] Checking for privileged containers..." >> $LOG_FILE
docker ps --filter "label=privileged=true" --format "table {{.Names}}\t{{.Status}}" >> $LOG_FILE

# Monitor container resource usage
echo "[$DATE] Container resource usage:" >> $LOG_FILE
docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}" >> $LOG_FILE

# Check for containers running as root
echo "[$DATE] Checking containers running as root:" >> $LOG_FILE
for container in $(docker ps -q); do
    user=$(docker exec $container whoami 2>/dev/null || echo "unknown")
    name=$(docker inspect --format '{{.Name}}' $container)
    if [ "$user" = "root" ]; then
        echo "WARNING: Container $name running as root user" >> $LOG_FILE
    fi
done

# Verify security options
echo "[$DATE] Security options check:" >> $LOG_FILE
docker ps --format "table {{.Names}}" | tail -n +2 | while read container; do
    security_opts=$(docker inspect --format '{{.HostConfig.SecurityOpt}}' $container)
    echo "$container: $security_opts" >> $LOG_FILE
done
chmod +x config/docker-security.sh
sudo mkdir -p /var/log
sudo touch /var/log/container-security.log
sudo chmod 640 /var/log/container-security.log

Configure automated security monitoring

Set up a systemd timer to run security monitoring checks every hour.

Automated install script

Run this to automate the entire setup

Don't want to manage this yourself?

We handle infrastructure for businesses that depend on uptime. Fully managed, with one fixed contact who knows your setup.

You get one fixed contact who knows your setup

At their desk in Rotterdam 14:09 · reachable in a message, no ticket form