Ghost migration fails - How to migrate the default collation from MySQL 5 to MySQL 8

How to migrate the Ghost MySQL database collation from MySQL 5.7 default to MySQL 8.0 default.

Ghost migration fails - How to migrate the default collation from MySQL 5 to MySQL 8
Ghost Logo (Source)

I am running this blog with Ghost on Docker.

Normally the update is done automatically when a new image is available and it works flawlessly all the time. Except for that one time.

For Google and people who search for this here is the error message I got:

root@webspace:~# docker logs -f blog_ghost_1
[2022-02-08 05:47:09] INFO Ghost is running in production...
[2022-02-08 05:47:09] INFO Your site is now available on https://www.ajfriesen.com/
[2022-02-08 05:47:09] INFO Ctrl+C to shut down
[2022-02-08 05:47:09] INFO Ghost server started in 0.751s
[2022-02-08 05:47:09] WARN Database state requires migration.
[2022-02-08 05:47:10] INFO Creating database backup
[2022-02-08 05:47:10] INFO Database backup written to: /var/lib/ghost/content/data/ajfriesen.ghost.2022-02-08-05-47-10.json
[2022-02-08 05:47:10] INFO Running migrations.
[2022-02-08 05:47:10] INFO Adding table: posts_products
[2022-02-08 05:47:10] INFO Rolling back: alter table `posts_products` add constraint `posts_products_post_id_foreign` foreign key (`post_id`) references `posts` (`id`) on delete CASCADE - UNKNOWN_CODE_PLEASE_REPORT: Referencing column 'post_id' and referenced column 'id' in foreign key constraint 'posts_products_post_id_foreign' are incompatible..
[2022-02-08 05:47:10] INFO Dropping table: posts_products
[2022-02-08 05:47:10] INFO Rollback was successful.
[2022-02-08 05:47:10] ERROR alter table `posts_products` add constraint `posts_products_post_id_foreign` foreign key (`post_id`) references `posts` (`id`) on delete CASCADE - UNKNOWN_CODE_PLEASE_REPORT: Referencing column 'post_id' and referenced column 'id' in foreign key constraint 'posts_products_post_id_foreign' are incompatible.

alter table `posts_products` add constraint `posts_products_post_id_foreign` foreign key (`post_id`) references `posts` (`id`) on delete CASCADE - UNKNOWN_CODE_PLEASE_REPORT: Referencing column 'post_id' and referenced column 'id' in foreign key constraint 'posts_products_post_id_foreign' are incompatible.

{"config":{"transaction":false},"name":"2022-01-20-05-55-add-post-products-table.js"}
"Error occurred while executing the following migration: 2022-01-20-05-55-add-post-products-table.js"

Error ID:
    300

Error Code: 
    UNKNOWN_CODE_PLEASE_REPORT

----------------------------------------

MigrationScriptError: alter table `posts_products` add constraint `posts_products_post_id_foreign` foreign key (`post_id`) references `posts` (`id`) on delete CASCADE - UNKNOWN_CODE_PLEASE_REPORT: Referencing column 'post_id' and referenced column 'id' in foreign key constraint 'posts_products_post_id_foreign' are incompatible.
    at DatabaseStateManager.makeReady (/var/lib/ghost/versions/4.35.0/core/server/data/db/state-manager.js:95:32)
    at MigrationScriptError.KnexMigrateError (/var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/lib/errors.js:7:26)
    at new MigrationScriptError (/var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/lib/errors.js:25:26)
    at /var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/lib/index.js:1032:19
    at processTicksAndRejections (internal/process/task_queues.js:95:5)

Error: UNKNOWN_CODE_PLEASE_REPORT: Referencing column 'post_id' and referenced column 'id' in foreign key constraint 'posts_products_post_id_foreign' are incompatible.
    at Query.Sequence._packetToError (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14)
    at Query.ErrorPacket (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/protocol/sequences/Query.js:79:18)
    at Protocol._parsePacket (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/protocol/Protocol.js:291:23)
    at Parser._parsePacket (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/protocol/Parser.js:433:10)
    at Parser.write (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/protocol/Parser.js:43:10)
    at Protocol.write (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/protocol/Protocol.js:38:16)
    at Socket.<anonymous> (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/Connection.js:88:28)
    at Socket.<anonymous> (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/Connection.js:526:10)
    at Socket.emit (events.js:400:28)
    at addChunk (internal/streams/readable.js:293:12)
    at readableAddChunk (internal/streams/readable.js:267:9)
    at Socket.Readable.push (internal/streams/readable.js:206:10)
    at TCP.onStreamRead (internal/stream_base_commons.js:188:23)
    --------------------
    at Protocol._enqueue (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/protocol/Protocol.js:144:48)
    at Connection.query (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/Connection.js:198:25)
    at /var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/node_modules/knex/lib/dialects/mysql/index.js:134:18
    at new Promise (<anonymous>)
    at Client_MySQL._query (/var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/node_modules/knex/lib/dialects/mysql/index.js:128:12)
    at Client_MySQL.query (/var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/node_modules/knex/lib/client.js:168:17)
    at Runner.query (/var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/node_modules/knex/lib/runner.js:151:36)
    at Runner.queryArray (/var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/node_modules/knex/lib/runner.js:248:31)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)
    at async /var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/node_modules/knex/lib/runner.js:277:18
    at async Object.up (/var/lib/ghost/versions/4.35.0/core/server/data/migrations/utils.js:257:13)

