diff --git a/lib/manager/npm/post-update/node-version.spec.ts b/lib/manager/npm/post-update/node-version.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..af76ab43ec3a65b6c65575d7b42b48ec2bc9e903
--- /dev/null
+++ b/lib/manager/npm/post-update/node-version.spec.ts
@@ -0,0 +1,45 @@
+import { mocked } from '../../../../test/util';
+import * as fs_ from '../../../util/fs';
+import { getNodeConstraint } from './node-version';
+
+const fs = mocked(fs_);
+
+describe('getNodeConstraint', () => {
+  it('returns package.json range', async () => {
+    fs.readLocalFile = jest.fn();
+    fs.readLocalFile.mockResolvedValueOnce(null);
+    fs.readLocalFile.mockResolvedValueOnce(null);
+    fs.readLocalFile.mockResolvedValueOnce('{"engines":{"node":"^12.16.0"}}');
+    const res = await getNodeConstraint('package.json');
+    expect(res).toEqual('^12.16.0');
+  });
+  it('returns .node-version value', async () => {
+    fs.readLocalFile = jest.fn();
+    fs.readLocalFile.mockResolvedValueOnce(null);
+    fs.readLocalFile.mockResolvedValueOnce('12.16.1\n');
+    const res = await getNodeConstraint('package.json');
+    expect(res).toEqual('12.16.1');
+  });
+  it('returns .nvmrc value', async () => {
+    fs.readLocalFile = jest.fn();
+    fs.readLocalFile.mockResolvedValueOnce('12.16.2\n');
+    const res = await getNodeConstraint('package.json');
+    expect(res).toEqual('12.16.2');
+  });
+  it('ignores unusable ranges in dotfiles', async () => {
+    fs.readLocalFile = jest.fn();
+    fs.readLocalFile.mockResolvedValueOnce('latest');
+    fs.readLocalFile.mockResolvedValueOnce('lts');
+    fs.readLocalFile.mockResolvedValueOnce('{"engines":{"node":"^12.16.0"}}');
+    const res = await getNodeConstraint('package.json');
+    expect(res).toEqual('^12.16.0');
+  });
+  it('returns no constraint', async () => {
+    fs.readLocalFile = jest.fn();
+    fs.readLocalFile.mockResolvedValueOnce(null);
+    fs.readLocalFile.mockResolvedValueOnce(null);
+    fs.readLocalFile.mockResolvedValueOnce('{}');
+    const res = await getNodeConstraint('package.json');
+    expect(res).toBeNull();
+  });
+});
diff --git a/lib/manager/npm/post-update/node-version.ts b/lib/manager/npm/post-update/node-version.ts
new file mode 100644
index 0000000000000000000000000000000000000000..28936276c935123be69c918da06a97a0f10b418f
--- /dev/null
+++ b/lib/manager/npm/post-update/node-version.ts
@@ -0,0 +1,47 @@
+import { validRange } from 'semver';
+import { logger } from '../../../logger';
+import { getSiblingFileName, readLocalFile } from '../../../util/fs';
+
+async function getNodeFile(filename: string): Promise<string> | null {
+  try {
+    const constraint = (await readLocalFile(filename, 'utf8'))
+      .split('\n')[0]
+      .replace(/^v/, '');
+    if (validRange(constraint)) {
+      logger.debug(`Using node constraint "${constraint}" from ${filename}`);
+      return constraint;
+    }
+  } catch (err) {
+    // do nothing
+  }
+  return null;
+}
+
+async function getPackageJsonConstraint(
+  filename: string
+): Promise<string> | null {
+  try {
+    const pj = JSON.parse(await readLocalFile(filename, 'utf8'));
+    const constraint = pj?.engines?.node;
+    if (constraint && validRange(constraint)) {
+      logger.debug(`Using node constraint "${constraint}" from package.json`);
+      return constraint;
+    }
+  } catch (err) {
+    // do nothing
+  }
+  return null;
+}
+
+export async function getNodeConstraint(
+  filename: string
+): Promise<string> | null {
+  const constraint =
+    (await getNodeFile(getSiblingFileName(filename, '.nvmrc'))) ||
+    (await getNodeFile(getSiblingFileName(filename, '.node-version'))) ||
+    (await getPackageJsonConstraint(filename));
+  if (!constraint) {
+    logger.debug('No node constraint found - using latest');
+  }
+  return constraint;
+}
diff --git a/lib/manager/npm/post-update/npm.spec.ts b/lib/manager/npm/post-update/npm.spec.ts
index f27e6db4e7f00a3d72afa8d9860d12b42f4ae9bc..83081050bd8c947333e10d0f6166cc4c27c2b572 100644
--- a/lib/manager/npm/post-update/npm.spec.ts
+++ b/lib/manager/npm/post-update/npm.spec.ts
@@ -10,6 +10,7 @@ import * as npmHelper from './npm';
 jest.mock('fs-extra');
 jest.mock('child_process');
 jest.mock('../../../util/exec/env');
+jest.mock('./node-version');
 
 const exec: jest.Mock<typeof _exec> = _exec as any;
 const env = mocked(_env);
