How I Saved ~77% Docker Memory by Migrating MySQL to MariaDB

How I Saved ~77% Docker Memory by Migrating MySQL to MariaDB
Photo by Julia Craice / Unsplash
This is not a benchmark and not a recommendation for high-traffic workloads. All numbers here are real observations from a low-resource VPS running Ghost CMS.

I’m running Ghost CMS on a very small VPS: 1 vCPU, 2 GB RAM, zero swap because it’s OpenVZ. At some point the server was already sitting at around 1.75 GB RAM usage, and I still needed to host another PHP + MariaDB app for a college assignment. So yeah, memory was getting dangerously tight.

To understand what was eating my RAM, I checked the system using ps aux --sort=-%mem | head and docker stats. One process immediately stood out:

30215  0.4 19.7 1304564 404200 ?  Ssl  01:45   0:05 mysqld

Docker confirmed it. The Ghost MySQL container alone was consuming more than 400 MB of memory, roughly 20% of my total RAM.

NAME          CPU %   MEM USAGE / LIMIT      MEM %
ghost-cms     0.03%   168.8 MiB / 1.953 GiB  8.44%
ghost-mysql   0.41%   405.0 MiB / 1.953 GiB  20.25%

For a personal blog, this was ridiculous. I don’t care about database performance here. This is not a high-traffic production system. It’s just Ghost.

So the goal was very simple: make the Ghost database container use as little memory as possible.

After some research, I realized that there is no official Alpine-based image for either MySQL or MariaDB. Both projects no longer publish Alpine variants on Docker Hub, so using an ultra-minimal base image was not an option here.

Instead, I started comparing the available MariaDB tags directly on Docker Hub, focusing on compressed image size as a rough indicator of baseline resource usage. After scrolling through the tags, I found mariadb:10.6.24-jammy, which stood out as one of the smallest stable options, with a compressed size of 98.45 MB. For comparison, the commonly used mariadb:10.6.24-ubi9 image weighs around 153.7 MB, which is significantly larger.

The migration itself was intentionally simple. No Kubernetes, no replicas, no fancy stuff — just plain Docker. The downtime was around one minute, and it actually happened, not an estimate. I prepared the new database container first, completed the data import, and only then updated Ghost’s database configuration to point to the new MariaDB instance. Once the Ghost container was restarted, the site was back online almost immediately.


First, this was my original Docker Compose setup using MySQL:

version: "3.9"

services:
  ghost:
    image: ghost:6.10.3-alpine3.23
    container_name: ghost-cms
    restart: always
    ports:
      - 8088:2368
    environment:
      database__client: mysql
      database__connection__host: db
      database__connection__user: ${MYSQL_USER}
      database__connection__password: ${MYSQL_PASSWORD}
      database__connection__database: ${MYSQL_DATABASE}
      url: ${GHOST_URL}
    volumes:
      - ghost:/var/lib/ghost/content
    depends_on:
      - db

  db:
    image: mysql:8.0-bookworm
    container_name: ghost-mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - db:/var/lib/mysql

volumes:
  ghost:
  db:

The first step was adding a new MariaDB service without touching the existing MySQL container, so rollback was always possible.

mariadb:
  image: mariadb:10.6.24-jammy
  container_name: ghost-mariadb
  restart: always
  environment:
    MARIADB_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    MARIADB_DATABASE: ${MYSQL_DATABASE}
    MARIADB_USER: ${MYSQL_USER}
    MARIADB_PASSWORD: ${MYSQL_PASSWORD}
  volumes:
    - mariadb:/var/lib/mysql
    - ./my.cnf:/etc/mysql/conf.d/my.cnf:ro

Before migrating anything, I exported the database from the old MySQL container. I already had a daily backup script, so I reused it:

#!/bin/bash

DATE=$(date +%F_%H-%M)
DIR=/root/ghost/db-backups

docker exec ghost-mysql \
  mysqldump -u root -p'PASSWORD' ghost \
  > ${DIR}/ghost-${DATE}.sql

