Migrating a Wordpress local install to a Traefik Dockerised stack

I have two legacy Wordpress sites on a VPS with 2GB memory. I'd like to move them to an upgraded Ubuntu keeping Nginx, and at the same time - Dockerise!

Migrating a Wordpress local install to a Traefik Dockerised stack

I have two legacy Wordpress sites on a VPS with 2GB memory. I'd like to move them to an upgraded Ubuntu keeping Nginx, and at the same time - Dockerise!

My migration target is a Wordpress dual stack, with Nginx and Maria-DB which I just built, as per this post. The addition of an Nginx layer might not be necessary for your purposes, in which case you would probably substitute the Wordpress Docker image I'm using here, with a version that has Apache included.

The before picture - locally installed Wordpress with a shared MySQL instance.

The after picture - multiple Wordpress in a Traefik based Docker stack

Interim domain names

As I am building all of this while my production sites are up and running, I will need to create interim domain names and point at the new server. I will use the convention of "new" at the front, such as new.yourblog1.com.

The second stack is Wordpress Multisite, so it will be interesting to see how I can forward multiple domains in this configuration. I'll cover that migration in a subsequent post.

If you have the cahones there is the option of pointing the production domains to your new server and just being down while you hack about, but I wouldn't recommend it. Having said that, I might have a go at this for my second stack, once I have a solid config for the first.

Import and configure Nginx

There are a number of parts of the Nginx build that have important configuration that I think I will need to use.

