diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index dfd2b35c634ede9b101d2c832cc50268989c09d0..fa062c81e59b3dccd06402daa78617cee1bf0b12 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -48,6 +48,14 @@ const options = [
     type: 'boolean',
     default: false,
   },
+  {
+    name: 'dryRun',
+    description:
+      'If enabled, Renovate will log messages instead of creating/updating/deleting branches and PRs',
+    type: 'boolean',
+    admin: true,
+    default: false,
+  },
   {
     name: 'binarySource',
     description: 'Where to source binaries like `npm` and `yarn` from',
diff --git a/lib/workers/branch/commit.js b/lib/workers/branch/commit.js
index d5a63e1222fd3d36c9058d6bf271f4622f3aa31e..3fcd4a330c5f5c91100cecfd337991aff7b4292a 100644
--- a/lib/workers/branch/commit.js
+++ b/lib/workers/branch/commit.js
@@ -11,15 +11,20 @@ async function commitFilesToBranch(config) {
   if (is.nonEmptyArray(updatedFiles)) {
     logger.debug(`${updatedFiles.length} file(s) to commit`);
 
-    // API will know whether to create new branch or not
-    const res = await platform.commitFilesToBranch(
-      config.branchName,
-      updatedFiles,
-      config.commitMessage,
-      config.parentBranch || config.baseBranch || undefined
-    );
-    if (res) {
-      logger.info({ branch: config.branchName }, `Branch ${res}`);
+    // istanbul ignore if
+    if (config.dryRun) {
+      logger.info('DRY-RUN: Would commit files to branch ' + config.branchName);
+    } else {
+      // API will know whether to create new branch or not
+      const res = await platform.commitFilesToBranch(
+        config.branchName,
+        updatedFiles,
+        config.commitMessage,
+        config.parentBranch || config.baseBranch || undefined
+      );
+      if (res) {
+        logger.info({ branch: config.branchName }, `Branch ${res}`);
+      }
     }
   } else {
     logger.debug(`No files to commit`);
diff --git a/lib/workers/branch/index.js b/lib/workers/branch/index.js
index 1fc91dec54156c27b72a471098ab1ecdd6a48871..b7d8c70f906288b764f0354cf064bce0e2588da6 100644
--- a/lib/workers/branch/index.js
+++ b/lib/workers/branch/index.js
@@ -72,9 +72,22 @@ async function processBranch(branchConfig, prHourlyLimitReached, packageFiles) {
         }
         content +=
           '\n\nIf this PR was closed by mistake or you changed your mind, you can simply rename this PR and you will soon get a fresh replacement PR opened.';
-        await platform.ensureComment(existingPr.number, subject, content);
+        // istanbul ignore if
+        if (config.dryRun) {
+          logger.info(
+            'DRY-RUN: Would ensure closed PR comment in PR #' +
+              existingPr.number
+          );
+        } else {
+          await platform.ensureComment(existingPr.number, subject, content);
+        }
         if (branchExists) {
-          await platform.deleteBranch(config.branchName);
+          // istanbul ignore if
+          if (config.dryRun) {
+            logger.info('DRY-RUN: Would delete branch ' + config.branchName);
+          } else {
+            await platform.deleteBranch(config.branchName);
+          }
         }
       } else if (existingPr.state === 'merged') {
         logger.info(
@@ -114,14 +127,27 @@ async function processBranch(branchConfig, prHourlyLimitReached, packageFiles) {
           const labelRebase =
             branchPr.labels && branchPr.labels.includes(config.rebaseLabel);
           if (titleRebase || labelRebase) {
-            await platform.ensureCommentRemoval(branchPr.number, subject);
+            // istanbul ignore if
+            if (config.dryRun) {
+              logger.info(
+                'DRY-RUN: Would ensure PR edited comment removal in PR #' +
+                  branchPr.number
+              );
+            } else {
+              await platform.ensureCommentRemoval(branchPr.number, subject);
+            }
           } else {
             let content =
               ':construction_worker: This PR has received other commits, so Renovate will stop updating it to avoid conflicts or other problems.';
             content += ` If you wish to abandon your changes and have Renovate start over then you can add the label \`${
               config.rebaseLabel
             }\` to this PR and Renovate will reset/recreate it.`;
-            await platform.ensureComment(branchPr.number, subject, content);
+            // istanbul ignore if
+            if (config.dryRun) {
+              logger.info('DRY-RUN: ensure comment in PR #' + branchPr.number);
+            } else {
+              await platform.ensureComment(branchPr.number, subject, content);
+            }
             return 'pr-edited';
           }
         }
@@ -227,7 +253,12 @@ async function processBranch(branchConfig, prHourlyLimitReached, packageFiles) {
       logger.info(
         'Deleting lock file maintenance branch as master lock file no longer needs updating'
       );
-      await platform.deleteBranch(config.branchName);
+      // istanbul ignore if
+      if (config.dryRun) {
+        logger.info('DRY-RUN: Would delete lock file maintenance branch');
+      } else {
+        await platform.deleteBranch(config.branchName);
+      }
       return 'done';
     }
     if (!(config.committedFiles || branchExists)) {
@@ -319,7 +350,14 @@ async function processBranch(branchConfig, prHourlyLimitReached, packageFiles) {
           content += `##### ${error.lockFile}\n\n`;
           content += `\`\`\`\n${error.stderr}\n\`\`\`\n\n`;
         });
-        await platform.ensureComment(pr.number, topic, content);
+        // istanbul ignore if
+        if (config.dryRun) {
+          logger.info(
+            'DRY-RUN: Would ensure lock file error comment in PR #' + pr.number
+          );
+        } else {
+          await platform.ensureComment(pr.number, topic, content);
+        }
         const context = 'renovate/lock-files';
         const description = 'Lock file update failure';
         const state = 'failure';
@@ -330,16 +368,30 @@ async function processBranch(branchConfig, prHourlyLimitReached, packageFiles) {
         // Check if state needs setting
         if (existingState !== state) {
           logger.debug(`Updating status check state to failed`);
-          await platform.setBranchStatus(
-            config.branchName,
-            context,
-            description,
-            state
-          );
+          // istanbul ignore if
+          if (config.dryRun) {
+            logger.info(
+              'DRY-RUN: Would set branch status in ' + config.branchName
+            );
+          } else {
+            await platform.setBranchStatus(
+              config.branchName,
+              context,
+              description,
+              state
+            );
+          }
         }
       } else {
         if (config.updatedLockFiles && config.updatedLockFiles.length) {
-          await platform.ensureCommentRemoval(pr.number, topic);
+          // istanbul ignore if
+          if (config.dryRun) {
+            logger.info(
+              'DRY-RUN: Would ensure comment removal in PR #' + pr.number
+            );
+          } else {
+            await platform.ensureCommentRemoval(pr.number, topic);
+          }
         }
         const prAutomerged = await prWorker.checkAutoMerge(pr, config);
         if (prAutomerged) {
diff --git a/lib/workers/pr/index.js b/lib/workers/pr/index.js
index ca357909bdbbca8d7ff2ad5bcb4201ab37d9b206..a0b7e73a91909e5ce44440e3237d752600c83da3 100644
--- a/lib/workers/pr/index.js
+++ b/lib/workers/pr/index.js
@@ -210,12 +210,16 @@ async function ensurePr(prConfig) {
           'PR body changed'
         );
       }
-
-      await platform.updatePr(existingPr.number, prTitle, prBody);
-      logger.info(
-        { committedFiles: config.committedFiles, pr: existingPr.number },
-        `PR updated`
-      );
+      // istanbul ignore if
+      if (config.dryRun) {
+        logger.info('DRY-RUN: Would update PR #' + existingPr.number);
+      } else {
+        await platform.updatePr(existingPr.number, prTitle, prBody);
+        logger.info(
+          { committedFiles: config.committedFiles, pr: existingPr.number },
+          `PR updated`
+        );
+      }
       return existingPr;
     }
     logger.debug({ branch: branchName, prTitle }, `Creating PR`);
@@ -225,20 +229,31 @@ async function ensurePr(prConfig) {
     }
     let pr;
     try {
-      pr = await platform.createPr(
-        branchName,
-        prTitle,
-        prBody,
-        config.labels,
-        false,
-        config.statusCheckVerify
-      );
-      logger.info({ branch: branchName, pr: pr.number }, 'PR created');
+      // istanbul ignore if
+      if (config.dryRun) {
+        logger.info('DRY-RUN: Would create PR: ' + prTitle);
+        pr = { number: 0, displayNumber: 'Dry run PR' };
+      } else {
+        pr = await platform.createPr(
+          branchName,
+          prTitle,
+          prBody,
+          config.labels,
+          false,
+          config.statusCheckVerify
+        );
+        logger.info({ branch: branchName, pr: pr.number }, 'PR created');
+      }
     } catch (err) {
       logger.warn({ err }, `Failed to create PR`);
       if (err.message === 'Validation Failed (422)') {
         logger.info({ branch: branchName }, 'Deleting invalid branch');
-        await platform.deleteBranch(branchName);
+        // istanbul ignore if
+        if (config.dryRun) {
+          logger.info('DRY-RUN: Would delete branch: ' + config.branchName);
+        } else {
+          await platform.deleteBranch(branchName);
+        }
       }
       // istanbul ignore if
       if (err.statusCode === 502) {
@@ -246,7 +261,12 @@ async function ensurePr(prConfig) {
           { branch: branchName },
           'Deleting branch due to server error'
         );
-        await platform.deleteBranch(branchName);
+        // istanbul ignore if
+        if (config.dryRun) {
+          logger.info('DRY-RUN: Would delete branch: ' + config.branchName);
+        } else {
+          await platform.deleteBranch(branchName);
+        }
       }
       return null;
     }
@@ -258,7 +278,12 @@ async function ensurePr(prConfig) {
         content += '\n___\n * Branch has one or more failed status checks';
       }
       logger.info('Adding branch automerge failure message to PR');
-      await platform.ensureComment(pr.number, subject, content);
+      // istanbul ignore if
+      if (config.dryRun) {
+        logger.info('Would add comment to PR #' + pr.number);
+      } else {
+        await platform.ensureComment(pr.number, subject, content);
+      }
     }
     // Skip assign and review if automerging PR
     if (config.automerge && (await getBranchStatus()) !== 'failure') {
@@ -291,8 +316,13 @@ async function addAssigneesReviewers(config, pr) {
         assignee =>
           assignee.length && assignee[0] === '@' ? assignee.slice(1) : assignee
       );
-      await platform.addAssignees(pr.number, assignees);
-      logger.info({ assignees: config.assignees }, 'Added assignees');
+      // istanbul ignore if
+      if (config.dryRun) {
+        logger.info('DRY-RUN: Would add assignees to PR #', pr.number);
+      } else {
+        await platform.addAssignees(pr.number, assignees);
+        logger.info({ assignees: config.assignees }, 'Added assignees');
+      }
     } catch (err) {
       logger.info(
         { assignees: config.assignees, err },
@@ -306,8 +336,13 @@ async function addAssigneesReviewers(config, pr) {
         reviewer =>
           reviewer.length && reviewer[0] === '@' ? reviewer.slice(1) : reviewer
       );
-      await platform.addReviewers(pr.number, reviewers);
-      logger.info({ reviewers: config.reviewers }, 'Added reviewers');
+      // istanbul ignore if
+      if (config.dryRun) {
+        logger.info('DRY-RUN: Would add assignees to PR #', pr.number);
+      } else {
+        await platform.addReviewers(pr.number, reviewers);
+        logger.info({ reviewers: config.reviewers }, 'Added reviewers');
+      }
     } catch (err) {
       logger.info(
         { assignees: config.assignees, err },
@@ -359,10 +394,22 @@ async function checkAutoMerge(pr, config) {
     }
     if (automergeType === 'pr-comment') {
       logger.info(`Applying automerge comment: ${automergeComment}`);
+      // istanbul ignore if
+      if (config.dryRun) {
+        logger.info(
+          'DRY-RUN: Would add PR automerge comment to PR #' + pr.number
+        );
+        return false;
+      }
       return platform.ensureComment(pr.number, null, automergeComment);
     }
     // Let's merge this
     logger.debug(`Automerging #${pr.number}`);
+    // istanbul ignore if
+    if (config.dryRun) {
+      logger.info('DRY-RUN: Would merge PR #' + pr.number);
+      return false;
+    }
     const res = platform.mergePr(pr.number, branchName);
     logger.info({ pr: pr.number }, 'PR automerged');
     return res;
diff --git a/lib/workers/repository/onboarding/branch/create.js b/lib/workers/repository/onboarding/branch/create.js
index 46fa0f007f5cca37dbd9fa6da9866c227a3e1d17..e60fe8387aaa7972c521d267c368e107b5482b65 100644
--- a/lib/workers/repository/onboarding/branch/create.js
+++ b/lib/workers/repository/onboarding/branch/create.js
@@ -16,16 +16,21 @@ async function createOnboardingBranch(config) {
   } else {
     commitMessage = 'Add renovate.json';
   }
-  await platform.commitFilesToBranch(
-    `renovate/configure`,
-    [
-      {
-        name: 'renovate.json',
-        contents,
-      },
-    ],
-    commitMessage
-  );
+  // istanbul ignore if
+  if (config.dryRun) {
+    logger.info('DRY-RUN: Would commit files to onboaring branch');
+  } else {
+    await platform.commitFilesToBranch(
+      `renovate/configure`,
+      [
+        {
+          name: 'renovate.json',
+          contents,
+        },
+      ],
+      commitMessage
+    );
+  }
 }
 
 module.exports = {
diff --git a/lib/workers/repository/onboarding/branch/rebase.js b/lib/workers/repository/onboarding/branch/rebase.js
index b5caafd7d15a2e62524d89855fe5669842577f49..f8d6785faf82489bdb9c4e8296fe9c5e6a9e8298 100644
--- a/lib/workers/repository/onboarding/branch/rebase.js
+++ b/lib/workers/repository/onboarding/branch/rebase.js
@@ -30,16 +30,21 @@ async function rebaseOnboardingBranch(config) {
   } else {
     commitMessage = 'Add renovate.json';
   }
-  await platform.commitFilesToBranch(
-    onboardingBranch,
-    [
-      {
-        name: 'renovate.json',
-        contents: existingContents || contents,
-      },
-    ],
-    commitMessage
-  );
+  // istanbul ignore if
+  if (config.dryRun) {
+    logger.info('DRY-RUN: Would rebase files in onboaring branch');
+  } else {
+    await platform.commitFilesToBranch(
+      onboardingBranch,
+      [
+        {
+          name: 'renovate.json',
+          contents: existingContents || contents,
+        },
+      ],
+      commitMessage
+    );
+  }
 }
 
 module.exports = {
diff --git a/lib/workers/repository/onboarding/pr/index.js b/lib/workers/repository/onboarding/pr/index.js
index 7f569cbf366a3590e56017023ddde11f32419558..65b03e6683e75eb3d29c00c0adbcf05d110f6970 100644
--- a/lib/workers/repository/onboarding/pr/index.js
+++ b/lib/workers/repository/onboarding/pr/index.js
@@ -99,14 +99,19 @@ Also, you can post questions about your config in [Renovate's Config Help reposi
   const labels = [];
   const useDefaultBranch = true;
   try {
-    const pr = await platform.createPr(
-      onboardingBranch,
-      onboardingPrTitle,
-      prBody,
-      labels,
-      useDefaultBranch
-    );
-    logger.info({ pr: pr.displayNumber }, 'Created onboarding PR');
+    // istanbul ignore if
+    if (config.dryRun) {
+      logger.info('DRY-RUN: Would create onboarding PR');
+    } else {
+      const pr = await platform.createPr(
+        onboardingBranch,
+        onboardingPrTitle,
+        prBody,
+        labels,
+        useDefaultBranch
+      );
+      logger.info({ pr: pr.displayNumber }, 'Created onboarding PR');
+    }
   } catch (err) /* istanbul ignore next */ {
     if (
       err.statusCode === 422 &&
diff --git a/test/workers/branch/commit.spec.js b/test/workers/branch/commit.spec.js
index 2e9eaf644f11b85e0a4e031a72888b6930326a2c..0f9bd621e5d8967a147034f96607fa8a00eee8d9 100644
--- a/test/workers/branch/commit.spec.js
+++ b/test/workers/branch/commit.spec.js
@@ -31,5 +31,14 @@ describe('workers/branch/automerge', () => {
       expect(platform.commitFilesToBranch.mock.calls.length).toBe(1);
       expect(platform.commitFilesToBranch.mock.calls).toMatchSnapshot();
     });
+    it('dry runs', async () => {
+      config.dryRun = true;
+      config.updatedPackageFiles.push({
+        name: 'package.json',
+        contents: 'some contents',
+      });
+      await commitFilesToBranch(config);
+      expect(platform.commitFilesToBranch.mock.calls.length).toBe(0);
+    });
   });
 });
diff --git a/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap b/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap
index c6fa3104854d1950675bab3f1f14ff540d46d7c7..67d800a83bcfeca3cb2fad63e6e6d450808953ad 100644
--- a/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap
+++ b/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap
@@ -21,6 +21,7 @@ Array [
     "commitMessageTopic": "dependency {{depName}}",
     "depName": "@org/a",
     "depNameSanitized": "org-a",
+    "dryRun": false,
     "errors": Array [],
     "gitAuthor": null,
     "gitPrivateKey": null,
@@ -113,6 +114,7 @@ Array [
     "commitMessageTopic": "dependency {{depName}}",
     "depName": "foo",
     "depNameSanitized": "foo",
+    "dryRun": false,
     "errors": Array [],
     "gitAuthor": null,
     "gitPrivateKey": null,
@@ -203,6 +205,7 @@ Array [
     "commitMessagePrefix": null,
     "commitMessageSuffix": null,
     "commitMessageTopic": null,
+    "dryRun": false,
     "errors": Array [],
     "gitAuthor": null,
     "gitPrivateKey": null,
@@ -297,6 +300,7 @@ Array [
     "commitMessageTopic": "dependency {{depName}}",
     "depName": "bar",
     "depNameSanitized": "bar",
+    "dryRun": false,
     "errors": Array [],
     "gitAuthor": null,
     "gitPrivateKey": null,
@@ -387,6 +391,7 @@ Array [
     "commitMessagePrefix": null,
     "commitMessageSuffix": null,
     "commitMessageTopic": null,
+    "dryRun": false,
     "errors": Array [],
     "gitAuthor": null,
     "gitPrivateKey": null,
@@ -481,6 +486,7 @@ Array [
     "commitMessageTopic": "dependency {{depName}}",
     "depName": "baz",
     "depNameSanitized": "baz",
+    "dryRun": false,
     "errors": Array [],
     "gitAuthor": null,
     "gitPrivateKey": null,
@@ -573,6 +579,7 @@ Array [
     "commitMessageTopic": "{{{depName}}} Docker tag",
     "depName": "amd64/node",
     "depNameSanitized": "node",
+    "dryRun": false,
     "errors": Array [],
     "gitAuthor": null,
     "gitPrivateKey": null,
@@ -665,6 +672,7 @@ Array [
     "commitMessageTopic": "{{{depName}}} Docker tag",
     "depName": "calico/node",
     "depNameSanitized": "calico-node",
+    "dryRun": false,
     "errors": Array [],
     "gitAuthor": null,
     "gitPrivateKey": null,
diff --git a/website/docs/self-hosted-configuration.md b/website/docs/self-hosted-configuration.md
index ac09e1963ef993307b71aae15fc2a4e9f8a17ccb..b9a0eee7f7fafb3364c28c09787cbc29ba7e52af 100644
--- a/website/docs/self-hosted-configuration.md
+++ b/website/docs/self-hosted-configuration.md
@@ -15,6 +15,8 @@ Be cautious when using this option - it will run Renovate over _every_ repositor
 
 Set this to 'global' if you wish Renovate to use globally-installed binaries (`npm`, `yarn`, etc) instead of using its bundled versions.
 
+## dryRun
+
 ## endpoint
 
 ## exposeEnv