diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md
index b45d28f04a076b192390746942d76ceaddf612ff..36b803d423aa57cf4a00d41519ca274cc98a9e4b 100644
--- a/docs/usage/self-hosted-configuration.md
+++ b/docs/usage/self-hosted-configuration.md
@@ -77,6 +77,15 @@ RFC5322-compliant string if you wish to customise the git author for commits.
 
 ## gitPrivateKey
 
+This should be an armored private key, e.g. the type you get from running `gpg --export-secret-keys --armor 92066A17F0D1707B4E96863955FEF5171C45FAE5 > private.key`. Replace the newlines with `\n` before adding the resulting single-line value to your bot's config.
+
+It will be loaded _lazily_. Before the first commit in a repository, Renovate will:
+
+- First, run `gpg import` if it hasn't been run before
+- Then, run `git config user.signingkey` and `git config commit.gpgsign true`
+
+The `git` commands are run locally in the cloned repo instead of globally to reduce the chance of causing unintended consequences with global git configs on shared systems.
+
 ## logContext
 
 `logContext` is included with each log entry only if `logFormat="json"` - it is not included in the pretty log output. If left as default (null), a random short ID will be selected.
diff --git a/lib/config/common.ts b/lib/config/common.ts
index 4be8e8a8b3e4c0655263ed6479c0a05f801bf9f6..2401e793ce87a3d40b9b7f5f8d427f567f56905e 100644
--- a/lib/config/common.ts
+++ b/lib/config/common.ts
@@ -92,6 +92,7 @@ export interface RenovateAdminConfig {
   requireConfig?: boolean;
   trustLevel?: 'low' | 'high';
   redisUrl?: string;
+  gitPrivateKey?: string;
 }
 
 export type PostUpgradeTasks = {
diff --git a/lib/constants/error-messages.ts b/lib/constants/error-messages.ts
index d477d8da0d41e46374e433d02e056a6ceabb4474..b1bb53ef6de252c5f0f3f4c984fdbba2d24f9b74 100644
--- a/lib/constants/error-messages.ts
+++ b/lib/constants/error-messages.ts
@@ -6,6 +6,7 @@ export const SYSTEM_INSUFFICIENT_MEMORY = 'out-of-memory';
 export const PLATFORM_AUTHENTICATION_ERROR = 'authentication-error';
 export const PLATFORM_BAD_CREDENTIALS = 'bad-credentials';
 export const PLATFORM_FAILURE = 'platform-failure';
+export const PLATFORM_GPG_FAILED = 'gpg-failed';
 export const PLATFORM_INTEGRATION_UNAUTHORIZED = 'integration-unauthorized';
 export const PLATFORM_NOT_FOUND = 'platform-not-found';
 export const PLATFORM_RATE_LIMIT_EXCEEDED = 'rate-limit-exceeded';
diff --git a/lib/platform/bitbucket-server/index.ts b/lib/platform/bitbucket-server/index.ts
index d9990be694624d3c1b0f6098378718ed507e9d21..1147ab5d033dcae8555417161a8f0917b45dedca 100644
--- a/lib/platform/bitbucket-server/index.ts
+++ b/lib/platform/bitbucket-server/index.ts
@@ -112,7 +112,6 @@ export function cleanRepo(): Promise<void> {
 // Initialize GitLab by getting base branch
 export async function initRepo({
   repository,
-  gitPrivateKey,
   localDir,
   optimizeForDisabled,
   bbUseDefaultReviewers,
@@ -161,7 +160,6 @@ export async function initRepo({
   config = {
     projectKey,
     repositorySlug,
-    gitPrivateKey,
     repository,
     prVersions: new Map<number, number>(),
     username: opts.username,
diff --git a/lib/platform/common.ts b/lib/platform/common.ts
index 77ba0ff1048fcedffb8f0e88f1aedfa9bca16ed5..bea0d2864009c8dff367ea8cd6b289563e4986de 100644
--- a/lib/platform/common.ts
+++ b/lib/platform/common.ts
@@ -85,7 +85,6 @@ export interface RepoConfig {
 export interface RepoParams {
   azureWorkItemId?: number; // shouldn't this be configurable within a renovate.json?
   bbUseDefaultReviewers?: boolean; // shouldn't this be configurable within a renovate.json?
-  gitPrivateKey?: string;
   localDir: string;
   optimizeForDisabled: boolean;
   repository: string;
diff --git a/lib/platform/git/private-key.spec.ts b/lib/platform/git/private-key.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7a8cdf633571336c3755904c666123639fe0ac8a
--- /dev/null
+++ b/lib/platform/git/private-key.spec.ts
@@ -0,0 +1,35 @@
+import { getName, mocked } from '../../../test/util';
+import * as exec_ from '../../util/exec';
+import { setPrivateKey, writePrivateKey } from './private-key';
+
+jest.mock('fs-extra');
+jest.mock('../../util/exec');
+
+const exec = mocked(exec_);
+
+describe(getName(__filename), () => {
+  describe('writePrivateKey()', () => {
+    it('returns if no private key', async () => {
+      await expect(writePrivateKey('/tmp/some-repo')).resolves.not.toThrow();
+    });
+    it('throws error if failing', async () => {
+      setPrivateKey('some-key');
+      exec.exec.mockResolvedValueOnce({
+        stderr: `something wrong`,
+        stdout: '',
+      });
+      await expect(writePrivateKey('/tmp/some-repo')).rejects.toThrow();
+    });
+    it('imports the private key', async () => {
+      setPrivateKey('some-key');
+      exec.exec.mockResolvedValueOnce({
+        stderr: `gpg: key BADC0FFEE: secret key imported\nfoo\n`,
+        stdout: '',
+      });
+      await expect(writePrivateKey('/tmp/some-repo')).resolves.not.toThrow();
+    });
+    it('does not import the key again', async () => {
+      await expect(writePrivateKey('/tmp/some-repo')).resolves.not.toThrow();
+    });
+  });
+});
diff --git a/lib/platform/git/private-key.ts b/lib/platform/git/private-key.ts
new file mode 100644
index 0000000000000000000000000000000000000000..680134f4b5632ac1072cfa2634a43a08591132e8
--- /dev/null
+++ b/lib/platform/git/private-key.ts
@@ -0,0 +1,45 @@
+import os from 'os';
+import path from 'path';
+import fs from 'fs-extra';
+import { PLATFORM_GPG_FAILED } from '../../constants/error-messages';
+import { logger } from '../../logger';
+import { exec } from '../../util/exec';
+
+let gitPrivateKey: string;
+let keyId: string;
+
+export function setPrivateKey(key: string): void {
+  gitPrivateKey = key;
+}
+
+async function importKey(): Promise<void> {
+  if (keyId) {
+    return;
+  }
+  const keyFileName = path.join(os.tmpdir() + '/git-private.key');
+  await fs.outputFile(keyFileName, gitPrivateKey);
+  const { stdout, stderr } = await exec(`gpg --import ${keyFileName}`);
+  logger.debug({ stdout, stderr }, 'Private key import result');
+  keyId = (stdout + stderr)
+    .split('\n')
+    .find((line) => line.includes('secret key imported'))
+    .replace('gpg: key ', '')
+    .split(':')
+    .shift();
+  await fs.remove(keyFileName);
+}
+
+export async function writePrivateKey(cwd: string): Promise<void> {
+  if (!gitPrivateKey) {
+    return;
+  }
+  logger.debug('Setting git private key');
+  try {
+    await importKey();
+    await exec(`git config user.signingkey ${keyId}`, { cwd });
+    await exec(`git config commit.gpgsign true`, { cwd });
+  } catch (err) {
+    logger.warn({ err }, 'Error writing git private key');
+    throw new Error(PLATFORM_GPG_FAILED);
+  }
+}
diff --git a/lib/platform/git/storage.ts b/lib/platform/git/storage.ts
index b3253f1b3ef0d535e44fac3f77d102803680a71a..a94eede6486e0d4a7921288388e0376398b1e0d3 100644
--- a/lib/platform/git/storage.ts
+++ b/lib/platform/git/storage.ts
@@ -13,6 +13,7 @@ import {
 import { logger } from '../../logger';
 import * as limits from '../../workers/global/limits';
 import { CommitFilesConfig } from '../common';
+import { writePrivateKey } from './private-key';
 
 declare module 'fs-extra' {
   export function exists(pathLike: string): Promise<boolean>;
@@ -24,7 +25,6 @@ interface StorageConfig {
   localDir: string;
   baseBranch?: string;
   url: string;
-  gitPrivateKey?: string;
   extraCloneOpts?: Git.Options;
 }
 
@@ -84,6 +84,8 @@ export class Storage {
 
   private _cwd: string | undefined;
 
+  private _privateKeySet = false;
+
   private async _resetToBranch(branchName: string): Promise<void> {
     logger.debug(`resetToBranch(${branchName})`);
     await this._git.raw(['reset', '--hard']);
@@ -196,14 +198,6 @@ export class Storage {
       }
       logger.warn({ err }, 'Cannot retrieve latest commit date');
     }
-    // istanbul ignore if
-    if (config.gitPrivateKey) {
-      logger.debug('Git private key configured, but not being set');
-    } else {
-      logger.debug('No git private key present - commits will be unsigned');
-      await this._git.raw(['config', 'commit.gpgsign', 'false']);
-    }
-
     if (global.gitAuthor) {
       logger.debug({ gitAuthor: global.gitAuthor }, 'Setting git author');
       try {
@@ -470,6 +464,10 @@ export class Storage {
     message,
   }: CommitFilesConfig): Promise<string | null> {
     logger.debug(`Committing files to branch ${branchName}`);
+    if (!this._privateKeySet) {
+      await writePrivateKey(this._cwd);
+      this._privateKeySet = true;
+    }
     try {
       await this._git.reset('hard');
       await this._git.raw(['clean', '-fd']);
diff --git a/lib/platform/gitea/index.ts b/lib/platform/gitea/index.ts
index f470d407c701f07c9aa154ef17fe95b0bd679cce..520e01636d104bc9431c3d46a0f7ca28acb551c1 100644
--- a/lib/platform/gitea/index.ts
+++ b/lib/platform/gitea/index.ts
@@ -45,7 +45,6 @@ type GiteaRenovateConfig = {
 
 interface GiteaRepoConfig {
   storage: GitStorage;
-  gitPrivateKey?: string;
   repository: string;
   localDir: string;
   defaultBranch: string;
@@ -248,7 +247,6 @@ const platform: Platform = {
 
   async initRepo({
     repository,
-    gitPrivateKey,
     localDir,
     optimizeForDisabled,
   }: RepoParams): Promise<RepoConfig> {
@@ -257,7 +255,6 @@ const platform: Platform = {
 
     config = {} as any;
     config.repository = repository;
-    config.gitPrivateKey = gitPrivateKey;
     config.localDir = localDir;
 
     // Attempt to fetch information about repository
diff --git a/lib/platform/github/index.ts b/lib/platform/github/index.ts
index adf62069ebdff925f6e41982a26cbc3d01cf4f55..a03c648cc5b781fc4384fe133894d63244386426 100644
--- a/lib/platform/github/index.ts
+++ b/lib/platform/github/index.ts
@@ -204,7 +204,6 @@ export async function initRepo({
   repository,
   forkMode,
   forkToken,
-  gitPrivateKey,
   localDir,
   includeForks,
   renovateUsername,
@@ -229,7 +228,6 @@ export async function initRepo({
   config.localDir = localDir;
   config.repository = repository;
   [config.repositoryOwner, config.repositoryName] = repository.split('/');
-  config.gitPrivateKey = gitPrivateKey;
   let res;
   try {
     res = await githubApi.getJson<{ fork: boolean }>(`repos/${repository}`);
diff --git a/lib/platform/github/types.ts b/lib/platform/github/types.ts
index a4f6ea91b6e091bc8fb89401ca0b63f2d7ff24ed..1dad39f300c75704477364114f6475c13307d444 100644
--- a/lib/platform/github/types.ts
+++ b/lib/platform/github/types.ts
@@ -43,7 +43,6 @@ export interface LocalRepoConfig {
   baseBranch: string;
   defaultBranch: string;
   enterpriseVersion: string;
-  gitPrivateKey?: string;
   repositoryOwner: string;
   repository: string | null;
   localDir: string;
diff --git a/lib/platform/gitlab/index.ts b/lib/platform/gitlab/index.ts
index 486dc278d64917db3808d74eaa132d886a326138..a22c27afcd0a195bdd5667e36eb0ea9f543bbbff 100644
--- a/lib/platform/gitlab/index.ts
+++ b/lib/platform/gitlab/index.ts
@@ -47,7 +47,6 @@ type MergeMethod = 'merge' | 'rebase_merge' | 'ff';
 const defaultConfigFile = configFileNames[0];
 let config: {
   storage: GitStorage;
-  gitPrivateKey?: string;
   repository: string;
   localDir: string;
   defaultBranch: string;
@@ -140,13 +139,11 @@ export function cleanRepo(): Promise<void> {
 // Initialize GitLab by getting base branch
 export async function initRepo({
   repository,
-  gitPrivateKey,
   localDir,
   optimizeForDisabled,
 }: RepoParams): Promise<RepoConfig> {
   config = {} as any;
   config.repository = urlEscape(repository);
-  config.gitPrivateKey = gitPrivateKey;
   config.localDir = localDir;
 
   type RepoResponse = {
diff --git a/lib/platform/index.ts b/lib/platform/index.ts
index ff4dec6229c8de43a86dcd6d1d7e95bb388c546e..531be8c536bc1e3685c5f7abf68042ba08af3242 100644
--- a/lib/platform/index.ts
+++ b/lib/platform/index.ts
@@ -6,6 +6,7 @@ import { logger } from '../logger';
 import * as hostRules from '../util/host-rules';
 import platforms from './api.generated';
 import { Platform } from './common';
+import { setPrivateKey } from './git/private-key';
 
 export * from './common';
 
@@ -39,6 +40,7 @@ export function setPlatformApi(name: string): void {
 export async function initPlatform(
   config: RenovateConfig
 ): Promise<RenovateConfig> {
+  setPrivateKey(config.gitPrivateKey);
   setPlatformApi(config.platform);
   // TODO: types
   const platformInfo = await platform.initPlatform(config);
diff --git a/lib/workers/repository/init/apis.ts b/lib/workers/repository/init/apis.ts
index 526825540084a7317b864d8b55e79244c31f1df4..497474c5cfe288c4eb8ef928d44967341d8175b3 100644
--- a/lib/workers/repository/init/apis.ts
+++ b/lib/workers/repository/init/apis.ts
@@ -26,6 +26,5 @@ export async function initApis(
   config = await getPlatformConfig(config as never);
   npmApi.resetMemCache();
   npmApi.setNpmrc(config.npmrc);
-  delete config.gitPrivateKey;
   return config;
 }