diff --git a/packages/env/lib/env.js b/packages/env/lib/env.js
index 80b0415e7085d..1f69461e291fb 100644
--- a/packages/env/lib/env.js
+++ b/packages/env/lib/env.js
@@ -50,6 +50,7 @@ module.exports = {
 		await module.exports.stop( { spinner, debug } );
 
 		await checkForLegacyInstall( spinner );
+
 		const config = await initConfig( { spinner, debug } );
 
 		spinner.text = 'Downloading WordPress.';
@@ -114,6 +115,14 @@ module.exports = {
 			log: config.debug,
 		} );
 
+		if ( config.coreSource === null ) {
+			// Don't chown wp-content when it exists on the user's local filesystem.
+			await Promise.all( [
+				makeContentDirectoriesWritable( 'development', config ),
+				makeContentDirectoriesWritable( 'tests', config ),
+			] );
+		}
+
 		try {
 			await checkDatabaseConnection( config );
 		} catch ( error ) {
@@ -355,6 +364,35 @@ async function copyCoreFiles( fromPath, toPath ) {
 	} );
 }
 
+/**
+ * Makes the WordPress content directories (wp-content, wp-content/plugins,
+ * wp-content/themes) owned by the www-data user. This ensures that WordPress
+ * can write to these directories.
+ *
+ * This is necessary when running wp-env with `"core": null` because Docker
+ * will automatically create these directories as the root user when binding
+ * volumes during `docker-compose up`, and `docker-compose up` doesn't support
+ * the `-u` option.
+ *
+ * See https://github.com/docker-library/wordpress/issues/436.
+ *
+ * @param {string} environment The environment to check. Either 'development' or 'tests'.
+ * @param {Config} config The wp-env config object.
+ */
+async function makeContentDirectoriesWritable(
+	environment,
+	{ dockerComposeConfigPath, debug }
+) {
+	await dockerCompose.exec(
+		environment === 'development' ? 'wordpress' : 'tests-wordpress',
+		'chown www-data:www-data wp-content wp-content/plugins wp-content/themes',
+		{
+			config: dockerComposeConfigPath,
+			log: debug,
+		}
+	);
+}
+
 /**
  * Performs the given action again and again until it does not throw an error.
  *