14 min read

Choosing a Reverse Proxy: Nginx Proxy Manager versus Traefik

Setup and comparison of the popular reverse proxies Nginx Proxy Manager and Traefik
Choosing a Reverse Proxy: Nginx Proxy Manager versus Traefik
Photo by Thomas Jensen / Unsplash

Introduction

A reverse proxy acts as a middleman between clients and servers, forwarding client requests to servers and returning the servers' responses back to clients. They are often used to load balance traffic, terminate SSL and stream-line security configurations for multiple services.

There are many software options to choose from in order to set up a reverse proxy, including NGINX, Openresty, Caddy, HAProxy, Traefik and many more. NGINX is a popular open-source reverse proxy that is known for its high performance and reliability. Traefik is another open-source reverse proxy that is gaining popularity due to its support for dynamic configurations as well as the ability to discover new containerized services to automatically proxy to.

In this blog post, we will compare two popular reverse proxies for selfhosters. Nginx Proxy Manager (often abbreviated by NPM; based on Openresty) and Traefik.

Comparison

Both reverse proxies are popular choices to proxy traffic and support the following common core features:

  • Automatic management of SSL/TLS certificates as well as renewal. Both reverse proxies support a wide range of providers.
  • Proxying various protocols such as HTTP, HTTPS, TCP and UDP. NPM uses NGINX Streams to forward TCP and UDP traffic.
  • Providing a web-based management interface.

Nonetheless, there are a few key differences that should be outlined:

  • Nginx Proxy Manager provides a web-based GUI that allows users to easily manage and configure the reverse proxy. It provides a user-friendly web interface for creating and managing proxy hosts, as well as managing SSL/TLS certificates and access controls. Although Traefik also provides a web-based GUI, the management UI is rather used to inspect the currently active configuration instead of actively configuring or adjusting things. The configuration is done in configuration files or via docker labels instead.
  • Nginx Proxy Manager supports multiple user accounts, roles and audit logs. The reverse proxy can therefore be managed or inspected by different users and access roles. The audit logs can help to track down who made what changes. Traefik does not support multiple user accounts, roles or audit logs. The web-based management UI can be accessed without authentication per default.
  • Nginx Proxy Manager stores its configuration for proxy hosts, users etc. in a database. Supported are SQLite and MariaDB/MySQL. Traefik on the other hand does not require a database. Its configuration is defined as code (yml files) and proxy host configurations are applied during runtime and persist in-memory. Therefore, Traefik is less prone to database corruption and a potential downtime.
  • Nginx Proxy Manager allows users to proxy to a single host. Therefore, load balancing is not natively supported by the web-based management GUI. Traefik instead supports load balancing natively via its configuration options.
  • Since NPM uses Openresty under the hood, an improved NGINX, reverse proxying completely fails as soon as one proxy host configuration is falsely defined. This for example happens, if your NPM docker instance cannot reach a hostname or IP address that was configured as proxy host via the web-based management UI. Alternatively, if e.g. an SSL certificate file is actively referenced but not available or accessible. If this occurs, the NGINX service won't start correctly and all your proxy hosts are inaccessible. Furthermore, the NPM management web UI to fix things will also be unresponsive until the problem is fixed. You end up having a non-operational reverse proxy. Traefik instead will fail for a single proxy host only and the management web UI to inspect things will still stay up. All other proxy hosts will remain working and are not affected by a single proxy host misconfiguration. Since there is no database to maintain, you run less risk of bricking Traefik as reverse proxy.

Docker Setup

The following section will focus on setting up both reverse proxies as docker container. Since both containers will bind the required ports TCP/80 and TCP/443 to the docker host system, it is not possible to run them in parallel. If you want to do so, please adjust the port mappings.

Setup of Nginx Proxy Manager

The following docker-compose.yml file can be used to spawn up a dockerized NPM instance. By using bind volume mounts, we ensure that our upcoming user and proxy configurations will be persistently stored on disk. Therefore, our configuration and issued SSL certificates will persist even when the container is restarted or re-deployed.

For NPM to work, we should first create a new docker network:

docker network create npm_proxy
version: "3"

services:
  npm:
    container_name: npm
    environment:
      - TZ=Europe/Berlin # change to your timezone
    hostname: npm
    user: 0:1000 # change to your user UID/GUID
    image: jc21/nginx-proxy-manager:latest
    networks:
      - npm_proxy
    ports:
      - 80:80/tcp # HTTP
      - 443:443/tcp # HTTPS
      - 81:81/tcp # Management UI
    restart: unless-stopped
    volumes:
      - ./docker-volumes/npm/data:/data
      - ./docker-volumes/npm/letsencrypt:/etc/letsencrypt
 
 networks:
  npm_proxy:
    external: true
