diff --git a/lib/manager/composer/artifacts.spec.ts b/lib/manager/composer/artifacts.spec.ts
index 09b55b48e3cd9dada7f2a5dc62d4cfca15972355..a5fd3343b1a71fe7620d3c9f4e639bc9c7d3fe6f 100644
--- a/lib/manager/composer/artifacts.spec.ts
+++ b/lib/manager/composer/artifacts.spec.ts
@@ -1,5 +1,4 @@
 import { exec as _exec } from 'child_process';
-import _fs from 'fs-extra';
 import { join } from 'upath';
 import { envMock, mockExecAll } from '../../../test/execUtil';
 import { mocked } from '../../../test/util';
@@ -9,11 +8,12 @@ import { setUtilConfig } from '../../util';
 import { BinarySource } from '../../util/exec/common';
 import { resetPrefetchedImages } from '../../util/exec/docker';
 import * as _env from '../../util/exec/env';
+import * as _fs from '../../util/fs';
 import * as composer from './artifacts';
 
-jest.mock('fs-extra');
 jest.mock('child_process');
 jest.mock('../../util/exec/env');
+jest.mock('../../util/fs');
 jest.mock('../../util/host-rules');
 
 const hostRules = require('../../util/host-rules');
@@ -55,9 +55,9 @@ describe('.updateArtifacts()', () => {
     ).toBeNull();
   });
   it('returns null if unchanged', async () => {
-    fs.readFile.mockResolvedValueOnce('Current composer.lock' as any);
+    fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
     const execSnapshots = mockExecAll(exec);
-    fs.readFile.mockReturnValueOnce('Current composer.lock' as any);
+    fs.readLocalFile.mockReturnValueOnce('Current composer.lock' as any);
     platform.getRepoStatus.mockResolvedValue({ modified: [] } as StatusResult);
     expect(
       await composer.updateArtifacts({
@@ -70,9 +70,9 @@ describe('.updateArtifacts()', () => {
     expect(execSnapshots).toMatchSnapshot();
   });
   it('uses hostRules to write auth.json', async () => {
-    fs.readFile.mockResolvedValueOnce('Current composer.lock' as any);
+    fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
     const execSnapshots = mockExecAll(exec);
-    fs.readFile.mockReturnValueOnce('Current composer.lock' as any);
+    fs.readLocalFile.mockReturnValueOnce('Current composer.lock' as any);
     const authConfig = {
       ...config,
       registryUrls: ['https://packagist.renovatebot.com'],
@@ -93,9 +93,9 @@ describe('.updateArtifacts()', () => {
     expect(execSnapshots).toMatchSnapshot();
   });
   it('returns updated composer.lock', async () => {
-    fs.readFile.mockResolvedValueOnce('Current composer.lock' as any);
+    fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
     const execSnapshots = mockExecAll(exec);
-    fs.readFile.mockReturnValueOnce('New composer.lock' as any);
+    fs.readLocalFile.mockReturnValueOnce('New composer.lock' as any);
     platform.getRepoStatus.mockResolvedValue({
       modified: ['composer.lock'],
     } as StatusResult);
@@ -110,9 +110,9 @@ describe('.updateArtifacts()', () => {
     expect(execSnapshots).toMatchSnapshot();
   });
   it('performs lockFileMaintenance', async () => {
-    fs.readFile.mockResolvedValueOnce('Current composer.lock' as any);
+    fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
     const execSnapshots = mockExecAll(exec);
-    fs.readFile.mockReturnValueOnce('New composer.lock' as any);
+    fs.readLocalFile.mockReturnValueOnce('New composer.lock' as any);
     platform.getRepoStatus.mockResolvedValue({
       modified: ['composer.lock'],
     } as StatusResult);
@@ -131,11 +131,11 @@ describe('.updateArtifacts()', () => {
   });
   it('supports docker mode', async () => {
     await setUtilConfig({ ...config, binarySource: BinarySource.Docker });
-    fs.readFile.mockResolvedValueOnce('Current composer.lock' as any);
+    fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
 
     const execSnapshots = mockExecAll(exec);
 
-    fs.readFile.mockReturnValueOnce('New composer.lock' as any);
+    fs.readLocalFile.mockReturnValueOnce('New composer.lock' as any);
     expect(
       await composer.updateArtifacts({
         packageFileName: 'composer.json',
@@ -147,9 +147,9 @@ describe('.updateArtifacts()', () => {
     expect(execSnapshots).toMatchSnapshot();
   });
   it('supports global mode', async () => {
-    fs.readFile.mockResolvedValueOnce('Current composer.lock' as any);
+    fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
     const execSnapshots = mockExecAll(exec);
-    fs.readFile.mockReturnValueOnce('New composer.lock' as any);
+    fs.readLocalFile.mockReturnValueOnce('New composer.lock' as any);
     expect(
       await composer.updateArtifacts({
         packageFileName: 'composer.json',
@@ -164,8 +164,8 @@ describe('.updateArtifacts()', () => {
     expect(execSnapshots).toMatchSnapshot();
   });
   it('catches errors', async () => {
-    fs.readFile.mockResolvedValueOnce('Current composer.lock' as any);
-    fs.outputFile.mockImplementationOnce(() => {
+    fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
+    fs.writeLocalFile.mockImplementationOnce(() => {
       throw new Error('not found');
     });
     expect(
@@ -178,8 +178,8 @@ describe('.updateArtifacts()', () => {
     ).toMatchSnapshot();
   });
   it('catches unmet requirements errors', async () => {
-    fs.readFile.mockResolvedValueOnce('Current composer.lock' as any);
-    fs.outputFile.mockImplementationOnce(() => {
+    fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
+    fs.writeLocalFile.mockImplementationOnce(() => {
       throw new Error(
         'fooYour requirements could not be resolved to an installable set of packages.bar'
       );
@@ -194,8 +194,8 @@ describe('.updateArtifacts()', () => {
     ).toMatchSnapshot();
   });
   it('throws for disk space', async () => {
-    fs.readFile.mockResolvedValueOnce('Current composer.lock' as any);
-    fs.outputFile.mockImplementationOnce(() => {
+    fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
+    fs.writeLocalFile.mockImplementationOnce(() => {
       throw new Error(
         'vendor/composer/07fe2366/sebastianbergmann-php-code-coverage-c896779/src/Report/Html/Renderer/Template/js/d3.min.js:  write error (disk full?).  Continue? (y/n/^C) '
       );
@@ -210,9 +210,9 @@ describe('.updateArtifacts()', () => {
     ).rejects.toThrow();
   });
   it('disables ignorePlatformReqs', async () => {
-    fs.readFile.mockResolvedValueOnce('Current composer.lock' as any);
+    fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
     const execSnapshots = mockExecAll(exec);
-    fs.readFile.mockReturnValueOnce('New composer.lock' as any);
+    fs.readLocalFile.mockReturnValueOnce('New composer.lock' as any);
     platform.getRepoStatus.mockResolvedValue({
       modified: ['composer.lock'],
     } as StatusResult);
diff --git a/lib/manager/composer/artifacts.ts b/lib/manager/composer/artifacts.ts
index b856ee288cae2494371a56ccc33967301d49485a..76835c836e54278f305f58d64bd089bb5a732d32 100644
--- a/lib/manager/composer/artifacts.ts
+++ b/lib/manager/composer/artifacts.ts
@@ -1,6 +1,5 @@
 import URL from 'url';
 import is from '@sindresorhus/is';
-import fs from 'fs-extra';
 import { quote } from 'shlex';
 import upath from 'upath';
 import { SYSTEM_INSUFFICIENT_DISK_SPACE } from '../../constants/error-messages';
@@ -12,7 +11,15 @@ import * as datasourcePackagist from '../../datasource/packagist';
 import { logger } from '../../logger';
 import { platform } from '../../platform';
 import { ExecOptions, exec } from '../../util/exec';
-import { deleteLocalFile, readLocalFile, writeLocalFile } from '../../util/fs';
+import {
+  deleteLocalFile,
+  ensureDir,
+  ensureLocalDir,
+  getSiblingFileName,
+  getSubDirectory,
+  readLocalFile,
+  writeLocalFile,
+} from '../../util/fs';
 import * as hostRules from '../../util/host-rules';
 import { UpdateArtifact, UpdateArtifactsResult } from '../common';
 
@@ -27,7 +34,7 @@ export async function updateArtifacts({
   const cacheDir =
     process.env.COMPOSER_CACHE_DIR ||
     upath.join(config.cacheDir, './others/composer');
-  await fs.ensureDir(cacheDir);
+  await ensureDir(cacheDir);
   logger.debug(`Using composer cache ${cacheDir}`);
 
   const lockFileName = packageFileName.replace(/\.json$/, '.lock');
@@ -36,8 +43,7 @@ export async function updateArtifacts({
     logger.debug('No composer.lock found');
     return null;
   }
-  const cwd = upath.join(config.localDir, upath.dirname(packageFileName));
-  await fs.ensureDir(upath.join(cwd, 'vendor'));
+  await ensureLocalDir(getSiblingFileName(packageFileName, 'vendor'));
   try {
     await writeLocalFile(packageFileName, newPackageFileContent);
     if (config.isLockFileMaintenance) {
@@ -99,6 +105,7 @@ export async function updateArtifacts({
     if (authJson) {
       await writeLocalFile('auth.json', JSON.stringify(authJson));
     }
+    const cwd = getSubDirectory(packageFileName);
     const execOptions: ExecOptions = {
       cwd,
       extraEnv: {
diff --git a/lib/util/fs.ts b/lib/util/fs.ts
index 1a472d65028ec369666bb12e12a235bd41545384..d94bb19c37386a77fcb601a837ed7c60d28f19a2 100644
--- a/lib/util/fs.ts
+++ b/lib/util/fs.ts
@@ -51,3 +51,14 @@ export async function writeLocalFile(
 export async function deleteLocalFile(fileName: string): Promise<void> {
   await fs.remove(fileName);
 }
+
+// istanbul ignore next
+export async function ensureDir(dirName): Promise<void> {
+  await fs.ensureDir(dirName);
+}
+
+// istanbul ignore next
+export async function ensureLocalDir(dirName): Promise<void> {
+  const localDirName = join(localDir, dirName);
+  await fs.ensureDir(localDirName);
+}