WordPress, Nextcloud and Portainer behind Traefik Reverse Proxy with Docker Compose

Traefik is a Reverse Proxy that’s highly-scalable with load-balancing features with support for various providers such as Docker, Kubernetes. The file provider also exists if you do not wish to use Docker/Kubernetes. ACME providers such as Let’s Encrypt through HTTP/DNS challenge is also supported for easy installation of certificates. The DNS challenge is the method to request a wildcard certificate but it requires you to add 2x TXT records to your public DNS records (see your registrar).

The primary reason to use a reverse proxy is when you need multiple hosts (webservers) behind a single public IP. Multiple hosts as in; physical hosts or virtual machines with different local IP addresses. We can’t translate a pre-NAT port e.g. 443 more than once because that port is already translated.

34.34.34.34:443 (Pre-NAT) -> 192.168.10.10:443 (Post-NAT)

A simple but terrible solution is translating e.g. port 9001 to 443 but that requires users to enter https://domainname:9001 to reach the given webserver.

Another is example is when some web applications only listen on certain ports e.g. 8080 which you might not be able to change. A reverse proxy removes the need of including the port number in the URL.

Avoid Reverse Proxies if you only intend to serve a few domains (e.g. example.com, subdomain.example.com) on a single physical/virtual host. Instead utilize Apache2’s Virtual Hosts or Nginx’s Server Blocks instead. It’s a lot easier to set up and both of them support Reverse Proxies configurations in the Virtual Host/Server Block files – in case you want to remove the need of entering the port in the URL.

Prerequisites

  • Docker and Docker-Compose installed
  • Basic knowledge of containers
  • Prior experience with Nginx Server Blocks or Apache2 Virtual Hosts helps understanding it better
  • A valid domain name
  • Webserver is publicly routable in order to receive certificates via the HTTP-challenge method

Terminology used by Traefik

  • Providers are either Docker, Kubernetes or file-based.
  • An Entrypoint is your front-end Traefik will listen to.
  • Services are back-ends like a WordPress/Nextcloud instance.
  • Routers connect Entrypoints with Services or in other words the front-end with the back-end. Also it’s the parameter that will define your FQDN (example.com).
  • Middlewares adhere to your routers that provide additional functionality such as http-to-https redirecting or HTTP headers like HSTS.
  • Dynamic configurations from a provider (e.g. Docker), configurable with a .yaml or .toml file and with the use of labels in a docker-compose file.
  • Static configurations such as command-line arguments like “–providers.docker=true” and often referred to as startup-configuration. These will establish a connection with the provider (e.g. Docker)

Traefik’s docker-compose.yml

The idea is that we separate Traefik’s own docker-compose file from the rest like the back-end instances such as WordPress, Nextcloud or Portainer. It’s not necessary, but I find it more modular that way in the sense that it’s “centrally located”. Use nano instead of vi as your text ediot because I found vi to mess up the format after copy pasting. Yaml files are format sensitive. If the file contains unnecessary spaces and such docker-compose will fail to parse it.

$ sudo mkdir ~/traefik
$ cd ~/traefik
$ docker network create traefik
$ sudo nano docker-compose.yml

The network we used for the Traefik Container is just called “traefik” with external access being allowed. Our back-ends are able to communicate with that network.

version: "3.3"

networks:
    # Allow back-ends to communicate with this
    traefik:
        external: true
services:

  traefik:
    image: "traefik:v2.4.8"
    container_name: "traefik"
    restart: always
    command:
      # Only if you want to use the Traefik Dashboard
      - "--log.level=DEBUG"
      - "--api.dashboard=true"
      - "--api.insecure=false"
      # Define Docker as the provider here
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      # The location of where we will store our dynamic file with various configurations possible "on the fly"
      - "--providers.file.directory=/etc/traefik/dynamic"
      # Entrypoint -> is a front-end named "web" on HTTP Port 80
      - "--entrypoints.web.address=:80"
      # Entrypoint -> is a front-end named "web-secure" on HTTPS Port 443
      - "--entrypoints.web-secure.address=:443"

