diff --git a/docs/usage/config-presets.md b/docs/usage/config-presets.md
index 867aac1a4e067dc6fc9e2672c0f22237d519db28..abb41cfbe19520e1e2a98318cb217b7da54996f1 100644
--- a/docs/usage/config-presets.md
+++ b/docs/usage/config-presets.md
@@ -202,7 +202,7 @@ To host your preset config on Gitea:
 
 Renovate also supports local presets, e.g. presets that are hosted on the same platform as the target repository.
 This is especially helpful in self-hosted scenarios where public presets cannot be used.
-Local presets are only supported on GitHub, GitLab, Gitea and Bitbucket Server.
+Local presets are only supported on GitHub, GitLab, Gitea, Bitbucket Cloud and Bitbucket Server.
 Local presets are specified either by leaving out any prefix, e.g. `owner/name`, or explicitly by adding a `local>` prefix, e.g. `local>owner/name`.
 Renovate will determine the current platform and look up the preset from there.
 
diff --git a/lib/config/presets/bitbucket/__snapshots__/index.spec.ts.snap b/lib/config/presets/bitbucket/__snapshots__/index.spec.ts.snap
new file mode 100644
index 0000000000000000000000000000000000000000..ec37a0babb73f154f9b1b7e00601d4f6a2d9cf6d
--- /dev/null
+++ b/lib/config/presets/bitbucket/__snapshots__/index.spec.ts.snap
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`config/presets/bitbucket/index fetchJSONFile() returns JSON 1`] = `
+Array [
+  Object {
+    "headers": Object {
+      "accept-encoding": "gzip, deflate",
+      "host": "api.bitbucket.org",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "GET",
+    "url": "https://api.bitbucket.org/2.0/repositories/some/repo/src/HEAD/some-filename.json",
+  },
+]
+`;
+
+exports[`config/presets/bitbucket/index fetchJSONFile() throws on error 1`] = `
+Array [
+  Object {
+    "headers": Object {
+      "accept-encoding": "gzip, deflate",
+      "host": "api.bitbucket.org",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "GET",
+    "url": "https://api.bitbucket.org/2.0/repositories/some/repo/src/HEAD/some-filename.json",
+  },
+]
+`;
+
+exports[`config/presets/bitbucket/index fetchJSONFile() throws on invalid json 1`] = `
+Array [
+  Object {
+    "headers": Object {
+      "accept-encoding": "gzip, deflate",
+      "host": "api.bitbucket.org",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "GET",
+    "url": "https://api.bitbucket.org/2.0/repositories/some/repo/src/HEAD/some-filename.json",
+  },
+]
+`;
diff --git a/lib/config/presets/bitbucket/index.spec.ts b/lib/config/presets/bitbucket/index.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f1f97b45d82fc7ca4ab76c362791d1b43c2e2140
--- /dev/null
+++ b/lib/config/presets/bitbucket/index.spec.ts
@@ -0,0 +1,77 @@
+import * as httpMock from '../../../../test/http-mock';
+import { getName } from '../../../../test/util';
+import { setPlatformApi } from '../../../platform';
+import { PRESET_DEP_NOT_FOUND, PRESET_INVALID_JSON } from '../util';
+import * as bitbucket from '.';
+
+jest.unmock('../../../platform');
+
+const baseUrl = 'https://api.bitbucket.org';
+const basePath = '/2.0/repositories/some/repo/src/HEAD';
+
+describe(getName(__filename), () => {
+  beforeAll(() => {
+    setPlatformApi('bitbucket');
+  });
+
+  beforeEach(() => {
+    httpMock.setup();
+  });
+
+  afterEach(() => {
+    httpMock.reset();
+  });
+
+  describe('fetchJSONFile()', () => {
+    it('returns JSON', async () => {
+      const data = { foo: 'bar' };
+      httpMock
+        .scope(baseUrl)
+        .get(`${basePath}/some-filename.json`)
+        .reply(200, JSON.stringify(data));
+
+      const res = await bitbucket.fetchJSONFile(
+        'some/repo',
+        'some-filename.json'
+      );
+      expect(res).toEqual(data);
+      expect(httpMock.getTrace()).toMatchSnapshot();
+    });
+
+    it('throws on error', async () => {
+      httpMock.scope(baseUrl).get(`${basePath}/some-filename.json`).reply(404);
+      await expect(
+        bitbucket.fetchJSONFile('some/repo', 'some-filename.json')
+      ).rejects.toThrow(PRESET_DEP_NOT_FOUND);
+      expect(httpMock.getTrace()).toMatchSnapshot();
+    });
+
+    it('throws on invalid json', async () => {
+      httpMock
+        .scope(baseUrl)
+        .get(`${basePath}/some-filename.json`)
+        .reply(200, '!@#');
+      await expect(
+        bitbucket.fetchJSONFile('some/repo', 'some-filename.json')
+      ).rejects.toThrow(PRESET_INVALID_JSON);
+      expect(httpMock.getTrace()).toMatchSnapshot();
+    });
+  });
+
+  describe('getPresetFromEndpoint()', () => {
+    it('uses custom path', async () => {
+      const data = { foo: 'bar' };
+      httpMock
+        .scope(baseUrl)
+        .get(`${basePath}/foo/bar/some-filename.json`)
+        .reply(200, JSON.stringify(data));
+      const res = await bitbucket.getPresetFromEndpoint(
+        'some/repo',
+        'some-filename',
+        'foo/bar',
+        baseUrl
+      );
+      expect(res).toEqual(data);
+    });
+  });
+});
diff --git a/lib/config/presets/bitbucket/index.ts b/lib/config/presets/bitbucket/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..683c271510a3ad08fa6fedb453129e14d1306a22
--- /dev/null
+++ b/lib/config/presets/bitbucket/index.ts
@@ -0,0 +1 @@
+export { fetchJSONFile, getPresetFromEndpoint } from '../local/common';
diff --git a/lib/config/presets/local/__snapshots__/index.spec.ts.snap b/lib/config/presets/local/__snapshots__/index.spec.ts.snap
index 09f1a6658ba302fc99a1b8e69d2a3a8210356539..83a107844f3de628c6577875f71979839f3f1d62 100644
--- a/lib/config/presets/local/__snapshots__/index.spec.ts.snap
+++ b/lib/config/presets/local/__snapshots__/index.spec.ts.snap
@@ -1,5 +1,22 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`config/presets/local/index getPreset() forwards to bitbucket 1`] = `
+Array [
+  Array [
+    "some/repo",
+    "default",
+    undefined,
+    undefined,
+  ],
+]
+`;
+
+exports[`config/presets/local/index getPreset() forwards to bitbucket 2`] = `
+Object {
+  "resolved": "preset",
+}
+`;
+
 exports[`config/presets/local/index getPreset() forwards to custom bitbucket-server 1`] = `
 Array [
   Array [
diff --git a/lib/config/presets/local/common.ts b/lib/config/presets/local/common.ts
new file mode 100644
index 0000000000000000000000000000000000000000..457ccb3851ecd31bff250a017a6c23e8efd3692c
--- /dev/null
+++ b/lib/config/presets/local/common.ts
@@ -0,0 +1,53 @@
+import { logger } from '../../../logger';
+import { platform } from '../../../platform';
+import { ExternalHostError } from '../../../types/errors/external-host-error';
+import type { Preset } from '../types';
+import {
+  PRESET_DEP_NOT_FOUND,
+  PRESET_INVALID_JSON,
+  fetchPreset,
+} from '../util';
+
+export async function fetchJSONFile(
+  repo: string,
+  fileName: string,
+  _endpoint: string = null
+): Promise<Preset> {
+  let raw: string;
+  try {
+    raw = await platform.getRawFile(fileName, repo);
+  } catch (err) {
+    // istanbul ignore if: not testable with nock
+    if (err instanceof ExternalHostError) {
+      throw err;
+    }
+
+    logger.debug(
+      { err, repo, fileName },
+      `Failed to retrieve ${fileName} from repo ${repo}`
+    );
+
+    throw new Error(PRESET_DEP_NOT_FOUND);
+  }
+
+  try {
+    return JSON.parse(raw);
+  } catch (err) {
+    throw new Error(PRESET_INVALID_JSON);
+  }
+}
+
+export function getPresetFromEndpoint(
+  pkgName: string,
+  filePreset: string,
+  presetPath: string,
+  endpoint: string
+): Promise<Preset> {
+  return fetchPreset({
+    pkgName,
+    filePreset,
+    presetPath,
+    endpoint,
+    fetch: fetchJSONFile,
+  });
+}
diff --git a/lib/config/presets/local/index.spec.ts b/lib/config/presets/local/index.spec.ts
index 6bc484566c2ebcbaa2fdd73f4b47d1a23f400aec..0d72cea5e996c564079871e4fa97d4d6a2e81fd1 100644
--- a/lib/config/presets/local/index.spec.ts
+++ b/lib/config/presets/local/index.spec.ts
@@ -1,15 +1,18 @@
 import { getName, mocked } from '../../../../test/util';
+import * as _bitbucket from '../bitbucket';
 import * as _bitbucketServer from '../bitbucket-server';
 import * as _gitea from '../gitea';
 import * as _github from '../github';
 import * as _gitlab from '../gitlab';
 import * as local from '.';
 
+jest.mock('../bitbucket');
 jest.mock('../bitbucket-server');
 jest.mock('../gitea');
 jest.mock('../github');
 jest.mock('../gitlab');
 
+const bitbucket = mocked(_bitbucket);
 const bitbucketServer = mocked(_bitbucketServer);
 const gitea = mocked(_gitea);
 const github = mocked(_github);
@@ -19,6 +22,7 @@ describe(getName(__filename), () => {
   beforeEach(() => {
     jest.resetAllMocks();
     const preset = { resolved: 'preset' };
+    bitbucket.getPresetFromEndpoint.mockResolvedValueOnce(preset);
     bitbucketServer.getPresetFromEndpoint.mockResolvedValueOnce(preset);
     gitea.getPresetFromEndpoint.mockResolvedValueOnce(preset);
     github.getPresetFromEndpoint.mockResolvedValueOnce(preset);
@@ -48,6 +52,18 @@ describe(getName(__filename), () => {
       }).rejects.toThrow();
     });
 
+    it('forwards to bitbucket', async () => {
+      const content = await local.getPreset({
+        packageName: 'some/repo',
+        presetName: 'default',
+        baseConfig: {
+          platform: 'bitbucket',
+        },
+      });
+      expect(bitbucket.getPresetFromEndpoint.mock.calls).toMatchSnapshot();
+      expect(content).toMatchSnapshot();
+    });
+
     it('forwards to custom bitbucket-server', async () => {
       const content = await local.getPreset({
         packageName: 'some/repo',
diff --git a/lib/config/presets/local/index.ts b/lib/config/presets/local/index.ts
index c53ae3d1b98ccef251e253ca30c4530a34587288..87f5822f2bfcc9b6a624a8a945c21f377cd20b81 100644
--- a/lib/config/presets/local/index.ts
+++ b/lib/config/presets/local/index.ts
@@ -1,9 +1,11 @@
 import {
+  PLATFORM_TYPE_BITBUCKET,
   PLATFORM_TYPE_BITBUCKET_SERVER,
   PLATFORM_TYPE_GITEA,
   PLATFORM_TYPE_GITHUB,
   PLATFORM_TYPE_GITLAB,
 } from '../../../constants/platforms';
+import * as bitbucket from '../bitbucket';
 import * as bitbucketServer from '../bitbucket-server';
 import * as gitea from '../gitea';
 import * as github from '../github';
@@ -11,6 +13,7 @@ import * as gitlab from '../gitlab';
 import type { Preset, PresetConfig } from '../types';
 
 const resolvers = {
+  [PLATFORM_TYPE_BITBUCKET]: bitbucket,
   [PLATFORM_TYPE_BITBUCKET_SERVER]: bitbucketServer,
   [PLATFORM_TYPE_GITEA]: gitea,
   [PLATFORM_TYPE_GITHUB]: github,