How to Self-host Multiple WordPress Sites on the Same Server With Docker

Installing WordPress is not a big deal. You can install it using LAMP server for local development. You can also deploy it for the real world on a proper public facing Linux server.

Cloud servers like Linode, DigitalOcean etc also provide the option to deploy a brand-new Linux server preconfigured with WordPress. Even easier, right?

But what if you want to install more than one WordPress on a single server?

An easy way would be to use a service like ServerPilot. Not only it updates your server automatically, it also lets you easily deploy multiple WordPress instances on the same server.

That would cost you a bit more money and if you do not want that, you can use Docker to deploy multiple WordPress instances.

Through this guide, you'll learn how to easily set up a dual WordPress setup running from their individual containers and add more instances!

Prerequisites

It is not mandatory, but the tutorial will be much easier for you to follow if you have:

You'll be deploying WordPress behind a reverse proxy container, with SSL enabled domain and subdomains.

Apart from the aforementioned topical knowledge, you'll need the following infrastructure requirements:

  • A public facing Ubuntu Linux server. You can use a cloud service provider like Linode. A Nano server with 1 GB RAM will be sufficient for this deployment. But if you plan to self-host more WordPress instances, do consider upgrading it.
  • Access to your domains/subdomains and their DNS settings
  • Docker and Docker Compose installed on your Linux server.

If you need help, you can follow our guides on:

In all our other tutorials based on self-hosting web applications with Docker, I've discussed deploying Nginx containers and Web App containers separately. This time, however, I'll be using Docker Compose for setting up multiple WordPress containers, all based on a single yml file.

So this is also an example to show you how you can integrate an Nginx configuration with a Web App you consider deploying, through a single Docker Compose file.

I'll be using domain.com as an example for the first WordPress blog and wordpress.domain.com as the second self-hosted blog. In the end, I'll also describe how to add more WordPress instances based on the configuration that I'm going to discuss here.

GridPane
GridPane helps WordPress agencies and developers build their own self managed WordPress hosting business.

Create a directory and make it active. Let’s call it multi-wordpress.

mkdir multi-wordpress

And then switch to this newly created directory using the cd command:

cd multi-wordpress

Set WordPress file upload limit to 512 MB

Inside this directory, we’ll store our uploads.ini file that has the necessary upload configuration for uploading files up to 512 MB in size. Enter the following command on the terminal:

avimanyu@localhost:~/multi-wordpress$ printf "file_uploads = On\nmemory_limit = 512M\nupload_max_filesize = 512M\npost_max_size = 512M\nmax_execution_time = 600" > uploads.ini

Set a file upload limit for Nginx

As an additional measure, also create a file called client_max_upload_size.conf and save it in the same directory for mounting to the Nginx container. For the sake of it, I set it as 1 GB. This is helpful for configuring other apps in the future.

avimanyu@localhost:~/multi-wordpress$ printf "client_max_body_size 1G;" > client_max_upload_size.conf

WWW Redirection through Nginx (Updating CNAME not required)

If you want to use non-www to www redirection with WordPress, you can set the redirector with the cat command on your Linux server:

avimanyu@localhost:~/multi-wordpress$ cat >> domain.com
rewrite ^/(.*)$ https://www.domain.com/$1 permanent;

Press enter and then Ctrl+D to save it. We did not use printf here because $1 will not get saved in the file as shown above. This could lead to future issues with URLs, as we want redirection to work in all cases. For example, domain.com/test should always redirect to www.domain.com/test.

non-WWW Redirection through Nginx (Updating CNAME not required)

If you want to use www to non-www redirection repeat similar steps to do the opposite:

avimanyu@localhost:~/multi-wordpress$ cat >> www.domain.com
rewrite ^/(.*)$ https://domain.com/$1 permanent;

Create an external Docker network

I'll be using a network called net for this tutorial. Therefore, I create it with the following command:

docker network create net

Preparing a single Docker Compose file ready for deployment