Next, we’ll set up our ACME provider as Let’s Encrypt with HTTP challenges

      # HTTP challenge as our method to acquire SSL certificates
      - "--certificatesresolvers.letsencrypthttpchallenge.acme.httpchallenge=true"
      - "--certificatesresolvers.letsencrypthttpchallenge.acme.httpchallenge.entrypoint=web"
      # Define Let's Encrypt as the issuer
      - "--certificatesresolvers.letsencrypthttpchallenge.acme.email=somemail@mail.com"
      # Location of the acme.json file
      - "--certificatesresolvers.letsencrypthttpchallenge.acme.storage=/letsencrypt/acme.json"

(Optional) The next section is just for Traefik’s own web-interface that has the dashboard for an overview of your routers, entry-points and services. Totally optional though and skip it if you’re concerned with security issues or if you don’t need it. Personally, I don’t find it that useful.

First run the following command to generate a password:

htpasswd -nb someuser somepass

Make sure to include it here in the traefik-auth.basicauth middleware.

    labels:
      # Enable the world of traefik here
      - "traefik.enable=true"
      # This will enable SSL
      - "traefik.http.routers.traefik-secure.tls=true"
      # Enable Let's Encrypt HTTP challenge
      - "traefik.http.routers.traefik-secure-secured.tls.certresolver=letsencrypthttpchallenge"
      # Use the web-secure as our entrypoint at 443
      - "traefik.http.routers.traefik-secure.entrypoints=web-secure"
      # The router here is just called traefik-secure and the FQDN rule
      - "traefik.http.routers.traefik-secure.rule=Host(`dashboard.example.com`)"
      # Refer to traefik's dashboard service
      - "traefik.http.routers.traefik-secure.service=api@internal"
      # Either use one of the following two below.
      - "traefik.http.routers.traefik-secure.middlewares=traefik-auth@file"
      #- "traefik.http.routers.traefik-secure.middlewares=homenetwork-ipwhitelist@file"

The following labels should be added for global HTTP -> HTTPS redirections and www.example.com -> example.com

      # Global redirection: http to https
      - traefik.http.routers.http-catchall.rule=HostRegexp(`{host:(www\.)?.+}`)
      - traefik.http.routers.http-catchall.entrypoints=web
      - traefik.http.routers.http-catchall.middlewares=wwwtohttps

      # Global redirection: https (www.) to https
      - traefik.http.routers.wwwsecure-catchall.rule=HostRegexp(`{host:(www\.).+}`)
      - traefik.http.routers.wwwsecure-catchall.entrypoints=web-secure
      - traefik.http.routers.wwwsecure-catchall.tls=true
      - traefik.http.routers.wwwsecure-catchall.middlewares=wwwtohttps

      # middleware: http(s)://(www.) to  https://
      - traefik.http.middlewares.wwwtohttps.redirectregex.regex=^https?://(?:www\.)?(.+)
      - traefik.http.middlewares.wwwtohttps.redirectregex.replacement=https://$${1}
      - traefik.http.middlewares.wwwtohttps.redirectregex.permanent=true

Finally we will define the ports and the volumes where the certificates and dynamic-config file are stored.

    # Specifying the Ports Traefik will listen on
    ports:
      - "80:80"
      - "443:443"
    # The network Traefik will use is just traefik
    networks:
      - "traefik"
    volumes:
      # Docker socket
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      # Let's Encrypt certificates in ./letsencrypt/acme.json
      - "./letsencrypt:/letsencrypt"
      # Mount the dynamic file configuration
      - ./dynamic-config.yaml:/etc/traefik/dynamic/dynamic-config.yaml

The file should look something like this.

Dynamic file: dynamic-config.yaml

Let’s create our dynamic-config.yaml file and specify the Min and Max TLS values along with strict SNI checking to strengthen our security.

$ cd ~/traefik
$ sudo nano dynamic-config.yaml

Copy paste this content and add any configurations you’ve digged up in the Traefik Docs can be inserted here without needing to reload your instances as it’s a dynamic configuration and not static.

tls:
  ### If you wish to use your paid SSL Certificates from e.g. DigiCert or GlobalSign###
  #certificates:
  #  - certFile: /etc/certs/fullchain.pem
  #    keyFile: /etc/certs/privkey.pem
  options:
    default:
      sniStrict: true
      minVersion: VersionTLS12
      maxVersion: VersionTLS13

    mintls13:
      minVersion: VersionTLS13
    maxtls12:
      maxVersion: VersionTLS12

