Using multiple databases and PHPMyAdmin in a single MySQL Docker instance

; Date: Sat Mar 20 2021

Tags: Docker »»»» Docker MAMP »»»» Docker MySQL »»»» MySQL

Sometimes our applications need multiple database, but with a single database server instance. The typical recommendation is for each Docker container to serve a single application. For databases, that means one database server for each application. That's the result of following a theoretical best practice, but isn't there another best practice to avoid excess processes and overhead? MySQL, like most other database servers, can support multiple databases, multiple user identities, and has an extensive system for controlling access on IP address, user names, and passwords. The MySQL container for Docker is easy to use, but supports only a single database instance per server container. In this article let's learn how to go ahead and configure the MySQL container for Docker to support multiple databases each with their own user ID.

Suppose you have five applications to host on a server, each with a separate database? Theoretically the best practice is for each Docker container to serve a single well defined purpose. That means five (or more) containers for the applications, but what about the database server? Do you host five database server containers, each holding a single database? Or do you host a single database server container, holding five databases?

The default way to configure the MySQL Docker container is with environment variables. Those environment variables support creating one user ID and one database.

The case I faced which inspired this tutorial is that I have three websites, two using Drupal, the third using Wordpress, to deploy to a single VPS. Following the best practice that would mean three separate MySQL container instances. Using MySQL in the default fashion would result in three containers. But it seemed to me excess overhead to follow that practice, especially since MySQL can easily support multiple databases per server.

In the old days we'd configure The Database Server (or servers), storing all data from a long list of applications. As a result MySQL is quite capable of storing multiple databases in each server instance, along with multiple user ID's. These features are hold-overs from the old days.

What I chose was to work out how to reconfigure a Docker-based MySQL instance to support multiple user ID's and multiple databases. To maintain separation between the databases for each application, we define separate user ID's with separate access rights. Each application will therefore only have access to its own data, and not to the data of other applications.

Configuring MySQL to support multiple databases and multiple user ID's is not unique to a Docker deployment.

Configuring the MySQL Docker container to support multiple databases and multiple user ID's starts with the configuration of a single database and single user ID. Once the database is running you can use SQL commands (or PHPMyAdmin) to create additional databases and user ID's. There is one user ID per database, and each user ID only has access rights for its database.

Setting up a MySQL database server, in Docker, to serve multiple applications

What I'll do is to describe a setup similar to what I developed on my server. Previously the three websites had been hosted on a VPS rented from a web hosting provider, using a MySQL server also rented from that provider. The change was to rent a single VPS where I had the freedom to install Docker (Docker Swarm) and to host a self-managed database.

This article is focused on configuring the MySQL server for this purpose.

With that in mind, let's look a simplified Docker Compose file derived from what's running on my server:

version: '3.8'

services:

    db:
        image: "mysql/mysql-server:5.7"
        container_name: db
        networks:
           - dbnet
        volumes:
           - /opt/db/data:/var/lib/mysql
           - /opt/db:/db
        restart: always
        environment:
           MYSQL_ROOT_PASSWORD: "ROOT PASSWORD"
           MYSQL_ROOT_HOST: "10.28.5.%"
           MYSQL_USER: USER-NAME
           MYSQL_PASSWORD: PASSWORD
           MYSQL_DATABASE: DATABASE-NAME

    phpmyadmin:
        image: phpmyadmin/phpmyadmin
        networks:
            - dbnet
            - servernet
        environment:
            PMA_ARBITRARY: 1
            PHP_UPLOAD_MAX_FILESIZE: 1G
            PHP_MAX_INPUT_VARS: 1G
        ports:
            - "8001:80"

networks:
    dbnet:
        external: true
    servernet:
        external: true

This sets up a MySQL server, along with a PHPMyAdmin instance that could be used for administering the database.

