diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index ba8174893dfc8fa1ed385d31429a73ba89a6f9e8..1565fc70d33710d8b2de89a26a6d667d0ea0f96d 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -1172,6 +1172,49 @@ By default, Renovate will detect if it has proposed an update to a project befor
 
 Typically you shouldn't need to modify this setting.
 
+## regexManagers
+
+`regexManagers` entries are used to configure the `regex` Manager in Renovate.
+
+Users can define custom managers for cases such as:
+
+- Proprietary file formats or conventions
+- Popular file formats not yet supported as a manager by Renovate
+
+The custom manager concept is based on using Regular Expression named capture groups. For the fields `datasource`, `depName` and `currentValue`, it's mandatory to have either a named capture group matching them (e.g. `(?<depName>.*)`) or to configure it's corresponding template (e.g. `depNameTemplate`). It's not recommended to do both, due to the potential for confusion. It is recommended to also include `versioning` however if it is missing then it will default to `semver`.
+
+For more details and examples, see the documentation page the for the regex manager [here](/modules/managers/regex/).
+
+### matchStrings
+
+`matchStrings` should each be a valid regular expression, optionally with named capture groups. Currently only a length of one `matchString` is supported.
+
+Example:
+
+```json
+{
+  "matchStrings": [
+    "ENV .*?_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)\\s"
+  ]
+}
+```
+
+### depNameTemplate
+
+If `depName` cannot be captured with a named capture group in `matchString` then it can be defined manually using this field. It will be compiled using `handlebars` and the regex `groups` result.
+
+### lookupNameTemplate
+
+`lookupName` is used for looking up dependency versions. It will be compiled using `handlebars` and the regex `groups` result. It will default to the value of `depName` if left unconfigured/undefined.
+
+### datasourceTemplate
+
+If the `datasource` for a dependency is not captured with a named group then it can be defined in config using this field. It will be compiled using `handlebars` and the regex `groups` result.
+
+### versioningTemplate
+
+If the `versioning` for a dependency is not captured with a named group then it can be defined in config using this field. It will be compiled using `handlebars` and the regex `groups` result.
+
 ## registryUrls
 
 This is only necessary in case you need to manually configure a registry URL to use for datasource lookups. Applies to PyPI (pip) only for now. Supports only one URL for now but is defined as a list for forward compatibility.
diff --git a/lib/config/__snapshots__/validation.spec.ts.snap b/lib/config/__snapshots__/validation.spec.ts.snap
index 1cefb6bf69df24f6e69254e243fd1833347507ca..83336f9b5cb4fa5e3ae6296495f16f666cc57ee5 100644
--- a/lib/config/__snapshots__/validation.spec.ts.snap
+++ b/lib/config/__snapshots__/validation.spec.ts.snap
@@ -75,6 +75,15 @@ Array [
 ]
 `;
 
+exports[`config/validation validateConfig(config) errors if regexManager fields are missing 1`] = `
+Array [
+  Object {
+    "depName": "Configuration Error",
+    "message": "Regex Managers must contain currentValueTemplate configuration or regex group named currentValue",
+  },
+]
+`;
+
 exports[`config/validation validateConfig(config) ignore packageRule nesting validation for presets 1`] = `Array []`;
 
 exports[`config/validation validateConfig(config) included managers of the wrong type 1`] = `
diff --git a/lib/config/common.ts b/lib/config/common.ts
index 993c140d8d0ac8c5a60c0dcfde40189d8b61f658..59e162a31f8e25f2f4fdb2d7b7abcafdc635aaef 100644
--- a/lib/config/common.ts
+++ b/lib/config/common.ts
@@ -102,6 +102,14 @@ export type RenovateRepository =
       repository: string;
     };
 
+export interface CustomManager {
+  matchStrings: string[];
+  depNameTemplate?: string;
+  datasourceTemplate?: string;
+  lookupNameTemplate?: string;
+  versioningTemplate?: string;
+}
+
 // TODO: Proper typings
 export interface RenovateConfig
   extends RenovateAdminConfig,
@@ -140,6 +148,7 @@ export interface RenovateConfig
 
   warnings?: ValidationMessage[];
   vulnerabilityAlerts?: RenovateSharedConfig;
+  regexManagers?: CustomManager[];
 }
 
 export type UpdateType =
diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts
index 52630e0bb7096fafddeb6346ae30efe1d0723626..b77ae0962bf39857d4d42a1bc21554fb70b77fb4 100644
--- a/lib/config/definitions.ts
+++ b/lib/config/definitions.ts
@@ -28,7 +28,7 @@ export interface RenovateOptionBase {
 
   name: string;
 
-  parent?: 'hostRules' | 'packageRules' | 'postUpgradeTasks';
+  parent?: 'hostRules' | 'packageRules' | 'postUpgradeTasks' | 'regexManagers';
 
   // used by tests
   relatedOptions?: string[];
@@ -1675,6 +1675,63 @@ const options: RenovateOptions[] = [
     type: 'boolean',
     default: false,
   },
+  {
+    name: 'regexManagers',
+    description: 'Custom managers using regex matching.',
+    type: 'array',
+    subType: 'object',
+    default: [],
+    stage: 'package',
+    cli: true,
+    mergeable: true,
+  },
+  {
+    name: 'matchStrings',
+    description:
+      'Regex capture rule to use. Valid only within `regexManagers` object.',
+    type: 'array',
+    subType: 'string',
+    format: 'regex',
+    parent: 'regexManagers',
+    cli: false,
+    env: false,
+  },
+  {
+    name: 'depNameTemplate',
+    description:
+      'Optional depName for extracted dependencies. Valid only within `regexManagers` object.',
+    type: 'string',
+    parent: 'regexManagers',
+    cli: false,
+    env: false,
+  },
+  {
+    name: 'lookupNameTemplate',
+    description:
+      'Optional lookupName for extracted dependencies, else defaults to depName value. Valid only within `regexManagers` object.',
+    type: 'string',
+    parent: 'regexManagers',
+    cli: false,
+    env: false,
+  },
+  {
+    name: 'datasourceTemplate',
+    description:
+      'Optional datasource for extracted dependencies. Valid only within `regexManagers` object.',
+    type: 'string',
+    parent: 'regexManagers',
+    cli: false,
+    env: false,
+  },
+  {
+    name: 'versioningTemplate',
+    description:
+      'Optional versioning for extracted dependencies. Valid only within `regexManagers` object.',
+    type: 'string',
+    parent: 'regexManagers',
+    cli: false,
+    env: false,
+  },
 ];
 
 export function getOptions(): RenovateOptions[] {
diff --git a/lib/config/validation.spec.ts b/lib/config/validation.spec.ts
index 3e20b4db233674825fabf6f2eabc2792a75267fc..494b9f292a70b0832daaf3d90d77affb9aa84675 100644
--- a/lib/config/validation.spec.ts
+++ b/lib/config/validation.spec.ts
@@ -175,5 +175,88 @@ describe('config/validation', () => {
       expect(warnings).toHaveLength(0);
       expect(errors).toHaveLength(1);
     });
+    it('errors if no regexManager matchStrings', async () => {
+      const config = {
+        regexManagers: [
+          {
+            matchStrings: [],
+          },
+        ],
+      };
+      const { warnings, errors } = await configValidation.validateConfig(
+        config,
+        true
+      );
+      expect(warnings).toHaveLength(0);
+      expect(errors).toHaveLength(1);
+    });
+    it('validates regEx for each matchStrings', async () => {
+      const config = {
+        regexManagers: [
+          {
+            matchStrings: ['***$}{]]['],
+          },
+        ],
+      };
+      const { warnings, errors } = await configValidation.validateConfig(
+        config,
+        true
+      );
+      expect(warnings).toHaveLength(0);
+      expect(errors).toHaveLength(1);
+    });
+    it('passes if regexManager fields are present', async () => {
+      const config = {
+        regexManagers: [
+          {
+            matchStrings: ['ENV (?<currentValue>.*?)\\s'],
+            depNameTemplate: 'foo',
+            datasourceTemplate: 'bar',
+          },
+        ],
+      };
+      const { warnings, errors } = await configValidation.validateConfig(
+        config,
+        true
+      );
+      expect(warnings).toHaveLength(0);
+      expect(errors).toHaveLength(0);
+    });
+    it('errors if extra regexManager fields are present', async () => {
+      const config = {
+        regexManagers: [
+          {
+            matchStrings: ['ENV (?<currentValue>.*?)\\s'],
+            depNameTemplate: 'foo',
+            datasourceTemplate: 'bar',
+            automerge: true,
+          },
+        ],
+      };
+      const { warnings, errors } = await configValidation.validateConfig(
+        config,
+        true
+      );
+      expect(warnings).toHaveLength(0);
+      expect(errors).toHaveLength(1);
+    });
+    it('errors if regexManager fields are missing', async () => {
+      const config = {
+        regexManagers: [
+          {
+            matchStrings: ['ENV (.*?)\\s'],
+            depNameTemplate: 'foo',
+            datasourceTemplate: 'bar',
+          },
+        ],
+      };
+      const { warnings, errors } = await configValidation.validateConfig(
+        config,
+        true
+      );
+      expect(warnings).toHaveLength(0);
+      expect(errors).toMatchSnapshot();
+      expect(errors).toHaveLength(1);
+    });
   });
 });
diff --git a/lib/config/validation.ts b/lib/config/validation.ts
index 694ec8f9e53125fd64ae309411a8be0ca9e3b194..4e6047fc6cc660e1ca018e54d40d2f800a1d7344 100644
--- a/lib/config/validation.ts
+++ b/lib/config/validation.ts
@@ -75,7 +75,7 @@ export async function validateConfig(
         'prTitle',
         'semanticCommitScope',
       ];