http:
  middlewares:
    homenetwork-ipwhitelist:
      ipWhiteList:
        sourceRange:
          - 192.168.0.0/24
          - 127.0.0.1/32
    traefik-auth:
      basicAuth:
        removeHeader: true
        users:
          - USER:Hash-Password
    myheaders:
      headers:
        ### HSTS Headers ###
        stsSeconds: 315360000
        forceSTSHeader: true
        stsPreload: true
        stsIncludeSubdomains: true
        ## Other Headers ###
        referrerPolicy: no-referrer
        contentTypeNosniff: true
        frameDeny: true
        sslRedirect: true
        browserXssFilter: true

To start Traefik container:

$ docker-compose up -d

Portainer’s docker-compose.yml

First of all, make sure that you do not expose this publicly because if someone gains access to this instance – it’d be all over because significant damage can be inflicted. Instead implement zero-trust least privileges with granular policies. For example; create firewall security policies based on URL category and not just IP addresses and then restrict the source to a local subnet or an address from a single-host machine like your workstation.

We’ll use Portainer 2-0 CE which has a ton of features, such as updating or creating your containers and so much more. Hop we go.

Create a new directory in ~/traefik

$ sudo mkdir ~/traefik/portainer.example.com
$ cd ~/traefik/portainer.example.com

Next, we’ll configure our docker-compose file for portainer

$ sudo nano docker-compose.yml
version: '3'

networks:
  # here we will define the traefik network
  traefik:
    external: true
  # the network for the application backend (portainer) itself
  backend:
services:
  portainer:
    image: portainer/portainer-ce
    container_name: portainer
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      # here we'll specify the traefik and the backend network
      - traefik
      - backend
    volumes:
      # Sync Time config
      - /etc/localtime:/etc/localtime:ro 
      # Load Docker sock
      - /var/run/docker.sock:/var/run/docker.sock:ro
 
      # Local access portainer contents
      - ./portainer-data:/data
    labels:
      # Enable Traefik and specify the network
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      # Web-secure https entrypoint
      - "traefik.http.routers.portainer-secure.entrypoints=web-secure"
      # FQDN
      - "traefik.http.routers.portainer-secure.rule=Host(`portainer.example.com`)"
      # Enable TLS
      - "traefik.http.routers.portainer-secure.tls=true"
      # Enable Let's Encrypt HTTP challenge
      - "traefik.http.routers.traefik-secure-secured.tls.certresolver=letsencrypthttpchallenge"
      - "traefik.http.routers.portainer-secure.service=portainer"
      - "traefik.http.services.portainer.loadbalancer.server.port=9000"
      # Limit access to Portainer with IP whitelisting
      - "traefik.http.routers.portainer-secure.middlewares=homenetwork-ipwhitelist@file"

To launch Portainer 2.0

The file should look something like this.

docker-compose up -d

WordPress’s docker-compose.yml

WordPress depends on a database such as MySQL or MariaDB which we have to configure. Optional dependencies such as Redis for caching purposes can also be added. But to keep it simple, I’ve excluded Redis but if there’s demand for it then I’ll add it. We will also add HSTS headers (Strict-Transport-Security) which will also prevent downgrade attacks. In Traefik it’s falls under the middlewares category. Although it’s optional, your website will score much better at ssllabs.com.

Create a new directory in ~/traefik:

$ sudo mkdir ~/traefik/example.com
$ cd ~/traefik/example.com

And our docker-compose file:

$ sudo nano docker-compose.yml

Network configuration:

version: '3'

networks:
  # here we will define the traefik network
  traefik:
    external: true
  # the network for the application backend (WordPress) itself
  backend:

WordPress container configuration:

  wordpress:
    image: wordpress
    restart: always
    environment:
      # Hook WordPress up with MySQL 
      WORDPRESS_DB_HOST: mysql:3306
      WORDPRESS_DB_USER: wordpressuser
      WORDPRESS_DB_PASSWORD: password
      WORDPRESS_DB_NAME: wordpressdb
    volumes:
      # save the content of WordPress an enable local modifications
      - ./wordpress/data:/var/www/html # allows for local modifications editing php files etc
    networks:
      # define the traefik and the backend network
      - traefik
      - backend
    depends_on:
        # Enable the MySQL dependency
        - mysql
    labels:
      # Turn on Traefik
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      # HTTPS Entrypoint
 and FQDN
      - "traefik.http.routers.wordpress-secure.rule=Host(`example.com`)"
      - "traefik.http.routers.wordpress-secure.entrypoints=web-secure" 
      # Enable TLS
      - "traefik.http.routers.wordpress-secure.tls=true"
      # Enable Let's Encrypt HTTP challenge
      - "traefik.http.routers.traefik-secure-secured.tls.certresolver=letsencrypthttpchallenge"
      # Enable HSTS headers and other security headers
      - "traefik.http.routers.wordpress-secure.middlewares=myheaders@file"

