diff --git a/docs/usage/config-presets.md b/docs/usage/config-presets.md
index fe0b5776e774819740aa51fdb94da0d3dd8bdc64..867aac1a4e067dc6fc9e2672c0f22237d519db28 100644
--- a/docs/usage/config-presets.md
+++ b/docs/usage/config-presets.md
@@ -33,15 +33,20 @@ In order to achieve these goals, preset configs allow for a very modular approac
 In general, GitHub, GitLab or Gitea-based preset hosting is easier than npm because you avoid the "publish" step - simply commit preset code to the default branch and it will be picked up by Renovate the next time it runs.
 An additional benefit of using source code hosting is that the same token/authentication can be reused by Renovate in case you want to make your config private.
 
-| name                    | example use          | preset    | resolves as                          | filename                          |
-| ----------------------- | -------------------- | --------- | ------------------------------------ | --------------------------------- |
-| GitHub default          | `github>abc/foo`     | `default` | `https://github.com/abc/foo`         | `default.json` or `renovate.json` |
-| GitHub with preset name | `github>abc/foo:xyz` | `xyz`     | `https://github.com/abc/foo`         | `xyz.json`                        |
-| GitLab default          | `gitlab>abc/foo`     | `default` | `https://gitlab.com/abc/foo`         | `default.json` or `renovate.json` |
-| GitLab with preset name | `gitlab>abc/foo:xyz` | `xyz`     | `https://gitlab.com/abc/foo`         | `xyz.json`                        |
-| Gitea default           | `gitea>abc/foo`      | `default` | `https://gitea.com/abc/foo`          | `default.json` or `renovate.json` |
-| Gitea with preset name  | `gitea>abc/foo:xyz`  | `xyz`     | `https://gitea.com/abc/foo`          | `xyz.json`                        |
-| Local default           | `local>abc/foo`      | `default` | `https://github.company.com/abc/foo` | `default.json` or `renovate.json` |
+| name                    | example use                | preset    | resolves as                          | filename                          |
+| ----------------------- | -------------------------- | --------- | ------------------------------------ | --------------------------------- |
+| GitHub default          | `github>abc/foo`           | `default` | `https://github.com/abc/foo`         | `default.json` or `renovate.json` |
+| GitHub with preset name | `github>abc/foo:xyz`       | `xyz`     | `https://github.com/abc/foo`         | `xyz.json`                        |
+| GitHub with preset path | `github>abc/foo//path/xyz` | `xyz`     | `https://github.com/abc/foo`         | `path/xyz.json`                   |
+| GitLab default          | `gitlab>abc/foo`           | `default` | `https://gitlab.com/abc/foo`         | `default.json` or `renovate.json` |
+| GitLab with preset name | `gitlab>abc/foo:xyz`       | `xyz`     | `https://gitlab.com/abc/foo`         | `xyz.json`                        |
+| GitLab with preset path | `gitlab>abc/foo//path/xyz` | `xyz`     | `https://gitlab.com/abc/foo`         | `path/xyz.json`                   |
+| Gitea default           | `gitea>abc/foo`            | `default` | `https://gitea.com/abc/foo`          | `default.json` or `renovate.json` |
+| Gitea with preset name  | `gitea>abc/foo:xyz`        | `xyz`     | `https://gitea.com/abc/foo`          | `xyz.json`                        |
+| Local default           | `local>abc/foo`            | `default` | `https://github.company.com/abc/foo` | `default.json` or `renovate.json` |
+| Local with preset path  | `local>abc/foo//path/xyz`  | `default` | `https://github.company.com/abc/foo` | `path/xyz.json`                   |
+
+Note that you can't combine the path and sub-preset syntaxes (i.e. anything in the form `provider>owner/repo//path/to/file:subsubpreset`) is not supported. One workaround is to use distinct files instead of sub-presets.
 
 ## Example configs
 
diff --git a/lib/config/presets/__snapshots__/index.spec.ts.snap b/lib/config/presets/__snapshots__/index.spec.ts.snap
index ae16bf2708643956ecb5abd74c4b74cd3cff3744..f3ab55e37aa2650d0c3e6c3b776551a793f80e3f 100644
--- a/lib/config/presets/__snapshots__/index.spec.ts.snap
+++ b/lib/config/presets/__snapshots__/index.spec.ts.snap
@@ -87,6 +87,7 @@ Object {
   "packageName": "some/repo",
   "params": undefined,
   "presetName": "default",
+  "presetPath": undefined,
   "presetSource": "gitea",
 }
 `;
@@ -96,6 +97,17 @@ Object {
   "packageName": "some/repo",
   "params": undefined,
   "presetName": "default",
+  "presetPath": undefined,
+  "presetSource": "github",
+}
+`;
+
+exports[`config/presets parsePreset parses github subdirectories 1`] = `
+Object {
+  "packageName": "some/repo",
+  "params": undefined,
+  "presetName": "somefile",
+  "presetPath": "somepath/somesubpath",
   "presetSource": "github",
 }
 `;