The MySQL instance is configured using the typical fashion. A directory is mounted onto /var/lib/mysql which will hold the database files, and persist them outside the container. To learn more about MySQL setup see Set up MySQL using Docker on your laptop and Deploying MySQL to get started using a Docker Compose file

The environment variables specify a password for root, and allows access to that ID from the 10.28.5.% network. Since the root ID has full privileges to the database the MySQL container normally limits access using root to the given container. But, we want to use PHPMyAdmin to administer the database, requiring access to the root ID. Using MYSQL_ROOT_HOST as shown here instructs the MySQL container to create a second root ID to access the database, limiting access to IP addresses matching 10.28.5.%.

We therefore configure the dbnet virtual network to match this IP range. Because it's specified as an externally defined network, we can run docker commands to setup dbnet:

$ docker network create \
    --driver=bridge \
    --subnet=10.28.5.0/24 \
    dbnet

This is an example of creating a network in Docker, while controlling the IP address range used in the network. The dbnet network will allocate IP addresses from 10.28.5.0/24, matching the address mask we configured into the MySQL server. Any application requiring database access must also use the dbnet network.

The bridge driver is used for regular Docker deployments. If you're using Docker Swarm, then you must use the overlay driver instead.

The PHPMyAdmin container is also connected to the dbnet network to have access to the database. It is also connected to servernet which could be used for the public facing website services. By using PMA_ARBITRARY=1, PHPMyAdmin will always prompt for database access particulars. It has a published port, 8001, so we can access it from our laptop.

We another network to create, servernet:

$ docker network create --driver=bridge servernet

We won't do much with this network in this tutorial. On my server there is a NGINX-based remote proxy used for implementing HTTPS for the websites. The reverse proxy and the application containers all attach to servernet. The database container is not connected servernet which is meant to keep the database insulated from the public Internet.

Defining an example service that uses the database

We aren't going to discuss the services that use the database. We know that there will be multiple such services, so let's talk about an example service:

version: '3.8'

services:

    wp:
        image: wordpress:php7.4-apache
        container_name: wp
        networks:
            - dbnet
            - servernet
        restart: always
        ports:
            - '80:80'
        volumes:
            - /opt/wp/logs:/var/log/apache2:rw
            - /opt/wp/docroot:/var/www/html:rw

networks:
    dbnet:
        external: true
    servernet:
        external: true

This is derived from a companion article Wordpress local development environment with Docker and Docker Compose on your laptop

It uses the standard wordpress container to manage a Wordpress instance. Some directories are mounted into the container to persist their content.

In production use, it's strongly recommended to implement HTTPS. While HTTPS can be implemented in this container, I use NGINX-Proxy-Manager to do so. This is a reverse proxy built around NGINX, and uses Lets Encrypt to procure SSL certificates. This container therefore only needs to export port 80 because NGINX-Proxy-Manager takes care of HTTPS.

For most applications (Drupal, Gogs, Jenkins, Kanboard, etc) it's only been necessary to expose port 80 and let NGINX-Proxy-Manager handle HTTPS and the redirect from port 80 to port 443. But Wordpress is finicky about implementing HTTPS, and it was necessary to install the Really Simple SSL plugin.

The virtual host name for the container is determined by the service name and/or the container_name attribute. This is important when it comes to configuring a reverse proxy as just discussed. The reverse proxy must know to which virtual host name to forward the HTTP traffic.

Structuring directories to hold database plus multiple application containers

Let's talk about a practical consideration - the directory structures required to hold data related to the database and application containers.

On my server I declared the /opt directory as the place to store data about each container. Maybe that's not technically the correct place, but that directory was conveniently empty when the server was created.

This meant that the MySQL container has a directory /opt/db containing:

  • /opt/db/docker-compose.yml - The Compose file defining the service
  • /opt/db/data - Data files for the database
  • Consider adding a mount to export /var/log/mysqld.log so you can easily see logs
  • Consider adding a configuration setting to change the MySQL socket so it's not visible in the data directory

