Reviving an Old WordPress Site in Docker: The 301 Trap
Containers healthy, browser dragged to production. Three first-run Docker mechanisms skip old volumes silently, and wp_options holds one more surprise.
TL;DR
Reviving an old WordPress site in Docker, I got redirected to production because stale URLs were baked into wp_options. Docker's first-run scripts silently skip work when old volumes are attached, so restore the database explicitly and fix URLs directly. Scary-looking MariaDB warnings are often harmless; watch the logs patiently before assuming anything is broken.
Three first-run mechanisms that skip themselves
Containers are designed for clean installs, not for resurrecting a site's remains. The init scripts in MariaDB's docker-entrypoint-initdb.d only run when the data directory is genuinely new, on the create-a-new-installation path of the official entrypoint [10]. With an existing database volume attached, the SQL dump import never happens on its own. Restoration has to run explicitly, on the database container side.
The application side tells a similar story. The official WordPress image copies core files from /usr/src/wordpress into /var/www/html on the container's initial startup [12], but only when index.php and wp-includes/version.php are both absent [13]. An old wp-content volume full of restored plugins gets skipped, so the image never downgrades them.
One more misunderstanding worth killing: WORDPRESS_DB_NAME must already exist on the database server; the WordPress container will not create it [12]. The order is not compose-up-and-pray. Restore the database first, then start WordPress. For WORDPRESS_DB_HOST, the value is the compose service name with its port, like db:3306, not a loopback address. With old data, the first two mechanisms skipped me without a single error message, leaving a half-wired setup that almost worked.
Defusing the 301 redirect trap
Back to the browser dragging me to production. The fastest fix is editing the siteurl and home rows in wp_options, or adding WP_HOME and WP_SITEURL to wp-config.php [8]. There is a catch: WP_HOME overrides the home value in wp_options without changing the database itself [9]. The stale row stays put, waiting to bite again the day the constants come off. Hard-coding also removes the General Settings editor from the dashboard. For a restore job I prefer fixing wp_options directly and saving the constants for cases I truly want locked.
In the Apache logs, do not panic at warnings about an old host's .htaccess, like the cPanel-specific lsapi_module directive. That file is just per-directory configuration, read without touching the main server config [11]. Stock Apache ignores the unknown directive and logs a warning. Noise, not a fire.
So imports never boomerang when containers get recreated, my init script now counts tables first and skips the dump if the schema is already there. That simple idempotent pattern makes restarts safe, because docker-entrypoint-initdb.d has no concept of "already done".
The MariaDB warning that looks scary
Finally, the MariaDB log showed io_uring_queue_init() failed with errno 1, which I first read as disk corruption. It is not. When io_uring is disabled in the environment, MariaDB falls back to the older libaio interface [14]. The issue is officially tracked in MariaDB's JIRA, informative in nature, and not fatal for ordinary workloads.
If the dump is large, the import can run quietly for minutes. My ritual now: keep the database container alive, watch its logs, and never declare failure while the log is still moving. Twice I nearly deleted a volume that was busy importing.
The small lesson from this migration: respect the first-run mechanisms instead of fighting them. When the data is old, restore explicitly, fix the URL in the database or pin it with constants, and read the logs to the end before deciding something is broken. Almost every "error" I saw was just a container faithfully following its clean-install rules.