Ghost - How to migrate from MariaDB to MySQL in a Docker stack

How to migrate from MariaDB to MySQL in a Docker stack.

Ghost - How to migrate from MariaDB to MySQL in a Docker stack
Photo by Tandem X Visuals / Unsplash

This blog is powered by Ghost. It's run from a Docker compose stack and initially uses a MariaDB database server.

Since Ghost's version 5 the instance is complaining about MariaDB, because

MySQL 8 is the only supported database in production.

therefore it's time to migrate to a MySQL database server.

Here's how to do this.

Ghost using MariaDB

This is Ghost's docker-compose.yml file with the MariaDB service:

#---
version: '3.9'

services:
  ghost:
    image: ghost:5
    container_name: ghost
    restart: unless-stopped
    depends_on:
      - ghost-database
    environment:
      database__client: mysql
      database__connection__host: ghost-database
      database__connection__user: ${MARIADB_USER}
      database__connection__password: ${MARIADB_PASSWORD}
      database__connection__database: ${MARIADB_DATABASE}
      privacy__useUpdateCheck: false
      privacy__useGravatar: false
      privacy__useRpcPing: true
      privacy__useStructuredData: true
      url: http://localhost/
    ports:
      - 8080:2368
    volumes:
      - ./ghost-data:/var/lib/ghost/content
    networks:
      - ghost-external
      - ghost-internal

  ghost-database:
    image: mariadb:10.8
    container_name: ghost-database
    restart: unless-stopped
    environment:
      MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
      MARIADB_DATABASE: ${MARIADB_DATABASE}
      MARIADB_USER: ${MARIADB_USER}
      MARIADB_PASSWORD: ${MARIADB_PASSWORD}
    volumes:
      - ./database:/var/lib/mysql
    networks:
      - ghost-internal


networks:
  ghost-internal:
    external: false
  ghost-external:
    external: true

Add MySQL service

Let's add the MySQL service to the compose file:

  ghost-db:
    image: mysql:8.0
    container_name: ghost-db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MARIADB_DATABASE}
      MYSQL_USER: ${MARIADB_USER}
      MYSQL_PASSWORD: ${MARIADB_PASSWORD}
    volumes:
      - ./ghost-database:/var/lib/mysql
    networks:
      - ghost-internal

Migrate script

Run following migration script. This script will

  • Start the database services
  • Dump the data from the MariaDB service
  • Import the dump into the MySQL service
  • Stop the database services
#!/usr/bin/env bash

set -o errexit
set -o pipefail
set -o nounset
#set -o xtrace

DOCKER="/usr/bin/docker"
DOCKER_COMPOSE="${DOCKER} compose"
DOCKER_COMPOSE_EXEC="${DOCKER_COMPOSE} exec"

GZIP="/usr/bin/gzip"

MARIADB_SERVICE="ghost-database"
MYSQL_SERVICE="ghost-db"

CREDITAL_ENVS=".env"
SQL_DUMP="ghost.sql.gz"

function startDatabaseServices() {
    echo "Starting database services"
    ${DOCKER_COMPOSE} up -d ${MARIADB_SERVICE} ${MYSQL_SERVICE}
}

function sourceCredentials() {
    echo "Sourcing credentials"
    #shellcheck disable=SC1090
    source "${CREDITAL_ENVS}"
}

function dumpMariaDB() {
    echo "Waiting for ${MARIADB_SERVICE}"
    local WAIT_COMMAND="${DOCKER_COMPOSE_EXEC} ${MARIADB_SERVICE} mysql --user=root --password=${MARIADB_ROOT_PASSWORD} --database=${MARIADB_DATABASE} --execute 'select 1'"
    waitUntil "${WAIT_COMMAND}"

    echo "Dumping ${MARIADB_SERVICE}"
    ${DOCKER_COMPOSE_EXEC} ${MARIADB_SERVICE} mysqldump --user="${MARIADB_USER}" --password="${MARIADB_PASSWORD}" "${MARIADB_DATABASE}" | ${GZIP} -9 -f >"${SQL_DUMP}"
}

function importMySQL() {
    echo "Waiting for ${MYSQL_SERVICE}"
    local WAIT_COMMAND="${DOCKER_COMPOSE_EXEC} ${MYSQL_SERVICE} mysql --user=root --password=${MARIADB_ROOT_PASSWORD} --database=${MARIADB_DATABASE} --execute 'select 1'"
    waitUntil "${WAIT_COMMAND}"

    echo "Importing ${MYSQL_SERVICE}"
    ${GZIP} -c -d -f "${SQL_DUMP}" | ${DOCKER_COMPOSE_EXEC} -T ${MYSQL_SERVICE} mysql --user="${MARIADB_USER}" --password="${MARIADB_PASSWORD}" --database="${MARIADB_DATABASE}"
}

function stopDatabaseServices() {
    echo "Stopping database services"
    ${DOCKER_COMPOSE} stop ${MARIADB_SERVICE} ${MYSQL_SERVICE}
}

function waitUntil() {
    command="${1}"
    timeout="${2:-30}"

    i=1
    until eval "${command}"; do
        ((i++))

        if [ "${i}" -gt "${timeout}" ]; then
            echo "command was never successful, aborting due to ${timeout}s timeout!"
            exit 1
        fi

        sleep 1
    done
}

function main() {
    startDatabaseServices
    sourceCredentials
    dumpMariaDB
    importMySQL
    stopDatabaseServices
}

main

Remove MariaDB service from stack

After the migration the MariaDB service can be removed from the stack. This boils the compose file down to:

#---
version: '3.9'

services:
  ghost:
    image: ghost:5
    container_name: ghost
    restart: unless-stopped
    depends_on:
      - ghost-db
    environment:
      database__client: mysql
      database__connection__host: ghost-db
      database__connection__user: ${MARIADB_USER}
      database__connection__password: ${MARIADB_PASSWORD}
      database__connection__database: ${MARIADB_DATABASE}
      privacy__useUpdateCheck: false
      privacy__useGravatar: false
      privacy__useRpcPing: true
      privacy__useStructuredData: true
      url: http://localhost/
    ports:
      - 8080:2368
    volumes:
      - ./ghost-data:/var/lib/ghost/content
    networks:
      - ghost-external
      - ghost-internal

  ghost-db:
    image: mysql:8.0
    container_name: ghost-db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MARIADB_DATABASE}
      MYSQL_USER: ${MARIADB_USER}
      MYSQL_PASSWORD: ${MARIADB_PASSWORD}
    volumes:
      - ./ghost-database:/var/lib/mysql
    networks:
      - ghost-internal

networks:
  ghost-internal:
    external: false
  ghost-external:
    external: true  

Run the new stack

Finally, the new stack be started:

docker compose up -d --remove-orphans