Because my VPS only has 2 GB RAM and I genuinely don’t care about database performance, I aggressively tuned MariaDB memory usage using a custom my.cnf:

[mariadb]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

innodb_buffer_pool_size=64M
innodb_log_buffer_size=8M
max_connections=20
performance_schema=OFF
skip-name-resolve

Yes, 64 MB buffer pool. And yes, it’s perfectly fine for a blog.

When importing the SQL dump into MariaDB, the first error I hit was:

ERROR 1046 (3D000): No database selected

That happened because the dump didn’t include a USE ghost; statement. The fix was simply specifying the database explicitly:

docker exec -i ghost-mariadb \
  mysql -u root -pPASSWORD ghost \
  < ghost-2026-01-07_00-00.sql

Immediately after that, another error appeared:

ERROR 1273 (HY000): Unknown collation 'utf8mb4_unicode_ci'

This was expected. MySQL 8 uses utf8mb4_unicode_ci, which MariaDB does not support. I fixed it by replacing the collation inside the SQL file:

sed -i \
  's/utf8mb4_unicode_ci/utf8mb4_unicode_ci/g' \
  ghost-2026-01-07_00-00.sql

Because the import had already failed halfway, I dropped and recreated the database to start clean:

docker exec -it ghost-mariadb \
  mysql -u root -pPASSWORD \
  -e "DROP DATABASE ghost; CREATE DATABASE ghost CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"

Then I re-imported the SQL dump:

docker exec -i ghost-mariadb \
  mysql -u root -pPASSWORD ghost \
  < ghost-2026-01-07_00-00.sql

Once the database was successfully imported, the last step was pointing Ghost to the new MariaDB container. I changed this line in docker-compose.yml:

database__connection__host: db

to:

database__connection__host: mariadb

After restarting Ghost, the site loaded perfectly with no issues. Only after confirming everything was stable did I delete the old MySQL container and volume.

Here’s the final Docker Compose setup:

version: "3.9"

services:
  ghost:
    image: ghost:6.10.3-alpine3.23
    container_name: ghost-cms
    restart: always
    ports:
      - 8088:2368
    environment:
      database__client: mysql
      database__connection__host: mariadb
      database__connection__user: ${MYSQL_USER}
      database__connection__password: ${MYSQL_PASSWORD}
      database__connection__database: ${MYSQL_DATABASE}
      url: ${GHOST_URL}
    volumes:
      - ghost:/var/lib/ghost/content
    depends_on:
      - mariadb

  mariadb:
    image: mariadb:10.6.24-jammy
    container_name: ghost-mariadb
    restart: always
    environment:
      MARIADB_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MARIADB_DATABASE: ${MYSQL_DATABASE}
      MARIADB_USER: ${MYSQL_USER}
      MARIADB_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - mariadb:/var/lib/mysql
      - ./my.cnf:/etc/mysql/conf.d/my.cnf:ro

volumes:
  ghost:
  mariadb:

The result was honestly better than I expected. The MariaDB container now sits at around 93 MB of RAM, while Ghost itself uses around 167 MB.

NAME            CPU %   MEM USAGE / LIMIT      MEM %
ghost-cms       0.01%   167.8 MiB / 1.953 GiB  8.39%
ghost-mariadb   0.01%   93.21MiB  / 1.953 GiB  4.66%

Compared to the original ~405 MB MySQL usage, that’s roughly an 77% reduction in database memory usage, achieved purely by switching to MariaDB and tuning it aggressively.

Finally, the impact was also clearly visible from my monitoring system. Right after the ghost-mysql container was stopped, there was a sharp and immediate drop in overall RAM usage, with the memory graph showing a steep decline that confirmed MySQL as the biggest memory consumer on this VPS. There are no benchmarks and no performance claims here — this setup isn’t designed for high-traffic workloads, and that’s completely fine. The only goal was to keep Ghost running comfortably on a tiny VPS, and for that purpose, it worked perfectly.