diff --git a/lib/modules/manager/npm/extract/__snapshots__/monorepo.spec.ts.snap b/lib/modules/manager/npm/extract/__snapshots__/monorepo.spec.ts.snap
index 7c1028ec4471797060b18476daab598a717795c1..70e2789482f224259b8f20260cfafc68cab78017 100644
--- a/lib/modules/manager/npm/extract/__snapshots__/monorepo.spec.ts.snap
+++ b/lib/modules/manager/npm/extract/__snapshots__/monorepo.spec.ts.snap
@@ -42,11 +42,11 @@ exports[`modules/manager/npm/extract/monorepo .extractPackageFile() updates inte
     ],
     "managerData": {
       "hasPackageManager": undefined,
-      "hasWorkspaces": false,
       "lernaClient": undefined,
       "lernaJsonFile": "lerna.json",
       "npmLock": undefined,
       "packageJsonName": "@org/a",
+      "workspacesPackages": undefined,
       "yarnLock": undefined,
       "yarnZeroInstall": undefined,
     },
@@ -57,11 +57,11 @@ exports[`modules/manager/npm/extract/monorepo .extractPackageFile() updates inte
   {
     "managerData": {
       "hasPackageManager": undefined,
-      "hasWorkspaces": false,
       "lernaClient": undefined,
       "lernaJsonFile": "lerna.json",
       "npmLock": undefined,
       "packageJsonName": "@org/b",
+      "workspacesPackages": undefined,
       "yarnLock": undefined,
       "yarnZeroInstall": undefined,
     },
@@ -114,11 +114,11 @@ exports[`modules/manager/npm/extract/monorepo .extractPackageFile() uses lerna p
     ],
     "managerData": {
       "hasPackageManager": undefined,
-      "hasWorkspaces": false,
       "lernaClient": undefined,
       "lernaJsonFile": "lerna.json",
       "npmLock": undefined,
       "packageJsonName": "@org/a",
+      "workspacesPackages": undefined,
       "yarnLock": undefined,
       "yarnZeroInstall": undefined,
     },
@@ -129,11 +129,11 @@ exports[`modules/manager/npm/extract/monorepo .extractPackageFile() uses lerna p
   {
     "managerData": {
       "hasPackageManager": undefined,
-      "hasWorkspaces": false,
       "lernaClient": undefined,
       "lernaJsonFile": "lerna.json",
       "npmLock": undefined,
       "packageJsonName": "@org/b",
+      "workspacesPackages": undefined,
       "yarnLock": undefined,
       "yarnZeroInstall": undefined,
     },
@@ -162,11 +162,13 @@ exports[`modules/manager/npm/extract/monorepo .extractPackageFile() uses yarn wo
   {
     "managerData": {
       "hasPackageManager": undefined,
-      "hasWorkspaces": true,
       "lernaClient": "yarn",
       "lernaJsonFile": "lerna.json",
       "npmLock": undefined,
       "packageJsonName": "@org/a",
+      "workspacesPackages": [
+        "packages/*",
+      ],
       "yarnLock": undefined,
       "yarnZeroInstall": undefined,
     },
@@ -177,11 +179,13 @@ exports[`modules/manager/npm/extract/monorepo .extractPackageFile() uses yarn wo
   {
     "managerData": {
       "hasPackageManager": undefined,
-      "hasWorkspaces": true,
       "lernaClient": "yarn",
       "lernaJsonFile": "lerna.json",
       "npmLock": undefined,
       "packageJsonName": "@org/b",
+      "workspacesPackages": [
+        "packages/*",
+      ],
       "yarnLock": undefined,
       "yarnZeroInstall": undefined,
     },
diff --git a/lib/modules/manager/npm/extract/monorepo.ts b/lib/modules/manager/npm/extract/monorepo.ts
index 2f3c2cc3c4a54453b7ba2a1a0bd9bb1d8278ad3a..d296c28af8d8468350650c2a65f6ca867508a76e 100644
--- a/lib/modules/manager/npm/extract/monorepo.ts
+++ b/lib/modules/manager/npm/extract/monorepo.ts
@@ -59,7 +59,7 @@ export async function detectMonorepos(
         subPackage.managerData.yarnLock ??= yarnLock;
         subPackage.managerData.npmLock ??= npmLock;
         subPackage.skipInstalls = skipInstalls && subPackage.skipInstalls; // skip if both are true
-        subPackage.managerData.hasWorkspaces = !!workspacesPackages;
+        subPackage.managerData.workspacesPackages = workspacesPackages;
         subPackage.npmrc ??= npmrc;
 
         if (p.extractedConstraints) {
diff --git a/lib/modules/manager/npm/post-update/index.ts b/lib/modules/manager/npm/post-update/index.ts
index 9bb9e793f9ee4cd52a4e0185ddbe4b1dce9337ec..5fa20722a8b18bfa07404126c6f80b3e46c12307 100644
--- a/lib/modules/manager/npm/post-update/index.ts
+++ b/lib/modules/manager/npm/post-update/index.ts
@@ -119,7 +119,7 @@ export function determineLockFileDirs(
     } else if (
       packageFile.managerData?.lernaJsonFile &&
       packageFile.managerData.yarnLock &&
-      !packageFile.managerData.hasWorkspaces
+      !packageFile.managerData.workspacesPackages?.length
     ) {
       lernaJsonFiles.push(packageFile.managerData.lernaJsonFile);
     } else {
diff --git a/lib/modules/manager/npm/post-update/npm.spec.ts b/lib/modules/manager/npm/post-update/npm.spec.ts
index 3ecda1373e87a034c10df9d1b9e850883dc6a098..42d4851b9781e686735cf7f14b229b39cf1b7876 100644
--- a/lib/modules/manager/npm/post-update/npm.spec.ts
+++ b/lib/modules/manager/npm/post-update/npm.spec.ts
@@ -30,7 +30,7 @@ describe('modules/manager/npm/post-update/npm', () => {
     const skipInstalls = true;
     const postUpdateOptions = ['npmDedupe'];
     const updates = [
-      { depName: 'some-dep', newVersion: '1.0.1', isLockfileUpdate: false },
+      { packageName: 'some-dep', newVersion: '1.0.1', isLockfileUpdate: false },
     ];
     const res = await npmHelper.generateLockFile(
       'some-dir',
@@ -50,7 +50,7 @@ describe('modules/manager/npm/post-update/npm', () => {
     fs.readLocalFile.mockResolvedValueOnce('package-lock-contents');
     const skipInstalls = true;
     const updates = [
-      { depName: 'some-dep', newVersion: '1.0.1', isLockfileUpdate: true },
+      { packageName: 'some-dep', newVersion: '1.0.1', isLockfileUpdate: true },
     ];
     const res = await npmHelper.generateLockFile(
       'some-dir',
@@ -73,11 +73,12 @@ describe('modules/manager/npm/post-update/npm', () => {
     const skipInstalls = true;
     const updates = [
       {
-        depName: 'postcss',
+        packageName: 'postcss',
         depType: 'dependencies',
         newVersion: '8.4.8',
         newValue: '^8.0.0',
         isLockfileUpdate: true,
+        managerData: {}, // intentional: edge-case test for workspaces
       },
     ];
     const res = await npmHelper.generateLockFile(
@@ -307,4 +308,261 @@ describe('modules/manager/npm/post-update/npm', () => {
       },
     ]);
   });
+
+  describe('installs workspace only packages separately', () => {
+    const updates = [
+      {
+        packageFile: 'some-dir/docs/a/package.json',
+        packageName: 'abbrev',
+        depType: 'dependencies',
+        newVersion: '1.1.0',
+        newValue: '^1.0.0',
+        isLockfileUpdate: true,
+        managerData: {
+          workspacesPackages: ['docs/*', 'web/*'],
+        },
+      },
+      {
+        packageFile: 'some-dir/web/b/package.json',
+        packageName: 'xmldoc',
+        depType: 'dependencies',
+        newVersion: '2.2.0',
+        newValue: '^2.0.0',
+        isLockfileUpdate: true,
+        managerData: {
+          workspacesPackages: ['docs/*', 'web/*'],
+        },
+      },
+      {
+        packageFile: 'some-dir/docs/a/package.json',
+        packageName: 'postcss',
+        depType: 'dependencies',
+        newVersion: '8.4.8',
+        newValue: '^8.0.0',
+        isLockfileUpdate: true,
+        managerData: {
+          workspacesPackages: ['docs/*', 'web/*'],
+        },
+      },
+      {
+        packageFile: 'some-dir/package.json',
+        packageName: 'chalk',
+        depType: 'dependencies',
+        newVersion: '9.4.8',
+        newValue: '^9.0.0',
+        isLockfileUpdate: true,
+        managerData: {
+          workspacesPackages: ['docs/*', 'web/*'],
+        },
+      },
+      {
+        packageFile: 'some-dir/web/b/package.json',
+        packageName: 'postcss',
+        depType: 'dependencies',
+        newVersion: '8.4.8',
+        newValue: '^8.0.0',
+        isLockfileUpdate: true,
+        managerData: {
+          workspacesPackages: ['docs/*', 'web/*'],
+        },
+      },
+      {
+        packageFile: 'some-dir/package.json',
+        packageName: 'postcss',
+        depType: 'dependencies',
+        newVersion: '8.4.8',
+        newValue: '^8.0.0',
+        isLockfileUpdate: true,
+        managerData: {
+          workspacesPackages: ['docs/*', 'web/*'],
+        },
+      },
+      {
+        packageFile: 'some-dir/web/b/package.json',
+        packageName: 'hello',
+        depType: 'dependencies',
+        newVersion: '1.1.1',
+        newValue: '^1.0.0',
+        isLockfileUpdate: true,
+        managerData: {
+          workspacesPackages: ['docs/*', 'web/*'],
+        },
+      },
+      {
+        packageFile: 'some-dir/docs/a/package.json',
+        packageName: 'hello',
+        depType: 'dependencies',
+        newVersion: '1.1.1',
+        newValue: '^1.0.0',
+        isLockfileUpdate: true,
+        managerData: {
+          workspacesPackages: ['docs/*', 'web/*'],
+        },
+      },
+    ];
+
+    it('workspace in sub-folder', async () => {
+      const execSnapshots = mockExecAll();
+      fs.readLocalFile.mockResolvedValueOnce('package-lock content');
+      const skipInstalls = true;
+      const res = await npmHelper.generateLockFile(
+        'some-dir',
+        {},
+        'package-lock.json',
+        { skipInstalls },
+        updates
+      );
+      expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
+      expect(res.error).toBeUndefined();
+      expect(execSnapshots).toMatchObject([
+        {
+          cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --workspace=docs/a abbrev@1.1.0 hello@1.1.1',
+        },
+        {
+          cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --workspace=web/b xmldoc@2.2.0 hello@1.1.1',
+        },
+
+        {
+          cmd: 'npm install --package-lock-only --no-audit --ignore-scripts chalk@9.4.8 postcss@8.4.8',
+        },
+      ]);
+    });
+
+    it('workspace in root folder', async () => {
+      const modifiedUpdates = updates.map((update) => {
+        return {
+          ...update,
+          packageFile: update.packageFile.replace('some-dir/', ''),
+        };
+      });
+      const execSnapshots = mockExecAll();
+      fs.readLocalFile.mockResolvedValueOnce('package-lock content');
+      const skipInstalls = true;
+      const res = await npmHelper.generateLockFile(
+        '.',
+        {},
+        'package-lock.json',
+        { skipInstalls },
+        modifiedUpdates
+      );
+      expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
+      expect(res.error).toBeUndefined();
+      expect(execSnapshots).toMatchObject([
+        {
+          cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --workspace=docs/a abbrev@1.1.0 hello@1.1.1',
+        },
+        {
+          cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --workspace=web/b xmldoc@2.2.0 hello@1.1.1',
+        },
+
+        {
+          cmd: 'npm install --package-lock-only --no-audit --ignore-scripts chalk@9.4.8 postcss@8.4.8',
+        },
+      ]);
+      expect(
+        npmHelper.divideWorkspaceAndRootDeps('.', modifiedUpdates)
+      ).toMatchObject({
+        lockRootUpdates: [
+          {
+            packageFile: 'package.json',
+            packageName: 'chalk',
+            depType: 'dependencies',
+            newVersion: '9.4.8',
+            newValue: '^9.0.0',
+            isLockfileUpdate: true,
+            managerData: {
+              workspacesPackages: ['docs/*', 'web/*'],
+            },
+          },
+          {
+            packageFile: 'package.json',
+            packageName: 'postcss',
+            depType: 'dependencies',
+            newVersion: '8.4.8',
+            newValue: '^8.0.0',
+            isLockfileUpdate: true,
+            managerData: {
+              workspacesPackages: ['docs/*', 'web/*'],
+            },
+          },
+        ],
+        lockWorkspacesUpdates: [
+          {
+            packageFile: 'docs/a/package.json',
+            packageName: 'abbrev',
+            depType: 'dependencies',
+            newVersion: '1.1.0',
+            newValue: '^1.0.0',
+            isLockfileUpdate: true,
+            managerData: {
+              workspacesPackages: ['docs/*', 'web/*'],
+            },
+            workspace: 'docs/a',
+          },
+          {
+            packageFile: 'web/b/package.json',
+            packageName: 'xmldoc',
+            depType: 'dependencies',
+            newVersion: '2.2.0',
+            newValue: '^2.0.0',
+            isLockfileUpdate: true,
+            managerData: {
+              workspacesPackages: ['docs/*', 'web/*'],
+            },
+            workspace: 'web/b',
+          },
+          {
+            packageFile: 'docs/a/package.json',
+            packageName: 'postcss',
+            depType: 'dependencies',
+            newVersion: '8.4.8',
+            newValue: '^8.0.0',
+            isLockfileUpdate: true,
+            managerData: {
+              workspacesPackages: ['docs/*', 'web/*'],
+            },
+            workspace: 'docs/a',
+          },
+          {
+            packageFile: 'web/b/package.json',
+            packageName: 'postcss',
+            depType: 'dependencies',
+            newVersion: '8.4.8',
+            newValue: '^8.0.0',
+            isLockfileUpdate: true,
+            managerData: {
+              workspacesPackages: ['docs/*', 'web/*'],
+            },
+            workspace: 'web/b',
+          },
+          {
+            packageFile: 'web/b/package.json',
+            packageName: 'hello',
+            depType: 'dependencies',
+            newVersion: '1.1.1',
+            newValue: '^1.0.0',
+            isLockfileUpdate: true,
+            managerData: {
+              workspacesPackages: ['docs/*', 'web/*'],
+            },
+            workspace: 'web/b',
+          },
+          {
+            packageFile: 'docs/a/package.json',
+            packageName: 'hello',
+            depType: 'dependencies',
+            newVersion: '1.1.1',
+            newValue: '^1.0.0',
+            isLockfileUpdate: true,
+            managerData: {
+              workspacesPackages: ['docs/*', 'web/*'],
+            },
+            workspace: 'docs/a',
+          },
+        ],
+        workspaces: new Set(['docs/a', 'web/b']),
+        rootDeps: new Set(['chalk@9.4.8', 'postcss@8.4.8']),
+      });
+    });
+  });
 });
diff --git a/lib/modules/manager/npm/post-update/npm.ts b/lib/modules/manager/npm/post-update/npm.ts
index f53bf63a0bea34b89dfd444fde375e8acba8911e..fa41fdad02601a577d56a9f40ce2ed50184ec2bd 100644
--- a/lib/modules/manager/npm/post-update/npm.ts
+++ b/lib/modules/manager/npm/post-update/npm.ts
@@ -1,3 +1,6 @@
+// TODO: types (#7154)
+import is from '@sindresorhus/is';
+import minimatch from 'minimatch';
 import upath from 'upath';
 import { GlobalConfig } from '../../../../config/global';
 import {
@@ -17,6 +20,7 @@ import {
   readLocalFile,
   renameLocalFile,
 } from '../../../../util/fs';
+import { trimSlashes } from '../../../../util/url';
 import type { PostUpdateConfig, Upgrade } from '../../types';
 import { composeLockFile, parseLockFile } from '../utils';
 import { getNodeToolConstraint } from './node-version';
@@ -81,15 +85,35 @@ export async function generateLockFile(
 
     // rangeStrategy = update-lockfile
     const lockUpdates = upgrades.filter((upgrade) => upgrade.isLockfileUpdate);
-    if (lockUpdates.length) {
+
+    // divide the deps in two categories: workspace and root
+    const { lockRootUpdates, lockWorkspacesUpdates, workspaces, rootDeps } =
+      divideWorkspaceAndRootDeps(lockFileDir, lockUpdates);
+
+    if (workspaces.size && lockWorkspacesUpdates.length) {
+      logger.debug('Performing lockfileUpdate (npm-workspaces)');
+      for (const workspace of workspaces) {
+        const currentWorkspaceUpdates = lockWorkspacesUpdates
+          .filter((update) => update.workspace === workspace)
+          .map((update) => update.managerData?.packageKey)
+          .filter((packageKey) => !rootDeps.has(packageKey));
+
+        if (currentWorkspaceUpdates.length) {
+          const updateCmd = `npm install ${cmdOptions} --workspace=${workspace} ${currentWorkspaceUpdates.join(
+            ' '
+          )}`;
+          commands.push(updateCmd);
+        }
+      }
+    }
+
+    if (lockRootUpdates.length) {
       logger.debug('Performing lockfileUpdate (npm)');
       const updateCmd =
-        `npm install ${cmdOptions}` +
-        lockUpdates
-          // TODO: types (#7154)
-          // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-          .map((update) => ` ${update.depName}@${update.newVersion}`)
-          .join('');
+        `npm install ${cmdOptions} ` +
+        lockRootUpdates
+          .map((update) => update.managerData?.packageKey)
+          .join(' ');
       commands.push(updateCmd);
     }
 
@@ -150,8 +174,10 @@ export async function generateLockFile(
             | 'optionalDependencies';
 
           // TODO #7154
-          if (lockFileParsed.packages?.['']?.[depType]?.[lockUpdate.depName!]) {
-            lockFileParsed.packages[''][depType]![lockUpdate.depName!] =
+          if (
+            lockFileParsed.packages?.['']?.[depType]?.[lockUpdate.packageName!]
+          ) {
+            lockFileParsed.packages[''][depType]![lockUpdate.packageName!] =
               lockUpdate.newValue!;
           }
         });
@@ -176,3 +202,69 @@ export async function generateLockFile(
   }
   return { lockFile };
 }
+
+export function divideWorkspaceAndRootDeps(
+  lockFileDir: string,
+  lockUpdates: Upgrade[]
+): {
+  lockRootUpdates: Upgrade[];
+  lockWorkspacesUpdates: Upgrade[];
+  workspaces: Set<string>;
+  rootDeps: Set<string>;
+} {
+  const lockRootUpdates: Upgrade[] = []; // stores all upgrades which are present in root package.json
+  const lockWorkspacesUpdates: Upgrade[] = []; // stores all upgrades which are present in workspaces package.json
+  const workspaces = new Set<string>(); // name of all workspaces
+  const rootDeps = new Set<string>(); // packageName of all upgrades in root package.json (makes it check duplicate deps in root)
+
+  // divide the deps in two categories: workspace and root
+  for (const upgrade of lockUpdates) {
+    upgrade.managerData ??= {};
+    upgrade.managerData.packageKey = generatePackageKey(
+      upgrade.packageName!,
+      upgrade.newVersion!
+    );
+    if (
+      upgrade.managerData.workspacesPackages?.length &&
+      is.string(upgrade.packageFile)
+    ) {
+      const workspacePatterns = upgrade.managerData.workspacesPackages; // glob pattern or directory name/path
+      const packageFileDir = trimSlashes(
+        upgrade.packageFile.replace('package.json', '')
+      );
+
+      // workspaceDir = packageFileDir - lockFileDir
+      const workspaceDir = trimSlashes(packageFileDir.replace(lockFileDir, ''));
+
+      if (is.nonEmptyString(workspaceDir)) {
+        let workspaceName: string | undefined;
+        // compare workspaceDir to workspace patterns
+        // stop when the first match is found and
+        // add workspaceDir to workspaces set and upgrade object
+        for (const workspacePattern of workspacePatterns ?? []) {
+          if (minimatch(workspaceDir, workspacePattern)) {
+            workspaceName = workspaceDir;
+            break;
+          }
+        }
+        if (
+          workspaceName &&
+          !rootDeps.has(upgrade.managerData.packageKey) // prevent same dep from existing in root and workspace
+        ) {
+          workspaces.add(workspaceName);
+          upgrade.workspace = workspaceName;
+          lockWorkspacesUpdates.push(upgrade);
+        }
+        continue;
+      }
+    }
+    lockRootUpdates.push(upgrade);
+    rootDeps.add(upgrade.managerData.packageKey);
+  }
+
+  return { lockRootUpdates, lockWorkspacesUpdates, workspaces, rootDeps };
+}
+
+function generatePackageKey(packageName: string, version: string): string {
+  return `${packageName}@${version}`;
+}
diff --git a/lib/modules/manager/npm/types.ts b/lib/modules/manager/npm/types.ts
index 9fa430cd8d74307724705801a0de42adb674832d..526afafbf17382550f53358f8c14acb92c29b797 100644
--- a/lib/modules/manager/npm/types.ts
+++ b/lib/modules/manager/npm/types.ts
@@ -81,7 +81,6 @@ export interface NpmLockFiles {
 
 export interface NpmManagerData extends NpmLockFiles, Record<string, any> {
   hasPackageManager?: boolean;
-  hasWorkspaces?: boolean;
   lernaClient?: string;
   lernaJsonFile?: string;
   lernaPackages?: string[];
diff --git a/lib/modules/manager/types.ts b/lib/modules/manager/types.ts
index c81b729d1c8470c5a09e2a22a3419d711339df61..6e26451238eff879ac66fd878d5eb5670212be88 100644
--- a/lib/modules/manager/types.ts
+++ b/lib/modules/manager/types.ts
@@ -155,6 +155,7 @@ export interface PackageDependency<T = Record<string, any>>
 }
 
 export interface Upgrade<T = Record<string, any>> extends PackageDependency<T> {
+  workspace?: string;
   isLockfileUpdate?: boolean;
   currentRawValue?: any;
   depGroup?: string;
diff --git a/lib/util/url.spec.ts b/lib/util/url.spec.ts
index 27652dc9c515f648b06ab308e9ffea5781c8a76e..b9a5b723a5be62a2e8bcec35a93ddbe5b0821c81 100644
--- a/lib/util/url.spec.ts
+++ b/lib/util/url.spec.ts
@@ -8,6 +8,7 @@ import {
   parseUrl,
   replaceUrlPath,
   resolveBaseUrl,
+  trimSlashes,
   trimTrailingSlash,
   validateUrl,
 } from './url';
@@ -122,6 +123,17 @@ describe('util/url', () => {
     expect(trimTrailingSlash('foo//////')).toBe('foo');
   });
 
+  it('trimSlashes', () => {
+    expect(trimSlashes('foo')).toBe('foo');
+    expect(trimSlashes('/foo')).toBe('foo');
+    expect(trimSlashes('foo/')).toBe('foo');
+    expect(trimSlashes('//////foo//////')).toBe('foo');
+    expect(trimSlashes('foo/bar')).toBe('foo/bar');
+    expect(trimSlashes('/foo/bar')).toBe('foo/bar');
+    expect(trimSlashes('foo/bar/')).toBe('foo/bar');
+    expect(trimSlashes('/foo/bar/')).toBe('foo/bar');
+  });
+
   it('ensureTrailingSlash', () => {
     expect(ensureTrailingSlash('')).toBe('/');
     expect(ensureTrailingSlash('/')).toBe('/');
diff --git a/lib/util/url.ts b/lib/util/url.ts
index 7d4c1551d819d2a6b676d1c3c87ecd4784497c7d..a082ba8aebdb87e1e19e3e7c975ef961df180527 100644
--- a/lib/util/url.ts
+++ b/lib/util/url.ts
@@ -30,6 +30,10 @@ export function trimLeadingSlash(path: string): string {
   return path.replace(/^\/+/, '');
 }
 
+export function trimSlashes(path: string): string {
+  return trimLeadingSlash(trimTrailingSlash(path));
+}
+
 /**
  * Resolves an input path against a base URL
  *