@@ -105,6 +117,7 @@ Object {
   "packageName": "some/repo",
   "params": undefined,
   "presetName": "somefile",
+  "presetPath": undefined,
   "presetSource": "github",
 }
 `;
@@ -114,6 +127,7 @@ Object {
   "packageName": "some/repo",
   "params": undefined,
   "presetName": "somefile/somepreset/somesubpreset",
+  "presetPath": undefined,
   "presetSource": "github",
 }
 `;
@@ -123,6 +137,17 @@ Object {
   "packageName": "some/repo",
   "params": undefined,
   "presetName": "somefile/somepreset",
+  "presetPath": undefined,
+  "presetSource": "github",
+}
+`;
+
+exports[`config/presets parsePreset parses github toplevel file using subdirectory syntax 1`] = `
+Object {
+  "packageName": "some/repo",
+  "params": undefined,
+  "presetName": "somefile",
+  "presetPath": undefined,
   "presetSource": "github",
 }
 `;
@@ -132,6 +157,7 @@ Object {
   "packageName": "some/repo",
   "params": undefined,
   "presetName": "default",
+  "presetPath": undefined,
   "presetSource": "gitlab",
 }
 `;
@@ -141,6 +167,7 @@ Object {
   "packageName": "some/repo",
   "params": undefined,
   "presetName": "default",
+  "presetPath": undefined,
   "presetSource": "local",
 }
 `;
@@ -150,6 +177,7 @@ Object {
   "packageName": "some/repo",
   "params": undefined,
   "presetName": "default",
+  "presetPath": undefined,
   "presetSource": "local",
 }
 `;
@@ -159,6 +187,7 @@ Object {
   "packageName": "default",
   "params": undefined,
   "presetName": "base",
+  "presetPath": undefined,
   "presetSource": "internal",
 }
 `;
@@ -171,6 +200,7 @@ Object {
     "eslint",
   ],
   "presetName": "group",
+  "presetPath": undefined,
   "presetSource": "internal",
 }
 `;
@@ -180,6 +210,7 @@ Object {
   "packageName": "renovate-config-somepackage",
   "params": undefined,
   "presetName": "default",
+  "presetPath": undefined,
   "presetSource": "npm",
 }
 `;
@@ -189,6 +220,7 @@ Object {
   "packageName": "renovate-config-somepackage",
   "params": undefined,
   "presetName": "webapp",
+  "presetPath": undefined,
   "presetSource": "npm",
 }
 `;
@@ -198,6 +230,7 @@ Object {
   "packageName": "renovate-config-somepackage",
   "params": undefined,
   "presetName": "webapp",
+  "presetPath": undefined,
   "presetSource": "npm",
 }
 `;
@@ -209,6 +242,7 @@ Object {
     "param1",
   ],
   "presetName": "webapp",
+  "presetPath": undefined,
   "presetSource": "npm",
 }
 `;
@@ -218,6 +252,7 @@ Object {
   "packageName": "@somescope/somepackagename",
   "params": undefined,
   "presetName": "default",
+  "presetPath": undefined,
   "presetSource": "npm",
 }
 `;
@@ -231,6 +266,7 @@ Object {
     "param3",
   ],
   "presetName": "default",
+  "presetPath": undefined,
   "presetSource": "npm",
 }
 `;
@@ -240,6 +276,7 @@ Object {
   "packageName": "@somescope/somepackagename",
   "params": undefined,
   "presetName": "somePresetName",
+  "presetPath": undefined,
   "presetSource": "npm",
 }
 `;
@@ -252,6 +289,7 @@ Object {
     "param2",
   ],
   "presetName": "somePresetName",
+  "presetPath": undefined,
   "presetSource": "npm",
 }
 `;
@@ -261,6 +299,7 @@ Object {
   "packageName": "@somescope/renovate-config",
   "params": undefined,
   "presetName": "somePresetName",
+  "presetPath": undefined,
   "presetSource": "npm",
 }
 `;
@@ -272,6 +311,7 @@ Object {
     "param1",
   ],
   "presetName": "somePresetName",
+  "presetPath": undefined,
   "presetSource": "npm",
 }
 `;
@@ -281,6 +321,7 @@ Object {
   "packageName": "@somescope/renovate-config",
   "params": undefined,
   "presetName": "default",
+  "presetPath": undefined,
   "presetSource": "npm",
 }
 `;
@@ -292,6 +333,7 @@ Object {
     "param1",
   ],
   "presetName": "default",
+  "presetPath": undefined,
   "presetSource": "npm",
 }
 `;
