diff --git a/docs/usage/config-presets.md b/docs/usage/config-presets.md
index 6d44b3ae2f1fad185a3a6e040350f6bfc07c3cc3..5b8907d5c4f1eaa0d7c8525257f5720c24e28b5b 100644
--- a/docs/usage/config-presets.md
+++ b/docs/usage/config-presets.md
@@ -168,6 +168,10 @@ To host your preset config on GitLab:
 
 Note: Unlike npmjs-hosted presets, GitLab-hosted ones can contain only one config.
 
+## Local presets
+
+Renovate also supports local presets, i.e. 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 and GitLab. 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.
+
 ## Presets and Private Modules
 
 Using your own preset config along with private npm modules can present a chicken and egg problem. You want to configure the encrypted token just once, which means in the preset. But you also probably want the preset to be private too, so how can the other repos reference it?
diff --git a/lib/config/common.ts b/lib/config/common.ts
index 59e162a31f8e25f2f4fdb2d7b7abcafdc635aaef..2e6469e29bc2e7d98bf8d7acc662eedddd755871 100644
--- a/lib/config/common.ts
+++ b/lib/config/common.ts
@@ -67,6 +67,8 @@ export interface RenovateAdminConfig {
   configWarningReuseIssue?: boolean;
   dryRun?: boolean;
 
+  endpoint?: string;
+
   global?: GlobalConfig;
 
   localDir?: string;
@@ -80,6 +82,7 @@ export interface RenovateAdminConfig {
   onboardingPrTitle?: string;
   onboardingConfig?: RenovateSharedConfig;
 
+  platform?: string;
   postUpdateOptions?: string[];
   privateKey?: string | Buffer;
   repositories?: RenovateRepository[];
@@ -121,7 +124,6 @@ export interface RenovateConfig
   branchList?: string[];
   description?: string[];
 
-  endpoint?: string;
   errors?: ValidationMessage[];
   extends?: string[];
 
diff --git a/lib/config/index.ts b/lib/config/index.ts
index 047bc94f57e9d6617ca156e988c11c69d73ffa73..041950ad6c59a0ea3e9c753dad63235e7823a602 100644
--- a/lib/config/index.ts
+++ b/lib/config/index.ts
@@ -8,6 +8,7 @@ import { resolveConfigPresets } from './presets';
 import { get, getLanguageList, getManagerList } from '../manager';
 import { RenovateConfig, RenovateConfigStage } from './common';
 import { mergeChildConfig } from './utils';
+import { ensureTrailingSlash } from '../util/url';
 
 export * from './common';
 export { mergeChildConfig };
@@ -102,7 +103,7 @@ export async function parseConfigs(
   // Massage endpoint to have a trailing slash
   if (config.endpoint) {
     logger.debug('Adding trailing slash to endpoint');
-    config.endpoint = config.endpoint.replace(/\/?$/, '/');
+    config.endpoint = ensureTrailingSlash(config.endpoint);
   }
 
   // Remove log file entries
diff --git a/lib/config/presets/__snapshots__/index.spec.ts.snap b/lib/config/presets/__snapshots__/index.spec.ts.snap
index 395529ede751fe035544287f847a01aa79a83208..a33b18cacd3741aa5152e602c255f8dc540ac68c 100644
--- a/lib/config/presets/__snapshots__/index.spec.ts.snap
+++ b/lib/config/presets/__snapshots__/index.spec.ts.snap
@@ -98,6 +98,24 @@ Object {
 }
 `;
 
+exports[`config/presets parsePreset parses local 1`] = `
+Object {
+  "packageName": "some/repo",
+  "params": undefined,
+  "presetName": "default",
+  "presetSource": "local",
+}
+`;
+
+exports[`config/presets parsePreset parses no prefix as local 1`] = `
+Object {
+  "packageName": "some/repo",
+  "params": undefined,
+  "presetName": "default",
+  "presetSource": "local",
+}
+`;
+
 exports[`config/presets parsePreset returns default package name 1`] = `
 Object {
   "packageName": "renovate-config-default",
diff --git a/lib/config/presets/__snapshots__/local.spec.ts.snap b/lib/config/presets/__snapshots__/local.spec.ts.snap
new file mode 100644
index 0000000000000000000000000000000000000000..8cbbc16a2444f6fae184771a7bcad70b111f9476
--- /dev/null
+++ b/lib/config/presets/__snapshots__/local.spec.ts.snap
@@ -0,0 +1,75 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`config/presets/local getPreset() forwards to custom github 1`] = `
+Array [
+  Array [
+    "some/repo",
+    "",
+    Object {
+      "endpoint": "https://api.github.example.com",
+      "platform": "GitHub",
+    },
+  ],
+]
+`;
+
+exports[`config/presets/local getPreset() forwards to custom github 2`] = `
+Object {
+  "resolved": "preset",
+}
+`;
+
+exports[`config/presets/local getPreset() forwards to custom gitlab 1`] = `
+Array [
+  Array [
+    "some/repo",
+    "",
+    Object {
+      "endpoint": "https://gitlab.example.com/api/v4",
+      "platform": "gitlab",
+    },
+  ],
+]
+`;
+
+exports[`config/presets/local getPreset() forwards to custom gitlab 2`] = `
+Object {
+  "resolved": "preset",
+}
+`;
+
+exports[`config/presets/local getPreset() forwards to github 1`] = `
+Array [
+  Array [
+    "some/repo",
+    "",
+    Object {
+      "platform": "github",
+    },
+  ],
+]
+`;
+
+exports[`config/presets/local getPreset() forwards to github 2`] = `
+Object {
+  "resolved": "preset",
+}
+`;
+
+exports[`config/presets/local getPreset() forwards to gitlab 1`] = `
+Array [
+  Array [
+    "some/repo",
+    "",
+    Object {
+      "platform": "GitLab",
+    },
+  ],
+]
+`;
+
+exports[`config/presets/local getPreset() forwards to gitlab 2`] = `
+Object {
+  "resolved": "preset",
+}
+`;
diff --git a/lib/config/presets/github.spec.ts b/lib/config/presets/github.spec.ts
index dac3a47c801f219afbdd7b716f386fdd2dda3ada..04680ba5f019bd845bd3aa542306f234821025d7 100644
--- a/lib/config/presets/github.spec.ts
+++ b/lib/config/presets/github.spec.ts
@@ -11,7 +11,10 @@ const got: any = _got;
 const hostRules: any = _hostRules;
 
 describe('config/presets/github', () => {
-  beforeEach(() => global.renovateCache.rmAll());
+  beforeEach(() => {
+    got.mockReset();
+    return global.renovateCache.rmAll();
+  });
   describe('getPreset()', () => {
     it('passes up platform-failure', async () => {
       got.mockImplementationOnce(() => {
@@ -66,5 +69,22 @@ describe('config/presets/github', () => {
         delete global.appMode;
       }
     });
+
+    it('uses default endpoint', async () => {
+      await github.getPreset('some/repo', 'default').catch((_) => {});
+      expect(got.mock.calls[0][0]).toEqual(
+        'https://api.github.com/repos/some/repo/contents/default.json'
+      );
+    });
+    it('uses custom endpoint', async () => {
+      await github
+        .getPreset('some/repo', 'default', {
+          endpoint: 'https://api.github.example.org',
+        })
+        .catch((_) => {});
+      expect(got.mock.calls[0][0]).toEqual(
+        'https://api.github.example.org/repos/some/repo/contents/default.json'
+      );
+    });
   });
 });
diff --git a/lib/config/presets/github.ts b/lib/config/presets/github.ts
index addef24b7a4cc2236703ff52aaaf88db8bc55e8d..365294b7c858b85cff3b8d386d8c8742257c369e 100644
--- a/lib/config/presets/github.ts
+++ b/lib/config/presets/github.ts
@@ -2,12 +2,18 @@ import { logger } from '../../logger';
 import { Preset } from './common';
 import { Http, HttpOptions } from '../../util/http';
 import { PLATFORM_FAILURE } from '../../constants/error-messages';
+import { ensureTrailingSlash } from '../../util/url';
+import { RenovateConfig } from '../common';
 
 const id = 'github';
 const http = new Http(id);
 
-async function fetchJSONFile(repo: string, fileName: string): Promise<Preset> {
-  const url = `https://api.github.com/repos/${repo}/contents/${fileName}`;
+async function fetchJSONFile(
+  repo: string,
+  fileName: string,
+  endpoint: string
+): Promise<Preset> {
+  const url = `${endpoint}repos/${repo}/contents/${fileName}`;
   const opts: HttpOptions = {
     headers: {
       accept: global.appMode
@@ -39,11 +45,19 @@ async function fetchJSONFile(repo: string, fileName: string): Promise<Preset> {
 
 export async function getPreset(
   pkgName: string,
-  presetName = 'default'
+  presetName = 'default',
+  baseConfig?: RenovateConfig
 ): Promise<Preset> {
+  const endpoint = ensureTrailingSlash(
+    baseConfig?.endpoint ?? 'https://api.github.com/'
+  );
   if (presetName === 'default') {
     try {
-      const defaultJson = await fetchJSONFile(pkgName, 'default.json');
+      const defaultJson = await fetchJSONFile(
+        pkgName,
+        'default.json',
+        endpoint
+      );
       return defaultJson;
     } catch (err) {
       if (err.message === PLATFORM_FAILURE) {
@@ -51,10 +65,10 @@ export async function getPreset(
       }
       if (err.message === 'dep not found') {
         logger.debug('default.json preset not found - trying renovate.json');
-        return fetchJSONFile(pkgName, 'renovate.json');
+        return fetchJSONFile(pkgName, 'renovate.json', endpoint);
       }
       throw err;
     }
   }
-  return fetchJSONFile(pkgName, `${presetName}.json`);
+  return fetchJSONFile(pkgName, `${presetName}.json`, endpoint);
 }
diff --git a/lib/config/presets/gitlab.spec.ts b/lib/config/presets/gitlab.spec.ts
index dc503daddd2c3e373c63d2146912d8eb136b403c..f9f3373bb44074af41b337b3e2d8d7858136076a 100644
--- a/lib/config/presets/gitlab.spec.ts
+++ b/lib/config/presets/gitlab.spec.ts
@@ -9,6 +9,7 @@ const glGot: jest.Mock<Promise<Partial<GotResponse>>> = api.get as never;
 
 describe('config/presets/gitlab', () => {
   beforeEach(() => {
+    glGot.mockReset();
     global.repoCache = {};
     return global.renovateCache.rmAll();
   });
@@ -52,5 +53,21 @@ describe('config/presets/gitlab', () => {
       const content = await gitlab.getPreset('some/repo');
       expect(content).toEqual({ foo: 'bar' });
     });
+    it('uses default endpoint', async () => {
+      await gitlab.getPreset('some/repo', 'default').catch((_) => {});
+      expect(glGot.mock.calls[0][0]).toEqual(
+        'https://gitlab.com/api/v4/projects/some%2Frepo/repository/branches'
+      );
+    });
+    it('uses custom endpoint', async () => {
+      await gitlab
+        .getPreset('some/repo', 'default', {
+          endpoint: 'https://gitlab.example.org/api/v4',
+        })
+        .catch((_) => {});
+      expect(glGot.mock.calls[0][0]).toEqual(
+        'https://gitlab.example.org/api/v4/projects/some%2Frepo/repository/branches'
+      );
+    });
   });
 });
diff --git a/lib/config/presets/gitlab.ts b/lib/config/presets/gitlab.ts
index 1b8cc1cc8ccc8338440a9302db2af13e439a0411..b765c773e67bf18c3cbcb12ffa19e8a1b2e3db36 100644
--- a/lib/config/presets/gitlab.ts
+++ b/lib/config/presets/gitlab.ts
@@ -1,15 +1,16 @@
 import { api } from '../../platform/gitlab/gl-got-wrapper';
 import { logger } from '../../logger';
 import { Preset } from './common';
+import { ensureTrailingSlash } from '../../util/url';
+import { RenovateConfig } from '../common';
 
 const { get: glGot } = api;
 
-const GitLabApiUrl = 'https://gitlab.com/api/v4/projects';
-
 async function getDefaultBranchName(
-  urlEncodedPkgName: string
+  urlEncodedPkgName: string,
+  endpoint: string
 ): Promise<string> {
-  const branchesUrl = `${GitLabApiUrl}/${urlEncodedPkgName}/repository/branches`;
+  const branchesUrl = `${endpoint}projects/${urlEncodedPkgName}/repository/branches`;
   type GlBranch = {
     default: boolean;
     name: string;
@@ -30,8 +31,12 @@ async function getDefaultBranchName(
 
 export async function getPreset(
   pkgName: string,
-  presetName = 'default'
+  presetName = 'default',
+  baseConfig?: RenovateConfig
 ): Promise<Preset> {
+  const endpoint = ensureTrailingSlash(
+    baseConfig?.endpoint ?? 'https://gitlab.com/api/v4/'
+  );
   if (presetName !== 'default') {
     // TODO: proper error contructor
     throw new Error(
@@ -42,9 +47,12 @@ export async function getPreset(
   let res: string;
   try {
     const urlEncodedPkgName = encodeURIComponent(pkgName);
-    const defautlBranchName = await getDefaultBranchName(urlEncodedPkgName);
+    const defautlBranchName = await getDefaultBranchName(
+      urlEncodedPkgName,
+      endpoint
+    );
 
-    const presetUrl = `${GitLabApiUrl}/${urlEncodedPkgName}/repository/files/renovate.json?ref=${defautlBranchName}`;
+    const presetUrl = `${endpoint}projects/${urlEncodedPkgName}/repository/files/renovate.json?ref=${defautlBranchName}`;
     res = Buffer.from(
       (await glGot(presetUrl)).body.content,
       'base64'
diff --git a/lib/config/presets/index.spec.ts b/lib/config/presets/index.spec.ts
index 70a750a2b288fd5f5db1b8c9fb929a11510fc4de..6e757c213ab6a6ec7152a4708c6fc6cf1bb3a828 100644
--- a/lib/config/presets/index.spec.ts
+++ b/lib/config/presets/index.spec.ts
@@ -198,7 +198,9 @@ describe('config/presets', () => {
 
     it('ignores presets', async () => {
       config.extends = ['config:base'];
-      const res = await presets.resolveConfigPresets(config, ['config:base']);
+      const res = await presets.resolveConfigPresets(config, {}, [
+        'config:base',
+      ]);
       expect(config).toMatchObject(res);
       expect(res).toMatchSnapshot();
     });
@@ -257,6 +259,12 @@ describe('config/presets', () => {
     it('parses gitlab', () => {
       expect(presets.parsePreset('gitlab>some/repo')).toMatchSnapshot();
     });
+    it('parses local', () => {
+      expect(presets.parsePreset('local>some/repo')).toMatchSnapshot();
+    });
+    it('parses no prefix as local', () => {
+      expect(presets.parsePreset('some/repo')).toMatchSnapshot();
+    });
     it('returns default package name with params', () => {
       expect(
         presets.parsePreset(':group(packages/eslint, eslint)')
@@ -323,27 +331,30 @@ describe('config/presets', () => {
   });
   describe('getPreset', () => {
     it('gets linters', async () => {
-      const res = await presets.getPreset('packages:linters');
+      const res = await presets.getPreset('packages:linters', {});
       expect(res).toMatchSnapshot();
       expect(res.packageNames).toHaveLength(1);
       expect(res.extends).toHaveLength(2);
     });
     it('gets parameterised configs', async () => {
-      const res = await presets.getPreset(':group(packages:eslint, eslint)');
+      const res = await presets.getPreset(
+        ':group(packages:eslint, eslint)',
+        {}
+      );
       expect(res).toMatchSnapshot();
     });
     it('handles missing params', async () => {
-      const res = await presets.getPreset(':group()');
+      const res = await presets.getPreset(':group()', {});
       expect(res).toMatchSnapshot();
     });
     it('ignores irrelevant params', async () => {
-      const res = await presets.getPreset(':pinVersions(foo, bar)');
+      const res = await presets.getPreset(':pinVersions(foo, bar)', {});
       expect(res).toMatchSnapshot();
     });
     it('handles 404 packages', async () => {
       let e: Error;
       try {
-        await presets.getPreset('notfound:foo');
+        await presets.getPreset('notfound:foo', {});
       } catch (err) {
         e = err;
       }
@@ -355,7 +366,7 @@ describe('config/presets', () => {
     it('handles no config', async () => {
       let e: Error;
       try {
-        await presets.getPreset('noconfig:foo');
+        await presets.getPreset('noconfig:foo', {});
       } catch (err) {
         e = err;
       }
@@ -367,7 +378,7 @@ describe('config/presets', () => {
     it('handles throw errors', async () => {
       let e: Error;
       try {
-        await presets.getPreset('throw:foo');
+        await presets.getPreset('throw:foo', {});
       } catch (err) {
         e = err;
       }
@@ -379,7 +390,7 @@ describe('config/presets', () => {
     it('handles preset not found', async () => {
       let e: Error;
       try {
-        await presets.getPreset('wrongpreset:foo');
+        await presets.getPreset('wrongpreset:foo', {});
       } catch (err) {
         e = err;
       }
diff --git a/lib/config/presets/index.ts b/lib/config/presets/index.ts
index ee2bdc2d515075bfb815916c2e022c78f952667b..a84f527ac85301e7958e185227c23402358209a8 100644
--- a/lib/config/presets/index.ts
+++ b/lib/config/presets/index.ts
@@ -5,6 +5,7 @@ import * as migration from '../migration';
 import * as github from './github';
 import * as npm from './npm';
 import * as gitlab from './gitlab';
+import * as local from './local';
 import { RenovateConfig } from '../common';
 import { mergeChildConfig } from '../utils';
 import { regEx } from '../../util/regex';
@@ -18,6 +19,7 @@ const presetSources = {
   github,
   npm,
   gitlab,
+  local,
 };
 
 export function replaceArgs(
@@ -61,6 +63,15 @@ export function parsePreset(input: string): ParsedPreset {
   } else if (str.startsWith('gitlab>')) {
     presetSource = 'gitlab';
     str = str.substring('gitlab>'.length);
+  } else if (str.startsWith('local>')) {
+    presetSource = 'local';
+    str = str.substring('local>'.length);
+  } else if (
+    !str.startsWith('@') &&
+    !str.startsWith(':') &&
+    str.includes('/')
+  ) {
+    presetSource = 'local';
   }
   str = str.replace(/^npm>/, '');
   presetSource = presetSource || 'npm';
@@ -101,12 +112,16 @@ export function parsePreset(input: string): ParsedPreset {
   return { presetSource, packageName, presetName, params };
 }
 
-export async function getPreset(preset: string): Promise<RenovateConfig> {
+export async function getPreset(
+  preset: string,
+  baseConfig?: RenovateConfig
+): Promise<RenovateConfig> {
   logger.trace(`getPreset(${preset})`);
   const { presetSource, packageName, presetName, params } = parsePreset(preset);
   let presetConfig = await presetSources[presetSource].getPreset(
     packageName,
-    presetName
+    presetName,
+    baseConfig
   );
   logger.trace({ presetConfig }, `Found preset ${preset}`);
   if (params) {
@@ -142,6 +157,7 @@ export async function getPreset(preset: string): Promise<RenovateConfig> {
 
 export async function resolveConfigPresets(
   inputConfig: RenovateConfig,
+  baseConfig?: RenovateConfig,
   ignorePresets?: string[],
   existingPresets: string[] = []
 ): Promise<RenovateConfig> {
@@ -166,7 +182,7 @@ export async function resolveConfigPresets(
         logger.trace(`Resolving preset "${preset}"`);
         let fetchedPreset: RenovateConfig;
         try {
-          fetchedPreset = await getPreset(preset);
+          fetchedPreset = await getPreset(preset, baseConfig);
         } catch (err) {
           logger.debug({ err }, 'Preset fetch error');
           // istanbul ignore if
@@ -194,6 +210,7 @@ export async function resolveConfigPresets(
         }
         const presetConfig = await resolveConfigPresets(
           fetchedPreset,
+          baseConfig,
           ignorePresets,
           existingPresets.concat([preset])
         );
@@ -225,6 +242,7 @@ export async function resolveConfigPresets(
           (config[key] as RenovateConfig[]).push(
             await resolveConfigPresets(
               element as RenovateConfig,
+              baseConfig,
               ignorePresets,
               existingPresets
             )
@@ -238,6 +256,7 @@ export async function resolveConfigPresets(
       logger.trace(`Resolving object "${key}"`);
       config[key] = await resolveConfigPresets(
         val as RenovateConfig,
+        baseConfig,
         ignorePresets,
         existingPresets
       );
diff --git a/lib/config/presets/local.spec.ts b/lib/config/presets/local.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0c7a22f59fe8464b6cf80b81615241355cc6961c
--- /dev/null
+++ b/lib/config/presets/local.spec.ts
@@ -0,0 +1,60 @@
+import * as gitlab from './gitlab';
+import * as github from './github';
+import * as local from './local';
+
+jest.mock('./gitlab');
+jest.mock('./github');
+
+const gitlabGetPreset: jest.Mock<Promise<any>> = gitlab.getPreset as never;
+const githubGetPreset: jest.Mock<Promise<any>> = github.getPreset as never;
+
+describe('config/presets/local', () => {
+  beforeEach(() => {
+    gitlabGetPreset.mockReset();
+    gitlabGetPreset.mockResolvedValueOnce({ resolved: 'preset' });
+    githubGetPreset.mockReset();
+    githubGetPreset.mockResolvedValueOnce({ resolved: 'preset' });
+    global.repoCache = {};
+    return global.renovateCache.rmAll();
+  });
+  describe('getPreset()', () => {
+    it('throws for unsupported platform', async () => {
+      await expect(
+        local.getPreset('some/repo', 'default', {
+          platform: 'unsupported-platform',
+        })
+      ).rejects.toThrow();
+    });
+    it('forwards to gitlab', async () => {
+      const content = await local.getPreset('some/repo', '', {
+        platform: 'GitLab',
+      });
+      expect(gitlabGetPreset.mock.calls).toMatchSnapshot();
+      expect(content).toMatchSnapshot();
+    });
+    it('forwards to custom gitlab', async () => {
+      const content = await local.getPreset('some/repo', '', {
+        platform: 'gitlab',
+        endpoint: 'https://gitlab.example.com/api/v4',
+      });
+      expect(gitlabGetPreset.mock.calls).toMatchSnapshot();
+      expect(content).toMatchSnapshot();
+    });
+
+    it('forwards to github', async () => {
+      const content = await local.getPreset('some/repo', '', {
+        platform: 'github',
+      });
+      expect(githubGetPreset.mock.calls).toMatchSnapshot();
+      expect(content).toMatchSnapshot();
+    });
+    it('forwards to custom github', async () => {
+      const content = await local.getPreset('some/repo', '', {
+        platform: 'GitHub',
+        endpoint: 'https://api.github.example.com',
+      });
+      expect(githubGetPreset.mock.calls).toMatchSnapshot();
+      expect(content).toMatchSnapshot();
+    });
+  });
+});
diff --git a/lib/config/presets/local.ts b/lib/config/presets/local.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5b466f52f238b8d57ca908bc959f6869ff8e6963
--- /dev/null
+++ b/lib/config/presets/local.ts
@@ -0,0 +1,20 @@
+import { Preset } from './common';
+import * as gitlab from './gitlab';
+import * as github from './github';
+import { RenovateConfig } from '../common';
+
+export async function getPreset(
+  pkgName: string,
+  presetName = 'default',
+  baseConfig: RenovateConfig
+): Promise<Preset> {
+  if (baseConfig.platform?.toLowerCase() === 'gitlab') {
+    return gitlab.getPreset(pkgName, presetName, baseConfig);
+  }
+  if (baseConfig.platform?.toLowerCase() === 'github') {
+    return github.getPreset(pkgName, presetName, baseConfig);
+  }
+  throw new Error(
+    `Unsupported platform '${baseConfig.platform}' for local preset.`
+  );
+}
diff --git a/lib/config/validation.ts b/lib/config/validation.ts
index 203a860981ff918e2cdde27b3e3863a0aa1c0e07..6df821c381bc3c09ab855fb425ffffeef24a88e7 100644
--- a/lib/config/validation.ts
+++ b/lib/config/validation.ts
@@ -174,7 +174,8 @@ export async function validateConfig(
                 let hasSelector = false;
                 if (is.object(packageRule)) {
                   const resolvedRule = await resolveConfigPresets(
-                    packageRule as RenovateConfig
+                    packageRule as RenovateConfig,
+                    config
                   );
                   errors.push(
                     ...managerValidator.check({ resolvedRule, currentPath })
diff --git a/lib/platform/azure/index.ts b/lib/platform/azure/index.ts
index 4c423a67bb6d7032e34098e3050d4eca44c3b04c..4b7c5abe71351e89c7ddd5433b345a69444f2832 100644
--- a/lib/platform/azure/index.ts
+++ b/lib/platform/azure/index.ts
@@ -33,6 +33,7 @@ import {
 } from '../../constants/pull-requests';
 import { BranchStatus } from '../../types';
 import { RenovateConfig } from '../../config/common';
+import { ensureTrailingSlash } from '../../util/url';
 
 interface Config {
   storage: GitStorage;
@@ -77,7 +78,7 @@ export function initPlatform({
   }
   // TODO: Add a connection check that endpoint/token combination are valid
   const res = {
-    endpoint: endpoint.replace(/\/?$/, '/'), // always add a trailing slash
+    endpoint: ensureTrailingSlash(endpoint),
   };
   defaults.endpoint = res.endpoint;
   azureApi.setEndpoint(res.endpoint);
diff --git a/lib/platform/bitbucket-server/index.ts b/lib/platform/bitbucket-server/index.ts
index bac9e13a44fa8b36914918bf74e836c8f8ff1bd1..7a785f563ba5d4767bbc6884450bf064d316ca62 100644
--- a/lib/platform/bitbucket-server/index.ts
+++ b/lib/platform/bitbucket-server/index.ts
@@ -33,6 +33,7 @@ import {
 import { PR_STATE_ALL, PR_STATE_OPEN } from '../../constants/pull-requests';
 import { BranchStatus } from '../../types';
 import { RenovateConfig } from '../../config/common';
+import { ensureTrailingSlash } from '../../util/url';
 /*
  * Version: 5.3 (EOL Date: 15 Aug 2019)
  * See following docs for api information:
@@ -88,7 +89,7 @@ export function initPlatform({
     );
   }
   // TODO: Add a connection check that endpoint/username/password combination are valid
-  defaults.endpoint = endpoint.replace(/\/?$/, '/'); // always add a trailing slash
+  defaults.endpoint = ensureTrailingSlash(endpoint);
   api.setBaseUrl(defaults.endpoint);
   const platformConfig: PlatformConfig = {
     endpoint: defaults.endpoint,
diff --git a/lib/platform/gitea/index.ts b/lib/platform/gitea/index.ts
index a0d401a9496d7491054668a45e2277fc49a456f6..c2c23519636e4130eb54f9c54bb7618e6ce4970a 100644
--- a/lib/platform/gitea/index.ts
+++ b/lib/platform/gitea/index.ts
@@ -35,6 +35,7 @@ import { sanitize } from '../../util/sanitize';
 import { BranchStatus } from '../../types';
 import * as helper from './gitea-helper';
 import { PR_STATE_ALL, PR_STATE_OPEN } from '../../constants/pull-requests';
+import { ensureTrailingSlash } from '../../util/url';
 
 type GiteaRenovateConfig = {
   endpoint: string;
@@ -194,8 +195,7 @@ const platform: Platform = {
     }
 
     if (endpoint) {
-      // Ensure endpoint contains trailing slash
-      defaults.endpoint = endpoint.replace(/\/?$/, '/');
+      defaults.endpoint = ensureTrailingSlash(endpoint);
     } else {
       logger.debug('Using default Gitea endpoint: ' + defaults.endpoint);
     }
diff --git a/lib/platform/github/index.ts b/lib/platform/github/index.ts
index ac5d3985e4b9b563f1a1d63832b7536bf864d87c..4f17d075ba16602a50955e5985a58f2bbd385dd1 100644
--- a/lib/platform/github/index.ts
+++ b/lib/platform/github/index.ts
@@ -46,6 +46,7 @@ import {
   PR_STATE_CLOSED,
   PR_STATE_OPEN,
 } from '../../constants/pull-requests';
+import { ensureTrailingSlash } from '../../util/url';
 
 const defaultConfigFile = configFileNames[0];
 
@@ -122,7 +123,7 @@ export async function initPlatform({
   }
 
   if (endpoint) {
-    defaults.endpoint = endpoint.replace(/\/?$/, '/'); // always add a trailing slash
+    defaults.endpoint = ensureTrailingSlash(endpoint);
     api.setBaseUrl(defaults.endpoint);
   } else {
     logger.debug('Using default github endpoint: ' + defaults.endpoint);
diff --git a/lib/platform/gitlab/index.ts b/lib/platform/gitlab/index.ts
index 9dc3e88476df5d57be50c9acdd41063d0a6edd71..81ca938ae30c8e60dc7b73db44c03be2dec842d6 100644
--- a/lib/platform/gitlab/index.ts
+++ b/lib/platform/gitlab/index.ts
@@ -37,6 +37,7 @@ import {
 import { PR_STATE_ALL, PR_STATE_OPEN } from '../../constants/pull-requests';
 import { PLATFORM_TYPE_GITLAB } from '../../constants/platforms';
 import { BranchStatus } from '../../types';
+import { ensureTrailingSlash } from '../../util/url';
 
 type MergeMethod = 'merge' | 'rebase_merge' | 'ff';
 const defaultConfigFile = configFileNames[0];
@@ -72,7 +73,7 @@ export async function initPlatform({
     throw new Error('Init: You must configure a GitLab personal access token');
   }
   if (endpoint) {
-    defaults.endpoint = endpoint.replace(/\/?$/, '/'); // always add a trailing slash
+    defaults.endpoint = ensureTrailingSlash(endpoint);
     api.setBaseUrl(defaults.endpoint);
   } else {
     logger.debug('Using default GitLab endpoint: ' + defaults.endpoint);
diff --git a/lib/util/url.ts b/lib/util/url.ts
new file mode 100644
index 0000000000000000000000000000000000000000..347fbcd9adb42063e6034db147c03d414c5f2d3c
--- /dev/null
+++ b/lib/util/url.ts
@@ -0,0 +1,3 @@
+export function ensureTrailingSlash(url: string): string {
+  return url.replace(/\/?$/, '/');
+}
diff --git a/lib/workers/repository/init/config.ts b/lib/workers/repository/init/config.ts
index 66cfcfb844d3a5ac4908dcd8687c93acf18cf8c8..80706511aceb232fd5413c2a2d2990adf607e7d1 100644
--- a/lib/workers/repository/init/config.ts
+++ b/lib/workers/repository/init/config.ts
@@ -146,7 +146,7 @@ export async function mergeRenovateConfig(
   }
   // Decrypt after resolving in case the preset contains npm authentication instead
   const resolvedConfig = decryptConfig(
-    await presets.resolveConfigPresets(decryptedConfig),
+    await presets.resolveConfigPresets(decryptedConfig, config),
     config.privateKey
   );
   delete resolvedConfig.privateKey;