-      if (templateKeys.includes(key) && val) {
+      if ((key.endsWith('Template') || templateKeys.includes(key)) && val) {
         try {
           let res = handlebars.compile(val)(config);
           res = handlebars.compile(res)(config);
@@ -201,6 +201,72 @@ export async function validateConfig(
                 }
               }
             }
+            if (key === 'regexManagers') {
+              const allowedKeys = [
+                'fileMatch',
+                'matchStrings',
+                'depNameTemplate',
+                'lookupNameTemplate',
+                'datasourceTemplate',
+                'versioningTemplate',
+              ];
+              for (const regexManager of val) {
+                if (
+                  Object.keys(regexManager).some(k => !allowedKeys.includes(k))
+                ) {
+                  const disallowedKeys = Object.keys(regexManager).filter(
+                    k => !allowedKeys.includes(k)
+                  );
+                  errors.push({
+                    depName: 'Configuration Error',
+                    message: `Regex Manager contains disallowed fields: ${disallowedKeys.join(
+                      ', '
+                    )}`,
+                  });
+                } else if (
+                  !regexManager.matchStrings ||
+                  regexManager.matchStrings.length !== 1
+                ) {
+                  errors.push({
+                    depName: 'Configuration Error',
+                    message: `Regex Manager ${currentPath} must contain a matchStrings array of length one`,
+                  });
+                } else {
+                  let validRegex = false;
+                  for (const matchString of regexManager.matchStrings) {
+                    try {
+                      regEx(matchString);
+                      validRegex = true;
+                    } catch (e) {
+                      errors.push({
+                        depName: 'Configuration Error',
+                        message: `Invalid regExp for ${currentPath}: \`${matchString}\``,
+                      });
+                    }
+                  }
+                  if (validRegex) {
+                    const mandatoryFields = [
+                      'depName',
+                      'currentValue',
+                      'datasource',
+                    ];
+                    for (const field of mandatoryFields) {
+                      if (
+                        !regexManager[`${field}Template`] &&
+                        !regexManager.matchStrings.some(matchString =>
+                          matchString.includes(`(?<${field}>`)
+                        )
+                      ) {
+                        errors.push({
+                          depName: 'Configuration Error',
+                          message: `Regex Managers must contain ${field}Template configuration or regex group named ${field}`,
+                        });
+                      }
+                    }
+                  }
+                }
+              }
+            }
             if (key === 'packagePatterns' || key === 'excludePackagePatterns') {
               for (const pattern of val) {
                 if (pattern !== '*') {
diff --git a/lib/manager/common.ts b/lib/manager/common.ts
index 1ffb55469b99e9496ad86c5f6e2c1bc7cc28c3d9..bcbab989b6a6b94060bf100dc5b550bd04871b94 100644
--- a/lib/manager/common.ts
+++ b/lib/manager/common.ts
@@ -26,6 +26,14 @@ export interface ExtractConfig extends ManagerConfig {
   versioning?: string;
 }
 
+export interface CustomExtractConfig extends ExtractConfig {
+  matchStrings: string[];
+  depNameTemplate?: string;
+  lookupNameTemplate?: string;
+  datasourceTemplate?: string;
+  versioningTemplate?: string;
+}
+
 export interface UpdateArtifactsConfig extends ManagerConfig {
   isLockFileMaintenance?: boolean;
   compatibility?: Record<string, string>;
@@ -91,6 +99,7 @@ export interface PackageFile<T = Record<string, any>>
   skipInstalls?: boolean;
   yarnrc?: string;
   yarnWorkspacesPackages?: string[] | string;
+  matchStrings?: string[];
 }
 
 export interface Package<T> extends ManagerData<T> {
@@ -216,13 +225,13 @@ export interface ManagerApi {
     config: PackageUpdateConfig
   ): Result<PackageUpdateResult[]>;
 
-  getRangeStrategy(config: RangeConfig): RangeStrategy;
+  getRangeStrategy?(config: RangeConfig): RangeStrategy;
 
   updateArtifacts?(
     updateArtifact: UpdateArtifact
   ): Result<UpdateArtifactsResult[] | null>;
 
-  updateDependency(
+  updateDependency?(
     updateDependencyConfig: UpdateDependencyConfig
   ): Result<string | null>;
 }
diff --git a/lib/manager/index.ts b/lib/manager/index.ts
index 1cbb374fd2fac2245aa91aeebca7914e05af0eeb..4d419aac9f1fbc9d3d82429ac0ebe05fc10a945c 100644
--- a/lib/manager/index.ts
+++ b/lib/manager/index.ts
@@ -43,7 +43,6 @@ function validateManager(manager): boolean {
 }
 
 const managers = loadModules<ManagerApi>(__dirname, validateManager);
-
 const managerList = Object.keys(managers);
 
 const languageList = [
diff --git a/lib/manager/regex/__fixtures__/Dockerfile b/lib/manager/regex/__fixtures__/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..f75da5532d18c0a8d22c2db476ba419d8ddb1b6a
--- /dev/null
+++ b/lib/manager/regex/__fixtures__/Dockerfile
@@ -0,0 +1,251 @@
+FROM amd64/node:10.19.0@sha256:a9d108f82e34c84e6e2a9901fda2048b9f5a40f614c3ea1348cbf276a7c2031c AS tsbuild
+
+COPY package.json .
+COPY yarn.lock .
+COPY tools tools
+RUN yarn install --frozen-lockfile
+
+COPY lib lib
+COPY tsconfig.json tsconfig.json
+COPY tsconfig.app.json tsconfig.app.json
+
+RUN yarn build:docker
+
+
+FROM amd64/ubuntu:18.04@sha256:0925d086715714114c1988f7c947db94064fd385e171a63c07730f1fa014e6f9
+
+LABEL maintainer="Rhys Arkins <rhys@arkins.net>"
+LABEL name="renovate"
+LABEL org.opencontainers.image.source="https://github.com/renovatebot/renovate"
+
+ENV APP_ROOT=/usr/src/app
+WORKDIR ${APP_ROOT}
+
+ENV DEBIAN_FRONTEND=noninteractive
+ENV LC_ALL=C.UTF-8
+ENV LANG=C.UTF-8
+
+RUN apt-get update && \
+  apt-get install -y gpg curl wget unzip xz-utils openssh-client bsdtar build-essential openjdk-11-jre-headless dirmngr && \
+  rm -rf /var/lib/apt/lists/*
+
+# The git version of ubuntu 18.04 is too old to sort ref tags properly (see #5477), so update it to the latest stable version
+RUN echo "deb http://ppa.launchpad.net/git-core/ppa/ubuntu bionic main\ndeb-src http://ppa.launchpad.net/git-core/ppa/ubuntu bionic main" > /etc/apt/sources.list.d/git.list && \
+  apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E1DD270288B4E6030699E45FA1715D88E1DF1F24 && \
+  apt-get update && \
+  apt-get -y install git && \
+  rm -rf /var/lib/apt/lists/*
+
+## Gradle (needs java-jre, installed above)
+ENV GRADLE_VERSION=6.2 # gradle-version/gradle&versioning=maven
+
+RUN wget --no-verbose https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip && \
+  unzip -q -d /opt/ gradle-$GRADLE_VERSION-bin.zip && \
+  rm -f gradle-$GRADLE_VERSION-bin.zip && \
+  mv /opt/gradle-$GRADLE_VERSION /opt/gradle && \
+  ln -s /opt/gradle/bin/gradle /usr/local/bin/gradle
+
+## Node.js
+
+# START copy Node.js from https://github.com/nodejs/docker-node/blob/master/10/jessie/Dockerfile
+
+ENV NODE_VERSION=10.19.0 # github-tags/nodejs/node&versioning=node
+
+RUN ARCH= && dpkgArch="$(dpkg --print-architecture)" \
+  && case "${dpkgArch##*-}" in \
+  amd64) ARCH='x64';; \
+  ppc64el) ARCH='ppc64le';; \
+  s390x) ARCH='s390x';; \
+  arm64) ARCH='arm64';; \
+  armhf) ARCH='armv7l';; \
+  i386) ARCH='x86';; \
+  *) echo "unsupported architecture"; exit 1 ;; \
+  esac \
+  # gpg keys listed at https://github.com/nodejs/node#release-keys
+  && set -ex \
+  && for key in \
+  94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \
+  FD3A5288F042B6850C66B31F09FE44734EB7990E \
+  71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \
+  DD8F2338BAE7501E3DD5AC78C273792F7D83545D \
+  C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8 \
+  B9AE9905FFD7803F25714661B63B535A4C206CA9 \
+  77984A986EBC2AA786BC0F66B01FBB92821C587A \
+  8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600 \
+  4ED778F539E3634C779C87C6D7062848A1AB005C \
+  A48C2BEE680E841632CD4E44F07496B3EB3C1762 \
+  B9E2F5981AA6E0CD28160D9FF13993A75599653C \
+  ; do \
+  gpg --batch --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys "$key" || \
+  gpg --batch --keyserver hkp://ipv4.pool.sks-keyservers.net --recv-keys "$key" || \
+  gpg --batch --keyserver hkp://pgp.mit.edu:80 --recv-keys "$key" ; \
+  done \
+  && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH.tar.xz" \
+  && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
+  && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
+  && grep " node-v$NODE_VERSION-linux-$ARCH.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
+  && bsdtar -xJf "node-v$NODE_VERSION-linux-$ARCH.tar.xz" -C /usr/local --strip-components=1 --no-same-owner \
+  && rm "node-v$NODE_VERSION-linux-$ARCH.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
+  && ln -s /usr/local/bin/node /usr/local/bin/nodejs
+
+## END copy Node.js
+
+# Erlang
+
+RUN cd /tmp && \
+  curl https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb -o erlang-solutions_1.0_all.deb && \
+  dpkg -i erlang-solutions_1.0_all.deb && \
+  rm -f erlang-solutions_1.0_all.deb
+
+ENV ERLANG_VERSION=22.0.2-1
+
+RUN apt-get update && \
+  apt-cache policy esl-erlang && \
+  apt-get install -y esl-erlang=1:$ERLANG_VERSION && \
+  rm -rf /var/lib/apt/lists/*
+
+# Elixir
+
+ENV ELIXIR_VERSION=1.8.2
+
+RUN curl -L https://github.com/elixir-lang/elixir/releases/download/v${ELIXIR_VERSION}/Precompiled.zip -o Precompiled.zip && \
+  mkdir -p /opt/elixir-${ELIXIR_VERSION}/ && \
+  unzip Precompiled.zip -d /opt/elixir-${ELIXIR_VERSION}/ && \
+  rm Precompiled.zip
+
+ENV PATH=$PATH:/opt/elixir-${ELIXIR_VERSION}/bin
+
+# PHP Composer
+
+RUN apt-get update && apt-get install -y php-cli php-mbstring && \
+  rm -rf /var/lib/apt/lists/*
+
+ENV COMPOSER_VERSION=1.9.3 # github-releases/composer/composer
+
+RUN php -r "copy('https://github.com/composer/composer/releases/download/$COMPOSER_VERSION/composer.phar', '/usr/local/bin/composer');"
+
+RUN chmod +x /usr/local/bin/composer
+
+# Go Modules
+
+RUN apt-get update && apt-get install -y bzr mercurial && \
+  rm -rf /var/lib/apt/lists/*
+
+ENV GOLANG_VERSION=1.13.4
+
+# Disable GOPROXY and GOSUMDB until we offer a solid solution to configure
+# private repositories.
+ENV GOPROXY=direct GOSUMDB=off
+
+RUN wget -q -O go.tgz "https://golang.org/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz" && \
+  tar -C /usr/local -xzf go.tgz && \
+  rm go.tgz && \
+  export PATH="/usr/local/go/bin:$PATH"
+
+ENV GOPATH=/go
+ENV PATH=$GOPATH/bin:/usr/local/go/bin:$PATH
+
+RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
+
+ENV CGO_ENABLED=0
+
+# Python
+
+RUN apt-get update && apt-get install -y python3.8-dev python3.8-venv python3-distutils && \
+  rm -rf /var/lib/apt/lists/*
+
+RUN rm -fr /usr/bin/python3 && ln /usr/bin/python3.8 /usr/bin/python3
+RUN rm -rf /usr/bin/python && ln /usr/bin/python3.8 /usr/bin/python
+
+# Pip
+
+RUN curl --silent https://bootstrap.pypa.io/get-pip.py | python
+
+# CocoaPods
+RUN apt-get update && apt-get install -y ruby ruby2.5-dev && rm -rf /var/lib/apt/lists/*
+RUN ruby --version
+ENV COCOAPODS_VERSION=1.9.0 # rubygems/cocoapods&versioning=ruby
+RUN gem install --no-rdoc --no-ri cocoapods -v ${COCOAPODS_VERSION}
+
+# Set up ubuntu user and home directory with access to users in the root group (0)
+
+ENV HOME=/home/ubuntu
+RUN groupadd --gid 1000 ubuntu && \
+  useradd --uid 1000 --gid ubuntu --groups 0 --shell /bin/bash --home-dir ${HOME} --create-home ubuntu
+
+
+RUN chown -R ubuntu:0 ${APP_ROOT} ${HOME} && \
+  chmod -R g=u ${APP_ROOT} ${HOME}
+
+# Docker client and group
+
+RUN groupadd -g 999 docker
+RUN usermod -aG docker ubuntu
+
+ENV DOCKER_VERSION=19.03.1 # github-releases/docker/docker-ce&versioning=docker
+
+RUN curl -fsSLO https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz \
+  && tar xzvf docker-${DOCKER_VERSION}.tgz --strip 1 \
+  -C /usr/local/bin docker/docker \
+  && rm docker-${DOCKER_VERSION}.tgz
+
+USER ubuntu
+
+# Cargo
+
+ENV RUST_BACKTRACE=1 \
+  PATH=${HOME}/.cargo/bin:$PATH
+
+ENV RUST_VERSION=1.36.0
+
+RUN set -ex ;\
+  curl https://sh.rustup.rs -sSf | sh -s -- --no-modify-path --profile minimal --default-toolchain ${RUST_VERSION} -y
+
+# Mix and Rebar
+
+RUN mix local.hex --force
+RUN mix local.rebar --force
+
+# Pipenv
+
+ENV PATH="${HOME}/.local/bin:$PATH"
+
+RUN pip install --user pipenv
+
+# Poetry
+
+ENV POETRY_VERSION=1.0.0 # github-releases/python-poetry/poetry
+
+RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - --version ${POETRY_VERSION}
+
+ENV PATH="${HOME}/.poetry/bin:$PATH"
+RUN poetry config virtualenvs.in-project false
+
+# npm
+
+ENV NPM_VERSION=6.10.2 # npm/npm
+
+RUN npm install -g npm@$NPM_VERSION
+
+# Yarn
+
+ENV YARN_VERSION=1.19.1 # npm/yarn
+
+RUN curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version ${YARN_VERSION}
+
+ENV PATH="${HOME}/.yarn/bin:${HOME}/.config/yarn/global/node_modules/.bin:$PATH"
+
+COPY package.json .
+COPY yarn.lock .
+RUN yarn install --production --frozen-lockfile && yarn cache clean
+RUN rm -f yarn.lock
+COPY --from=tsbuild dist dist
+COPY bin bin
+COPY data data
+
+
+# Numeric user ID for the ubuntu user. Used to indicate a non-root user to OpenShift
+USER 1000
+
+ENTRYPOINT ["node", "/usr/src/app/dist/renovate.js"]
+CMD []
diff --git a/lib/manager/regex/__snapshots__/index.spec.ts.snap b/lib/manager/regex/__snapshots__/index.spec.ts.snap
new file mode 100644
index 0000000000000000000000000000000000000000..3cb01713fcf099de06d7c331232b80d9260a011d
--- /dev/null
+++ b/lib/manager/regex/__snapshots__/index.spec.ts.snap
@@ -0,0 +1,107 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`manager/custom/extract extracts multiple dependencies 1`] = `
+Object {
+  "deps": Array [
+    Object {
+      "autoReplaceData": Object {
+        "depIndex": 0,
+        "replaceString": "ENV GRADLE_VERSION=6.2 # gradle-version/gradle&versioning=maven
+",
+      },
+      "currentValue": "6.2",
+      "datasource": "gradle-version",
+      "depName": "gradle",
+      "lookupName": undefined,
+      "versioning": "maven",
+    },
+    Object {
+      "autoReplaceData": Object {
+        "depIndex": 1,
+        "replaceString": "ENV NODE_VERSION=10.19.0 # github-tags/nodejs/node&versioning=node
+",
+      },
+      "currentValue": "10.19.0",
+      "datasource": "github-tags",
+      "depName": "nodejs/node",
+      "lookupName": undefined,
+      "versioning": "node",
+    },
+    Object {
+      "autoReplaceData": Object {
+        "depIndex": 2,
+        "replaceString": "ENV COMPOSER_VERSION=1.9.3 # github-releases/composer/composer
+",
+      },
+      "currentValue": "1.9.3",
+      "datasource": "github-releases",
+      "depName": "composer/composer",
+      "lookupName": undefined,
+      "versioning": "semver",
+    },
+    Object {
+      "autoReplaceData": Object {
+        "depIndex": 3,
+        "replaceString": "ENV COCOAPODS_VERSION=1.9.0 # rubygems/cocoapods&versioning=ruby
+",
+      },
+      "currentValue": "1.9.0",
+      "datasource": "rubygems",
+      "depName": "cocoapods",
+      "lookupName": undefined,
+      "versioning": "ruby",
+    },
+    Object {
+      "autoReplaceData": Object {
+        "depIndex": 4,
+        "replaceString": "ENV DOCKER_VERSION=19.03.1 # github-releases/docker/docker-ce&versioning=docker
+",
+      },
+      "currentValue": "19.03.1",
+      "datasource": "github-releases",
+      "depName": "docker/docker-ce",
+      "lookupName": undefined,
+      "versioning": "docker",
+    },
+    Object {
+      "autoReplaceData": Object {
+        "depIndex": 5,
+        "replaceString": "ENV POETRY_VERSION=1.0.0 # github-releases/python-poetry/poetry
+",
+      },
+      "currentValue": "1.0.0",
+      "datasource": "github-releases",
+      "depName": "python-poetry/poetry",
+      "lookupName": undefined,
+      "versioning": "semver",
+    },
+    Object {
+      "autoReplaceData": Object {
+        "depIndex": 6,
+        "replaceString": "ENV NPM_VERSION=6.10.2 # npm/npm
+",
+      },
+      "currentValue": "6.10.2",
+      "datasource": "npm",
+      "depName": "npm",
+      "lookupName": undefined,
+      "versioning": "semver",
+    },
+    Object {
+      "autoReplaceData": Object {
+        "depIndex": 7,
+        "replaceString": "ENV YARN_VERSION=1.19.1 # npm/yarn
+",
+      },
+      "currentValue": "1.19.1",
+      "datasource": "npm",
+      "depName": "yarn",
+      "lookupName": undefined,
+      "versioning": "semver",
+    },
+  ],
+  "matchStrings": Array [
+    "ENV .*?_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)(\\\\&versioning=(?<versioning>.*?))?\\\\s",
+  ],
+}
+`;
diff --git a/lib/manager/regex/index.spec.ts b/lib/manager/regex/index.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..71cc814ae8cfebf1f823547c38af045f75e08149
--- /dev/null
+++ b/lib/manager/regex/index.spec.ts
@@ -0,0 +1,57 @@
+import { readFileSync } from 'fs';
+import { resolve } from 'path';
+import { extractPackageFile } from '.';
+
+const dockerfileContent = readFileSync(
+  resolve(__dirname, `./__fixtures__/Dockerfile`),
+  'utf8'
+);
+describe('manager/custom/extract', () => {
+  it('extracts multiple dependencies', async () => {
+    const config = {
+      matchStrings: [
+        'ENV .*?_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)(\\&versioning=(?<versioning>.*?))?\\s',
+      ],
+      versioningTemplate:
+        '{{#if versioning}}{{versioning}}{{else}}semver{{/if}}',
+    };
+    const res = await extractPackageFile(
+      dockerfileContent,
+      'Dockerfile',
+      config
+    );
+    expect(res).toMatchSnapshot();
+    expect(res.deps).toHaveLength(8);
+    expect(res.deps.find(dep => dep.depName === 'yarn').versioning).toEqual(
+      'semver'
+    );
+    expect(res.deps.find(dep => dep.depName === 'gradle').versioning).toEqual(
+      'maven'
+    );
+  });
+  it('returns null if no dependencies found', async () => {
+    const config = {
+      matchStrings: [
+        'ENV .*?_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)(\\&versioning=(?<versioning>.*?))?\\s',
+      ],
+      versioningTemplate:
+        '{{#if versioning}}{{versioning}}{{else}}semver{{/if}}',
+    };
+    const res = await extractPackageFile('', 'Dockerfile', config);
+    expect(res).toBeNull();
+  });
+  it('returns null if invalid handlebars template', async () => {
+    const config = {
+      matchStrings: [
+        'ENV .*?_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)(\\&versioning=(?<versioning>.*?))?\\s',
+      ],
+      versioningTemplate: '{{#if versioning}}{{versioning}}{{else}}semver',
+    };
+    const res = await extractPackageFile(
+      dockerfileContent,
+      'Dockerfile',
+      config
+    );
+    expect(res).toBeNull();
+  });
+});
diff --git a/lib/manager/regex/index.ts b/lib/manager/regex/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..866c0ed6ceeda713ac349c514ec1781373a0dbb4
--- /dev/null
+++ b/lib/manager/regex/index.ts
@@ -0,0 +1,57 @@
+import * as handlebars from 'handlebars';
+import { CustomExtractConfig, PackageFile, Result } from '../common';
+import { regEx } from '../../util/regex';
+import { logger } from '../../logger';
+
+export const autoReplace = true;
+
+export const defaultConfig = {};
+
+export function extractPackageFile(
+  content: string,
+  packageFile: string,
+  config: CustomExtractConfig
+): Result<PackageFile | null> {
+  const regexMatch = regEx(config.matchStrings[0], 'g');
+  const deps = [];
+  let matchResult;
+  let depIndex = 0;
+  do {
+    matchResult = regexMatch.exec(content);
+    if (matchResult) {
+      const dep: any = {};
+      const { groups } = matchResult;
+      const fields = [
+        'depName',
+        'lookupName',
+        'currentValue',
+        'datasource',
+        'versioning',
+      ];
+      for (const field of fields) {
+        const fieldTemplate = `${field}Template`;
+        if (config[fieldTemplate]) {
+          try {
+            dep[field] = handlebars.compile(config[fieldTemplate])(groups);
+          } catch (err) {
+            logger.warn(
+              { template: config[fieldTemplate] },
+              'Error compiling handlebars template for custom manager'
+            );
+            return null;
+          }
+        } else {
+          dep[field] = groups[field];
+        }
+      }
+      dep.autoReplaceData = {
+        depIndex,
+        replaceString: `${matchResult[0]}`,
+      };
+      deps.push(dep);
+    }
+    depIndex += 1;
+  } while (matchResult);
+  if (deps.length) return { deps, matchStrings: config.matchStrings };
+  return null;
+}
diff --git a/lib/manager/regex/readme.md b/lib/manager/regex/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..f13653051cd1ae823fb8ebf2d9eaa12b2cba6433
--- /dev/null
+++ b/lib/manager/regex/readme.md
@@ -0,0 +1,88 @@
+The `regex` manager is designed to allow users to manually configure Renovate for how to find dependencies that aren't detected by the built-in package managers.
+
+This manager is unique in Renovate in that:
+
+- It is configurable via regex named capture groups
+- Through the use of the `regexManagers` config, multiple "regex managers" can be created for the same repository.
+
+### Required Fields
+
+The first two required fields are `fileMatch` and `matchStrings`. `fileMatch` works the same as any manager, while `matchStrings` is a `regexManagers` concept and is used for configuring a regular expression with named capture groups.
+
+In order for Renovate to look up a dependency and decide about updates, it then needs the following information about each dependency:
+
+- The dependency's name
+- Which `datasource` to look up (e.g. npm, Docker, GitHub tags, etc)
+- Which version scheme to apply (defaults to `semver`, but also may be other values like `pep440`)
+
+Configuration-wise, it works like this:
+
+- You must capture the `currentValue` of the dependency in a named capture group
+- You must have either a `depName` capture group or a `depNameTemplate` config field
+- You can optionally have a `lookupName` capture group or a `lookupNameTemplate` if it differs from `depName`
+- You must have either a `datasource` capture group or a `datasourceTemplate` config field
+- You can optionally have a `versioning` capture group or a `versioningTemplate` config field. If neither are present, `semver` will be used as the default
+
+### Regular Expression Capture Groups
+
+To be fully effective with the regex manager, you will need to understand regular expressions and named capture groups, although sometimes enough examples can compensate for lack of experience.
+
+Consider this `Dockerfile`:
+
+```
+FROM node:12
+ENV YARN_VERSION=1.19.1
+RUN curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version ${YARN_VERSION}
+```
+
+You would need to capture the `currentValue` using a named capture group, like so: `ENV YARN_VERSION=(?<currentValue>.*?)\n`.
+
+If you're looking for an online regex testing tool that supports capture groups, try [https://regex101.com/](https://regex101.com/).
+
+### Configuration templates
+
+In many cases, named capture groups alone won't be enough and you'll need to configure Renovate with additional information about how to look up a dependency. Continuing the above example with Yarn, here is the full config:
+
+```json
+{
+  "regexManagers": [
+    {
+      "fileMatch": ["^Dockerfile$"],
+      "matchStrings": ["ENV YARN_VERSION=(?<currentValue>.*?)\n"],
+      "depNameTemplate": "yarn",
+      "datasourceTemplate": "npm"
+    }
+  ]
+}
+```
+
+### Advanced Capture
+
+Let's say that your `Dockerfile` has many `ENV` variables you want to keep updated and you prefer not to write one `regexManagers` rule per variable. Instead you could enhance your `Dockerfile` like the following:
+
+```
+ENV NODE_VERSION=10.19.0 # github-tags/nodejs/node&versioning=node
+ENV COMPOSER_VERSION=1.9.3 # github-releases/composer/composer
+ENV DOCKER_VERSION=19.03.1 # github-releases/docker/docker-ce&versioning=docker
+ENV YARN_VERSION=1.19.1 # npm/yarn
+```
+
+The above (obviously not a complete `Dockerfile`, but abbreviated for this example), could then be supported accordingly:
+
+```json
+{
+  "regexManagers": [
+    {
+      "fileMatch": ["^Dockerfile$"],
+      "matchStrings": [
+        "ENV .*?_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)(\\&versioning=(?<versioning>.*?))?\\s"
+      ],
+      "versioningTemplate": "{{#if versioning}}{{versioning}}{{else}}semver{{/if}}"
+    }
+  ]
+}
+```
+
+In the above the `versioningTemplate` is not actually necessary because Renovate already defaults to `semver` versioning, but it has been included to help illustrate why we call these fields _templates_. They are named this way because they are compiled using `handlebars` and so can be composed from values you collect in named capture groups.
+
+By adding the comments to the `Dockerfile`, you can see that instead of four separate `regexManagers` being required, there is now only one - and the `Dockerfile` itself is now somewhat better documented too. The syntax we used there is completely arbitrary and you may choose your own instead if you prefer - just be sure to update your `matchStrings` regex.
diff --git a/lib/workers/repository/extract/__snapshots__/index.spec.ts.snap b/lib/workers/repository/extract/__snapshots__/index.spec.ts.snap
index c3293e31cf1b3203791acc88238fd03d60c3a67f..310997713ce6dfb22c37062debc2221b51681243 100644
--- a/lib/workers/repository/extract/__snapshots__/index.spec.ts.snap
+++ b/lib/workers/repository/extract/__snapshots__/index.spec.ts.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`workers/repository/extract/index extractAllDependencies() skips non-enabled maangers 1`] = `
+exports[`workers/repository/extract/index extractAllDependencies() skips non-enabled managers 1`] = `
 Object {
   "npm": Array [
     Object {},
diff --git a/lib/workers/repository/extract/index.spec.ts b/lib/workers/repository/extract/index.spec.ts
index 7014d9ae4b6a9ce48bbfa71a996bcaa917cdb9b4..4703e6f1c1f18d94b84ef564951fe527fa61d9f9 100644
--- a/lib/workers/repository/extract/index.spec.ts
+++ b/lib/workers/repository/extract/index.spec.ts
@@ -19,11 +19,17 @@ describe('workers/repository/extract/index', () => {
       const res = await extractAllDependencies(config);
       expect(Object.keys(res).includes('ansible')).toBe(true);
     });
-    it('skips non-enabled maangers', async () => {
+    it('skips non-enabled managers', async () => {
       config.enabledManagers = ['npm'];
       managerFiles.getManagerPackageFiles.mockResolvedValue([{} as never]);
       const res = await extractAllDependencies(config);
       expect(res).toMatchSnapshot();
     });
+    it('checks custom managers', async () => {
+      managerFiles.getManagerPackageFiles.mockResolvedValue([{} as never]);
+      config.regexManagers = [{ matchStrings: [''] }];
+      const res = await extractAllDependencies(config);
+      expect(Object.keys(res).includes('regex')).toBe(true);
+    });
   });
 });
diff --git a/lib/workers/repository/extract/index.ts b/lib/workers/repository/extract/index.ts
index 5baf4643b156fab819b83ca617d5318113a3d6b6..639e3144bea5b10ac5e2af471ebae207a6d745b0 100644
--- a/lib/workers/repository/extract/index.ts
+++ b/lib/workers/repository/extract/index.ts
@@ -1,6 +1,10 @@
 import { logger } from '../../../logger';
 import { getManagerList } from '../../../manager';
-import { getManagerConfig, RenovateConfig } from '../../../config';
+import {
+  getManagerConfig,
+  mergeChildConfig,
+  RenovateConfig,
+} from '../../../config';
 import { getManagerPackageFiles } from './manager-files';
 import { PackageFile } from '../../../manager/common';
 
@@ -18,8 +22,24 @@ export async function extractAllDependencies(
       continue; // eslint-disable-line
     }
     const managerConfig = getManagerConfig(config, manager);
+    let packageFiles = [];
+    if (manager === 'regex') {
+      for (const regexManager of config.regexManagers) {
+        const regexManagerConfig = mergeChildConfig(
+          managerConfig,
+          regexManager
+        );
+        const customPackageFiles = await getManagerPackageFiles(
+          regexManagerConfig
+        );
+        if (customPackageFiles) {
+          packageFiles = packageFiles.concat(customPackageFiles);
+        }
+      }
+    } else {
+      packageFiles = await getManagerPackageFiles(managerConfig);
+    }
     managerConfig.manager = manager;
-    const packageFiles = await getManagerPackageFiles(managerConfig);
     if (packageFiles && packageFiles.length) {
       fileCount += packageFiles.length;
       logger.debug(`Found ${manager} package files`);