diff --git a/docs/usage/config-presets.md b/docs/usage/config-presets.md
index abb41cfbe19520e1e2a98318cb217b7da54996f1..aab57a90a7db38e7848fe1f471f2f1f6137bc591 100644
--- a/docs/usage/config-presets.md
+++ b/docs/usage/config-presets.md
@@ -202,7 +202,6 @@ 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, 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/azure/__snapshots__/index.spec.ts.snap b/lib/config/presets/azure/__snapshots__/index.spec.ts.snap
new file mode 100644
index 0000000000000000000000000000000000000000..c9758c77a590f428bbe0c93f90ce73d64ac07364
--- /dev/null
+++ b/lib/config/presets/azure/__snapshots__/index.spec.ts.snap
@@ -0,0 +1,10 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`config/presets/azure/index fetchJSONFile() returns JSON 1`] = `
+Array [
+  Array [
+    "123456",
+    "some-filename.json",
+  ],
+]
+`;
diff --git a/lib/config/presets/azure/index.spec.ts b/lib/config/presets/azure/index.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..03430d19e06f23f36cb73e0a71d7c0a92e5a9628
--- /dev/null
+++ b/lib/config/presets/azure/index.spec.ts
@@ -0,0 +1,103 @@
+import { Readable } from 'stream';
+import { getName, mocked } from '../../../../test/util';
+import { setPlatformApi } from '../../../platform';
+import * as _azureApi from '../../../platform/azure/azure-got-wrapper';
+import { PRESET_DEP_NOT_FOUND, PRESET_INVALID_JSON } from '../util';
+import * as azure from '.';
+
+jest.unmock('../../../platform');
+jest.mock('../../../platform/azure/azure-got-wrapper');
+
+const azureApi = mocked(_azureApi);
+
+describe(getName(__filename), () => {
+  beforeAll(() => {
+    setPlatformApi('azure');
+  });
+
+  describe('fetchJSONFile()', () => {
+    it('returns JSON', async () => {
+      const data = { foo: 'bar' };
+      const azureApiMock = {
+        getItemContent: jest.fn(() =>
+          Promise.resolve(Readable.from(JSON.stringify(data)))
+        ),
+        getRepositories: jest.fn(() =>
+          Promise.resolve([
+            { id: '123456', name: 'repo', project: { name: 'some' } },
+          ])
+        ),
+      };
+      azureApi.gitApi.mockImplementationOnce(() => azureApiMock as any);
+
+      const res = await azure.fetchJSONFile('some/repo', 'some-filename.json');
+      expect(res).toEqual(data);
+      expect(azureApiMock.getItemContent.mock.calls).toMatchSnapshot();
+    });
+
+    it('throws on error', async () => {
+      azureApi.gitApi.mockImplementationOnce(
+        () =>
+          ({
+            getItemContent: jest.fn(() => {
+              throw new Error('unknown');
+            }),
+            getRepositories: jest.fn(() =>
+              Promise.resolve([
+                { id: '123456', name: 'repo', project: { name: 'some' } },
+              ])
+            ),
+          } as any)
+      );
+      await expect(
+        azure.fetchJSONFile('some/repo', 'some-filename.json')
+      ).rejects.toThrow(PRESET_DEP_NOT_FOUND);
+    });
+
+    it('throws on invalid json', async () => {
+      azureApi.gitApi.mockImplementationOnce(
+        () =>
+          ({
+            getItemContent: jest.fn(() =>
+              Promise.resolve(Readable.from('!@#'))
+            ),
+            getRepositories: jest.fn(() =>
+              Promise.resolve([
+                { id: '123456', name: 'repo', project: { name: 'some' } },
+              ])
+            ),
+          } as any)
+      );
+
+      await expect(
+        azure.fetchJSONFile('some/repo', 'some-filename.json')
+      ).rejects.toThrow(PRESET_INVALID_JSON);
+    });
+  });
+
+  describe('getPresetFromEndpoint()', () => {
+    it('uses custom path', async () => {
+      const data = { foo: 'bar' };
+      azureApi.gitApi.mockImplementationOnce(
+        () =>
+          ({
+            getItemContent: jest.fn(() =>
+              Promise.resolve(Readable.from(JSON.stringify(data)))
+            ),
+            getRepositories: jest.fn(() =>
+              Promise.resolve([
+                { id: '123456', name: 'repo', project: { name: 'some' } },
+              ])
+            ),
+          } as any)
+      );
+      const res = await azure.getPresetFromEndpoint(
+        'some/repo',
+        'some-filename',
+        'foo/bar',
+        ''
+      );
+      expect(res).toEqual(data);
+    });
+  });
+});
diff --git a/lib/config/presets/azure/index.ts b/lib/config/presets/azure/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..683c271510a3ad08fa6fedb453129e14d1306a22
--- /dev/null
+++ b/lib/config/presets/azure/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 83a107844f3de628c6577875f71979839f3f1d62..9560d78b285c99bd28e9f37214446a213e8796c9 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 azure 1`] = `
+Array [
+  Array [
+    "some/repo",
+    "default",
+    undefined,
+    undefined,
+  ],
+]
+`;
+
+exports[`config/presets/local/index getPreset() forwards to azure 2`] = `
+Object {
+  "resolved": "preset",
+}
+`;
+
 exports[`config/presets/local/index getPreset() forwards to bitbucket 1`] = `
 Array [
   Array [
diff --git a/lib/config/presets/local/index.spec.ts b/lib/config/presets/local/index.spec.ts
index 0d72cea5e996c564079871e4fa97d4d6a2e81fd1..743c7334144d6a600c8e6f69eaa2847450a96610 100644
--- a/lib/config/presets/local/index.spec.ts
+++ b/lib/config/presets/local/index.spec.ts
@@ -1,4 +1,5 @@
 import { getName, mocked } from '../../../../test/util';
+import * as _azure from '../azure';
 import * as _bitbucket from '../bitbucket';
 import * as _bitbucketServer from '../bitbucket-server';
 import * as _gitea from '../gitea';
@@ -6,12 +7,14 @@ import * as _github from '../github';
 import * as _gitlab from '../gitlab';
 import * as local from '.';
 
+jest.mock('../azure');
 jest.mock('../bitbucket');
 jest.mock('../bitbucket-server');
 jest.mock('../gitea');
 jest.mock('../github');
 jest.mock('../gitlab');
 
+const azure = mocked(_azure);
 const bitbucket = mocked(_bitbucket);
 const bitbucketServer = mocked(_bitbucketServer);
 const gitea = mocked(_gitea);
@@ -22,6 +25,7 @@ describe(getName(__filename), () => {
   beforeEach(() => {
     jest.resetAllMocks();
     const preset = { resolved: 'preset' };
+    azure.getPresetFromEndpoint.mockResolvedValueOnce(preset);
     bitbucket.getPresetFromEndpoint.mockResolvedValueOnce(preset);
     bitbucketServer.getPresetFromEndpoint.mockResolvedValueOnce(preset);
     gitea.getPresetFromEndpoint.mockResolvedValueOnce(preset);
@@ -52,6 +56,18 @@ describe(getName(__filename), () => {
       }).rejects.toThrow();
     });
 
+    it('forwards to azure', async () => {
+      const content = await local.getPreset({
+        packageName: 'some/repo',
+        presetName: 'default',
+        baseConfig: {
+          platform: 'azure',
+        },
+      });
+      expect(azure.getPresetFromEndpoint.mock.calls).toMatchSnapshot();
+      expect(content).toMatchSnapshot();
+    });
+
     it('forwards to bitbucket', 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 87f5822f2bfcc9b6a624a8a945c21f377cd20b81..319d513ce58b70e5b8572eb638d43c6231e5a0aa 100644
--- a/lib/config/presets/local/index.ts
+++ b/lib/config/presets/local/index.ts
@@ -1,10 +1,12 @@
 import {
+  PLATFORM_TYPE_AZURE,
   PLATFORM_TYPE_BITBUCKET,
   PLATFORM_TYPE_BITBUCKET_SERVER,
   PLATFORM_TYPE_GITEA,
   PLATFORM_TYPE_GITHUB,
   PLATFORM_TYPE_GITLAB,
 } from '../../../constants/platforms';
+import * as azure from '../azure';
 import * as bitbucket from '../bitbucket';
 import * as bitbucketServer from '../bitbucket-server';
 import * as gitea from '../gitea';
@@ -13,6 +15,7 @@ import * as gitlab from '../gitlab';
 import type { Preset, PresetConfig } from '../types';
 
 const resolvers = {
+  [PLATFORM_TYPE_AZURE]: azure,
   [PLATFORM_TYPE_BITBUCKET]: bitbucket,
   [PLATFORM_TYPE_BITBUCKET_SERVER]: bitbucketServer,
   [PLATFORM_TYPE_GITEA]: gitea,