diff --git a/lib/workers/branch/lock-files/__snapshots__/npm.spec.ts.snap b/lib/manager/npm/post-update/__snapshots__/npm.spec.ts.snap
similarity index 77%
rename from lib/workers/branch/lock-files/__snapshots__/npm.spec.ts.snap
rename to lib/manager/npm/post-update/__snapshots__/npm.spec.ts.snap
index 56cb6db006853a80b1a00268826f8878499ac6ea..319120c337a96b5fee2b12e7731faf0c327f3e91 100644
--- a/lib/workers/branch/lock-files/__snapshots__/npm.spec.ts.snap
+++ b/lib/manager/npm/post-update/__snapshots__/npm.spec.ts.snap
@@ -6,6 +6,23 @@ exports[`generateLockFile finds npm globally 1`] = `Array []`;
 
 exports[`generateLockFile generates lock files 1`] = `
 Array [
+  Object {
+    "cmd": "npm install --ignore-scripts --no-audit",
+    "options": Object {
+      "cwd": "some-dir",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "timeout": 900000,
+    },
+  },
   Object {
     "cmd": "npm dedupe",
     "options": Object {
diff --git a/lib/workers/branch/lock-files/npm.spec.ts b/lib/manager/npm/post-update/npm.spec.ts
similarity index 95%
rename from lib/workers/branch/lock-files/npm.spec.ts
rename to lib/manager/npm/post-update/npm.spec.ts
index bd3c92bbc8db3fc8b0015d602a838b60ce2d348e..f27e6db4e7f00a3d72afa8d9860d12b42f4ae9bc 100644
--- a/lib/workers/branch/lock-files/npm.spec.ts
+++ b/lib/manager/npm/post-update/npm.spec.ts
@@ -3,9 +3,9 @@ import path from 'path';
 import _fs from 'fs-extra';
 import { envMock, mockExecAll } from '../../../../test/execUtil';
 import { mocked } from '../../../../test/util';
-import * as npmHelper from '../../../manager/npm/post-update/npm';
 import { BinarySource } from '../../../util/exec/common';
 import * as _env from '../../../util/exec/env';
+import * as npmHelper from './npm';
 
 jest.mock('fs-extra');
 jest.mock('child_process');
@@ -25,12 +25,17 @@ describe('generateLockFile', () => {
     const execSnapshots = mockExecAll(exec);
     fs.readFile = jest.fn(() => 'package-lock-contents') as never;
     const skipInstalls = true;
+    const dockerMapDotfiles = true;
     const postUpdateOptions = ['npmDedupe'];
+    const updates = [
+      { depName: 'some-dep', toVersion: '1.0.1', isLockfileUpdate: false },
+    ];
     const res = await npmHelper.generateLockFile(
       'some-dir',
       {},
       'package-lock.json',
-      { skipInstalls, postUpdateOptions }
+      { dockerMapDotfiles, skipInstalls, postUpdateOptions },
+      updates
     );
     expect(fs.readFile).toHaveBeenCalledTimes(1);
     expect(res.error).toBeUndefined();
diff --git a/lib/manager/npm/post-update/npm.ts b/lib/manager/npm/post-update/npm.ts
index 590306e862fd7a3da13980bf96c736beea5afa0b..abf0d19f4444ba3ca0d1f25a51c33ac8453294de 100644
--- a/lib/manager/npm/post-update/npm.ts
+++ b/lib/manager/npm/post-update/npm.ts
@@ -2,8 +2,7 @@ import { move, pathExists, readFile } from 'fs-extra';
 import { join } from 'upath';
 import { SYSTEM_INSUFFICIENT_DISK_SPACE } from '../../../constants/error-messages';
 import { logger } from '../../../logger';
-import { exec } from '../../../util/exec';
-import { BinarySource } from '../../../util/exec/common';
+import { ExecOptions, exec } from '../../../util/exec';
 import { PostUpdateConfig, Upgrade } from '../../common';
 
 export interface GenerateLockFileResult {
@@ -11,6 +10,7 @@ export interface GenerateLockFileResult {
   lockFile?: string;
   stderr?: string;
 }
+
 export async function generateLockFile(
   cwd: string,
   env: NodeJS.ProcessEnv,
@@ -20,84 +20,67 @@ export async function generateLockFile(
 ): Promise<GenerateLockFileResult> {
   logger.debug(`Spawning npm install to create ${cwd}/${filename}`);
   const { skipInstalls, postUpdateOptions } = config;
-  let lockFile: string = null;
-  let stdout = '';
-  let stderr = '';
-  let cmd = 'npm';
-  let args = '';
+
+  let lockFile = null;
   try {
-    // istanbul ignore if
-    if (config.binarySource === BinarySource.Docker) {
-      logger.debug('Running npm via docker');
-      cmd = `docker run --rm `;
-      // istanbul ignore if
-      if (config.dockerUser) {
-        cmd += `--user=${config.dockerUser} `;
-      }
-      const volumes = [cwd];
-      if (config.cacheDir) {
-        volumes.push(config.cacheDir);
-      }
-      cmd += volumes.map((v) => `-v "${v}":"${v}" `).join('');
-      if (config.dockerMapDotfiles) {
-        const homeDir =
-          process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
-        const homeNpmrc = join(homeDir, '.npmrc');
-        cmd += `-v ${homeNpmrc}:/home/ubuntu/.npmrc `;
-      }
-      const envVars = ['NPM_CONFIG_CACHE', 'npm_config_store'];
-      cmd += envVars.map((e) => `-e ${e} `).join('');
-      cmd += `-w "${cwd}" `;
-      cmd += `renovate/npm npm`;
-    }
-    logger.debug(`Using npm: ${cmd}`);
-    args = `install`;
+    const preCommands = ['npm i -g npm'];
+    const commands = [];
+    let cmdOptions = '';
     if (
       (postUpdateOptions && postUpdateOptions.includes('npmDedupe')) ||
       skipInstalls === false
     ) {
-      logger.debug('Performing full npm install');
-      args += ' --ignore-scripts --no-audit';
+      logger.debug('Performing node_modules install');
+      cmdOptions += '--ignore-scripts --no-audit';
     } else {
-      args += ' --package-lock-only --no-audit';
+      logger.debug('Updating lock file only');
+      cmdOptions += '--package-lock-only --no-audit';
+    }
+    const execOptions: ExecOptions = {
+      cwd,
+      extraEnv: {
+        NPM_CONFIG_CACHE: env.NPM_CONFIG_CACHE,
+        npm_config_store: env.npm_config_store,
+      },
+      docker: {
+        image: 'renovate/node',
+        preCommands,
+      },
+    };
+    if (config.dockerMapDotfiles) {
+      const homeDir =
+        process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
+      const homeNpmrc = join(homeDir, '.npmrc');
+      execOptions.docker.volumes = [[homeNpmrc, '/home/ubuntu/.npmrc']];
     }
-    logger.debug(`Using npm: ${cmd} ${args}`);
-    // istanbul ignore if
+
     if (!upgrades.every((upgrade) => upgrade.isLockfileUpdate)) {
-      // TODO: Switch to native util.promisify once using only node 8
-      ({ stdout, stderr } = await exec(`${cmd} ${args}`, {
-        cwd,
-        env,
-      }));
+      // This command updates the lock file based on package.json
+      commands.push(`npm install ${cmdOptions}`.trim());
     }
+
+    // rangeStrategy = update-lockfile
     const lockUpdates = upgrades.filter((upgrade) => upgrade.isLockfileUpdate);
     if (lockUpdates.length) {
       logger.debug('Performing lockfileUpdate (npm)');
       const updateCmd =
-        `${cmd} ${args}` +
+        `npm install ${cmdOptions}` +
         lockUpdates
           .map((update) => ` ${update.depName}@${update.toVersion}`)
           .join('');
-      const updateRes = await exec(updateCmd, {
-        cwd,
-        env,
-      });
-      stdout += updateRes.stdout ? updateRes.stdout : '';
-      stderr += updateRes.stderr ? updateRes.stderr : '';
+      commands.push(updateCmd);
     }
-    if (postUpdateOptions && postUpdateOptions.includes('npmDedupe')) {
+
+    // postUpdateOptions
+    if (config.postUpdateOptions?.includes('npmDedupe')) {
       logger.debug('Performing npm dedupe');
-      const dedupeRes = await exec(`${cmd} dedupe`, {
-        cwd,
-        env,
-      });
-      stdout += dedupeRes.stdout ? dedupeRes.stdout : '';
-      stderr += dedupeRes.stderr ? dedupeRes.stderr : '';
-    }
-    // istanbul ignore if
-    if (stderr && stderr.includes('ENOSPC: no space left on device')) {
-      throw new Error(SYSTEM_INSUFFICIENT_DISK_SPACE);
+      commands.push('npm dedupe');
     }
+
+    // Run the commands
+    await exec(commands, execOptions);
+
+    // massage to shrinkwrap if necessary
     if (
       filename === 'npm-shrinkwrap.json' &&
       (await pathExists(join(cwd, 'package-lock.json')))
@@ -107,19 +90,20 @@ export async function generateLockFile(
         join(cwd, 'npm-shrinkwrap.json')
       );
     }
+
+    // Read the result
     lockFile = await readFile(join(cwd, filename), 'utf8');
   } catch (err) /* istanbul ignore next */ {
     logger.debug(
       {
-        cmd,
-        args,
         err,
-        stdout,
-        stderr,
         type: 'npm',
       },
       'lock file error'
     );
+    if (err.stderr?.includes('ENOSPC: no space left on device')) {
+      throw new Error(SYSTEM_INSUFFICIENT_DISK_SPACE);
+    }
     return { error: true, stderr: err.stderr };
   }
   return { lockFile };