Skip to content

Commit 825d42b

Browse files
committed
wp-env: Add MySQL healthcheck to fix startup race condition
WordPress containers were starting before MySQL was ready to accept connections, causing intermittent "Error establishing a database connection" errors, especially in CI environments. This change: - Adds a healthcheck to mysql and tests-mysql services using mariadb-admin ping with root credentials - Updates WordPress depends_on to use condition: service_healthy - Starts both MySQL services in parallel for faster initialization - Removes redundant retry logic now handled by Docker Compose
1 parent 5cd65a4 commit 825d42b

File tree

4 files changed

+98
-18
lines changed

4 files changed

+98
-18
lines changed

packages/env/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Bug Fixes
6+
7+
- Add MySQL healthcheck to prevent race condition where WordPress containers start before MySQL is ready to accept connections.
8+
59
## 10.39.0 (2026-01-29)
610

711
### New Features

packages/env/lib/runtime/docker/build-docker-compose-config.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,20 @@ module.exports = function buildDockerComposeConfig( config ) {
181181
config.env.tests.phpmyadminPort ?? ''
182182
}}:80`;
183183

184+
// MySQL healthcheck using mariadb-admin ping.
185+
// Uses root credentials to avoid requiring special healthcheck user,
186+
// which would break existing wp-env installations.
187+
const mysqlHealthcheck = {
188+
test: [
189+
'CMD-SHELL',
190+
'mariadb-admin ping -h localhost -u root -p$MYSQL_ROOT_PASSWORD',
191+
],
192+
interval: '5s',
193+
timeout: '5s',
194+
retries: 10,
195+
start_period: '30s',
196+
};
197+
184198
return {
185199
services: {
186200
mysql: {
@@ -193,6 +207,7 @@ module.exports = function buildDockerComposeConfig( config ) {
193207
MYSQL_DATABASE: dbEnv.development.WORDPRESS_DB_NAME,
194208
},
195209
volumes: [ 'mysql:/var/lib/mysql' ],
210+
healthcheck: mysqlHealthcheck,
196211
},
197212
'tests-mysql': {
198213
image: 'mariadb:lts',
@@ -204,9 +219,14 @@ module.exports = function buildDockerComposeConfig( config ) {
204219
MYSQL_DATABASE: dbEnv.tests.WORDPRESS_DB_NAME,
205220
},
206221
volumes: [ 'mysql-test:/var/lib/mysql' ],
222+
healthcheck: mysqlHealthcheck,
207223
},
208224
wordpress: {
209-
depends_on: [ 'mysql' ],
225+
depends_on: {
226+
mysql: {
227+
condition: 'service_healthy',
228+
},
229+
},
210230
build: {
211231
context: '.',
212232
dockerfile: 'WordPress.Dockerfile',
@@ -224,7 +244,11 @@ module.exports = function buildDockerComposeConfig( config ) {
224244
extra_hosts: [ 'host.docker.internal:host-gateway' ],
225245
},
226246
'tests-wordpress': {
227-
depends_on: [ 'tests-mysql' ],
247+
depends_on: {
248+
'tests-mysql': {
249+
condition: 'service_healthy',
250+
},
251+
},
228252
build: {
229253
context: '.',
230254
dockerfile: 'Tests-WordPress.Dockerfile',

packages/env/lib/runtime/docker/index.js

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const { rimraf } = require( 'rimraf' );
1111
/**
1212
* Promisified dependencies
1313
*/
14-
const sleep = util.promisify( setTimeout );
1514
const exec = util.promisify( require( 'child_process' ).exec );
1615

1716
/**
@@ -26,7 +25,6 @@ const {
2625
validateRunContainer,
2726
} = require( './validate-run-container' );
2827
const {
29-
checkDatabaseConnection,
3028
configureWordPress,
3129
resetDatabase,
3230
setupWordPressDirectories,
@@ -174,7 +172,7 @@ class DockerRuntime {
174172
}
175173

176174
await Promise.all( [
177-
dockerCompose.upOne( 'mysql', {
175+
dockerCompose.upMany( [ 'mysql', 'tests-mysql' ], {
178176
...dockerComposeConfig,
179177
commandOptions: shouldConfigureWp
180178
? [ '--build', '--force-recreate' ]
@@ -250,19 +248,6 @@ class DockerRuntime {
250248
if ( shouldConfigureWp ) {
251249
spinner.text = 'Configuring WordPress.';
252250

253-
try {
254-
await checkDatabaseConnection( fullConfig );
255-
} catch ( error ) {
256-
// Wait 30 seconds for MySQL to accept connections.
257-
await retry( () => checkDatabaseConnection( fullConfig ), {
258-
times: 30,
259-
delay: 1000,
260-
} );
261-
262-
// It takes 3-4 seconds for MySQL to be ready after it starts accepting connections.
263-
await sleep( 4000 );
264-
}
265-
266251
// Retry WordPress installation in case MySQL *still* wasn't ready.
267252
await Promise.all( [
268253
retry(

packages/env/lib/test/build-docker-compose-config.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,71 @@ describe( 'buildDockerComposeConfig', () => {
131131
expect( dockerConfig.volumes.wordpress ).toBe( undefined );
132132
expect( dockerConfig.volumes[ 'tests-wordpress' ] ).toBe( undefined );
133133
} );
134+
135+
it( 'should add healthcheck to mysql services', () => {
136+
const config = buildDockerComposeConfig( {
137+
workDirectoryPath: '/some/path',
138+
env: {
139+
development: {
140+
port: 8888,
141+
mysqlPort: 3306,
142+
coreSource: null,
143+
pluginSources: [],
144+
themeSources: [],
145+
mappings: {},
146+
},
147+
tests: {
148+
port: 8889,
149+
mysqlPort: 3307,
150+
coreSource: null,
151+
pluginSources: [],
152+
themeSources: [],
153+
mappings: {},
154+
},
155+
},
156+
} );
157+
158+
expect( config.services.mysql.healthcheck ).toBeDefined();
159+
expect( config.services.mysql.healthcheck.test ).toContain(
160+
'CMD-SHELL'
161+
);
162+
expect( config.services.mysql.healthcheck.interval ).toBe( '5s' );
163+
expect( config.services.mysql.healthcheck.start_period ).toBe( '30s' );
164+
165+
expect( config.services[ 'tests-mysql' ].healthcheck ).toBeDefined();
166+
expect( config.services[ 'tests-mysql' ].healthcheck.test ).toContain(
167+
'CMD-SHELL'
168+
);
169+
} );
170+
171+
it( 'should use service_healthy condition for WordPress depends_on', () => {
172+
const config = buildDockerComposeConfig( {
173+
workDirectoryPath: '/some/path',
174+
env: {
175+
development: {
176+
port: 8888,
177+
mysqlPort: 3306,
178+
coreSource: null,
179+
pluginSources: [],
180+
themeSources: [],
181+
mappings: {},
182+
},
183+
tests: {
184+
port: 8889,
185+
mysqlPort: 3307,
186+
coreSource: null,
187+
pluginSources: [],
188+
themeSources: [],
189+
mappings: {},
190+
},
191+
},
192+
} );
193+
194+
expect( config.services.wordpress.depends_on ).toEqual( {
195+
mysql: { condition: 'service_healthy' },
196+
} );
197+
expect( config.services[ 'tests-wordpress' ].depends_on ).toEqual( {
198+
'tests-mysql': { condition: 'service_healthy' },
199+
} );
200+
} );
134201
} );

0 commit comments

Comments
 (0)