So first up lets get Traefik set-up so that we can give all our public services a domain to make it easier to access.

Create a new docker-compose.yml file in an empty folder and paste the following

version: '3.8'

services:
  traefik:
    image: traefik:latest
    restart: always
    ports:
      - 80:80
      - 443:443
    command:
      - --accesslog
      - --log.level=INFO
      - --api.dashboard=true
      - --api.insecure=true
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.web-secure.address=:443
      #- --certificatesresolvers.letsencrypt.acme.tlschallenge=true
      #- --certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
      #- [email protected]
      #- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - traefik:/letsencrypt
    labels:
      - traefik.enable=true
      - traefik.http.routers.api.rule=Host(`${TRAEFIK_URL}`)
      - traefik.http.routers.api.entrypoints=web
      - [email protected]
      #- traefik.http.services.traefik.loadbalancer.server.port=8080
      #- traefik.http.routers.api.entrypoints=web-secure
      #- traefik.http.routers.api.tls=true
      #- traefik.http.routers.api.tls.certresolver=letsencrypt

If you’ve looked at my previous post on WordPress and Traefik, this Traefik configuration script is essentially the same.

Next up configuring WordPress, WordPress by default works with Apache webserver, now while that setup isn’t much of a problem for very small sites, as your website grows and gets bulkier you’ll soon realize you need a bit more performance, thus Nginx.

First inside the same folder we just created for the docker-compose.yml file, create a new folder called nginx, and inside the nginx folder create another folder called conf, inside the conf folder create a new file called default.conf

Inside the default.conf file add the following:

# Upstream to abstract backend connection(s) for php
upstream php {
        #server unix:/var/run/php/php-cgi.socket;
        server wordpress:9000;
}

server {
        ## Your website name goes here.
        server_name localhost;
        ## Your only path reference.
        root /var/www/html;
        ## This should be in your http block and if it is, it's not needed here.
        index index.php;

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        location = /favicon.ico {
                log_not_found off;
                access_log off;
        }

        location = /robots.txt {
                allow all;
                log_not_found off;
                access_log off;
        }

        # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
        location ~ /\. {
            deny all;
        }
        
        # Deny access to any files with a .php extension in the uploads directory
        # Works in sub-directory installs and also in multisite network
        location ~* /(?:uploads|files)/.*\.php$ {
            deny all;
        }

        location / {
                # This is cool because no php is touched for static content.
                # include the "?$args" part so non-default permalinks doesn't break when using query string
                try_files $uri $uri/ /index.php?$args;
        }

        location ~ \.php$ {
                #NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
                include fastcgi_params;
                fastcgi_intercept_errors on;
                fastcgi_pass php;
                #The following parameter can be also included in fastcgi_params file
                fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        }

        # Caching of media: images, icons, video, audio
        location ~* \.(jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|woff|woff2)$ {
                expires max;
                log_not_found off;
        }

        # CSS and JavaScript
        location ~* \.(css|js)$ {
                expires max;
                log_not_found off;
        }
}

Next reopen the docker-compose.yml file and add:

nginx:
    image: nginx:alpine
    restart: always
    depends_on:
      - wordpress
    expose:
      - 80
      - 443
    volumes:
      - ./nginx/conf:/etc/nginx/conf.d
      - ./nginx/logs:/var/log/nginx
      - ./wordpress:/var/www/html
    labels:
      - traefik.enable=true
      - traefik.http.routers.wordpress.rule=Host(`${WP_URL}`, `${WP_WWW_URL}`)
      - traefik.http.routers.wordpress.entrypoints=web
      #- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
      #- [email protected]ker
      #- traefik.http.routers.wordpress-secure.rule=Host(`${WP_URL}`, `${WP_WWW_URL}`)
      #- traefik.http.routers.wordpress-secure.entrypoints=web-secure
      #- traefik.http.routers.wordpress-secure.tls.certresolver=letsencrypt

Now lets configure WordPress, because we are setting WordPress up to use Nginx, we must specify the version of the WordPress image tag for docker to use, rather than simply putting latest. On the WordPress Docker Hub page, the images that support Nginx would be denoted with fpm in the name.

In the existing docker-compose.yml file add:

wordpress:
    image: wordpress:php8.0-fpm-alpine
    restart: always
    depends_on: 
      - mariadb
      - redis
    expose:
      - 9000
    environment:
      WORDPRESS_DB_HOST: mariadb
      WORDPRESS_DB_NAME: ${WP_DB_NAME}
      WORDPRESS_DB_USER: ${WP_DB_USER}
      WORDPRESS_DB_PASSWORD: ${WP_DB_PASSWORD}
      WORDPRESS_TABLE_PREFIX: ${WP_DB_PREFIX}
      WORDPRESS_CONFIG_EXTRA:
        define( 'WP_REDIS_HOST', 'redis' );
        define( 'WP_REDIS_PORT', 6379 );
        define( 'COMPRESS_CSS', true );
        define( 'COMPRESS_SCRIPTS', true );
        define( 'CONCATENATE_SCRIPTS', true );
        define( 'ENFORCE_GZIP', true );
        define( 'AUTOSAVE_INTERVAL', 120 );
        define( 'WP_POST_REVISIONS', 10);
        define( 'MEDIA_TRASH', true );
        define( 'EMPTY_TRASH_DAYS', 30 );
        define( 'IMAGE_EDIT_OVERWRITE', true );
        define( 'DISALLOW_FILE_EDIT', true );
    volumes:
      - ./wordpress:/var/www/html
      - ./wordpress.ini:/usr/local/etc/php/conf.d/wordpress.ini

The biggest change is the default port on which WordPress responds, which defaults to port 9000

Next create a file wordpress.ini next to the docker-compose.yml file and add:

file_uploads = On
upload_max_filesize = 128M
post_max_size = 128M
memory_limit = 256M
max_execution_time = 60

Next up MariaDB and Adminer

mariadb:
    image: mariadb:latest
    restart: always
    expose:
      - 3306
    environment:
      MYSQL_DATABASE: ${WP_DB_NAME}
      MYSQL_USER: ${WP_DB_USER}
      MYSQL_PASSWORD: ${WP_DB_PASSWORD}
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - mariadb:/var/lib/mysql

  adminer:
    image: adminer:latest
    restart: always
    depends_on: 
      - mariadb
    expose:
      - 8080
    labels:
      - traefik.enable=true
      - traefik.http.routers.adminer.rule=Host(`${ADMINER_URL}`)
      - traefik.http.routers.adminer.entrypoints=web
      #- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
      #- [email protected]r
      #- traefik.http.routers.adminer-secure.rule=Host(`${ADMINER_URL}`)
      #- traefik.http.routers.adminer-secure.entrypoints=web-secure
      #- traefik.http.routers.adminer-secure.tls.certresolver=letsencrypt

Then we set up Redis to cache database queries, so add the following to your docker-compose.yml file

redis:
    image: redis:latest
    restart: always
    expose:
      - 6379
    command: 
      - redis-server
      - --save 60 1 
      - --loglevel warning
      - --maxmemory 128mb 
      - --maxmemory-policy allkeys-lru 
    volumes:
      - redis:/var/lib/redis
      - redis:/data
      #- ./redis.conf:/usr/local/etc/redis/redis.conf

I covered how to get WordPress setup with Redis in my previous post

Finally we create a .env file to store any variables set in the docker-compose.yml file

# WordPress
WP_DB_NAME=wp-dev-db
WP_DB_PREFIX=wp_
WP_DB_USER=wp-dev-user
WP_DB_PASSWORD=wp-dev-pass

# Traefik Domains
WP_URL=wpdev.test
WP_WWW_URL=www.wpdev.test
TRAEFIK_URL=traefik.wpdev.test
ADMINER_URL=adminer.wpdev.test

At the end your docker-compose.yml file should look similar to this:

version: '3.8'