diff --git a/lib/manager/npm/post-update/npm.ts b/lib/manager/npm/post-update/npm.ts
index abf0d19f4444ba3ca0d1f25a51c33ac8453294de..18511f8180fbd19e2cf2e9e65edf947859fb4914 100644
--- a/lib/manager/npm/post-update/npm.ts
+++ b/lib/manager/npm/post-update/npm.ts
@@ -4,6 +4,7 @@ import { SYSTEM_INSUFFICIENT_DISK_SPACE } from '../../../constants/error-message
 import { logger } from '../../../logger';
 import { ExecOptions, exec } from '../../../util/exec';
 import { PostUpdateConfig, Upgrade } from '../../common';
+import { getNodeConstraint } from './node-version';
 
 export interface GenerateLockFileResult {
   error?: boolean;
@@ -36,6 +37,7 @@ export async function generateLockFile(
       logger.debug('Updating lock file only');
       cmdOptions += '--package-lock-only --no-audit';
     }
+    const tagConstraint = await getNodeConstraint(config.packageFile);
     const execOptions: ExecOptions = {
       cwd,
       extraEnv: {
@@ -44,6 +46,8 @@ export async function generateLockFile(
       },
       docker: {
         image: 'renovate/node',
+        tagScheme: 'npm',
+        tagConstraint,
         preCommands,
       },
     };
diff --git a/lib/manager/npm/post-update/pnpm.spec.ts b/lib/manager/npm/post-update/pnpm.spec.ts
index 6eedd09a0b174063a677e1fe689934d18347e7fb..0c2991a4019d61c2d52611e6a975dd415687d43f 100644
--- a/lib/manager/npm/post-update/pnpm.spec.ts
+++ b/lib/manager/npm/post-update/pnpm.spec.ts
@@ -9,6 +9,7 @@ import * as _pnpmHelper from './pnpm';
 jest.mock('fs-extra');
 jest.mock('child_process');
 jest.mock('../../../util/exec/env');
+jest.mock('./node-version');
 
 const exec: jest.Mock<typeof _exec> = _exec as any;
 const env = mocked(_env);
diff --git a/lib/manager/npm/post-update/pnpm.ts b/lib/manager/npm/post-update/pnpm.ts
index 9b1d5383fd58e4d0921db50812326648fbc6f47d..aba81ac352996f15dc072f7276fd8a01b8d95ffd 100644
--- a/lib/manager/npm/post-update/pnpm.ts
+++ b/lib/manager/npm/post-update/pnpm.ts
@@ -3,6 +3,7 @@ import { join } from 'upath';
 import { logger } from '../../../logger';
 import { ExecOptions, exec } from '../../../util/exec';
 import { PostUpdateConfig } from '../../common';
+import { getNodeConstraint } from './node-version';
 
 export interface GenerateLockFileResult {
   error?: boolean;
@@ -23,6 +24,7 @@ export async function generateLockFile(
   let cmd = 'pnpm';
   try {
     const preCommands = ['npm i -g pnpm'];
+    const tagConstraint = await getNodeConstraint(config.packageFile);
     const execOptions: ExecOptions = {
       cwd,
       extraEnv: {
@@ -31,6 +33,8 @@ export async function generateLockFile(
       },
       docker: {
         image: 'renovate/node',
+        tagScheme: 'npm',
+        tagConstraint,
         preCommands,
       },
     };
diff --git a/lib/manager/npm/post-update/yarn.spec.ts b/lib/manager/npm/post-update/yarn.spec.ts
index 09f50f5eaa994c2c8fb9ce9d8b7346e5fd1466b3..2ad39862c96e0b5ee2813e6704c29b175a1475ed 100644
--- a/lib/manager/npm/post-update/yarn.spec.ts
+++ b/lib/manager/npm/post-update/yarn.spec.ts
@@ -8,6 +8,7 @@ import * as _yarnHelper from './yarn';
 jest.mock('fs-extra');
 jest.mock('child_process');
 jest.mock('../../../util/exec/env');
+jest.mock('./node-version');
 
 const exec: jest.Mock<typeof _exec> = _exec as any;
 const env = mocked(_env);
diff --git a/lib/manager/npm/post-update/yarn.ts b/lib/manager/npm/post-update/yarn.ts
index f2f70b482fdafce2027eadb1a4ea4b47d5001496..8a5c71a0ea9f7967e55a3982223e693d04b9f1da 100644
--- a/lib/manager/npm/post-update/yarn.ts
+++ b/lib/manager/npm/post-update/yarn.ts
@@ -6,6 +6,7 @@ import { DatasourceError } from '../../../datasource';
 import { logger } from '../../../logger';
 import { ExecOptions, exec } from '../../../util/exec';
 import { PostUpdateConfig, Upgrade } from '../../common';
+import { getNodeConstraint } from './node-version';
 
 export interface GenerateLockFileResult {
   error?: boolean;
@@ -55,6 +56,7 @@ export async function generateLockFile(
     if (global.trustLevel !== 'high' || config.ignoreScripts) {
       cmdOptions += ' --ignore-scripts --ignore-engines --ignore-platform';
     }
+    const tagConstraint = await getNodeConstraint(config.packageFile);
     const execOptions: ExecOptions = {
       cwd,
       extraEnv: {
@@ -63,6 +65,8 @@ export async function generateLockFile(
       },
       docker: {
         image: 'renovate/node',
+        tagScheme: 'npm',
+        tagConstraint,
         preCommands,
       },
     };