@@ -514,6 +556,18 @@ exports[`config/presets resolvePreset throws if invalid preset file 2`] = `"Cann
 
 exports[`config/presets resolvePreset throws if invalid preset file 3`] = `undefined`;
 
+exports[`config/presets resolvePreset throws if path + invalid syntax 1`] = `undefined`;
+
+exports[`config/presets resolvePreset throws if path + invalid syntax 2`] = `"Preset is invalid (github>user/repo//)"`;
+
+exports[`config/presets resolvePreset throws if path + invalid syntax 3`] = `undefined`;
+
+exports[`config/presets resolvePreset throws if path + sub-preset 1`] = `undefined`;
+
+exports[`config/presets resolvePreset throws if path + sub-preset 2`] = `"Sub-presets cannot be combined with a custom path (github>user/repo//path:subpreset)"`;
+
+exports[`config/presets resolvePreset throws if path + sub-preset 3`] = `undefined`;
+
 exports[`config/presets resolvePreset throws if valid and invalid 1`] = `undefined`;
 
 exports[`config/presets resolvePreset throws if valid and invalid 2`] = `"Preset name not found within published preset config (wrongpreset:invalid-preset)"`;
diff --git a/lib/config/presets/bitbucket-server/__snapshots__/index.spec.ts.snap b/lib/config/presets/bitbucket-server/__snapshots__/index.spec.ts.snap
index 6f362876bf40f12612f72a37270968bb9d774660..58e8780369bcfe4af8880654efddbfa528b13409 100644
--- a/lib/config/presets/bitbucket-server/__snapshots__/index.spec.ts.snap
+++ b/lib/config/presets/bitbucket-server/__snapshots__/index.spec.ts.snap
@@ -90,3 +90,20 @@ Array [
   },
 ]
 `;