docker-compose.yml for Nginx Proxy Manager

After adjusting the above configuration file, we can spawn up our instance of NPM by issuing the following bash command on our server:

docker compose up -d
Creating a docker network for NPM

The NPM management web UI should then be availabe on TCP/81. Just open a web browser of your choice and browse to http://<your-server-ip>:81.

💡
The default login credentials are [email protected] for the username and changeme for the initial password. You are tasked to change those default credentials during first login.

Setup of Traefik

The following docker-compose.yml file can be used to spawn up a dockerized Traefik instance. By using bind volume mounts, we ensure that our upcoming configuration files will be persistently stored on disk. Therefore, our configuration and issued SSL certificates will persist even when the container is restarted or re-deployed.

For Traefik to work, we should first create a new docker network:

docker network create proxy
Creating a docker network for Traefik
version: '3'
services:
  traefik:
    container_name: traefik
    image: traefik:2.9
    ports:
      - 80:80 # HTTP
      - 443:443 # HTTPS
      - 8080:8080 # API MGMT UI
    networks:
      - proxy
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./docker-volumes/traefik:/etc/traefik/
      - ./docker-volumes/traefik/logs:/logs
    environment:
      - TZ=Europe/Berlin # change to your timezone
      - CF_DNS_API_TOKEN=<YOUR-API-TOKEN> # define your Cloudflare API token
    labels:
      - traefik.enable=true
      - traefik.http.routers.api.rule=Host(`traefik.lrvt.de`) # Define the subdomain for the traefik dashboard.
      - traefik.http.routers.api.service=api@internal # Enable Traefik API.
      # - traefik.http.routers.api.middlewares=local-ipwhitelist@file # only allow access from private class subnets
      # - traefik.http.routers.api.middlewares=basic-auth-global # enable basic auth middleware
      # - traefik.http.middlewares.basic-auth-global.basicauth.users=admin:$$apr1$$epoKf5li$$QfTMJZOCS/halv3CiIUEu0 # protect the traefik dashboard by basic auth (pw=password)
    restart: unless-stopped
    
networks:
  proxy:
    external: true
docker-compose.yml for Traefik

After adjusting the above configuration file, we may spawn up our instance of Traefik by issuing the following bash command on our server:

docker compose up -d

The Traefik management web GUI should then be availabe on TCP/8080. Just open a web browser of your choice and browse to http://<your-server-ip>:8080. Since we have not yet configured any proxy hosts, Traefik's default configuration will be applied and the web dashboard will list default things only.

🛑
If Traefik is not yet working, we likely have to create a default configuration file first. Follow this blog post.

During first startup, our defined bind volume at /docker-compose/traefik/ should contain an example configuration file traefik.yml by now. In order to use Traefik, we have to adjust this default configuration file as well as setup another file for dynamic configurations.

Let's adjust or create the following files at the volume bind mentioned above.

# Traefik global configuration
global:
  checkNewVersion: true
  sendAnonymousUsage: false

# Enable traefik ui dashboard
api:
  dashboard: true
  insecure: true

# Log level INFO|DEBUG|ERROR
log:
  level: INFO

# Configuring Multiple Filters
accessLog:
  filePath: "/logs/traefik.log"
  format: json
  #filters:
  #  statusCodes:
  #    - "200"
  #    - "300-302"
    #retryAttempts: true
    #minDuration: "10ms"
  # collect logs as in-memory buffer before writing into log file
  bufferingSize: 0
  fields:
    headers:
      defaultMode: drop # drop all headers per default
      names:
          User-Agent: keep # log user agent strings

# The setting below is to allow insecure backend connections.  
serverTransport:
  insecureSkipVerify: true