For each application a similar structure is followed. Namely, there is a directory to contain a docker-compose.yml plus any data files.

For the Wordpress example this was:

  • /opt/wp/logs - Exposes the log files for easy viewing
  • /opt/wp/docroot - Persists the Wordpress files

All this handles the setup, so let's now talk about the MySQL configuration to support multiple applications.

Connecting as root to a MySQL database from other containers

In the docker-compose.yml we showed for MySQL, we included a PHPMyAdmin container. This is a popular tool for managing MySQL servers. For full effect it is best if PHPMyAdmin is connected to a MySQL account that has ALL privileges to the server. For the mysql/mysql-server container, that fully privileged account has the name root.

Let's start by learning how we enable accessing the root ID from containers other than the database container. By default the MySQL container only allows root connections only from the local host. But to support this PHPMyAdmin container, we need to support connections from other hosts. Remember that in Docker each container instance acts like its own host machine.

As we already discussed, by default the MySQL container restricts connections to the root user ID to the MySQL container itself. By configuring the MYSQL_ROOT_HOST variable the MySQL container will configure access to the root ID appropriately. But we'll learn something by studying how this works.

In MySQL, an account is created by adding an entry to the mysql.user table. One way to do that is with the CREATE USER command in SQL. The (dev.mysql.com) MySQL documentation on adding accounts and assigning privileges is worth your time to read.

Using solely MYSQL_ROOT_PASSWORD is equivalent to the following SQL:

CREATE USER 'root'@'localhost'
    IDENTIFIED BY 'PASSWORD';
GRANT ALL 
    ON *.* 
    TO 'root'@'localhost';

This creates a user ID root that is matched to connections from localhost. In Docker containers, localhost is the container itself.

We went over lots of details about user ID's in MySQL in: Understanding MySQL the Access Denied error in or outside a Docker container But summary is that for any incoming connection to MySQL, it matches connection attributes against entries in the mysql.user table.

The GRANT command then determines the level of access the identified account has. Because it is the root user, we use GRANT ALL to give the account full access to every last everything. The access options available in the GRANT command are:

Option Description
ALL This will allow a mysql user the complete access to the specified database or full access to all databases
SELECT Allow user to select data from tables
INSERT Allow user to insert data into tables
UPDATE Allow user to update data in tables
DELETE Allow user to delete rows in tables
CREATE Allow user to create new database and tables
DROP Allow user to delete databases and tables
ALTER Allow user to alter the structure of a table or to create triggers on a table.
GRANT OPTION Allow user to grant or remove other user privileges

To test this, in docker-compose.yml comment out the MYSQL_ROOT_HOST setting, then run docker-compose up to launch the containers. After the database starts, in your web browser visit http://localhost:9001 and you should find PHPMyAdmin. Enter these login particulars:

On clicking the Go button you should see an error. With no MYSQL_ROOT_HOST setting the root user ID can only be accessed from inside the MySQL container.

Another test is to login using these credentials:

You should be able to readily login using the wpuser ID. Your only access will be to the wpdb database.

The MYSQL_ROOT_HOST variable is how we support fully privileged access from another host. This variable changes things so the following SQL is executed:

CREATE USER 'root'@'MYSQL_ROOT_HOST'
    IDENTIFIED BY 'PASSWORD';
GRANT ALL 
    ON *.* 
    TO 'root'@'MYSQL_ROOT_HOST'
    WITH GRANT OPTION;

The first command is clear, it creates a user ID root, that is allowed to access the database from IP addresses matching the content of MYSQL_ROOT_HOST, using the given password. The second command allows this user ID all privileges on all databases and all tables. Additionally, because of the GRANT OPTION setting, this user ID can create other user ID's and grant privileges to those ID's.

Then, what value do we supply in the MYSQL_ROOT_HOST variable to get the desired effect? Here's a few examples:

Type Sample Discussion
Fully qualified domain name foo.example.com The incoming connection can only come from the named host
Fully qualified IP address 172.20.3.5 The incoming connection can only come from the named IP address
Wildcard domain name %.example.com, %.example.% The incoming connection can come from any host that matches the domain name
Wildcard IP address 172.%, 198.51.100.0/255.255.255.0 The incoming connection can come from any host with a matching IP address
Wildcard % Any host name or IP address will match

In the example above we specified 10.28.5.% as the wildcard IP address in MYSQL_ROOT_HOST. However under the current instructions the database was initialized without using that variable, meaning we were unable to login as root from PHPMyAdmin. The wildcard IP address was chosen to match the subnet assigned to the dbnet network.

We could reconfigure the database by changing MYSQL_ROOT_HOST in the Compose file. But instead, let's do it manually.

$ docker exec -it db mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 5
Server version: 5.7.33 MySQL Community Server (GPL)
...

mysql> use mysql;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> CREATE USER 'root'@'10.28.5.%' IDENTIFIED BY 'r00tr00t';
Query OK, 0 rows affected (0.03 sec)

mysql> GRANT ALL ON *.* TO 'root'@'10.28.5.%' WITH GRANT OPTION;
Query OK, 0 rows affected (0.01 sec)

mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.01 sec)

mysql>

This connects directly to the MySQL container to login to the database server. This works because the mysql command is executing in the same container as the database, and therefore the connection attempt comes from localhost. We then run the SQL commands required to add a new instance of the root user ID, this time with 10.28.5.% as the host name. It gets the required privileges, then we run FLUSH PRIVILEGES to ensure the server is properly updated.

Having run this you can now login using PHPMyAdmin using root from the phpmyadmin container.

What this does is prove that we can add additional user ID's using simple SQL commands.

Connecting to the database using the first user ID

So far we've set up the database and PHPMyAdmin containers, then we set up a root user ID so we can use PHPMyAdmin to administer the database. We'll also use PHPMyAdmin to test access using different user ID's.

When the database was created, we specified a user ID wpuser, and a database wpdb. You may have already tested access to this database by logging in using these credentials. If not, log out of PHPMyAdmin, and login again:

You should be able to readily login using the wpuser ID. Your only access will be to the wpdb and information_schema databases.

In other words, we've verified that wpuser is suitably limited in what it can do. We can verify this by looking at the GRANTS for wpuser:

mysql> SHOW GRANTS FOR wpuser;
+--------------------------------------------------+
| Grants for wpuser@%                              |
+--------------------------------------------------+
| GRANT USAGE ON *.* TO 'wpuser'@'%'               |
| GRANT ALL PRIVILEGES ON `wpdb`.* TO 'wpuser'@'%' |
+--------------------------------------------------+
2 rows in set (0.00 sec)

mysql>

This user ID has ALL privileges to wpdb but nothing else.

Adding additional databases and user ID's to the MySQL database

Here's where we get to the meat and learn how to set up multiple databases, and multiple user ID's, in a MySQL database. Refer back to the previous section on the root ID, and ponder the commands we ran. Namely, by running CREATE USER we created a second instance of the root user ID. The first instance was root@localhost, the second was root@10.28.5.%. The two are separate entries in the mysql.user table, and could easily have different access privileges.

To see this, in PHPMyAdmin, logged in to the root account, you can run the query SELECT host, user FROM user and see this:

Notice there are two rows where user is equal to root. That's all that happens when we create another user ID in MySQL.

Another thing to notice is that the wpuser user ID is allowed to access from %. Do you think that's too permissive? Consider that the % host matches not only the hosts containing your application, but the hosts used by any miscreant script kiddie looking to break into databases.

Modifying an existing MySQL user ID

To fix that, let's run these commands:

CREATE USER 'wpuser'@'10.%'
    IDENTIFIED BY 'w0rdw0rd';
GRANT ALL 
    ON wpdb.* 
    TO 'wpuser'@'10.%';
DROP USER 'wpuser'@'%';

