Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/env/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Bug Fixes

- Add MySQL healthcheck to prevent race condition where WordPress containers start before MySQL is fully initialized. Uses MariaDB's official `healthcheck.sh` script with `MARIADB_AUTO_UPGRADE` to support both new and existing installations.

## 10.39.0 (2026-01-29)

### New Features
Expand Down
31 changes: 29 additions & 2 deletions packages/env/lib/runtime/docker/build-docker-compose-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,19 @@ module.exports = function buildDockerComposeConfig( config ) {
config.env.tests.phpmyadminPort ?? ''
}}:80`;

// MySQL healthcheck using MariaDB's official healthcheck.sh script.
// --connect: verifies TCP connection and that entrypoint has finished
// --innodb_initialized: ensures InnoDB storage engine is fully initialized
// MARIADB_AUTO_UPGRADE env var ensures healthcheck user exists for existing installations.
// Timing is generous to support slow CI environments.
const mysqlHealthcheck = {
test: [ 'CMD', 'healthcheck.sh', '--connect', '--innodb_initialized' ],
interval: '5s',
timeout: '10s',
retries: 12,
start_period: '60s',
};

return {
services: {
mysql: {
Expand All @@ -191,8 +204,11 @@ module.exports = function buildDockerComposeConfig( config ) {
MYSQL_ROOT_PASSWORD:
dbEnv.credentials.WORDPRESS_DB_PASSWORD,
MYSQL_DATABASE: dbEnv.development.WORDPRESS_DB_NAME,
// Ensures healthcheck user is created for existing installations.
MARIADB_AUTO_UPGRADE: '1',
},
volumes: [ 'mysql:/var/lib/mysql' ],
healthcheck: mysqlHealthcheck,
},
'tests-mysql': {
image: 'mariadb:lts',
Expand All @@ -202,11 +218,18 @@ module.exports = function buildDockerComposeConfig( config ) {
MYSQL_ROOT_PASSWORD:
dbEnv.credentials.WORDPRESS_DB_PASSWORD,
MYSQL_DATABASE: dbEnv.tests.WORDPRESS_DB_NAME,
// Ensures healthcheck user is created for existing installations.
MARIADB_AUTO_UPGRADE: '1',
},
volumes: [ 'mysql-test:/var/lib/mysql' ],
healthcheck: mysqlHealthcheck,
},
wordpress: {
depends_on: [ 'mysql' ],
depends_on: {
mysql: {
condition: 'service_healthy',
},
},
build: {
context: '.',
dockerfile: 'WordPress.Dockerfile',
Expand All @@ -224,7 +247,11 @@ module.exports = function buildDockerComposeConfig( config ) {
extra_hosts: [ 'host.docker.internal:host-gateway' ],
},
'tests-wordpress': {
depends_on: [ 'tests-mysql' ],
depends_on: {
'tests-mysql': {
condition: 'service_healthy',
},
},
build: {
context: '.',
dockerfile: 'Tests-WordPress.Dockerfile',
Expand Down
33 changes: 14 additions & 19 deletions packages/env/lib/runtime/docker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const { rimraf } = require( 'rimraf' );
/**
* Promisified dependencies
*/
const sleep = util.promisify( setTimeout );
const exec = util.promisify( require( 'child_process' ).exec );

/**
Expand All @@ -26,7 +25,6 @@ const {
validateRunContainer,
} = require( './validate-run-container' );
const {
checkDatabaseConnection,
configureWordPress,
resetDatabase,
setupWordPressDirectories,
Expand Down Expand Up @@ -174,7 +172,7 @@ class DockerRuntime {
}

await Promise.all( [
dockerCompose.upOne( 'mysql', {
dockerCompose.upMany( [ 'mysql', 'tests-mysql' ], {
...dockerComposeConfig,
commandOptions: shouldConfigureWp
? [ '--build', '--force-recreate' ]
Expand Down Expand Up @@ -250,19 +248,6 @@ class DockerRuntime {
if ( shouldConfigureWp ) {
spinner.text = 'Configuring WordPress.';

try {
await checkDatabaseConnection( fullConfig );
} catch ( error ) {
// Wait 30 seconds for MySQL to accept connections.
await retry( () => checkDatabaseConnection( fullConfig ), {
times: 30,
delay: 1000,
} );

// It takes 3-4 seconds for MySQL to be ready after it starts accepting connections.
await sleep( 4000 );
}

// Retry WordPress installation in case MySQL *still* wasn't ready.
await Promise.all( [
retry(
Expand Down Expand Up @@ -439,9 +424,19 @@ class DockerRuntime {

const tasks = [];

// Start the database first to avoid race conditions where all tasks create
// different docker networks with the same name.
await dockerCompose.upOne( 'mysql', {
// Start the appropriate MySQL service(s) first to avoid race conditions
// where parallel tasks try to create docker networks with the same name.
// The dependency chain (cli -> wordpress -> mysql with service_healthy)
// ensures MySQL is ready before database operations run.
const mysqlServices = [];
if ( environment === 'all' || environment === 'development' ) {
mysqlServices.push( 'mysql' );
}
if ( environment === 'all' || environment === 'tests' ) {
mysqlServices.push( 'tests-mysql' );
}

await dockerCompose.upMany( mysqlServices, {
config: fullConfig.dockerComposeConfigPath,
log: fullConfig.debug,
} );
Expand Down
15 changes: 0 additions & 15 deletions packages/env/lib/runtime/docker/wordpress.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,6 @@ function isWPMajorMinorVersionLower( version, compareVersion ) {
return versionNumber < compareVersionNumber;
}

/**
* Checks a WordPress database connection. An error is thrown if the test is
* unsuccessful.
*
* @param {WPConfig} config The wp-env config object.
*/
async function checkDatabaseConnection( { dockerComposeConfigPath, debug } ) {
await dockerCompose.run( 'cli', 'wp db check', {
config: dockerComposeConfigPath,
commandOptions: [ '--rm' ],
log: debug,
} );
}

/**
* Configures WordPress for the given environment by installing WordPress,
* activating all plugins, and activating the first theme. These steps are
Expand Down Expand Up @@ -316,7 +302,6 @@ async function copyCoreFiles( fromPath, toPath ) {
}

module.exports = {
checkDatabaseConnection,
configureWordPress,
resetDatabase,
setupWordPressDirectories,
Expand Down
83 changes: 83 additions & 0 deletions packages/env/lib/test/build-docker-compose-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,87 @@ describe( 'buildDockerComposeConfig', () => {
expect( dockerConfig.volumes.wordpress ).toBe( undefined );
expect( dockerConfig.volumes[ 'tests-wordpress' ] ).toBe( undefined );
} );

it( 'should add healthcheck to mysql services', () => {
const config = buildDockerComposeConfig( {
workDirectoryPath: '/some/path',
env: {
development: {
port: 8888,
mysqlPort: 3306,
coreSource: null,
pluginSources: [],
themeSources: [],
mappings: {},
},
tests: {
port: 8889,
mysqlPort: 3307,
coreSource: null,
pluginSources: [],
themeSources: [],
mappings: {},
},
},
} );

expect( config.services.mysql.healthcheck ).toBeDefined();
expect( config.services.mysql.healthcheck.test ).toEqual( [
'CMD',
'healthcheck.sh',
'--connect',
'--innodb_initialized',
] );
expect( config.services.mysql.healthcheck.interval ).toBe( '5s' );
expect( config.services.mysql.healthcheck.timeout ).toBe( '10s' );
expect( config.services.mysql.healthcheck.retries ).toBe( 12 );
expect( config.services.mysql.healthcheck.start_period ).toBe( '60s' );

// Verify MARIADB_AUTO_UPGRADE is set for existing installations
expect( config.services.mysql.environment.MARIADB_AUTO_UPGRADE ).toBe(
'1'
);

expect( config.services[ 'tests-mysql' ].healthcheck ).toBeDefined();
expect( config.services[ 'tests-mysql' ].healthcheck.test ).toEqual( [
'CMD',
'healthcheck.sh',
'--connect',
'--innodb_initialized',
] );
expect(
config.services[ 'tests-mysql' ].environment.MARIADB_AUTO_UPGRADE
).toBe( '1' );
} );

it( 'should use service_healthy condition for WordPress depends_on', () => {
const config = buildDockerComposeConfig( {
workDirectoryPath: '/some/path',
env: {
development: {
port: 8888,
mysqlPort: 3306,
coreSource: null,
pluginSources: [],
themeSources: [],
mappings: {},
},
tests: {
port: 8889,
mysqlPort: 3307,
coreSource: null,
pluginSources: [],
themeSources: [],
mappings: {},
},
},
} );

expect( config.services.wordpress.depends_on ).toEqual( {
mysql: { condition: 'service_healthy' },
} );
expect( config.services[ 'tests-wordpress' ].depends_on ).toEqual( {
'tests-mysql': { condition: 'service_healthy' },
} );
} );
} );
Loading