# Traefik entrypoints (network ports) configuration
entryPoints:
  # Not used in apps, but redirect everything from HTTP to HTTPS
  http:
    address: :80
    forwardedHeaders:
      trustedIPs: &trustedIps
        # Start of Clouflare public IP list for HTTP requests, remove this if you don't use it; https://www.cloudflare.com/de-de/ips/
        - 103.21.244.0/22
        - 103.22.200.0/22
        - 103.31.4.0/22
        - 104.16.0.0/13
        - 104.24.0.0/14
        - 108.162.192.0/18
        - 131.0.72.0/22
        - 141.101.64.0/18
        - 162.158.0.0/15
        - 172.64.0.0/13
        - 173.245.48.0/20
        - 188.114.96.0/20
        - 190.93.240.0/20
        - 197.234.240.0/22
        - 198.41.128.0/17
        - 2400:cb00::/32
        - 2606:4700::/32
        - 2803:f800::/32
        - 2405:b500::/32
        - 2405:8100::/32
        - 2a06:98c0::/29
        - 2c0f:f248::/32
        # End of Cloudlare public IP list
    http:
      redirections:
        entryPoint:
          to: https
          scheme: https

  # HTTPS endpoint, with domain wildcard
  https:
    address: :443
    forwardedHeaders:
      # Reuse list of Cloudflare Trusted IP's above for HTTPS requests
      trustedIPs: *trustedIps
    http:
      tls:
        # Generate a wildcard domain certificate
        certResolver: letsencrypt
        domains:
          - main: example.com # change this to your proxy domain
            sans:
              - '*.example.com' # change this to your proxy domain
      middlewares:
        - security-headers@file # reference to a dynamic configuration

providers:
  providersThrottleDuration: 2s

  # File provider for connecting things that are outside of docker / defining middleware
  file:
    filename: /etc/traefik/fileConfig.yml
    watch: true

  # Docker provider for connecting all apps that are inside of the docker network
  docker:
    watch: true
    network: proxy # Add Your Docker Network Name Here
    # Default host rule to containername.domain.example
    defaultRule: "Host(`{{ index .Labels \"com.docker.compose.service\"}}.example.com`)" # change 'example.com' to your proxy domain
    swarmModeRefreshSeconds: 15s
    exposedByDefault: false

# Use letsencrypt to generate ssl certificates
certificatesResolvers:
  letsencrypt:
    acme:
      email: <CF-EMAIL-ADDRESS>  # change to your provider account email address. The API token is defined in the docker-compose.yml as environment variable
      storage: /etc/traefik/acme.json
      dnsChallenge:
        provider: cloudflare
        # Used to make sure the dns challenge is propagated to the rights dns servers
        resolvers:
          - "1.1.1.1:53"
          - "1.0.0.1:53"
global configuration file traefik.yml
💡
The above traefik.yml configuration file will redirect incoming, unencrypted HTTP traffic automatically to HTTPS. Furthermore, Cloudflare is used to obtain SSL certificates and a default middleware is enabled to set various recommended HTTP response headers for all upcoming proxy hosts. Finally, docker is defined as provider and containers are not exposed by default.

Furthermore, create a file called fileConfig.yml, which will reflect our dynamic configuration definitions.

http:

  ## EXTERNAL ROUTING EXAMPLE - Only use if you want to proxy something manually ##
  #routers:
  #  homeassistant:
  #    entryPoints:
  #      - https
  #      - http
  #    rule: 'Host(`ha.example.com`)'
  #    service: homeassistant
  #    middlewares:
  #      - "local-ipwhitelist@file"

  #  pve:
  #    entryPoints:
  #      - https
  #      - http
  #    rule: 'Host(`pve.example.com`)'
  #    service: pve
  #    middlewares:
  #      - "local-ipwhitelist@file"

  ## SERVICES EXAMPLE ##
  #services:
  #  homeassistant:
  #    loadBalancer:
  #      serversTransport: insecureTransport
  #      servers:
  #        - url: http://192.168.1.10:8123

  #  pve:
  #    loadBalancer:
  #      serversTransport: insecureTransport
  #      servers:
  #        - url: https://192.168.1.20:8006

  # allow self-signed certificates for proxied web services
  serversTransports:
    insecureTransport:
      insecureSkipVerify: true

  ## MIDDLEWARES ##
  middlewares:
    # Only Allow Local networks
    local-ipwhitelist:
      ipWhiteList:
        sourceRange: 
          - 127.0.0.1/32 # localhost
          - 10.0.0.0/8 # private class A
          - 172.16.0.0/12 # private class B
          - 192.168.0.0/16 # private class C

    # Security headers
    security-headers:
      headers:
        customResponseHeaders: # field names are case-insensitive
          X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex"
          Server: "" # prevent version disclosure
          X-Powered-By: "" # prevent version disclosure
          X-Forwarded-Proto: "https"
        sslProxyHeaders:
          X-Forwarded-Proto: "https"
        hostsProxyHeaders:
          - "X-Forwarded-Host"
        customRequestHeaders:
          X-Forwarded-Proto: "https"
        contentTypeNosniff: true # X-Content-Type-Options
        customFrameOptionsValue: "SAMEORIGIN" # X-Frame-Options
        browserXssFilter: false # X-XSS-Protection; deprecated
        referrerPolicy: "strict-origin-when-cross-origin" # Referrer-Policy
        forceSTSHeader: true # HTTP-Strict-Transport-Security (HSTS)
        stsIncludeSubdomains: true # HTTP-Strict-Transport-Security (HSTS)
        stsSeconds: 63072000 # HTTP-Strict-Transport-Security (HSTS)
        stsPreload: true # HTTP-Strict-Transport-Security (HSTS)
        #contentSecurityPolicy: "block-all-mixed-content" # Content-Security-Policy (CSP)