[2022-02-08 05:47:10] WARN Ghost is shutting down
[2022-02-08 05:47:10] WARN Ghost has shut down
[2022-02-08 05:47:10] WARN Your site is now offline
[2022-02-08 05:47:10] WARN Ghost was running for a few seconds
[2022-02-08 05:47:12] INFO Ghost is running in production...
[2022-02-08 05:47:12] INFO Your site is now available on https://www.ajfriesen.com/
[2022-02-08 05:47:12] INFO Ctrl+C to shut down
[2022-02-08 05:47:12] INFO Ghost server started in 0.635s
[2022-02-08 05:47:12] WARN Database state requires migration.
[2022-02-08 05:47:13] INFO Creating database backup
[2022-02-08 05:47:13] INFO Database backup written to: /var/lib/ghost/content/data/ajfriesen.ghost.2022-02-08-05-47-13.json
[2022-02-08 05:47:13] INFO Running migrations.
[2022-02-08 05:47:13] INFO Adding table: posts_products
[2022-02-08 05:47:13] INFO Rolling back: alter table `posts_products` add constraint `posts_products_post_id_foreign` foreign key (`post_id`) references `posts` (`id`) on delete CASCADE - UNKNOWN_CODE_PLEASE_REPORT: Referencing column 'post_id' and referenced column 'id' in foreign key constraint 'posts_products_post_id_foreign' are incompatible..
[2022-02-08 05:47:13] INFO Dropping table: posts_products
[2022-02-08 05:47:13] INFO Rollback was successful.
[2022-02-08 05:47:13] ERROR alter table `posts_products` add constraint `posts_products_post_id_foreign` foreign key (`post_id`) references `posts` (`id`) on delete CASCADE - UNKNOWN_CODE_PLEASE_REPORT: Referencing column 'post_id' and referenced column 'id' in foreign key constraint 'posts_products_post_id_foreign' are incompatible.

alter table `posts_products` add constraint `posts_products_post_id_foreign` foreign key (`post_id`) references `posts` (`id`) on delete CASCADE - UNKNOWN_CODE_PLEASE_REPORT: Referencing column 'post_id' and referenced column 'id' in foreign key constraint 'posts_products_post_id_foreign' are incompatible.

{"config":{"transaction":false},"name":"2022-01-20-05-55-add-post-products-table.js"}
"Error occurred while executing the following migration: 2022-01-20-05-55-add-post-products-table.js"

Error ID:
    300

Error Code: 
    UNKNOWN_CODE_PLEASE_REPORT

----------------------------------------

MigrationScriptError: alter table `posts_products` add constraint `posts_products_post_id_foreign` foreign key (`post_id`) references `posts` (`id`) on delete CASCADE - UNKNOWN_CODE_PLEASE_REPORT: Referencing column 'post_id' and referenced column 'id' in foreign key constraint 'posts_products_post_id_foreign' are incompatible.
    at DatabaseStateManager.makeReady (/var/lib/ghost/versions/4.35.0/core/server/data/db/state-manager.js:95:32)
    at MigrationScriptError.KnexMigrateError (/var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/lib/errors.js:7:26)
    at new MigrationScriptError (/var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/lib/errors.js:25:26)
    at /var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/lib/index.js:1032:19
    at processTicksAndRejections (internal/process/task_queues.js:95:5)

