Deploying Mariadb into a Docker container

Similar to the maxscale article, this one is more or less a conceptual checklist for myself.

This article is intentionally minimal and focused on mechanics, rather than optimal deployment. Production hardening and orchestration are separate concerns, not addressed here.

Quick Recap on Docker

Docker is a containerization solution. It’s a way to package an application together with all of its exact dependencies and libraries, and ship it all together, allowing for the portable sharing of said application across different machines. The most important distinction is that there is no hypervisor, and it is not a virtualization solution.

Virtual Machines

A virtual machine (VM) emulates a complete physical computer, including hardware components, like the CPU, disk devices, network interfaces and so on. A VM will run its own operating system, and it is fully isolated from the host system.

A VM is created and managed by a hypervisor, and these images are considerably larger than container images. Moving a VM image between different hypervisors might lead to compatibility issues and require conversion.

Docker Containers

A docker container shares the host’s kernel, and only includes application specific dependencies and libraries. This makes them somewhat more portable than VM images, since as long as the kernel itself supports the userland applications, the docker container will port over just fine.

Docker containers are also more resource efficient compared to VMs, since the layer of abstraction is far thinner. Not having to emulate physical devices means that a larger share of system resources can be allocated for binaries.

However, docker containers are not fully isolated from the host system, by the very nature of sharing its kernel.

Any changes made to the container are recorded on a separate layer, and the image used to create the container will remain unaffected. This means if the container is deleted, or a new one based on the same image is created, such changes won’t be there.

Docker Files and Images

This is the file needed to generate docker images. The Dockerfileis a text file that docker will read from top to bottom, in it, instructions to tell docker on how the docker image should get built. This is the thing that you can put into a git repo and meaningfully discuss with others.

The resulting docker image, is a read only binary, which acts as a template of an environment. This is the thing that you can distribute to machines for deployment.

The Setup

The Dockerfile should be able to call a script, and it is likely that the script would want to either import SQL files, or run other scripts, so some basic directory structure to allow that would be a nice to have. For example:

mariadb-pkg/
├─ Dockerfile
├─ entrypoint.sh
└─ docker-entrypoint-initdb.d/
   ├─ 01-schema.sql
   ├─ 02-tables.sql
   ├─ 03-users.sql
   └─ 04-script.sh

The idea with this is the following:

The Dockerfile

The dockerfile here in this case will be short:

FROM debian:12-slim

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update \
 && apt-get install -y --no-install-recommends \
      mariadb-server mariadb-client \
      ca-certificates tzdata \
 && rm -rf /var/lib/apt/lists/*

COPY docker-entrypoint-initdb.d/ /docker-entrypoint-initdb.d/

COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

VOLUME ["/var/lib/mysql"]

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["mariadbd"]

Unpacking what does what:

To create the image based on this dockerfile, we need to run the following command:

docker build -t mariadb-pkg:dev .

Running the command should result in output like:

Sending build context to Docker daemon  5.632kB
Step 1/10 : FROM debian:12-slim
 ---> dc564059eedb
Step 2/10 : ENV DEBIAN_FRONTEND=noninteractive
 ---> Using cache
 ---> 5ecf4f941427
Step 3/10 : RUN apt-get update  && apt-get install -y --no-install-recommends       mariadb-server mariadb-client       ca-certificates tzdata  && rm -rf /var/lib/apt/lists/*
...
Successfully built 6b05d6f0d040
Successfully tagged mariadb-pkg:dev

The Entrypoint Script

As detailed above, this is the first thing the docker image executes, so here’s where we want to set up custom things, like importing data into a DB, creating users and granting grants and so on.

#!/bin/bash

MARIADB_PORT=${MARIADB_PORT:-3311}
DATADIR="/var/lib/mysql"
RUNDIR="/run/mysqld"

mkdir -p "$RUNDIR"
chown -R mysql:mysql "$RUNDIR"
chown -R mysql:mysql "$DATADIR"

if [ ! -d "${DATADIR}/mysql" ]; then
    echo "Initializing MariaDB data directory..."
    mariadb-install-db --user=mysql --datadir="$DATADIR" >/dev/null
fi

#Initialize DB here for importing SQL files, don't allow connections
echo "Starting temporary MariaDB (no networking) for init..."
mariadbd --user=mysql --datadir="$DATADIR" --skip-networking --socket=/tmp/mysqld.sock &
pid="$!"

#Wait until mariadb server finished initializing before trying to import anything
for i in {1..60}; do
    if mariadb-admin --protocol=socket --socket=/tmp/mysqld.sock ping >/dev/null 2>&1; then
      break
    fi
    sleep 0.5
done

echo "Running init scripts in /docker-entrypoint-initdb.d ..."
shopt -s nullglob
for f in /docker-entrypoint-initdb.d/*; do
    case "$f" in
        *.sql)
            echo "    -> importing $f"
            mariadb --protocol=socket --socket=/tmp/mysqld.sock < "$f"
        ;;
        *.sql.gz)
            echo "    -> importing $f"
            gunzip -c "$f" | mariadb --protocol=socket --socket=/tmp/mysqld.sock
        ;;
        *.sh)
            echo "    -> running $f"
            bash "$f"
        ;;
        *)
            echo "    -> ignoring $f"
        ;;
    esac
done

echo "Shutting down temporary MariaDB..."
mariadb-admin --protocol=socket --socket=/tmp/mysqld.sock shutdown
wait "$pid" || true


echo "Starting MariaDB on port ${MARIADB_PORT}..."
exec "$@" --user=mysql --datadir="$DATADIR" --bind-address=0.0.0.0 --port="$MARIADB_PORT"

A couple of noteworthy points:

Let’s also add a small SQL file for this example’s sake:

CREATE DATABASE dbtest;
CREATE USER a IDENTIFIED BY 'a';
GRANT ALL PRIVILEGES ON *.* TO a;

so that we can check that the initialization did really work like we expected it to.

Running the command:

docker run --name mariadb-blog -p 3311:3311 mariadb-pkg:dev

Initializes the instance.

Checking inside docker:

CONTAINER ID   IMAGE             COMMAND                  CREATED          STATUS          PORTS                                       NAMES
e20e6b23053c   mariadb-pkg:dev   "/usr/local/bin/entr…"   11 seconds ago   Up 10 seconds   0.0.0.0:3311->3311/tcp, :::3311->3311/tcp   mariadb-blog
root@debian-test:~/mariadb-pkg/docker-entrypoint-initdb.d# docker exec -it e20e6b23053c /bin/bash
root@e20e6b23053c:/# mysql -u root --silent
MariaDB [(none)]> SHOW DATABASES;
Database
dbtest
information_schema
mysql
performance_schema
sys

Checking outside docker:

root@debian-test:~# mysql -u a -h127.0.0.1 -P3311 -pa --silent
MariaDB [(none)]> SHOW DATABASES;
Database
dbtest
information_schema
mysql
performance_schema
sys

To link it to an application living in another container, use docker network. E.g.

docker network create someNetwork
docker network connect someNetwork db
docker network connect someNetwork app

Containers on the same docker network can reach each other by container name.

Conclusion

Docker allows for a simple way to provide throwaway databases, once a pipeline for creating data seeds exists. This way QA, or Devs can perform non-performance related tests in an isolated environment, easily reset datasets after testing, and have no risk of cross-team interference.