# Only use secure ciphers - https://ssl-config.mozilla.org/#server=traefik&version=2.9&config=intermediate&guideline=5.6
tls:
  options:
    default:
      minVersion: VersionTLS12
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
dynamic configuration file fileConfig.yml
💡
The above fileConfig.yml configuration file defines a hardened SSL/TLS configuration by using strong algorithms and cipher suites only. Furthermore, it defines the aforementioned middleware securityHeaders to set various recommended HTTP response headers for security.

Finally, we also defined a new middleware called local-ipwhitelist, which can be later used to restrict access to proxied services by validating whether the request originated from local lan only.

This configuration file allows for dynamic changes during runtime. Traefik will notice changes and will apply them without the need of a container restart.

Creating our first proxy host

The following section will guide you through the process of defining our first proxy host. For NPM, we will use the provided management web-interface. For Traefik, we will use the configuration files and docker labels.

Proxy Host in Nginx Proxy Manager

A new proxy configuration can be defined by using the provided management web GUI on TCP/81. After logging into the web mgmt ui, we are greeted by various application areas such as dashboard, hosts, access lists, users, ssl certificates and many more.

The user interface is fairly intuitive and prompts for required input data such as your domain name, IP address or hostname and the port of your proxied service etc.

To create a new proxy host entry, go to Hosts > Proxy Hosts > Add Proxy Host. Insert your domain name and the IP address or hostname of your web service you want to proxy. If the NPM docker container runs in the same docker network as your to be proxied web service, you can neglect specifying an IP address. You can directly define the docker container name (e.g. nginx) and port (e.g. 80) of the docker container providing your web service to be proxied by NPM. Let's assume we want to proxy to a running nginx docker container:

Example proxy configuration
💡
In the NPM management UI you can configure various additional things such as SSL certificates, user accounts or advanced nginx configurations like custom locations etc. We will neglect these things for now, as it is thoroughly explained on various other blogs or Youtube tutorials.

Proxy Host in Traefik

Creating a new proxy host in Traefik is done differently then for Nginx Proxy Manager. While both reverse proxies provide a management web ui, only NPM allows you to actively create, modify and adjust all settings via the web GUI.

Traefik instead provides a management web UI solely to inspect your active configuration. The actual proxy configuration is done separately by either adjusting the configuration files or using labels for your to be proxied docker containers. We will use the latter option and define various labels to our example web service to proxy to.

Let's assume we want to proxy to an NGINX docker container. Our NGINX container is defined by the below docker-compose.yml. We will append a few Traefik labels before spawning up the container. These labels are automatically read and applied by the Traefik reverse proxy during runtime.

Labels define various things such as the subdomain our newly proxied web service will be accessible (traefik.http.routers.nginx.rule) as well as on which port the Traefik reverse proxy should forward client requests to (traefik.http.services.nginx.loadbalancer.server.port). With labels we can also enable additional middlewares or specify the docker network of our container.

version: "3"

services:
  web:
    image: nginx:latest
    hostname: nginx
    container_name: nginx
    restart: unless-stopped
    networks:
      - npm_proxy
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.nginx.rule=Host(`myfirstproxyhost.example.com`)
      - traefik.http.services.nginx.loadbalancer.server.port=80
      # - traefik.docker.network=nginx_default
      # Part for local lan services only
      # - traefik.http.routers.nginx.middlewares=local-ipwhitelist@file
      
networks:
  proxy:
    external: true
  npm_proxy:
    external: true 

After spawning up this NGINX docker container by docker compose up -d, Traefik will automatically pick up the labels and create a new proxy host.

💡
Ensure that the NGINX docker container is in the same docker network proxy as the Traefik container (or vice versa). Otherwise, the NGINX web service on port 80 may not be accessible by Traefik and proxying fails.

If everything went correctly, you should be able to see a new proxy host in Traefik's management web ui. Traefik will automatically obtain the NGINX container's docker IP address and proxy client requests to the defined container port TCP/80. If your DNS setup and/or port forwardings work, you should now be able to access your NGINX web service by browsing to https://myfirstproxyhost.example.com, as defined within the above docker labels.

