diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md
index 294dd221287cb059b58d83bbe7862f36001354b4..d9f61c1ed5e8e5cb2a3aed3c5d185f1562b7531a 100644
--- a/docs/usage/self-hosted-configuration.md
+++ b/docs/usage/self-hosted-configuration.md
@@ -107,10 +107,21 @@ e.g.
 Renovate often needs to use third-party binaries in its PRs, e.g. `npm` to update `package-lock.json` or `go` to update `go.sum`.
 By default, Renovate will use a child process to run such tools, so they need to be pre-installed before running Renovate and available in the path.
 
-As an alternative, Renovate can use "sidecar" containers for third-party tools.
+Renovate can instead use "sidecar" containers for third-party tools when `binarySource=docker`.
 If configured, Renovate will use `docker run` to create containers such as Node.js or Python to run tools within as-needed.
 For this to work, `docker` needs to be installed and the Docker socket available to Renovate.
 
+Additionally, when Renovate is run inside a container built using [`containerbase/buildpack`](https://github.com/containerbase/buildpack), such as the official Renovate images on Docker Hub, then `binarySource=install` can be used.
+This mode means that Renovate will dynamically install the version of tools available, if supported.
+
+Supported tools for dynamic install are:
+
+- `composer`
+- `jb`
+- `npm`
+
+Unsupported tools will fall back to `binarySource=global`.
+
 ## cacheDir
 
 By default Renovate uses a temporary directory like `/tmp/renovate/cache` to store cache data.
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index 3f12e873009b0d2dfc49a67e1badb1510d143e12..84b34978c2f00ef70c942ff9151834b76c1a21cd 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -262,10 +262,10 @@ const options: RenovateOptions[] = [
   {
     name: 'binarySource',
     description:
-      'Controls whether third-party tools like npm or Gradle are called directly, or via Docker sidecar containers.',
+      'Controls whether third-party tools like npm or Gradle are called directly, via Docker sidecar containers, or dynamic install.',
     globalOnly: true,
     type: 'string',
-    allowedValues: ['global', 'docker'],
+    allowedValues: ['global', 'docker', 'install'],
     default: 'global',
   },
   {
diff --git a/lib/config/types.ts b/lib/config/types.ts
index c349a7066c016ae20ee295861b1c3d6d667b1fb9..cd4e2e32aef47d5c1a2a3bebdde75939811f81d7 100644
--- a/lib/config/types.ts
+++ b/lib/config/types.ts
@@ -96,7 +96,7 @@ export interface RepoGlobalConfig {
   allowPostUpgradeCommandTemplating?: boolean;
   allowScripts?: boolean;
   allowedPostUpgradeCommands?: string[];
-  binarySource?: 'docker' | 'global';
+  binarySource?: 'docker' | 'global' | 'install';
   customEnvVariables?: Record<string, string>;
   dockerChildPrefix?: string;
   dockerImagePrefix?: string;
diff --git a/lib/util/exec/buildpack.spec.ts b/lib/util/exec/buildpack.spec.ts
index 6cc85cde5bfcb232bdcd331d6064ba68cce380d5..479b0cbed4d32bd5b5dc8e8fedfd3e2da2e81d4f 100644
--- a/lib/util/exec/buildpack.spec.ts
+++ b/lib/util/exec/buildpack.spec.ts
@@ -1,6 +1,11 @@
 import { mocked } from '../../../test/util';
+import { GlobalConfig } from '../../config/global';
 import * as _datasource from '../../datasource';
-import { generateInstallCommands, resolveConstraint } from './buildpack';
+import {
+  generateInstallCommands,
+  isDynamicInstall,
+  resolveConstraint,
+} from './buildpack';
 import type { ToolConstraint } from './types';
 
 jest.mock('../../../lib/datasource');
@@ -8,6 +13,34 @@ jest.mock('../../../lib/datasource');
 const datasource = mocked(_datasource);
 
 describe('util/exec/buildpack', () => {
+  describe('isDynamicInstall()', () => {
+    beforeEach(() => {
+      GlobalConfig.reset();
+      delete process.env.BUILDPACK;
+    });
+    it('returns false if binarySource is not install', () => {
+      expect(isDynamicInstall()).toBeFalse();
+    });
+    it('returns false if not buildpack', () => {
+      GlobalConfig.set({ binarySource: 'install' });
+      expect(isDynamicInstall()).toBeFalse();
+    });
+    it('returns false if any unsupported tools', () => {
+      GlobalConfig.set({ binarySource: 'install' });
+      process.env.BUILDPACK = 'true';
+      const toolConstraints: ToolConstraint[] = [
+        { toolName: 'node' },
+        { toolName: 'npm' },
+      ];
+      expect(isDynamicInstall(toolConstraints)).toBeFalse();
+    });
+    it('returns false if supported tools', () => {
+      GlobalConfig.set({ binarySource: 'install' });
+      process.env.BUILDPACK = 'true';
+      const toolConstraints: ToolConstraint[] = [{ toolName: 'npm' }];
+      expect(isDynamicInstall(toolConstraints)).toBeTrue();
+    });
+  });
   describe('resolveConstraint()', () => {
     beforeEach(() => {
       datasource.getPkgReleases.mockResolvedValueOnce({
diff --git a/lib/util/exec/buildpack.ts b/lib/util/exec/buildpack.ts
index 75bae696d82c64690b0e09004889c463ffa38705..a056cd04c12af38862b520cbea21aaa7bf6a3228 100644
--- a/lib/util/exec/buildpack.ts
+++ b/lib/util/exec/buildpack.ts
@@ -1,4 +1,5 @@
 import { quote } from 'shlex';
+import { GlobalConfig } from '../../config/global';
 import { getPkgReleases } from '../../datasource';
 import { logger } from '../../logger';
 import * as allVersioning from '../../versioning';
@@ -26,6 +27,30 @@ const allToolConfig: Record<string, ToolConfig> = {
   },
 };
 
+export function supportsDynamicInstall(toolName: string): boolean {
+  return !!allToolConfig[toolName];
+}
+
+export function isBuildpack(): boolean {
+  return !!process.env.BUILDPACK;
+}
+
+export function isDynamicInstall(toolConstraints?: ToolConstraint[]): boolean {
+  const { binarySource } = GlobalConfig.get();
+  if (binarySource !== 'install') {
+    return false;
+  }
+  if (!isBuildpack()) {
+    logger.warn(
+      'binarySource=install is only compatible with images derived from containerbase/buildpack'
+    );
+    return false;
+  }
+  return !!toolConstraints?.every((toolConstraint) =>
+    supportsDynamicInstall(toolConstraint.toolName)
+  );
+}
+
 export async function resolveConstraint(
   toolConstraint: ToolConstraint
 ): Promise<string> {
diff --git a/lib/util/exec/index.spec.ts b/lib/util/exec/index.spec.ts
index 556ec654477136da61fedf48b612bfe709ed208e..6083ebac878960e7c911c0a292516f6eeb65ca57 100644
--- a/lib/util/exec/index.spec.ts
+++ b/lib/util/exec/index.spec.ts
@@ -13,6 +13,7 @@ import { exec } from '.';
 const cpExec: jest.Mock<typeof _cpExec> = _cpExec as any;
 
 jest.mock('child_process');
+jest.mock('../../../lib/datasource');
 
 interface TestInput {
   processEnv: Record<string, string>;
@@ -750,6 +751,16 @@ describe('util/exec/index', () => {
     // FIXME: explicit assert condition
     expect(actualCmd).toMatchSnapshot();
   });
+  it('Supports binarySource=install', async () => {
+    process.env = processEnv;
+    cpExec.mockImplementation(() => {
+      throw new Error('some error occurred');
+    });
+    GlobalConfig.set({ binarySource: 'install' });
+    process.env.BUILDPACK = 'true';
+    const promise = exec('foobar', { toolConstraints: [{ toolName: 'npm' }] });
+    await expect(promise).rejects.toThrow('No tool releases found.');
+  });
 
   it('only calls removeDockerContainer in catch block is useDocker is set', async () => {
     cpExec.mockImplementation(() => {
diff --git a/lib/util/exec/index.ts b/lib/util/exec/index.ts
index a04b9b710a0b633dfbb0cf98f343cdaa4b67df24..bc7b2078fa9f0db90619b67f5ee0167200257dda 100644
--- a/lib/util/exec/index.ts
+++ b/lib/util/exec/index.ts
@@ -2,7 +2,7 @@ import { dirname, join } from 'upath';
 import { GlobalConfig } from '../../config/global';
 import { TEMPORARY_ERROR } from '../../constants/error-messages';
 import { logger } from '../../logger';
-import { generateInstallCommands } from './buildpack';
+import { generateInstallCommands, isDynamicInstall } from './buildpack';
 import { rawExec } from './common';
 import { generateDockerCommand, removeDockerContainer } from './docker';
 import { getChildProcessEnv } from './env';
@@ -120,6 +120,12 @@ async function prepareRawExec(
       dockerOptions
     );
     rawCommands = [dockerCommand];
+  } else if (isDynamicInstall(opts.toolConstraints)) {
+    logger.debug('Using buildpack dynamic installs');
+    rawCommands = [
+      ...(await generateInstallCommands(opts.toolConstraints)),
+      ...rawCommands,
+    ];
   }
 
   return { rawCommands, rawOptions };
@@ -141,7 +147,7 @@ export async function exec(
     if (useDocker) {
       await removeDockerContainer(docker.image, dockerChildPrefix);
     }
-    logger.debug({ command: rawCommands }, 'Executing command');
+    logger.debug({ command: rawCmd }, 'Executing command');
     logger.trace({ commandOptions: rawOptions }, 'Command options');
     try {
       res = await rawExec(rawCmd, rawOptions);