diff --git a/lib/util/docker/index.ts b/lib/util/docker/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..095bb579c835558514913dd81897e04a54fd8251
--- /dev/null
+++ b/lib/util/docker/index.ts
@@ -0,0 +1,43 @@
+type Opt<T> = T | null | undefined;
+
+export interface DockerOptions {
+  image: string;
+  dockerUser?: Opt<string>;
+  volumes?: Opt<Opt<string>[]>;
+  envVars?: Opt<Opt<string>[]>;
+  cwd?: Opt<string>;
+  tag?: Opt<string>;
+  cmdWrap?: Opt<string>;
+}
+
+export function dockerCmd(cmd: string, options: DockerOptions): string {
+  const { dockerUser, volumes, envVars, cwd, image, tag, cmdWrap } = options;
+
+  const result = ['docker run --rm'];
+  if (dockerUser) result.push(`--user=${dockerUser}`);
+
+  if (volumes)
+    result.push(...volumes.filter(x => !!x).map(vol => `-v "${vol}":"${vol}"`));
+
+  if (envVars) result.push(...envVars.filter(x => !!x).map(e => `-e ${e}`));
+
+  if (cwd) result.push(`-w "${cwd}"`);
+
+  const taggedImage = tag ? `${image}:${tag}` : `${image}`;
+  result.push(taggedImage);
+
+  if (cmdWrap) {
+    const regex = /{{\s*cmd\s*}}/;
+    if (regex.test(cmdWrap)) {
+      result.push(cmdWrap.replace(regex, cmd));
+    } /* istanbul ignore next */ else {
+      throw new Error(
+        'dockerCmd(): Provide {{ cmd }} placeholder inside `wrapCmd` parameter'
+      );
+    }
+  } else {
+    result.push(cmd);
+  }
+
+  return result.join(' ');
+}
diff --git a/lib/util/exec.ts b/lib/util/exec.ts
index d9220217f3be5c9d78760d681a504d99dcdfa609..b8bcb59969ae697738fd4d51a81d498114303840 100644
--- a/lib/util/exec.ts
+++ b/lib/util/exec.ts
@@ -1,14 +1,36 @@
-// istanbul ignore file
 import { promisify } from 'util';
-import { exec as cpExec, ExecOptions } from 'child_process';
+import {
+  exec as cpExec,
+  ExecOptions as ChildProcessExecOptions,
+} from 'child_process';
+import { dockerCmd, DockerOptions } from './docker';
 
-const pExec = promisify(cpExec);
+const pExec: (
+  cmd: string,
+  opts: ChildProcessExecOptions & { encoding: string }
+) => Promise<ExecResult> = promisify(cpExec);
+
+export interface ExecOptions extends ChildProcessExecOptions {
+  docker?: DockerOptions;
+}
 
 export interface ExecResult {
   stdout: string;
   stderr: string;
 }
 
-export function exec(cmd: string, options?: ExecOptions): Promise<ExecResult> {
-  return pExec(cmd, { ...options, encoding: 'utf-8' });
+export function exec(
+  cmd: string,
+  options?: ExecOptions & { docker?: DockerOptions }
+): Promise<ExecResult> {
+  let pExecCommand = cmd;
+  const pExecOptions = { ...options, encoding: 'utf-8' };
+
+  if (options && options.docker) {
+    const { cwd, docker } = options;
+    pExecCommand = dockerCmd(cmd, { ...docker, cwd });
+    delete pExecOptions.docker;
+  }
+
+  return pExec(pExecCommand, pExecOptions);
 }
diff --git a/test/util/exec.spec.ts b/test/util/exec.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5a48003716971905ccf9e2866ec9bfe584699de2
--- /dev/null
+++ b/test/util/exec.spec.ts
@@ -0,0 +1,77 @@
+import {
+  exec as _cpExec,
+  ExecOptions as ChildProcessExecOptions,
+} from 'child_process';
+import { exec, ExecOptions } from '../../lib/util/exec';
+
+const cpExec: jest.Mock<typeof _cpExec> = _cpExec as any;
+jest.mock('child_process');
+
+describe('exec()', () => {
+  it('wraps original exec() from "child_process" module', async () => {
+    const cases = [
+      ['foo', {}, 'foo', { encoding: 'utf-8' }],
+      [
+        'foo',
+        { docker: { image: 'bar' } },
+        'docker run --rm bar foo',
+        { encoding: 'utf-8' },
+      ],
+      [
+        'foo',
+        { docker: { image: 'bar', cmdWrap: 'su user -c {{ cmd }}' } },
+        'docker run --rm bar su user -c foo',
+        { encoding: 'utf-8' },
+      ],
+      [
+        'foo',
+        { docker: { image: 'bar', tag: 'latest' } },
+        'docker run --rm bar:latest foo',
+        { encoding: 'utf-8' },
+      ],
+      [
+        'foo',
+        { docker: { image: 'bar' }, cwd: '/current/working/directory' },
+        'docker run --rm -w "/current/working/directory" bar foo',
+        { encoding: 'utf-8', cwd: '/current/working/directory' },
+      ],
+      [
+        'foo',
+        {
+          docker: { image: 'bar', dockerUser: 'baz' },
+        },
+        'docker run --rm --user=baz bar foo',
+        { encoding: 'utf-8' },
+      ],
+      [
+        'foo',
+        {
+          docker: { image: 'bar', volumes: ['/path/to/volume'] },
+        },
+        'docker run --rm -v "/path/to/volume":"/path/to/volume" bar foo',
+        { encoding: 'utf-8' },
+      ],
+      [
+        'foo',
+        {
+          docker: { image: 'bar', envVars: ['SOMETHING_SENSIBLE'] },
+        },
+        'docker run --rm -e SOMETHING_SENSIBLE bar foo',
+        { encoding: 'utf-8' },
+      ],
+    ];
+    for (const [cmd, opts, expectedCmd, expectedOpts] of cases) {
+      let actualCmd: string | null = null;
+      let actualOpts: ChildProcessExecOptions | null = null;
+      cpExec.mockImplementationOnce((execCmd, execOpts, callback) => {
+        actualCmd = execCmd;
+        actualOpts = execOpts;
+        callback(null, { stdout: '', stderr: '' });
+        return undefined;
+      });
+      await exec(cmd as string, opts as ExecOptions);
+      expect(actualCmd).toEqual(expectedCmd);
+      expect(actualOpts).toEqual(expectedOpts);
+    }
+  });
+});