diff --git a/lib/manager/composer/artifacts.ts b/lib/manager/composer/artifacts.ts
index 277b105ae4ed0ce9996e3d3f074b8ca49ecc42ef..1567276f405d57e90cb660547ef2cccd7426e550 100644
--- a/lib/manager/composer/artifacts.ts
+++ b/lib/manager/composer/artifacts.ts
@@ -2,14 +2,12 @@ import is from '@sindresorhus/is';
 import URL from 'url';
 import fs from 'fs-extra';
 import upath from 'upath';
-import { exec } from '../../util/exec';
+import { exec, ExecOptions } from '../../util/exec';
 import { UpdateArtifact, UpdateArtifactsResult } from '../common';
 import { logger } from '../../logger';
 import * as hostRules from '../../util/host-rules';
-import { getChildProcessEnv } from '../../util/exec/env';
 import { platform } from '../../platform';
 import { SYSTEM_INSUFFICIENT_DISK_SPACE } from '../../constants/error-messages';
-import { BinarySource } from '../../util/exec/common';
 
 export async function updateArtifacts({
   packageFileName,
@@ -18,11 +16,13 @@ export async function updateArtifacts({
   config,
 }: UpdateArtifact): Promise<UpdateArtifactsResult[] | null> {
   logger.debug(`composer.updateArtifacts(${packageFileName})`);
-  const env = getChildProcessEnv(['COMPOSER_CACHE_DIR']);
-  env.COMPOSER_CACHE_DIR =
-    env.COMPOSER_CACHE_DIR || upath.join(config.cacheDir, './others/composer');
-  await fs.ensureDir(env.COMPOSER_CACHE_DIR);
-  logger.debug('Using composer cache ' + env.COMPOSER_CACHE_DIR);
+
+  const cacheDir =
+    process.env.COMPOSER_CACHE_DIR ||
+    upath.join(config.cacheDir, './others/composer');
+  await fs.ensureDir(cacheDir);
+  logger.debug(`Using composer cache ${cacheDir}`);
+
   const lockFileName = packageFileName.replace(/\.json$/, '.lock');
   const existingLockFileContent = await platform.getFile(lockFileName);
   if (!existingLockFileContent) {
@@ -95,29 +95,15 @@ export async function updateArtifacts({
       const localAuthFileName = upath.join(cwd, 'auth.json');
       await fs.outputFile(localAuthFileName, JSON.stringify(authJson));
     }
-    let cmd: string;
-    if (config.binarySource === BinarySource.Docker) {
-      logger.info('Running composer via docker');
-      cmd = `docker run --rm `;
-      if (config.dockerUser) {
-        cmd += `--user=${config.dockerUser} `;
-      }
-      const volumes = [config.localDir, env.COMPOSER_CACHE_DIR];
-      cmd += volumes.map(v => `-v "${v}":"${v}" `).join('');
-      const envVars = ['COMPOSER_CACHE_DIR'];
-      cmd += envVars.map(e => `-e ${e} `);
-      cmd += `-w "${cwd}" `;
-      cmd += `renovate/composer composer`;
-    } else if (
-      config.binarySource === BinarySource.Auto ||
-      config.binarySource === BinarySource.Global
-    ) {
-      logger.info('Running composer via global composer');
-      cmd = 'composer';
-    } else {
-      logger.warn({ config }, 'Unsupported binarySource');
-      cmd = 'composer';
-    }
+    const execOptions: ExecOptions = {
+      extraEnv: {
+        COMPOSER_CACHE_DIR: cacheDir,
+      },
+      docker: {
+        image: 'renovate/composer',
+      },
+    };
+    const cmd = 'composer';
     let args;
     if (config.isLockFileMaintenance) {
       args = 'install';
@@ -130,10 +116,7 @@ export async function updateArtifacts({
       args += ' --no-scripts --no-autoloader';
     }
     logger.debug({ cmd, args }, 'composer command');
-    await exec(`${cmd} ${args}`, {
-      cwd,
-      env,
-    });
+    await exec(`${cmd} ${args}`, execOptions);
     const status = await platform.getRepoStatus();
     if (!status.modified.includes(lockFileName)) {
       return null;
diff --git a/test/manager/composer/__snapshots__/artifacts.spec.ts.snap b/test/manager/composer/__snapshots__/artifacts.spec.ts.snap
index d3abf88eff6176c6a573fcfbe823bba20208dfd2..2a25850fe4321ad705b9740ade44b1c4882cb049 100644
--- a/test/manager/composer/__snapshots__/artifacts.spec.ts.snap
+++ b/test/manager/composer/__snapshots__/artifacts.spec.ts.snap
@@ -85,7 +85,13 @@ Array [
 exports[`.updateArtifacts() supports docker mode 1`] = `
 Array [
   Object {
-    "cmd": "docker run --rm --user=foobar -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -v \\"/tmp/renovate/cache/others/composer\\":\\"/tmp/renovate/cache/others/composer\\" -e COMPOSER_CACHE_DIR -w \\"/tmp/github/some/repo\\" renovate/composer composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader",
+    "cmd": "docker pull renovate/composer",
+    "options": Object {
+      "encoding": "utf-8",
+    },
+  },
+  Object {
+    "cmd": "docker run --rm --user=foobar -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -v \\"/tmp/renovate/cache\\":\\"/tmp/renovate/cache\\" -e COMPOSER_CACHE_DIR -w \\"/tmp/github/some/repo\\" renovate/composer bash -l -c \\"composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader\\"",
     "options": Object {
       "cwd": "/tmp/github/some/repo",
       "encoding": "utf-8",
diff --git a/test/manager/composer/artifacts.spec.ts b/test/manager/composer/artifacts.spec.ts
index aef0f4b9487f4bf81b12b5c9950443fbebd62d31..514999cded861c54c08a4ac163946b24e1dfcda9 100644
--- a/test/manager/composer/artifacts.spec.ts
+++ b/test/manager/composer/artifacts.spec.ts
@@ -1,3 +1,4 @@
+import { join } from 'upath';
 import _fs from 'fs-extra';
 import { exec as _exec } from 'child_process';
 import * as composer from '../../../lib/manager/composer/artifacts';
@@ -7,6 +8,8 @@ import { StatusResult } from '../../../lib/platform/git/storage';
 import { envMock, mockExecAll } from '../../execUtil';
 import * as _env from '../../../lib/util/exec/env';
 import { BinarySource } from '../../../lib/util/exec/common';
+import { setUtilConfig } from '../../../lib/util';
+import { resetPrefetchedImages } from '../../../lib/util/exec/docker';
 
 jest.mock('fs-extra');
 jest.mock('child_process');
@@ -21,8 +24,10 @@ const env = mocked(_env);
 const platform = mocked(_platform);
 
 const config = {
-  localDir: '/tmp/github/some/repo',
-  cacheDir: '/tmp/renovate/cache',
+  // `join` fixes Windows CI
+  localDir: join('/tmp/github/some/repo'),
+  cacheDir: join('/tmp/renovate/cache'),
+  dockerUser: 'foobar',
 };
 
 describe('.updateArtifacts()', () => {
@@ -30,6 +35,8 @@ describe('.updateArtifacts()', () => {
     jest.resetAllMocks();
     jest.resetModules();
     env.getChildProcessEnv.mockReturnValue(envMock.basic);
+    setUtilConfig(config);
+    resetPrefetchedImages();
   });
   it('returns if no composer.lock found', async () => {
     expect(
@@ -117,6 +124,7 @@ describe('.updateArtifacts()', () => {
     expect(execSnapshots).toMatchSnapshot();
   });
   it('supports docker mode', async () => {
+    setUtilConfig({ ...config, binarySource: BinarySource.Docker });
     platform.getFile.mockResolvedValueOnce('Current composer.lock');
 
     const execSnapshots = mockExecAll(exec);
@@ -127,11 +135,7 @@ describe('.updateArtifacts()', () => {
         packageFileName: 'composer.json',
         updatedDeps: [],
         newPackageFileContent: '{}',
-        config: {
-          ...config,
-          binarySource: BinarySource.Docker,
-          dockerUser: 'foobar',
-        },
+        config,
       })
     ).not.toBeNull();
     expect(execSnapshots).toMatchSnapshot();