This creates a new user ID, wpuser@10.%, that has the same level of control over wpdb that wpuser@% had. We then delete the old user ID. Re-run SELECT host, user FROM user and you'll see the change has been made. And, you should still be able to login to PHPMyAdmin using wpuser.

There is an ALTER USER command, but it doesn't seem capable of changing wpuser@% to wpuser@10.%. That led us to this series of commands in which we create a new user ID and delete the old.

Creating a new database in MySQL, and matching user ID

We can now talk about creating a new database, and assigning a user ID.

Creating a database first requires logging in as root, then running this command:

CREATE DATABASE appdb;

This does not create any tables, just the database.

Best practices say to create a user ID to go along with this database. That user ID is to be granted access solely to this database, just as we see above with the wpuser account. The corollary best practice is for the application to use that account to access the database, to ensure the application cannot step on databases owned by other applications.

We can easily extrapolate from the root and wpuser accounts we just created, and see what to do:

CREATE USER 'appuser'@'10.%'
    IDENTIFIED BY 'PASSWORD';
GRANT ALL 
    ON appdb.* 
    TO 'appuser'@'10.%';
FLUSH PRIVILEGES;

This is a good start, because it limits access to a specific table. But there are several other possible GRANT settings to use besides ALL.

For example a more common arrangement might be:

CREATE USER 'appuser'@'10.%'
    IDENTIFIED BY 'PASSWORD';
GRANT ALTER, CREATE, DELETE, DROP, INDEX, INSERT, SELECT, UPDATE
    ON database.* 
    TO 'appuser'@'10.%';
FLUSH PRIVILEGES;

This allows for typical application operations (create, read, update, delete) on database table rows, as well as the ability to create and delete tables.

Go ahead and create a user ID appuser as shown here, with access to the appdb database. You can inspect the mysql.user table to ensure the users were set up as you expect.

We can also inspect the details using this command:

mysql> SHOW GRANTS FOR `appuser`@`10.%`;
+-------------------------------------------------------+
| Grants for appuser@10.%                               |
+-------------------------------------------------------+
| GRANT USAGE ON *.* TO 'appuser'@'10.%'                |
| GRANT ALL PRIVILEGES ON `appdb`.* TO 'appuser'@'10.%' |
+-------------------------------------------------------+
2 rows in set (0.00 sec)

The SHOW GRANTS FOR command does what it sounds like, it shows the access grants assigned to the specified user ID.

Having verified that the databases and user ID's have been created as expected, go back to PHPMyAdmin and log out. You can then login using the wpuser and appuser ID's, as well as any other ID you may have created. You should verify that for each user ID only the expected databases are visible.

Summary

In this tutorial we've learned that even though the MySQL Docker container only supports the root user ID and one other user ID, we can go ahead and create multiple databases and user ID's. All it takes is to use the SQL commands to create databases, user ID's, and grant access rights.

That is, we first investigated what the MySQL container does to create the initial user ID and database. We then used the same pattern to create another database and user ID, assigning it similar privileges.

What we've learned is that we can create any combination of databases, user ID's and access permissions, using simple SQL statements.

CREATE USER 'USER-NAME'@'%'
    IDENTIFIED BY 'PASSWORD';
CREATE DATABASE newdb;
GRANT ALL 
    ON newdb.* 
    TO 'USER-NAME'@'%';
FLUSH PRIVILEGES;

These are the commands which are required, feel free to mix and match as you wish.

Even though the MySQL container doesn't directly support multiple users and databases, a few simple SQL commands lets us configure the database server to our liking.

About the Author(s)

David Herron : David Herron is a writer and software engineer focusing on the wise use of technology. He is especially interested in clean energy technologies like solar power, wind power, and electric cars. David worked for nearly 30 years in Silicon Valley on software ranging from electronic mail systems, to video streaming, to the Java programming language, and has published several books on Node.js programming and electric vehicles.