Ghost Docker: Migration Issues — Fix

The Problem

You updated your Ghost Docker image to a newer version and Ghost won’t start. The container exits with database migration errors, crashes during startup, or shows a blank page. Common error messages include:

DatabaseError: ER_DUP_ENTRY
KnexMigrateError: Migration failed
Error: Cannot read properties of null
Ghost has shut down

The Cause

Ghost runs database migrations automatically on startup when it detects a version change. These migrations modify the database schema (adding columns, changing data types, updating indexes). Failures happen when:

  1. Skipping major versions — Ghost expects sequential migrations (e.g., 4.x → 5.x → 5.y, not 4.x → 5.y)
  2. Volume permission issues — Ghost can’t write to the content directory
  3. Database connection timeouts — MySQL isn’t ready when Ghost tries to migrate
  4. Interrupted previous migration — Container was killed mid-migration, leaving the database in a partial state
  5. SQLite to MySQL migration — Switching database backends during an upgrade

The Fix

Method 1: Sequential Version Upgrades

Ghost must be upgraded through each major version. You can’t skip from 4.x directly to 5.x if migrations for intermediate versions exist.

Check your current version:

docker exec ghost-app ghost version

Upgrade through each major version:

# Step 1: Upgrade to latest 4.x
image: ghost:4-alpine

# Step 2: Start, let migrations run, verify, stop

# Step 3: Upgrade to latest 5.x
image: ghost:5-alpine

# Step 4: Start, let migrations run, verify

For each step:

# Update docker-compose.yml with the next version
docker compose up -d
docker compose logs -f ghost  # Watch for migration completion
# Wait for "Ghost is running" message
docker compose down
# Proceed to next version

Method 2: Fix Partial Migration State

If Ghost died mid-migration, the database may be in an inconsistent state:

# 1. Back up the database first
docker exec ghost-db mysqldump -u ghost -p ghost > ghost-backup-$(date +%Y%m%d).sql

# 2. Check the migrations table
docker exec -it ghost-db mysql -u ghost -p ghost \
  -e "SELECT * FROM migrations ORDER BY id DESC LIMIT 10;"

# 3. Identify the failed migration version
docker exec -it ghost-db mysql -u ghost -p ghost \
  -e "SELECT * FROM migrations WHERE currentVersion != version;"

# 4. Delete the partial migration record
docker exec -it ghost-db mysql -u ghost -p ghost \
  -e "DELETE FROM migrations WHERE id = [FAILED_ID];"

# 5. Restart Ghost to retry the migration
docker compose restart ghost

Method 3: Fix Volume Permissions

Ghost runs as user node (UID 1000) inside the container. If volumes are owned by root:

# Check current permissions
docker exec ghost-app ls -la /var/lib/ghost/content/

# Fix permissions
docker exec -u root ghost-app chown -R node:node /var/lib/ghost/content/

Or set the correct ownership from the host:

sudo chown -R 1000:1000 /path/to/ghost/content

Method 4: Database Connection Timing

If Ghost starts before MySQL is ready, migrations fail with connection errors. Add a health check:

services:
  ghost:
    image: ghost:5-alpine
    depends_on:
      ghost-db:
        condition: service_healthy
    restart: unless-stopped

  ghost-db:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      timeout: 3s
      retries: 10
    restart: unless-stopped

Method 5: Clean Reinstall (Last Resort)

If the database is too corrupted to migrate:

# 1. Export content from the Ghost admin panel (if accessible)
# Settings → Labs → Export

# 2. Back up the database
docker exec ghost-db mysqldump -u ghost -p ghost > ghost-full-backup.sql

# 3. Remove the existing database
docker compose down
docker volume rm ghost_db_data

# 4. Start fresh with the target version
docker compose up -d

# 5. Import content via Ghost admin
# Settings → Labs → Import

Prevention

  • Always back up before upgrading. Run mysqldump or snapshot your volume before changing the Ghost image tag.
  • Never use :latest. Pin Ghost to a specific version (e.g., ghost:5.98.0-alpine). This prevents surprise upgrades.
  • Upgrade one major version at a time. Ghost 4 → 5 → current. Check the Ghost changelog for breaking changes.
  • Use health checks on the database service so Ghost waits for MySQL.
  • Don’t kill containers during startup. If you see migration logs, wait for them to complete before stopping the container.
  • Test upgrades on a copy first. Clone your volume, run the upgrade on the copy, verify, then upgrade production.

Comments