+
+exports[`config/presets/bitbucket-server/index getPresetFromEndpoint() uses custom path 1`] = `
+Array [
+  Object {
+    "headers": Object {
+      "accept": "application/json",
+      "accept-encoding": "gzip, deflate",
+      "authorization": "Bearer abc",
+      "host": "api.github.example.org",
+      "user-agent": "https://github.com/renovatebot/renovate",
+      "x-atlassian-token": "no-check",
+    },
+    "method": "GET",
+    "url": "https://api.github.example.org/rest/api/1.0/projects/some/repos/repo/browse/path/default.json?limit=20000",
+  },
+]
+`;
diff --git a/lib/config/presets/bitbucket-server/index.spec.ts b/lib/config/presets/bitbucket-server/index.spec.ts
index 0c709b84268aff9a9a3e2138380835324f75ae08..afa5e7022af186368e047097e89998c55c3695f8 100644
--- a/lib/config/presets/bitbucket-server/index.spec.ts
+++ b/lib/config/presets/bitbucket-server/index.spec.ts
@@ -112,6 +112,26 @@ describe(getName(__filename), () => {
         await bitbucketServer.getPresetFromEndpoint(
           'some/repo',
           'default',
+          undefined,
+          'https://api.github.example.org'
+        )
+      ).toEqual({ from: 'api' });
+      expect(httpMock.getTrace()).toMatchSnapshot();
+    });
+    it('uses custom path', async () => {
+      httpMock
+        .scope('https://api.github.example.org')
+        .get(`${basePath}/path/default.json`)
+        .query({ limit: 20000 })
+        .reply(200, {
+          isLastPage: true,
+          lines: [{ text: '{"from":"api"}' }],
+        });
+      expect(
+        await bitbucketServer.getPresetFromEndpoint(
+          'some/repo',
+          'default',
+          'path',
           'https://api.github.example.org'
         )
       ).toEqual({ from: 'api' });
diff --git a/lib/config/presets/bitbucket-server/index.ts b/lib/config/presets/bitbucket-server/index.ts
index c50497c408ddb8c2f4633d26b52d8c6062a76779..727d8246da80d6fefe177b440cddedc6538492a2 100644
--- a/lib/config/presets/bitbucket-server/index.ts
+++ b/lib/config/presets/bitbucket-server/index.ts
@@ -48,11 +48,13 @@ export async function fetchJSONFile(
 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/gitea/__snapshots__/index.spec.ts.snap b/lib/config/presets/gitea/__snapshots__/index.spec.ts.snap
index 80dcd962aaa98422eff43c049703a0d9661103b9..69dd66a3522e52b7421c68d64bd777d26cf037ad 100644
--- a/lib/config/presets/gitea/__snapshots__/index.spec.ts.snap
+++ b/lib/config/presets/gitea/__snapshots__/index.spec.ts.snap
@@ -22,6 +22,22 @@ Array [
 ]
 `;
 
+exports[`config/presets/gitea/index getPreset() should query custom paths 1`] = `
+Array [
+  Object {
+    "headers": Object {
+      "accept": "application/json",
+      "accept-encoding": "gzip, deflate",
+      "authorization": "token abc",
+      "host": "gitea.com",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "GET",
+    "url": "https://gitea.com/api/v1/repos/some/repo/contents/path%2Fcustom.json?",
+  },
+]
+`;
+
 exports[`config/presets/gitea/index getPreset() should query preset within the file 1`] = `
 Array [
   Object {
diff --git a/lib/config/presets/gitea/index.spec.ts b/lib/config/presets/gitea/index.spec.ts
index d011bf401a9a3ecc42aff4f8fe2912e1aa299ebd..ddab9e13ab2096b24b24132da4d1723986bb1f9b 100644
--- a/lib/config/presets/gitea/index.spec.ts
+++ b/lib/config/presets/gitea/index.spec.ts
@@ -142,6 +142,22 @@ describe(getName(__filename), () => {
       expect(httpMock.getTrace()).toMatchSnapshot();
     });
 
+    it('should query custom paths', async () => {
+      httpMock
+        .scope(giteaApiHost)
+        .get(`${basePath}/path%2Fcustom.json`)
+        .reply(200, {
+          content: Buffer.from('{"foo":"bar"}').toString('base64'),
+        });
+      const content = await gitea.getPreset({
+        packageName: 'some/repo',
+        presetName: 'custom',
+        presetPath: 'path',
+      });
+      expect(content).toEqual({ foo: 'bar' });
+      expect(httpMock.getTrace()).toMatchSnapshot();
+    });
+
     it('should throws not-found', async () => {
       httpMock
         .scope(giteaApiHost)
@@ -168,7 +184,7 @@ describe(getName(__filename), () => {
           content: Buffer.from('{"from":"api"}').toString('base64'),
         });
       expect(
-        await gitea.getPresetFromEndpoint('some/repo', 'default')
+        await gitea.getPresetFromEndpoint('some/repo', 'default', undefined)
       ).toEqual({ from: 'api' });
       expect(httpMock.getTrace()).toMatchSnapshot();
     });
@@ -185,6 +201,7 @@ describe(getName(__filename), () => {
           .getPresetFromEndpoint(
             'some/repo',
             'default',
+            undefined,
             'https://api.gitea.example.org'
           )
           .catch(() => ({ from: 'api' }))
diff --git a/lib/config/presets/gitea/index.ts b/lib/config/presets/gitea/index.ts
index b36eda679f1d7fddfd45a4f0ea7e7d68f9020ef3..ae3229b7213f3dc0582360f5c9bb5670f6fe7830 100644
--- a/lib/config/presets/gitea/index.ts
+++ b/lib/config/presets/gitea/index.ts
@@ -40,11 +40,13 @@ export async function fetchJSONFile(
 export function getPresetFromEndpoint(
   pkgName: string,
   filePreset: string,
+  presetPath: string,
   endpoint = Endpoint
 ): Promise<Preset> {
   return fetchPreset({
     pkgName,
     filePreset,
+    presetPath,
     endpoint,
     fetch: fetchJSONFile,
   });
@@ -53,6 +55,7 @@ export function getPresetFromEndpoint(
 export function getPreset({
   packageName: pkgName,
   presetName = 'default',
+  presetPath,
 }: PresetConfig): Promise<Preset> {
-  return getPresetFromEndpoint(pkgName, presetName, Endpoint);
+  return getPresetFromEndpoint(pkgName, presetName, presetPath, Endpoint);
 }
diff --git a/lib/config/presets/github/__snapshots__/index.spec.ts.snap b/lib/config/presets/github/__snapshots__/index.spec.ts.snap
index 778afb4c986a4c6e41e5380dd426521868e5ff52..a9c1e0512e7b3408db3f5871febf99bffab61d21 100644
--- a/lib/config/presets/github/__snapshots__/index.spec.ts.snap
+++ b/lib/config/presets/github/__snapshots__/index.spec.ts.snap
@@ -22,6 +22,22 @@ Array [
 ]
 `;
 
+exports[`config/presets/github/index getPreset() should query custom paths 1`] = `
+Array [
+  Object {
+    "headers": Object {
+      "accept": "application/vnd.github.v3+json",
+      "accept-encoding": "gzip, deflate",
+      "authorization": "token abc",
+      "host": "api.github.com",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "GET",
+    "url": "https://api.github.com/repos/some/repo/contents/path/custom.json",
+  },
+]
+`;
+
 exports[`config/presets/github/index getPreset() should query preset within the file 1`] = `
 Array [
   Object {
diff --git a/lib/config/presets/github/index.spec.ts b/lib/config/presets/github/index.spec.ts
index 97b0a5922fcfc0ad242a26efbd7561bb05b8e4fb..6de3ecc7b9df1f6058a45dc41f7bb9c51cab9c4f 100644
--- a/lib/config/presets/github/index.spec.ts
+++ b/lib/config/presets/github/index.spec.ts
@@ -140,6 +140,22 @@ describe(getName(__filename), () => {
       expect(httpMock.getTrace()).toMatchSnapshot();
     });
 
+    it('should query custom paths', async () => {
+      httpMock
+        .scope(githubApiHost)
+        .get(`${basePath}/path/custom.json`)
+        .reply(200, {
+          content: Buffer.from('{"foo":"bar"}').toString('base64'),
+        });
+      const content = await github.getPreset({
+        packageName: 'some/repo',
+        presetName: 'custom',
+        presetPath: 'path',
+      });
+      expect(content).toEqual({ foo: 'bar' });
+      expect(httpMock.getTrace()).toMatchSnapshot();
+    });
+
     it('should throws not-found', async () => {
       httpMock
         .scope(githubApiHost)
@@ -166,7 +182,7 @@ describe(getName(__filename), () => {
           content: Buffer.from('{"from":"api"}').toString('base64'),
         });
       expect(
-        await github.getPresetFromEndpoint('some/repo', 'default')
+        await github.getPresetFromEndpoint('some/repo', 'default', undefined)
       ).toEqual({ from: 'api' });
       expect(httpMock.getTrace()).toMatchSnapshot();
     });
@@ -183,6 +199,7 @@ describe(getName(__filename), () => {
           .getPresetFromEndpoint(
             'some/repo',
             'default',
+            undefined,
             'https://api.github.example.org'
           )
           .catch(() => ({ from: 'api' }))
diff --git a/lib/config/presets/github/index.ts b/lib/config/presets/github/index.ts
index 08f7d2eb41aad00fe2afaace418c2a5db1ac019d..fef6d0f12423778f87d0bde7ec61aadb93d62b50 100644
--- a/lib/config/presets/github/index.ts
+++ b/lib/config/presets/github/index.ts
@@ -40,11 +40,13 @@ export async function fetchJSONFile(
 export function getPresetFromEndpoint(
   pkgName: string,
   filePreset: string,
+  presetPath: string,
   endpoint = Endpoint
 ): Promise<Preset> {
   return fetchPreset({
     pkgName,
     filePreset,
+    presetPath,
     endpoint,
     fetch: fetchJSONFile,
   });
@@ -53,6 +55,7 @@ export function getPresetFromEndpoint(
 export function getPreset({
   packageName: pkgName,
   presetName = 'default',
+  presetPath,
 }: PresetConfig): Promise<Preset> {
-  return getPresetFromEndpoint(pkgName, presetName, Endpoint);
+  return getPresetFromEndpoint(pkgName, presetName, presetPath, Endpoint);
 }
diff --git a/lib/config/presets/gitlab/__snapshots__/index.spec.ts.snap b/lib/config/presets/gitlab/__snapshots__/index.spec.ts.snap
index b79273bd3835131b34c09da378f27a7a8bfee149..e612a7ae6045a9c96fe3b9e06a26452c57d7df45 100644
--- a/lib/config/presets/gitlab/__snapshots__/index.spec.ts.snap
+++ b/lib/config/presets/gitlab/__snapshots__/index.spec.ts.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`config/presets/gitlab/index getPreset() should return the preset 1`] = `
+exports[`config/presets/gitlab/index getPreset() should query custom paths 1`] = `
 Array [
   Object {
     "headers": Object {
@@ -20,12 +20,12 @@ Array [
       "user-agent": "https://github.com/renovatebot/renovate",
     },
     "method": "GET",
-    "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/files/default.json/raw?ref=master",
+    "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/files/path%2Fcustom.json/raw?ref=master",
   },
 ]
 `;
 
-exports[`config/presets/gitlab/index getPreset() throws if missing 1`] = `
+exports[`config/presets/gitlab/index getPreset() should return the preset 1`] = `
 Array [
   Object {
     "headers": Object {
@@ -47,6 +47,11 @@ Array [
     "method": "GET",
     "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/files/default.json/raw?ref=master",
   },
+]
+`;
+
+exports[`config/presets/gitlab/index getPreset() throws EXTERNAL_HOST_ERROR 1`] = `
+Array [
   Object {
     "headers": Object {
       "accept": "application/json",
@@ -57,6 +62,11 @@ Array [
     "method": "GET",
     "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/branches",
   },
+]
+`;
+
+exports[`config/presets/gitlab/index getPreset() throws if missing 1`] = `
+Array [
   Object {
     "headers": Object {
       "accept": "application/json",
@@ -65,13 +75,18 @@ Array [
       "user-agent": "https://github.com/renovatebot/renovate",
     },
     "method": "GET",
-    "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/files/renovate.json/raw?ref=master",
+    "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/branches",
+  },
+  Object {
+    "headers": Object {
+      "accept": "application/json",
+      "accept-encoding": "gzip, deflate",
+      "host": "gitlab.com",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "GET",
+    "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/files/default.json/raw?ref=master",
   },
-]
-`;
-
-exports[`config/presets/gitlab/index getPreset() throws EXTERNAL_HOST_ERROR 1`] = `
-Array [
   Object {
     "headers": Object {
       "accept": "application/json",
@@ -82,6 +97,16 @@ Array [
     "method": "GET",
     "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/branches",
   },
+  Object {
+    "headers": Object {
+      "accept": "application/json",
+      "accept-encoding": "gzip, deflate",
+      "host": "gitlab.com",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "GET",
+    "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/files/renovate.json/raw?ref=master",
+  },
 ]
 `;
 
diff --git a/lib/config/presets/gitlab/index.spec.ts b/lib/config/presets/gitlab/index.spec.ts
index f2ec07c8b4a95ddae03e0d4d54980df8a01c3725..c6a4dbe77d88b4da097c464d3cb2f1b945288732 100644
--- a/lib/config/presets/gitlab/index.spec.ts
+++ b/lib/config/presets/gitlab/index.spec.ts
@@ -65,6 +65,31 @@ describe(getName(__filename), () => {
       expect(content).toEqual({ foo: 'bar' });
       expect(httpMock.getTrace()).toMatchSnapshot();
     });
+
+    it('should query custom paths', async () => {
+      httpMock
+        .scope(gitlabApiHost)
+        .get(`${basePath}/branches`)
+        .reply(200, [
+          {
+            name: 'devel',
+          },
+          {
+            name: 'master',
+            default: true,
+          },
+        ])
+        .get(`${basePath}/files/path%2Fcustom.json/raw?ref=master`)
+        .reply(200, { foo: 'bar' }, {});
+
+      const content = await gitlab.getPreset({
+        packageName: 'some/repo',
+        presetPath: 'path',
+        presetName: 'custom',
+      });
+      expect(content).toEqual({ foo: 'bar' });
+      expect(httpMock.getTrace()).toMatchSnapshot();
+    });
   });
 
   describe('getPresetFromEndpoint()', () => {
@@ -81,7 +106,11 @@ describe(getName(__filename), () => {
         .get(`${basePath}/files/some.json/raw?ref=devel`)
         .reply(200, { preset: { file: {} } });
       expect(
-        await gitlab.getPresetFromEndpoint('some/repo', 'some/preset/file')
+        await gitlab.getPresetFromEndpoint(
+          'some/repo',
+          'some/preset/file',
+          undefined
+        )
       ).toEqual({});
       expect(httpMock.getTrace()).toMatchSnapshot();
     });
@@ -102,6 +131,7 @@ describe(getName(__filename), () => {
         gitlab.getPresetFromEndpoint(
           'some/repo',
           'some/preset/file',
+          undefined,
           'https://gitlab.example.org/api/v4'
         )
       ).rejects.toThrow(PRESET_DEP_NOT_FOUND);
diff --git a/lib/config/presets/gitlab/index.ts b/lib/config/presets/gitlab/index.ts
index 6b0f4b8e8bbde018c5cbac390722bd0f963c3aeb..829ee1e0223a57b42aceabe6e2a831f1150280ee 100644
--- a/lib/config/presets/gitlab/index.ts
+++ b/lib/config/presets/gitlab/index.ts
@@ -57,11 +57,13 @@ export async function fetchJSONFile(
 export function getPresetFromEndpoint(
   pkgName: string,
   presetName: string,
+  presetPath: string,
   endpoint = Endpoint
 ): Promise<Preset> {
   return fetchPreset({
     pkgName,
     filePreset: presetName,
+    presetPath,
     endpoint,
     fetch: fetchJSONFile,
   });
@@ -69,7 +71,8 @@ export function getPresetFromEndpoint(
 
 export function getPreset({
   packageName: pkgName,
+  presetPath,
   presetName = 'default',
 }: PresetConfig): Promise<Preset> {
-  return getPresetFromEndpoint(pkgName, presetName, Endpoint);
+  return getPresetFromEndpoint(pkgName, presetName, presetPath, Endpoint);
 }
diff --git a/lib/config/presets/index.spec.ts b/lib/config/presets/index.spec.ts
index cad4bf87a88193e62712853b15d8740c9fd48d21..06ab52a5028c03909a49eb136fdad38d8f66764d 100644
--- a/lib/config/presets/index.spec.ts
+++ b/lib/config/presets/index.spec.ts
@@ -75,6 +75,36 @@ describe('config/presets', () => {
       expect(e.validationMessage).toMatchSnapshot();
     });
 
+    it('throws if path + invalid syntax', async () => {
+      config.foo = 1;
+      config.extends = ['github>user/repo//'];
+      let e: Error;
+      try {
+        await presets.resolveConfigPresets(config);
+      } catch (err) {
+        e = err;
+      }
+      expect(e).toBeDefined();
+      expect(e.configFile).toMatchSnapshot();
+      expect(e.validationError).toMatchSnapshot();
+      expect(e.validationMessage).toMatchSnapshot();
+    });
+
+    it('throws if path + sub-preset', async () => {
+      config.foo = 1;
+      config.extends = ['github>user/repo//path:subpreset'];
+      let e: Error;
+      try {
+        await presets.resolveConfigPresets(config);
+      } catch (err) {
+        e = err;
+      }
+      expect(e).toBeDefined();
+      expect(e.configFile).toMatchSnapshot();
+      expect(e.validationError).toMatchSnapshot();
+      expect(e.validationMessage).toMatchSnapshot();
+    });
+
     it('throws noconfig', async () => {
       config.foo = 1;
       config.extends = ['noconfig:base'];
@@ -263,6 +293,16 @@ describe('config/presets', () => {
         )
       ).toMatchSnapshot();
     });
+    it('parses github subdirectories', () => {
+      expect(
+        presets.parsePreset('github>some/repo//somepath/somesubpath/somefile')
+      ).toMatchSnapshot();
+    });
+    it('parses github toplevel file using subdirectory syntax', () => {
+      expect(
+        presets.parsePreset('github>some/repo//somefile')
+      ).toMatchSnapshot();
+    });
     it('parses gitlab', () => {
       expect(presets.parsePreset('gitlab>some/repo')).toMatchSnapshot();
     });
diff --git a/lib/config/presets/index.ts b/lib/config/presets/index.ts
index 44836d5290811c468e56f63db6763d73484a4df9..b4dbce3c557d5067245d71df0b7465fe20ba62e4 100644
--- a/lib/config/presets/index.ts
+++ b/lib/config/presets/index.ts
@@ -60,6 +60,7 @@ export function replaceArgs(
 export function parsePreset(input: string): ParsedPreset {
   let str = input;
   let presetSource: string;
+  let presetPath: string;
   let packageName: string;
   let presetName: string;
   let params: string[];
@@ -127,6 +128,18 @@ export function parsePreset(input: string): ParsedPreset {
     } else {
       presetName = str.slice(1);
     }
+  } else if (str.includes('//')) {
+    // non-scoped namespace with a subdirectory preset
+    const re = /^([\w./]+?)\/\/(?:([\w./]+)\/)?([\w.]+)$/;
+
+    // Validation
+    if (str.includes(':')) {
+      throw new Error('prohibited sub-preset');
+    }
+    if (!re.test(str)) {
+      throw new Error('invalid preset');
+    }
+    [, packageName, presetPath, presetName] = re.exec(str);
   } else {
     // non-scoped namespace
     [, packageName] = /(.*?)(:|$)/.exec(str);
@@ -138,7 +151,7 @@ export function parsePreset(input: string): ParsedPreset {
       presetName = 'default';
     }
   }
-  return { presetSource, packageName, presetName, params };
+  return { presetSource, presetPath, packageName, presetName, params };
 }
 
 export async function getPreset(
@@ -146,9 +159,16 @@ export async function getPreset(
   baseConfig?: RenovateConfig
 ): Promise<RenovateConfig> {
   logger.trace(`getPreset(${preset})`);
-  const { presetSource, packageName, presetName, params } = parsePreset(preset);
+  const {
+    presetSource,
+    packageName,
+    presetPath,
+    presetName,
+    params,
+  } = parsePreset(preset);
   let presetConfig = await presetSources[presetSource].getPreset({
     packageName,
+    presetPath,
     presetName,
     baseConfig,
   });
@@ -237,6 +257,10 @@ export async function resolveConfigPresets(
             error.validationError = `Preset package is missing a renovate-config entry (${preset})`;
           } else if (err.message === 'preset not found') {
             error.validationError = `Preset name not found within published preset config (${preset})`;
+          } else if (err.message === 'invalid preset') {
+            error.validationError = `Preset is invalid (${preset})`;
+          } else if (err.message === 'prohibited sub-preset') {
+            error.validationError = `Sub-presets cannot be combined with a custom path (${preset})`;
           }
           // istanbul ignore if
           if (existingPresets.length) {
@@ -307,6 +331,7 @@ export async function resolveConfigPresets(
 export interface ParsedPreset {
   presetSource: string;
   packageName: string;
+  presetPath?: string;
   presetName: string;
   params?: string[];
 }
diff --git a/lib/config/presets/local/__snapshots__/index.spec.ts.snap b/lib/config/presets/local/__snapshots__/index.spec.ts.snap
index c0887b233783218fb837ac2aba3b1b5ad73216c9..c580f9c3ff40462d5ef593f179335e3e26f2d431 100644
--- a/lib/config/presets/local/__snapshots__/index.spec.ts.snap
+++ b/lib/config/presets/local/__snapshots__/index.spec.ts.snap
@@ -5,6 +5,7 @@ Array [
   Array [
     "some/repo",
     "default",
+    undefined,
     "https://git.example.com",
   ],
 ]
@@ -17,6 +18,7 @@ Array [
   Array [
     "some/repo",
     "default",
+    undefined,
     "https://api.gitea.example.com",
   ],
 ]
@@ -29,6 +31,7 @@ Array [
   Array [
     "some/repo",
     "default",
+    undefined,
     "https://api.github.example.com",
   ],
 ]
@@ -45,6 +48,7 @@ Array [
   Array [
     "some/repo",
     "default",
+    undefined,
     "https://gitlab.example.com/api/v4",
   ],
 ]
@@ -62,6 +66,7 @@ Array [
     "some/repo",
     "default",
     undefined,
+    undefined,
   ],
 ]
 `;
@@ -74,6 +79,7 @@ Array [
     "some/repo",
     "default",
     undefined,
+    undefined,
   ],
 ]
 `;
@@ -90,6 +96,7 @@ Array [
     "some/repo",
     "default",
     undefined,
+    undefined,
   ],
 ]
 `;
diff --git a/lib/config/presets/local/index.ts b/lib/config/presets/local/index.ts
index 63e4400bc9fa1fe11ca85828d62a68c376109b10..4a06d9f2274cdb10930531eb42c28403835fc27e 100644
--- a/lib/config/presets/local/index.ts
+++ b/lib/config/presets/local/index.ts
@@ -13,6 +13,7 @@ import type { Preset, PresetConfig } from '../types';
 export function getPreset({
   packageName: pkgName,
   presetName = 'default',
+  presetPath,
   baseConfig,
 }: PresetConfig): Promise<Preset> {
   const { platform, endpoint } = baseConfig;
@@ -21,17 +22,33 @@ export function getPreset({
   }
   switch (platform.toLowerCase()) {
     case PLATFORM_TYPE_GITLAB:
-      return gitlab.getPresetFromEndpoint(pkgName, presetName, endpoint);
+      return gitlab.getPresetFromEndpoint(
+        pkgName,
+        presetName,
+        presetPath,
+        endpoint
+      );
     case PLATFORM_TYPE_GITHUB:
-      return github.getPresetFromEndpoint(pkgName, presetName, endpoint);
+      return github.getPresetFromEndpoint(
+        pkgName,
+        presetName,
+        presetPath,
+        endpoint
+      );
     case PLATFORM_TYPE_BITBUCKET_SERVER:
       return bitbucketServer.getPresetFromEndpoint(
         pkgName,
         presetName,
+        presetPath,
         endpoint
       );
     case PLATFORM_TYPE_GITEA:
-      return gitea.getPresetFromEndpoint(pkgName, presetName, endpoint);
+      return gitea.getPresetFromEndpoint(
+        pkgName,
+        presetName,
+        presetPath,
+        endpoint
+      );
     default:
       throw new Error(
         `Unsupported platform '${baseConfig.platform}' for local preset.`
diff --git a/lib/config/presets/types.ts b/lib/config/presets/types.ts
index 6aac5a5c18b78248179fc606dff7c19ef6fedbc5..7296dda27aed33355542b843b14b5d3221e57d0b 100644
--- a/lib/config/presets/types.ts
+++ b/lib/config/presets/types.ts
@@ -5,6 +5,7 @@ export type Preset = RenovateConfig & Record<string, unknown>;
 
 export type PresetConfig = {
   packageName: string;
+  presetPath?: string;
   presetName?: string;
   baseConfig?: RenovateConfig;
 };
diff --git a/lib/config/presets/util.ts b/lib/config/presets/util.ts
index 500f439f12eb155a5af3f0e4fa064fe1094be251..12c0e88576dd126999fb744848ddc7979229945d 100644
--- a/lib/config/presets/util.ts
+++ b/lib/config/presets/util.ts
@@ -14,6 +14,7 @@ export type PresetFetcher = (
 export type FetchPresetConfig = {
   pkgName: string;
   filePreset: string;
+  presetPath?: string;
   endpoint: string;
   fetch: PresetFetcher;
 };
@@ -21,25 +22,40 @@ export type FetchPresetConfig = {
 export async function fetchPreset({
   pkgName,
   filePreset,
+  presetPath,
   endpoint,
   fetch,
 }: FetchPresetConfig): Promise<Preset | undefined> {
   // eslint-disable-next-line no-param-reassign
   endpoint = ensureTrailingSlash(endpoint);
   const [fileName, presetName, subPresetName] = filePreset.split('/');
+  const pathPrefix = presetPath ? `${presetPath}/` : '';
+  const buildFilePath = (name: string): string => `${pathPrefix}${name}`;
   let jsonContent: any | undefined;
   if (fileName === 'default') {
     try {
-      jsonContent = await fetch(pkgName, 'default.json', endpoint);
+      jsonContent = await fetch(
+        pkgName,
+        buildFilePath('default.json'),
+        endpoint
+      );
     } catch (err) {
       if (err.message !== PRESET_DEP_NOT_FOUND) {
         throw err;
       }
       logger.debug('default.json preset not found - trying renovate.json');
-      jsonContent = await fetch(pkgName, 'renovate.json', endpoint);
+      jsonContent = await fetch(
+        pkgName,
+        buildFilePath('renovate.json'),
+        endpoint
+      );
     }
   } else {
-    jsonContent = await fetch(pkgName, `${fileName}.json`, endpoint);
+    jsonContent = await fetch(
+      pkgName,
+      buildFilePath(`${fileName}.json`),
+      endpoint
+    );
   }
 
   if (!jsonContent) {