The MySQL container.

  mysql:
    # this is the database used by WordPress
    image: mysql:5.7
    restart: always
    environment:
      # WordPress Database
 entries
      MYSQL_DATABASE: wordpressdb
      MYSQL_USER: wordpressuser
      MYSQL_PASSWORD: password
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      # The database will also be stored in ./db on your local machine and container itself
      - ./db:/var/lib/mysql
    networks:
      # The Traefik network is not needed
      - backend

The file should look like something this.

Finally, start up the WordPress container and browse to https://example.com and make sure the hostname is just “mysql” when configuring the database.

docker-compose up -d

Nextcloud’s docker-compose.yml

Let’s complete this guide with Nextcloud as our last back-end instance. MySQL is also needed here. Nextcloud needs no further introduction, so we’ll jump straight in it.

Create a new directory for nextcloud

$ sudo mkdir ~/traefik/nextcloud.example.com
$ cd ~/traefik/nextcloud.example.com

And let’s create our last docker-compose file:

$ sudo nano docker-compose.yml

Edit it and let’s start off with our network configuration:

version: '3'

networks:
  # Enable connection with Traefik
  traefik:
    external: true
  # network for the app
  backend:

The Nextcloud container configuration:

services:

  nextcloud:
    restart: always
    image: nextcloud
    environment:
      # Hook Nextcloud up with MySQL 
      NEXTCLOUD_DB_HOST: mysql:3306
      NEXTCLOUD_DB_USER: nextclouduser
      NEXTCLOUD_DB_PASSWORD: password
      NEXTCLOUD_DB_NAME: nextclouddb
    volumes:
      # Store the contents of Nextcloud and enable local modifications
      - ./data/volumes/nextcloud/html:/var/www/html
    networks:
      - traefik
      - backend
    depends_on:
        - mysql
    labels:
      # Connect to Traefik
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      # Middlewares Caldav
      - "traefik.http.middlewares.nextcloud-caldav.redirectregex.permanent=true"
      - "traefik.http.middlewares.nextcloud-caldav.redirectregex.regex=^https://(.*)/.well-known/(card|cal)dav"
      - "traefik.http.middlewares.nextcloud-caldav.redirectregex.replacement=https://$${1}/remote.php/dav/"
      # Connect Middleware Caldav with Nextcloud-router
      - "traefik.http.routers.nextcloud-secure.middlewares=nextcloud-caldav@docker"
      # Route HTTPS
      - "traefik.http.routers.nextcloud-secure.entrypoints=web-secure"
      - "traefik.http.routers.nextcloud-secure.rule=Host(`nextcloud.example.com`)"
      # Enable TLS
      - "traefik.http.routers.nextcloud-secure.tls=true"
      # Enable Let's Encrypt HTTP challenge
      - "traefik.http.routers.traefik-secure-secured.tls.certresolver=letsencrypthttpchallenge"
      # Enable HSTS Headers and other security headers
      - "traefik.http.routers.nextcloud-secure.middlewares=myheaders@file"

At last, the MySQL container:

 mysql:
    # The database Nextcloud will connect to
    image: mysql:5.7
    restart: always
    environment:
      # Nextcloud Database
 entries
      MYSQL_DATABASE: nextclouddb
      MYSQL_USER: nextclouduser
      MYSQL_PASSWORD: password
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      # The database will also be stored in ./db on your local machine and container itself
      - ./db:/var/lib/mysql
    networks:
      # The Traefik network is not needed here
      - backend

The file should look something like this.

Run docker-compose:

$ docker-compose up -d

As for the database host, replace “localhost” with “mariadb”. Else the installation would fail because it can’t connect to the database.

Conclusion

Traefik is quite hard to learn, especially if you’ve never worked with docker before. Once you get the hang of it, everything starts to make sense as you go along and it doesn’t seem all too complicated. Make sure to check their documentation because it has plenty of valuable configuration examples and explanations.

Leave a Reply

Your email address will not be published. Required fields are marked *