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:
-> install MariaDB via
apt-> copy and execute sql and script files into the image
-> on the first start of the container, run these scripts and import these sql files
-> otherwise run mariadb normally
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:
->
FROMinitializes a new build stage, and sets the base image for subsequent instructions. A valid dockerfile must always start with this instruction. Containers don’t run a full OS, and depend on the kernel of the host OS. It is possible to useFROM scratchto start with a completely bank base image, but this example is in a completely debian environment, so a debian base image makes sense. Adjust the base image to whatever makes sense.->
ENV DEBIAN_FRONTEND=noninteractivedisables interactive prompts in the terminal. This is basically a nice to have for making sure thatapt installgoes through smoothly.->
RUNexecutes commands while the docker image is being built. Anything installed or modified this way will become a permanent part of the resulting docker image. In practice, it’s good to keep this section as short or lean as possible, because each RUN adds a new layer, increasing the size of the image.->
COPYcopies directories/files from the host and adds them to the filesystem of the container->
VOLUMEdecouples the data stored in the specified volume from the life of the container that created it. In other words,docker rmshould not wipe out the data. Docker will create a directory on the host, within the docker root path (default:/var/lib/docker).-> extra note on
VOLUME, usedocker volume pruneto remove all unused volumes. Otherwise, if the machine with the docker container is shared with others, it is better NOT to putVOLUME, and use named volumes when calling dockerdocker run -v mariadb-data:/var/lib/mysql mariadb-dataset-my-name, which can then be explicitly removed likedocker volume rm mariadb-dataset-my-nameTL;DR: make sure to delete unused volumes from time to time if using docker for throwaway tasks->
ENTRYPOINTruns the specified command when the container starts. Can not be overriden at run time withdocker run, this here is essentially what defines the main purpose of the container. (Can be overridden withdocker run --entrypoint…)->
CMDdefault command when the container starts (afterENTRYPOINT!), this is easily overridden at runtime withdocker run, though not important for this particular use case
To create the image based on this dockerfile, we need to run the following command:
docker build -t mariadb-pkg:dev .
->
docker buildtells Docker to build an image from a dockerfile->
-t mariadb-pkg:devtags the resulting image with a name (mariadb-pkg) and a tag (dev). The tag is just a label, and it does not enforce anything in any way.->
.sets the build context to the current directory. Docker can only access files within this directory (and its subdirectories) when executingCOPYinstructions.
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:
->
RUNDIR="/run/mysqld"on a normal Debian system, this directory is created by systemd at boot and used for the MariaDB socket and PID file. Containers do not run systemd, so this needs to be created, and ownership assigned to mysql:mysql, to make sure that MariaDB won’t refuse to start due to permission issues.->
MARIADB_PORT=${MARIADB_PORT:-3311}adds the option of a custom port, for the mariadb server running inside the container. As docker containers share the host kernel, and its networking stack, it may be that the default mysql port is already busy if the host OS, or another container is already running mariadb. Key takeaway: Containers do not get their own TCP port space.->
--skip-networkingprevents mariadb from accepting remote TCP/IP connections, this is just to be on the safe side while the data seed is still being populated
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.