With the following configuration within a single docker-compose.yml file, you can set up everything at one go with all six containers that serves your purpose, starting from the reverse proxy to the last WordPress container. Let us look at them service by service before finally merging all of them into one single file:

Nginx Reverse Proxy Container

Mount both the files client_max_upload_size.conf and www.domain.com in read only mode and specify the network net that was created manually.

  nginx-proxy:
    image: jwilder/nginx-proxy
    container_name: nginx-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - html:/usr/share/nginx/html
      - dhparam:/etc/nginx/dhparam
      - vhost:/etc/nginx/vhost.d
      - certs:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./client_max_upload_size.conf:/etc/nginx/conf.d/client_max_upload_size.conf:ro
      - ./www.domain.com:/etc/nginx/vhost.d/www.domain.com:ro
    labels:
      - "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy"
    restart: always
    networks:
      - net

Let’s Encrypt Container for SSL Certificates

Note that I use rw for the certs volume, unlike ro in the previous section. This is essential for creating the SSL certificates and updating the volume accordingly as they are generated. Also, set the default email for notifications from Let's Encrypt and specify the same “net” network.

  letsencrypt:
    image: jrcs/letsencrypt-nginx-proxy-companion
    container_name: lets-encrypt-proxy-companion
    depends_on:
      - "nginx-proxy"
    volumes:
      - certs:/etc/nginx/certs:rw
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      DEFAULT_EMAIL: you@your-email.com
    restart: always
    networks:
      - net

Database container for first WordPress site

Based on the official WordPress Docker Compose configuration, set the necessary environment variables as shown below and specify an internal network (seen as mysqldb0 here, used for the service name and volume name as well).

This network will be visible only to the database container and its corresponding WordPress container. When you deploy it, a MySQL database will be created with credentials shown as under:

  mysqldb0:
    image: mysql:5.7
    environment:
      MYSQL_DATABASE: db0
      MYSQL_USER: db0user
      MYSQL_PASSWORD: secret
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - 'mysqldb0:/var/lib/mysql'
    restart: always
    networks:
      - mysqldb0

First WordPress container

When you deploy the WordPress container, it will use the database configuration specified in the previous section above. Note that in addition to the same internal network seen by the database (mysqldb0), this WordPress service must also see the Nginx network called net since it is the frontend application for the outside world.

  wordpress0:
    image: wordpress:5.8.0-php8.0-apache
    environment:
      WORDPRESS_DB_HOST: mysqldb0
      WORDPRESS_DB_USER: db0user
      WORDPRESS_DB_PASSWORD: secret
      WORDPRESS_DB_NAME: db0
      WORDPRESS_CONFIG_EXTRA: |
        define('AUTOMATIC_UPDATER_DISABLED', true);
      VIRTUAL_HOST: domain.com,www.domain.com
      LETSENCRYPT_HOST: domain.com,www.domain.com
    volumes:
      - 'wordpress0:/var/www/html/wp-content'
      - './uploads.ini:/usr/local/etc/php/conf.d/uploads.ini'
    restart: always
    depends_on:
      - mysqldb0
    networks:
      - mysqldb0
      - net

At this point, you have the configuration for setting up a single WordPress Instance. To set up more, replicate step 3 and 4 with new names.

Here you go:

Database container for second WordPress site

  mysqldb1:
    image: mysql:5.7
    environment:
      MYSQL_DATABASE: db1
      MYSQL_USER: db1user
      MYSQL_PASSWORD: secret
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - 'mysqldb1:/var/lib/mysql'
    restart: always
    networks:
      - mysqldb1