Traefik management web UI shows our new nginx proxy host

Performance Benchmark

Traefik is known to be slower than NGINX. However, I wanted to conduct a small performance test between Nginx Proxy Manager and Traefik. We will use the container image openspeedtest to inspect, which reverse proxy provides faster speeds. We will benchmark the reverse proxies from the same client machine connected to the Internet and my server via Wi-Fi.

The following docker-compose.yml file for openspeedtest was used:

version: "3"
services:
  openspeedtest:
    image: openspeedtest/latest:latest
    container_name: openspeedtest
    restart: always
    networks:
      - npm_proxy
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.openspeedtest.rule=Host(`speedtest.lrvt.de`)
      - traefik.http.services.openspeedtest.loadbalancer.server.port=3000
      - traefik.docker.network=proxy
      - traefik.http.middlewares.limit.buffering.maxRequestBodyBytes=10000000000
      - traefik.http.middlewares.test-compress.compress=true
      - traefik.http.routers.openspeedtest.middlewares=limit
      
networks:
  proxy:
    external: true
  npm_proxy:
    external: true
docker-compose.yml for openspeedtest

The raw performance of the openspeedtest docker container via unencrypted HTTP on TCP/3000 without using a reverse proxy at all looks like this:

Raw performance of the openspeedtest container without using a proxy

Performance of Traefik

The Traefik reverse proxy will pick up and define the openspeedtest proxy host by applying our labels in the above docker-compose.yml file. We will terminate SSL at the reverse proxy and forward client requests to the unencrypted HTTP service on TCP/3000 of the openspeedtest container.

Afterwards, we can just browse to the subdomain and issue a speed test. Here an example result:

Results for Traefik (Client > HTTPS > Traefik > HTTP > Openspeedtest)

Performance of NPM

Nginx Proxy Manager as reverse proxy requires a manual proxy host setup. We will enable SSL and specify our subdomain. No caching, no compression. SSL will also be terminated at the NPM reverse proxy and client requests are forwarded to the unencrypted HTTP service on TCP/3000 of the openspeedtest container.

Afterwards, we can just browse to the subdomain and issue a speed test. Here an example result:

Results for NPM (Client > HTTPS > Traefik > HTTP > Openspeedtest)

Benchmark Conclusion

I have conducted the openspeedtest benchmark multiple times in a short period of time and noted the results in Excel. In average, Traefik as reverse proxy seems to be indeed slower than Nginx Proxy Manager, which uses Openresty under the hood.

However, the differences may be considered small and neglectable for a reverse proxy solely used in homelabs. However, for larger infrastructures or scaled services, the performance differences may have a bigger impact and must be considered.

Comparison of multiple openspeedtest results between NPM and Traefik

Final Recommendation

I would recommend Traefik as reverse proxy in the following cases:

  • You are an experienced, technical user and don't mind a steep learning curve. You like reading documentation and having various different configuration options to choose from.
  • You prefer the autodiscover feature of Traefik to detect new containerized services and create proxy hosts automatically for you.
  • You prefer infrastructure as code over configuring things via a web interface or GUI.
  • You want to circumvent the need and backup of a database for your reverse proxy.
  • You require native support for load balancing.
  • You like the support for dynamic configurations without having to restart your reverse proxy.
  • You want support for additional plugins like fail2ban, crowdsec, modsecurity, caching or geo blocking.
  • You want to implement an authentication provider like Authentik, Authelia etc. within your reverse proxy for Single-Sign-On (SSO). The setup should be well documented and supported by the reverse proxy, without having to find GitHub issues and implement unofficial configurations.
  • You proxy to containerized services solely (Docker, Kubernetes etc.), running on the same server or network stack. You only have a few external IP:Port proxy hosts that will require a manual Traefik configuration.
  • You don't mind the decreased performance of Traefik over NGINX.

I would recommend Nginx Proxy Manager as reverse proxy in the following cases:

  • You are a beginner and want to get things done quickly. No time for reading a lot of documentation and testing out various different configuration options.
  • You prefer an intuitive management web UI to configure things instead of crafting configuration files and your whole proxy infrastructure as code.
  • You do not require support for various authentication providers like Authentik or Authelia for Single-Sign-On (SSO).
  • You require support for audit logs to be able to tell which user made what changes.
  • You do not require native support for load balancing.
  • You want to proxy to services not solely running on the same server or being containerized. You want to be able to just specify an IP address and a port to create a new proxy host.
  • You require the improved performance of NGINX over Traefik.