services:
  traefik:
    image: traefik:latest
    restart: always
    ports:
      - 80:80
      - 443:443
    command:
      - --accesslog
      - --log.level=INFO
      - --api.dashboard=true
      - --api.insecure=true
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.web-secure.address=:443
      #- --certificatesresolvers.letsencrypt.acme.tlschallenge=true
      #- --certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
      #- [email protected]
      #- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - traefik:/letsencrypt
    labels:
      - traefik.enable=true
      - traefik.http.routers.api.rule=Host(`${TRAEFIK_URL}`)
      - traefik.http.routers.api.entrypoints=web
      - [email protected]
      #- traefik.http.services.traefik.loadbalancer.server.port=8080
      #- traefik.http.routers.api.entrypoints=web-secure
      #- traefik.http.routers.api.tls=true
      #- traefik.http.routers.api.tls.certresolver=letsencrypt
  
  nginx:
    image: nginx:alpine
    restart: always
    depends_on:
      - wordpress
    expose:
      - 80
      - 443
    volumes:
      - ./nginx/conf:/etc/nginx/conf.d
      - ./nginx/logs:/var/log/nginx
      - ./wordpress:/var/www/html
    labels:
      - traefik.enable=true
      - traefik.http.routers.wordpress.rule=Host(`${WP_URL}`, `${WP_WWW_URL}`)
      - traefik.http.routers.wordpress.entrypoints=web
      #- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
      #- [email protected]ker
      #- traefik.http.routers.wordpress-secure.rule=Host(`${WP_URL}`, `${WP_WWW_URL}`)
      #- traefik.http.routers.wordpress-secure.entrypoints=web-secure
      #- traefik.http.routers.wordpress-secure.tls.certresolver=letsencrypt
    
  wordpress:
    image: wordpress:php8.0-fpm-alpine
    restart: always
    depends_on: 
      - mariadb
      - redis
    expose:
      - 9000
    environment:
      WORDPRESS_DB_HOST: mariadb
      WORDPRESS_DB_NAME: ${WP_DB_NAME}
      WORDPRESS_DB_USER: ${WP_DB_USER}
      WORDPRESS_DB_PASSWORD: ${WP_DB_PASSWORD}
      WORDPRESS_TABLE_PREFIX: ${WP_DB_PREFIX}
      WORDPRESS_CONFIG_EXTRA:
        define( 'WP_REDIS_HOST', 'redis' );
        define( 'WP_REDIS_PORT', 6379 );
        define( 'COMPRESS_CSS', true );
        define( 'COMPRESS_SCRIPTS', true );
        define( 'CONCATENATE_SCRIPTS', true );
        define( 'ENFORCE_GZIP', true );
        define( 'AUTOSAVE_INTERVAL', 120 );
        define( 'WP_POST_REVISIONS', 10);
        define( 'MEDIA_TRASH', true );
        define( 'EMPTY_TRASH_DAYS', 30 );
        define( 'IMAGE_EDIT_OVERWRITE', true );
        define( 'DISALLOW_FILE_EDIT', true );
    volumes:
      - ./wordpress:/var/www/html
      - ./wordpress.ini:/usr/local/etc/php/conf.d/wordpress.ini
      
  mariadb:
    image: mariadb:latest
    restart: always
    expose:
      - 3306
    environment:
      MYSQL_DATABASE: ${WP_DB_NAME}
      MYSQL_USER: ${WP_DB_USER}
      MYSQL_PASSWORD: ${WP_DB_PASSWORD}
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - mariadb:/var/lib/mysql

  adminer:
    image: adminer:latest
    restart: always
    depends_on: 
      - mariadb
    expose:
      - 8080
    labels:
      - traefik.enable=true
      - traefik.http.routers.adminer.rule=Host(`${ADMINER_URL}`)
      - traefik.http.routers.adminer.entrypoints=web
      #- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
      #- [email protected]r
      #- traefik.http.routers.adminer-secure.rule=Host(`${ADMINER_URL}`)
      #- traefik.http.routers.adminer-secure.entrypoints=web-secure
      #- traefik.http.routers.adminer-secure.tls.certresolver=letsencrypt

  redis:
    image: redis:alpine
    restart: always
    expose:
      - 6379
    command: 
      - redis-server
      - --save 60 1 
      - --loglevel warning
      - --maxmemory 128mb 
      - --maxmemory-policy allkeys-lru 
    volumes:
      - redis:/var/lib/redis
      - redis:/data
      #- ./redis.conf:/usr/local/etc/redis/redis.conf

volumes:
  traefik:
  mariadb:
  redis:

Once finished you can Setup WordPress using the URL set in the .env file and everything should load up as usual.