Nginx File Purpose Strategy
/etc/nginx/nginx.conf Contains http block, param for xml-rpc rate limiting Try rate limiter in the main config file
/etc/nginx/modules-enabled Contains multiple Check it works ok without, possibly enable
/etc/nginx/sites-enabled/sitename Contains my server blocks. Migrate to default.conf file, remove SSL. Split the two WP instances.
/etc/nginx/global/ipfilter.conf IP filter whitelist Keep, migrate to new .inc files
/etc/nginx/global/restrictions.conf File I made with a few security tweaks Keep, migrate to new .inc files
/etc/nginx/snippets/* Various cache and SSL files Ignore SSL parts, repoint to fastcgi-php.conf

Nginx default modules

This is the list of modules in the default Nginx Alpine container. It doesn't match my current module list, so I am hoping there are no module changes needed.

ngx_http_geoip_module-debug.so, ngx_http_js_module-debug.so, ngx_stream_geoip_module-debug.so, ngx_http_geoip_module.so, ngx_http_js_module.so, ngx_stream_geoip_module.so, ngx_http_image_filter_module-debug.so, ngx_http_xslt_filter_module-debug.so, ngx_stream_js_module-debug.so, ngx_http_image_filter_module.so, ngx_http_xslt_filter_module.so, ngx_stream_js_module.so

Break apart the two site configs

My planned steps for Nginx setup.

  • Create /data/nginx-wp1 and 2, attach the volumes
  • Extract the supplied nginx.conf, add my xml-rpc rate limiter definition, add to the shared volumes, and attach the file
  • Copy in the ipfilter.conf and restrictions.conf to each shared volume with .inc suffixes and edit network information
  • Migrate the server blocks

Attaching the Nginx volumes

I will apply the volumes as such (full compose file at the end of the post). All of the "left side of the colon" directories will need to exist before applying. This example is the nginx-wp1 docker service:

                volumes:
                          - /data/nginx-wp1:/etc/nginx/conf.d
                          - /data/nginx-wp1/nginx.conf.new:/etc/nginx/nginx.conf
                          - /data/logs/nginx-wp1:/var/log/nginx
                          - /data/wp1:/var/www/html

The default nginx.conf extracted from the container

There are instructions on how to extract such things on the Nginx Docker Hub page. This is the supplied nginx.conf, which I am going to mess with as little as possible.

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

You can probably ignore this part but I am adding as it's interesting should you wish to customise the http block / nginx.conf.

I inject my line limit_req_zone $binary_remote_addr zone=one:10m rate=30r/m; which defines a rate limit spec with alias "one".

It's arbitrary as long as it's a shared location, I will use my /data/nginx-wp1 & 2 volumes and map the individual file. I add an entry to my volumes for both nginx services as: - /data/nginx-global/nginx.conf.new:/etc/nginx/nginx.conf

I save the file as /data/nginx-wp1/nginx.conf.new & also in 2, and map them.

Create a PHP invocation wrapper

Every time a php script is to be read, there are a number of lines repeated, so it's more concise to make a small include script. I create a fastcgi-php.inc script with these contents, copied from the default server block, with these contents:

try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;

Every time I have a PHP invocation, I precede it with the include, as you will see throughout the examples:

location ~ \.php$ {
        include conf.d/fastcgi-php.inc;
        fastcgi_pass wp1:9000;
}

Copy in my Nginx global include files

I copy my ipfilter.conf and restrictions.conf files into /data/nginx-wp1 & 2 and edit, changing the suffixes to .inc. If you are not interested in IP whitelisting or my small security tweaks, scroll forward to the server block config section.

My ipfilter.conf file is like this.

# Whitelist for login page
    location ^~ /wp-login.php {
        # Static IP of authorised location 1
        allow 1.2.3.4/32 ;
        # Coworking location
        allow 2.3.4.5/32 ;
        deny all;
        include conf.d/fastcgi-php.inc;
        fastcgi_pass wp1:9000; # wp1 is the wordpress docker network
    }
    location /wp-admin {
        location ~ /wp-admin/admin-ajax.php$ {
                include conf.d/fastcgi-php.inc;
                fastcgi_pass wp1:9000;   # wp1 is the wordpress docker network name
        }
        location ~* /wp-admin/.*\.php$ {
                # Static IP of authorised location 1
                allow 1.2.3.4/32 ;
                # Coworking location
                allow 2.3.4.5/32 ;
                deny all;
                include conf.d/fastcgi-php.inc;
                fastcgi_pass wp1:9000;   # wp1 is the wordpress docker network name
        }
    }

Note the wp1:9000 entries. The wp1 must correspond to your wordpress docker-compose service name. These are needed for any PHP invocation. The nginx-wp2 ipfilter file will need wp2 .

I copy in my restrictions.conf as restrictions.inc and tweak for the service names.

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

location = /xmlrpc.php {
        limit_req zone=one burst=3 nodelay;
        include conf.d/fastcgi-php.inc;
        fastcgi_pass wp1:9000; # This is the Wordpress docker network
 }

location ~* /(?:uploads|files)/.*\.php$ {
        deny all;
}

As with the ipfilter, I will make a copy for nginx-wp2 with update wp2:9000 in it.

Nginx server block configuration

I take my legacy server block config files and attempt to clean up, gleefully removing SSL and 80-> 443 redirects. The new file will be one per stack, so I will make it as a new default.conf, which lives in the /data/nginx-wp1 and 2 directories. Things will start to get interesting between 1 and 2 as site 2 is a multisite. For WP1 I am going with the following.

server {
        listen 80 ;
        listen [::]:80 ;

        root /var/www/html;
        index index.php index.html index.htm index.nginx-debian.html;

        real_ip_header X-Forwarded-For;
        set_real_ip_from traefik; # Your internal Traefik network name
        
        server_name yourblog1.com www.yourblog1.com ;
        include conf.d/restrictions.inc;

        location / {
                try_files $uri $uri/ /index.php$is_args$args;
        }
        include conf.d/ipfilter.inc ;
        location ~ \.php$ {
                include conf.d/fastcgi-php.inc;
                fastcgi_pass wp1:9000;
        }

        location ~ /\.ht {
        deny all;
        }
        location ~* \.(css|gif|ico|jpeg|jpg|js|png|woff|ttf)$ {
                expires max;
                log_not_found off;
        }
}

As with the other files, the fastcgi_pass must be pointed at the right WP docker network target, wp1:9000.

I went through all sorts of pain finding a way to pass through the real IP address to Nginx for whitelisting purposes. I thought it needed a manual addition of the ngx_http_realip_module module but nay, it appears the real IP can be accessed with the standard alpine image, by using Nginx to grab it from Traefik, hence the two lines:

real_ip_header X-Forwarded-For;
set_real_ip_from traefik; # Your internal Traefik network name

From this point on, allow/deny rules etc should automatically pick up the new addresses. You will need to use the Traefik network name for your service as defined in the docker-compose services: section. In all my examples it's just "traefik".

Permissions

I have run the following at the shell in order to set web server permissions and allow my ubuntu user to edit files:

$ cd /data
$ sudo chown -R www-data:www-data nginx-wp?
$ sudo chmod 660 nginx-wp?
$ sudo usermod -aG www-data ${USER}

Logging out and back in should apply the group to your user. I haven't spent a lot of time looking at permissions, this might need more tuning.

Test IP whitelist (optional)

If you are on the IP whitelist track, simply reloading the wp-login page while altering the wp-login part of the whitelist, or if already logged in, reloading the wp-admin page while altering that section of the whitelist should suffice.

Always test. You should see entries in the access log alternating between 200's and 403's. I fully tested this config while sitting at the default installation screen - it has wp-admin in the path.

Export/Import your production Wordpress database and site files

And you thought you were having fun so far! For the migration you will need two things, a dump of the database, and a copy of the Wordpress html/php files. They'll need to be at the same point in time, so you'll have to generate/copy them while there are no site updates going on.

Be aware that even after a successful import, a lot of the config will be dropped, such as theme settings and widgets, so you'll need some time to tweak all of that. If you have and API connections, licensed plugins or login credentials (e.g. SMTP) ensure you have these on hand ready to reapply, regenerate or reconnect.

Export the Database

I am making some assumptions all over the place here, such as you are capable of connecting to mysql as root, moving files between servers, etc, and won't dwell on how to do that sort of thing. This will be more of a bunch of specific steps I am following that might be of use to you.

Connect to mysql as root and run show databases;. If you have some legacy database that has been shared, as I have, there will be multiple databases. You might just have one which by default is called "wordpress". For I am doing (single DB container) I just want to extract a single DB.

If the DB was called wordpress, you would drop to the operating system prompt and run this (mysql passwd will be prompted). It's non-destructive so shouldn't hurt your running DB. If it's not called wordpress, change the wordpress name in the below command.

mysqldump -u root -p wordpress | gzip - > siteDump.wordpress.sql.gz

Move the file to your new server.

Export the Wordpress html directory tree

There's nothing too complex here, we're just rolling up the HTML files. I change directory to my HTML files and create a compressed tarball. This example assumes you have it all under /var/www/html, i.e. the path for your config file is /var/www/html/wp-config.php. You might want to have a wander through this tree and look for old junk filling it up and remove it first. It can get large with images, uploads, themes, plugins and general Wordpress bloat. And it's likely file transfers will eat into your bandwidth allowance.

From within the file tree, (the same directory as wp-config.php) we can create an archive in the directory above.

$ cd /var/www/html
$ tar -zcvf ../wordpress.tar.gz .

Move this new .gz file also to your new server.

Import the old database into the new image

On your new server, we set up the DB first. We import all the data as is, we can rename the database but we don't yet rename any URLs, it will still have production ones.

If you ran through the previous post about a default installation, there will already be a database for mariadb1, so let's delete the contents. Get the container name from "docker ps". Make sure you are in the right place before running that rm -r.

$ docker stop traefik_mariadb1_1  
$ cd /data/mariadb1 
$ sudo rm -r * 
$ cd [to wherever your compose file is]
$ docker-compose up -d

The docker compose should already be set up to recreate the whole instance with a database called wp1.

If you gzipped the SQL dump file, gunzip it. Here's my command which assumes you have a running container called traefik_mariadb1_1 (check with "docker ps").

docker exec -i traefik_mariadb1_1 mysql -uroot -psecure_password_123 wp1 < siteDump.wordpress.sql

This will import the legacy database into the wp1 database.

Alter the database URLs to "new" URLs.. or not.

Decision time. If you are going to fire this blog up with a temporary URL, we alter the name at this point. I imagine only the most unimportant blogs, where downtime doesn't matter, would be a big bang switch, otherwise we rename. This allows you at leisure to preconfigure before renaming back.

I've had a script lying around for eons, I don't even know where it's from. It will chug through the DB and rename all the URLs. Use at your own risk. I save as dbreplace.sh, remember to chmod +x it.

It is best to run this inside the container, but first let's create it in the shared volume.

CD to /data/mariadb1

vim (or favourite text editor) the new file.

sudo vim dbreplace.sh

..and paste in the below. After saving, chmod +x.

#!/bin/bash
echo -n "Enter username: " ; read db_user
echo -n "Enter $db_user password: " ; stty -echo ; read db_passwd ; stty echo ;
echo ""
echo -n "Enter database name: " ; read db_name
echo -n "Enter search string: " ; read search_string
echo -n "Enter replacement string: " ; read replacement_string

MYSQL="/usr/bin/mysql --skip-column-names -u${db_user} -p${db_passwd}"

echo "SHOW TABLES;" | $MYSQL $db_name | while read db_table
do
        echo "SHOW COLUMNS FROM $db_table;" | $MYSQL $db_name| \
        awk -F'\t' '{print $1}' |grep -v "default" |grep -v "values" | while read tbl_column
        do
                #echo "update $db_table set ${tbl_column} = replace(${tbl_column}, '${search_string}', '${replacement_string}');"
                echo "update $db_table set ${tbl_column} = replace(${tbl_column} , '${search_string}', '${replacement_string}');"  | $MYSQL $db_name
        done
done

We enter the container thus, giving your container name (found with docker ps).

docker exec -it traefik_mariadb1_1 bash

Again, use this script at your own risk. At the # prompt, cd to /var/lib/mysql and it should be visible - run it. The password will be your new DB root user, and the database name will be wp1. Your dialogue might look like this.

# ./dbreplace.sh
Enter username: root
Enter root password:
Enter database name: wp1
Enter search string: yourblog1.com
Enter replacement string: new.yourblog1.com

It will replace for example, yourblog1.com with new.yourblog1.com . Your DNS zone file will of course need to be able to resolve subdomains for this to work, e.g. *.yourblog1.com. When you are ready to switch over production, you can alter the blog name through the Wordpress admin panel itself but is still worth running it again in reverse, for the many scrappy strings lying about.

Your chosen name will need to match the traefik label, e.g.

- traefik.frontend.rule=Host:new.yourblog1.com,www.new.yourblog1.com

Import the Wordpress HTML directory tree

If you are following this build, we'll be unpacking wordpress.tar.gz into /data/wp1. If you set up the default WP install from the last post, it will have files already in there. remove them all first - before you move the tarball in there.

$ docker stop traefik_wp1_1 traefik_nginx-wp1_1
$ cd /data/wp1

Check the tarball is not here. In /data/wp1 because I am replacing an old build I clear all files:

$ sudo rm -r * .htaccess

You might not have a .htaccess - ignore errors removing. Copy/move the tarball into /data/wp1.

$ tar xvf wordpress.tar.gz
$ cd [to wherever your compose file is]
$ docker-compose up -d

This should unpack everything relative to your current directory. When done, you should see /data/wp1/wp-config.php, and everything else. As if by magic, after the compose run you should see a version of your old site on the new url. If you get redirected back to the original URL, try a private window, your browser session has probably been mashed.

Be aware - Wordpress re-config will be necessary

It probably looks different, right? A lot of config has been dropped and you will need to repair. Luckily, we have the production system available to to a side-by side reconfiguration. It depends on your site size and complexity, but it should be less that an hours work if you have access to all the external info you need.

A loose guide to reconfiguring the Wordpress clone

I suggest making notes as you go for all of this. Even after fixing up the new site, when I switched over, it dumped some settings again and I had to reapply, without the benefit of the production site to refer to. (Menu, widgets, backgorund)

The #1 thing to do with your new clone site is to go in to the Wordpress admin panel and disable crawling by checking the box at Settings -> Reading -> Search Engine Visibility -> Discourage search engines from indexing this site and saving.

It's also the first thing you'll need to uncheck at go-live time if you are switching over your production domain to this server.

Update Apperance -> Customize

There is no easy way to do this. Bring up the production site in a second screen, and go through every field until you have set the new site up the same.

You might find that if you have a third party theme they have their own config pages you will also need to update. e.g. Elegant themes Divi -> Theme Options. Sometimes this is where the site logo is assigned, for example.

Update Appearance -> Menus

If it hasn't already been done in the theme customiser, you will probably need to reattach the menu, assigning in the Display location.

Update Appearance -> Widgets

In my case the theme makers have their own bloatware widget system that I have to mess with.

Note - if you did have a bunch of widgets in a screen area that have been dropped, you might find them still there but in an "Inactive Sidebar" group which just needs to be replaced.

The external service juxtaposition

My "new" site had a polite message popping up telling me my SMTP plugin needed reconfiguring. Looking at the production site, it turns out that it's set up from back when I had a different email host. That account is long gone, so email sending would have been broken. Oh how I laughed. Anyway it's a good example of the sort of thing you will need to reconfigure.

But what about APIs? You probably have connections out to things like Google Analytics, Akismet, Wordfence, Jetpack, social media repeaters. I would say rather than reconfigure all of these to reconnect with/from hour new domain, perhaps leave this and be ready to go through them all immediately after switching over your new domain.

It's not a good idea having a clone site reporting to say, Google webmaster or analytics, APIs or social media app links, with a new interim name, at the same time as your production, for any period of time, so after you fire up your clone site best not dally for a long time.

Performance test of the clone site

Before I flip the switch I will do a quick comparison of the performance on a sample page using webpagetest.org. My original is in Europe and my new is on the US West coast, so I will try and get test servers geographically close.

  • Current production, on the middle run is returning 5.5s to visually complete with 1475KB bytes.
  • The new site is returning 5.7s to visually complete, but curiously with 2644KB bytes.

A quick skim of the larger files downloaded show the theme provider's CSS payload is 700KB, compared to 90KB on the old server. I can investigate this later but otherwise am good to go.

I remind myself that I have done nothing with the default Nginx configs either, so it's off the shelf settings there. I suspect gzip is not running, for example.

Go live with my new site

Time to press the big red button. I am going to have to do two things in fairly close succession. Do a database rename of the site, and switch over the DNS. Well actually three. Wordpress admin rename, database script rename, then DNS switch.

Go to Settings -> General. These are the two fields (Wordpress Address, Site Address) you will change to the production URL. Once you submit at this point the admin panel won't be accessible any longer - traffic will go back to the old production - until you finish the rest. I change the URLs.

Next, I repeat the database search and replace again, this time changing the new name to the production name.

Lastly, I change my DNS root domain and wildcard subdomain to point to the new address.

I edit the docker-compose.yml and change the Traefik Nginx service label from the temporary URL back to the production URL, and update the docker stack.

It can take some time for the DNS change to propagate. Monitor, and as soon as you get into the new production site, re-enable crawling.

Tragically I found that some of the settings were dropped again, notably widgets, menu assignment and background image. Luckily I could get these fixed within 5 minutes.

Cleanup after going live

I jump immediately to the Wordfence plugin pages to check my firewalls are up. They aren't - permission errors. I then realise that as PHP and Wordpress are not running as my www-data user, number 33. I return to the docker-compose.yml file and make the magic happen. I'll ripple back and update the previous post about that.

I check cache, minify plugins, search plugins, purge and reset caches on all of those. The extent of your testing is up to you relative to the importance of the site - you might want to step through every single plugin.

I won't be shutting down the old server just yet, as I still have "WP2" to migrate. Right now I am going to do a bit of side by side comparison of the Nginx settings to see if I can solve the mystery of the additional 1MB. From my old nginx.conf, I find the following gzip settings. They would have been there for a long time, and I can't even remember why they are what they are, and might not be the most efficient, but I'll start by porting these by just pasting into my new nginx.conf at /data/nginx-wp1/nginx.conf.new. It has a line #gzip  on; so underneath that seems to make sense.

        gzip on;
        gzip_disable "msie6";

        gzip_vary on;
        gzip_proxied any;
        gzip_comp_level 2;
        gzip_buffers 16 8k;
        gzip_min_length 1100;
        gzip_http_version 1.1;
        gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

I restart my nginx-wp1 container. Viola - the download payload has dropped back to 1.6MB, with the theme CSS down to 90KB again. My visually complete time is 6.8 seconds. I think, this is because Google Ads has actually started working. It seems my article is there in 3s but the ad bloat increases it. I am thinking of dumping Ads altogether on that site, it earns me virtually nothing anyway, in the hope that I might find a site sponsor instead, but that's another story.

If a little while later you restart and find that all traffic is dumped to the install URL, it may be that either the wp_ prefix is custom and needs to be added to the environment vars, or that caching is enabled. I had this problem and setting false the parameter: define(‘WP_CACHE’, false); in wp-config.php saved the day. If you find problems post-install cache and minify functions are a good place to start.

I would also recommend getting your logging sorted out - I have a post on that.

The end  - and the complete files as a reference.

I am sure even the crawlers have fallen asleep by now. More to come with my multisite migration post, but here's my compose file and nginx.conf.new file.

Traefik.toml is based on this Digital Ocean build tutorial.

Example docker-compose.yml - Traefik, Nginx, Wordpress, MariaDB

👉
Update May 2022: I found recursion issues with MariaDB 10.8+, this config should work with 10.7, so I am updating the example to use 10.7. MySQL seems to be diverging more as time goes on.
version: "3"

networks:
        web:
                external: true
        internal:
                external: false
                driver: bridge

services:
        traefik:
                image: traefik:maroilles-alpine
                restart: unless-stopped
                ports:
                        - "80:80"
                        - "443:443"
                volumes:
                        - /var/run/docker.sock:/var/run/docker.sock
                        - /data/traefik/traefik.toml:/traefik.toml
                        - /data/traefik/acme.json:/acme.json
                labels:
                        - traefik.backend=traefik
                        - traefik.frontend.rule=Host:monitor.yourhost.com
                        - traefik.port=8080
                networks:
                        - web
        nginx-wp1:
                image: nginx:alpine
                restart: unless-stopped
                ports:
                        - '80'
                volumes:
                          - /data/nginx-wp1:/etc/nginx/conf.d
                          - /data/nginx-wp1/nginx.conf.new:/etc/nginx/nginx.conf
                          - /data/logs/nginx-wp1:/var/log/nginx
                          - /data/wp1:/var/www/html
                labels:
                        - traefik.backend=nginxwp1
                        - traefik.frontend.rule=Host:yourblog1.com,www.yourblog1.com
                        - traefik.docker.network=web
                networks:
                        - web
                        - internal
                depends_on:
                        - wp1
        nginx-wp2:
                image: nginx:alpine
                restart: unless-stopped
                ports:
                        - '80'
                volumes:
                          - /data/nginx-wp2:/etc/nginx/conf.d
                          - /data/nginx-wp2/nginx.conf.new:/etc/nginx/nginx.conf
                          - /data/logs/nginx-wp2:/var/log/nginx
                          - /data/wp2:/var/www/html
                labels:
                        - traefik.backend=nginxwp2
                        - traefik.frontend.rule=Host:yourblog2.com,www.yourblog2.com
                        - traefik.docker.network=web
                networks:
                        - web
                        - internal
                depends_on:
                        - wp2
        mariadb1:    # This DB service name is also the DB Host name
                image: mariadb:10.7
                command: --max-allowed-packet=128MB
                restart: unless-stopped
                volumes:
                        - /data/mariadb1:/var/lib/mysql
                labels:
                        - traefik.enable=false
                environment:
                        MYSQL_ROOT_PASSWORD:
                        MYSQL_DATABASE: wp1   # This name is internal to the DB
                        MYSQL_USER: wp1_user
                        MYSQL_PASSWORD: secure_password_123
                networks:
                        - internal
        mariadb2:    # This DB service name is also the DB Host name
                image: mariadb:10.7
                command: --max-allowed-packet=128MB
                restart: unless-stopped
                volumes:
                        - /data/mariadb2:/var/lib/mysql
                labels:
                        - traefik.enable=false
                environment:
                        MYSQL_ROOT_PASSWORD:
                        MYSQL_DATABASE: wp2   # This name is internal to the DB
                        MYSQL_USER: wp2_user
                        MYSQL_PASSWORD: secure_password_123
                networks:
                        - internal
        wp1:    # This is called from the Nginx config file
                image: wordpress:php7.3-fpm-alpine
                restart: unless-stopped
                user: "33"
                environment:
                        WORDPRESS_DB_HOST: mariadb1  # This must match the DB service name
                        WORDPRESS_DB_NAME: wp1
                        WORDPRESS_DB_USER: wp1_user
                        WORDPRESS_DB_PASSWORD: secure_password_123
                labels:
                        - traefik.enable=false
                networks:
                        - internal
                volumes:
                        - /data/wp1:/var/www/html
                depends_on:
                        - mariadb1
        wp2:    # This is called from the Nginx config file
                image: wordpress:php7.3-fpm-alpine
                restart: unless-stopped
                user: "33"
                environment:
                        WORDPRESS_DB_HOST: mariadb2  # This must match the DB service name
                        WORDPRESS_DB_NAME: wp2
                        WORDPRESS_DB_USER: wp2_user
                        WORDPRESS_DB_PASSWORD: secure_password_123
                labels:
                        - traefik.enable=false
                networks:
                        - internal
                volumes:
                        - /data/wp2:/var/www/html
                depends_on:
                        - mariadb2

Example nginx.conf - mounted from nginx.conf.new

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;
        gzip on;
        gzip_disable "msie6";

        gzip_vary on;
        gzip_proxied any;
        gzip_comp_level 2;
        gzip_buffers 16 8k;
        gzip_min_length 1100;
        gzip_http_version 1.1;
        gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    limit_req_zone $binary_remote_addr zone=one:10m rate=30r/m;

    include /etc/nginx/conf.d/*.conf;
}

Main photo courtesy of Ray Hennessy on Unsplash.

💬
Your comments are welcome. Please COMMENT and read those of others on the Bluesky Post for this article.

Retrospective blog post to use BlueSky for comments: techroads.org/migrating-a-... #Wordpress #Docker

[image or embed]

— TechRoads blog (@techroads.org) Feb 22, 2024 at 11:35 am