Second WordPress container

  wordpress1:
    image: wordpress:5.8.0-php8.0-apache
    environment:
      WORDPRESS_DB_HOST: mysqldb1
      WORDPRESS_DB_USER: db1user
      WORDPRESS_DB_PASSWORD: secret
      WORDPRESS_DB_NAME: db1
      WORDPRESS_CONFIG_EXTRA: |
        define('AUTOMATIC_UPDATER_DISABLED', true);
      VIRTUAL_HOST: wordpress.domain.com
      LETSENCRYPT_HOST: wordpress.domain.com
    volumes:
      - 'wordpress1:/var/www/html/wp-content'
      - './uploads.ini:/usr/local/etc/php/conf.d/uploads.ini'
    restart: always
    depends_on:
      - mysqldb1
    networks:
      - mysqldb1
      - net

With your favorite editor, create a docker-compose.yml file with all the content discussed till this point (additional parameters have been included below). You would require editing the domain names and other credential data accordingly. Here, I’ve used Nano.

nano docker-compose.yml
version: '3.7'
services:
  nginx-proxy:
    image: jwilder/nginx-proxy
    container_name: nginx-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - html:/usr/share/nginx/html
      - dhparam:/etc/nginx/dhparam
      - vhost:/etc/nginx/vhost.d
      - certs:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./client_max_upload_size.conf:/etc/nginx/conf.d/client_max_upload_size.conf
      - ./www.domain.com:/etc/nginx/vhost.d/www.domain.com
    labels:
      - "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy"
    restart: always
    networks:
      - net

  letsencrypt:
    image: jrcs/letsencrypt-nginx-proxy-companion
    container_name: lets-encrypt-proxy-companion
    depends_on:
      - "nginx-proxy"
    volumes:
      - certs:/etc/nginx/certs:rw
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      DEFAULT_EMAIL: you@your-email.com
    restart: always
    networks:
      - net

  mysqldb0:
    image: mysql:5.7
    environment:
      MYSQL_DATABASE: db0
      MYSQL_USER: db0user
      MYSQL_PASSWORD: secret
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - 'mysqldb0:/var/lib/mysql'
    restart: always
    networks:
      - mysqldb0


  wordpress0:
    image: wordpress:5.8.0-php8.0-apache
    environment:
      WORDPRESS_DB_HOST: mysqldb0
      WORDPRESS_DB_USER: db0user
      WORDPRESS_DB_PASSWORD: secret
      WORDPRESS_DB_NAME: db0
      WORDPRESS_CONFIG_EXTRA: |
        define('AUTOMATIC_UPDATER_DISABLED', true);
      VIRTUAL_HOST: domain.com,www.domain.com
      LETSENCRYPT_HOST: domain.com,www.domain.com
    volumes:
      - 'wordpress0:/var/www/html/wp-content'
      - './uploads.ini:/usr/local/etc/php/conf.d/uploads.ini'
    restart: always
    depends_on:
      - mysqldb0
    networks:
      - mysqldb0
      - net

  mysqldb1:
    image: mysql:5.7
    environment:
      MYSQL_DATABASE: db1
      MYSQL_USER: db1user
      MYSQL_PASSWORD: secret
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - 'mysqldb1:/var/lib/mysql'
    restart: always
    networks:
      - mysqldb1


  wordpress1:
    image: wordpress:5.8.0-php8.0-apache
    environment:
      WORDPRESS_DB_HOST: mariadb1
      WORDPRESS_DB_USER: db1user   
      WORDPRESS_DB_PASSWORD: secret
      WORDPRESS_DB_NAME: db1
      WORDPRESS_CONFIG_EXTRA: |
        define('AUTOMATIC_UPDATER_DISABLED', true);
      VIRTUAL_HOST: wordpress.linuxhindi.com
      LETSENCRYPT_HOST: wordpress.linuxhindi.com
    volumes:
      - 'wordpress1:/var/www/html/wp-content'
      - './uploads.ini:/usr/local/etc/php/conf.d/uploads.ini'
    restart: always
    depends_on:
      - mysqldb1
    networks:
      - mysqldb1
      - net

volumes:
  certs:
  html:
  vhost:
  dhparam:
  mysqldb0:
  wordpress0:
  mysqldb1:
  wordpress1:

networks:
  mysqldb0:
    internal: true
  mysqldb1:
    internal: true
  net:
    external: true

Note that I have assigned specific internal networks only for each database and its corresponding WordPress container. The database containers need not see the Nginx net network. net is required only for the frontend WordPress containers.

In our previous article on Discourse, I used the --volumes-from flag to use the nginx-proxy container’s volumes with letsencrypt. In the same manner, the equivalent for the same flag was volumes_from, removed by Docker quite some time ago. Therefore, I've explicitly specified the common volumes for the Nginx and Let's Encrypt containers inside the docker-compose.yml file – namely, certs, html, vhost and /var/run/docker.sock.

Now, deploy the setup with the following docker-compose command from the same multi-wordpress directory we created in the beginning:

docker-compose up -d

You will get the following confirmations:

Creating network "multi-wordpress_mysqldb0" with the default driver
Creating network "multi-wordpress_mysqldb1" with the default driver
Creating volume "multi-wordpress_certs" with default driver
Creating volume "multi-wordpress_html" with default driver
Creating volume "multi-wordpress_vhost" with default driver
Creating volume "multi-wordpress_dhparam" with default driver
Creating volume "multi-wordpress_mysqldb0" with default driver
Creating volume "multi-wordpress_wordpress0" with default driver
Creating volume "multi-wordpress_mysqldb1" with default driver
Creating volume "multi-wordpress_wordpress1" with default driver
Creating multi-wordpress_mysqldb0_1   ... done
Creating nginx-proxy                  ... done
Creating multi-wordpress_mysqldb1_1 ... done
Creating multi-wordpress_wordpress1_1 ... done
Creating multi-wordpress_wordpress0_1 ... done
Creating lets-encrypt-proxy-companion ... done

The first two are the internal networks, followed by the volumes and finally the containers. Our external network net already exists, as I created it manually at the beginning of this tutorial.

Wait for a few moments. Open your Internet browser and type www.domain.com. It will redirect to domain.com and you will see the WordPress setup wizard waiting for you:

Select your preferred language, click on continue and proceed with claiming your site administration.

For wordpress.domain.com, you would get the same result. You can add more WordPress sites by repeating steps 5 (database service) and 6 (WordPress service), according to the similar syntax as shown for the existing containers. Also, please do not forget to revise the volumes and networks section in the YAML file accordingly.

For each new site, a new WordPress container and its corresponding MySQL database container would be created. If you want to know the command line equivalents of the above docker-compose.yml file, you can explore that here.

Tips for maintaining your self-hosted WordPress instances

Here are a few tips that will help you in maintaining your WordPress instances on the long run:

Monitor WordPress Logs in Real-time

If you want to check a WordPress container's logs(say the first one described in this tutorial) while it's deployed in real time, you can run:

docker logs -f multi-wordpress_wordpress0_1

In case you want to troubleshoot or learn what is going on inside every other relevant container for this deployment, using Docker Logs efficiently can be crucial:

View and Manage Docker Logs [Complete Beginner Guide]
Explore and learn some interesting ways to make log management easier on Docker.

Backup and Restore WordPress Volumes without Downtime

Using a cloud + local approach, you can backup and restore your WordPress volumes without downtime.

Definitive Guide on Backup and Restore of Docker Containers
Harness both the cloud and your local system to backup and restore your Docker containers.

Update WordPress Containers without Downtime

With the --scale flag on Docker Compose, you can create a new container based on the latest version of WordPress. When it's done, you can remove the old one. This results in zero downtime.

Updating Docker Containers With Zero Downtime
A step by step methodology that can be very helpful in your day to day DevOps activities without sacrificing invaluable uptime.

While your server will be able to send emails, opting for an SMTP service will be better.

10 Free & Paid SMTP Services For Transactional and Bulk Emails
Here are some of the most popular SMTP relay services with their plans for your web app notifications.

Hope you have found this article useful. If you have any thoughts, feedback, or suggestion to share, please let us know in the comments below.