Error: UNKNOWN_CODE_PLEASE_REPORT: Referencing column 'post_id' and referenced column 'id' in foreign key constraint 'posts_products_post_id_foreign' are incompatible.
    at Query.Sequence._packetToError (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14)
    at Query.ErrorPacket (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/protocol/sequences/Query.js:79:18)
    at Protocol._parsePacket (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/protocol/Protocol.js:291:23)
    at Parser._parsePacket (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/protocol/Parser.js:433:10)
    at Parser.write (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/protocol/Parser.js:43:10)
    at Protocol.write (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/protocol/Protocol.js:38:16)
    at Socket.<anonymous> (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/Connection.js:88:28)
    at Socket.<anonymous> (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/Connection.js:526:10)
    at Socket.emit (events.js:400:28)
    at addChunk (internal/streams/readable.js:293:12)
    at readableAddChunk (internal/streams/readable.js:267:9)
    at Socket.Readable.push (internal/streams/readable.js:206:10)
    at TCP.onStreamRead (internal/stream_base_commons.js:188:23)
    --------------------
    at Protocol._enqueue (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/protocol/Protocol.js:144:48)
    at Connection.query (/var/lib/ghost/versions/4.35.0/node_modules/mysql/lib/Connection.js:198:25)
    at /var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/node_modules/knex/lib/dialects/mysql/index.js:134:18
    at new Promise (<anonymous>)
    at Client_MySQL._query (/var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/node_modules/knex/lib/dialects/mysql/index.js:128:12)
    at Client_MySQL.query (/var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/node_modules/knex/lib/client.js:168:17)
    at Runner.query (/var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/node_modules/knex/lib/runner.js:151:36)
    at Runner.queryArray (/var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/node_modules/knex/lib/runner.js:248:31)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)
    at async /var/lib/ghost/versions/4.35.0/node_modules/knex-migrator/node_modules/knex/lib/runner.js:277:18
    at async Object.up (/var/lib/ghost/versions/4.35.0/core/server/data/migrations/utils.js:257:13)

[2022-02-08 05:47:13] WARN Ghost is shutting down
[2022-02-08 05:47:13] WARN Ghost has shut down
[2022-02-08 05:47:13] WARN Your site is now offline
[2022-02-08 05:47:13] WARN Ghost was running for a few seconds

I had no clue and time at that evening and just ran with the old version for a few days. After a few days, a few people in GitHub and the ghost forum were adding some more light in the dark.

Apparently, this is NOT an issue with Ghost. This is an issue with MySQL. So the inner database admin has to take care of this.

Actual Problem

The problem only occurs when you started your Ghost journey with MySQL version 5.x.

The default collation for charset utf8mb4 in MySQL 5.7 is utf8mb4_general_ci, but has been changed to utf8mb4_0900_ai_ci with MySQL 8.0. See https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_default_collation_for_utf8mb4.

For more details, see this GitHub issue.

People suggest changing the default collation in MySQL 8 to the old default of MySQL 5.7. It is a solution but I do want to use the sane default from MySQL 8. Especially because Ghost 5 will drop support for MySQL 5.7, that means all installations from then on should use MySQL 8 with the new default collation.

Instead I "migrated" my database from utf8mb4_general_ci to utf8mb4_0900_ai_ci and here is my process.

Solution

For docker users like me here are the steps from my shell history and memory. If you are running Ghost in a different way this should be almost the same, since we only interact with the MySQL container and not Ghost itself. You would just adjust the MySQL command to your database setup without the docker complexity.

High level:

  1. Create a SQL dump of your ghost database
  2. create a copy of that dump file
  3. Replace occurrences of utf8mb4_general_ci with utf8mb4_0900_ai_ci
    sed -i 's/utf8mb4_general_ci/utf8mb4_0900_ai_ci/g' new-db.sql
  4. Drop the ghost database
  5. Create a new ghost database
  6. Import the new-db.sql dump

Actual solution for ghost docker setups

Create a folder where you put in your backups

mkdir /backups

Check which docker network you have and your containers are running in:

docker network ls

docker inspect <mysql-container-name>

Create a MySQL dump (backup)

docker run --rm --name mysql-dump --network blog_docker-network mysql:8 mysqldump -h db -u root -p<your-password> --databases ghost > /backup/backup-ghost-db-dump.sql

Create a copy and replace utf8mb4_general_ci with utf8mb4_0900_ai_ci:

cp /backup/backup-ghost-db-dump.sql /backup/new-backup-ghost-db-dump.sql

sed -i 's/utf8mb4_general_ci/utf8mb4_0900_ai_ci/g' /backup/new-backup-ghost-db-dump.sql

You can run a diff between those files to make sure what has been changed:

diff /backup/backup-ghost-db-dump.sql /backup/new-backup-ghost-db-dump.sql

Run a MySQL container to interact with the database and mount the /backup directory to have access to your dumps

docker run -it --rm --name mysql-restore -v /backup:/restore --network blog_docker-network mysql:8 /bin/bash

Connect to your database. db is the hostname of the default docker-compose setup. Adjust to if you did changes there.

mysql -h db -u root -p<your-password>

Now you are in a MySQL shell and drop and create the database

DROP DATABASE ghost;
CREATE DATABASE ghost;

Now get out of the MySQL shell but stay in the mysql docker container and make a restore from the new database:

mysql -h db -u root -p<your-password> ghost < /restore/new-backup-ghost-db-dump.sql

Your site should be up and running again.

Now you can start the migration and everything works fine again. That is at least what I did.

I hope that helps!