diff --git a/docs/usage/config-overview.md b/docs/usage/config-overview.md
index 4f5959c036bebad18afa0cb4825c4002cc50d854..8ed23f918d566bac23114e8af5876f8892826020 100644
--- a/docs/usage/config-overview.md
+++ b/docs/usage/config-overview.md
@@ -108,6 +108,8 @@ Read the [Self-hosted experimental environment variables](./self-hosted-experime
 Finally, there are some special environment variables that are loaded _before_ configuration parsing because they are used during logging initialization:
 
 - `LOG_CONTEXT`: a unique identifier used in each log message to track context
+- `LOG_FILE`: used to enable file logging and specify the log file path
+- `LOG_FILE_LEVEL`: log file logging level, defaults to `debug`
 - `LOG_FORMAT`: defaults to a "pretty" human-readable output, but can be changed to "json"
 - `LOG_LEVEL`: most commonly used to change from the default `info` to `debug` logging
 
diff --git a/docs/usage/examples/self-hosting.md b/docs/usage/examples/self-hosting.md
index 07397e80345d970d18330aa9c7921641adb6ad17..a4753c126286533cca19c3857cc51d7ac3015170 100644
--- a/docs/usage/examples/self-hosting.md
+++ b/docs/usage/examples/self-hosting.md
@@ -248,7 +248,7 @@ module.exports = {
 };
 ```
 
-Here change the `logFile` and `repositories` to something appropriate.
+Here change the `repositories` to something appropriate.
 Also replace `gitlab-token` value with the one created during the previous step.
 
 If you're running against GitHub Enterprise Server, then change the `gitlab` values in the example to the equivalent GitHub ones.
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index 0ef942e29a7aebe0e06921e82746a8bce09d9526..0a523e8e03626e20276f8720c29da90a8999c36d 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -548,6 +548,8 @@ const options: RenovateOptions[] = [
     stage: 'global',
     type: 'string',
     globalOnly: true,
+    deprecationMsg:
+      'Instead of configuring log file path in the file config. Use the `LOG_FILE` environment variable instead.',
   },
   {
     name: 'logFileLevel',
@@ -556,6 +558,8 @@ const options: RenovateOptions[] = [
     type: 'string',
     default: 'debug',
     globalOnly: true,
+    deprecationMsg:
+      'Instead of configuring log file level in the file config. Use the `LOG_FILE_LEVEL` environment variable instead.',
   },
   {
     name: 'logContext',
diff --git a/lib/config/validation.spec.ts b/lib/config/validation.spec.ts
index 976c86b8c80f40c742b26c315260a46cc2852e96..410e5db26c0a5683525420975cb5817496063dd0 100644
--- a/lib/config/validation.spec.ts
+++ b/lib/config/validation.spec.ts
@@ -1388,6 +1388,23 @@ describe('config/validation', () => {
   });
 
   describe('validateConfig() -> globaOnly options', () => {
+    it('returns deprecation warnings', async () => {
+      const config = {
+        logFile: 'something',
+      };
+      const { warnings } = await configValidation.validateConfig(
+        'global',
+        config,
+      );
+      expect(warnings).toMatchObject([
+        {
+          message:
+            'Using logFile to specify log file name is deprecated now. Please use the enviroment variable LOG_FILE instead',
+          topic: 'Deprecation Warning',
+        },
+      ]);
+    });
+
     it('validates hostRules.headers', async () => {
       const config = {
         hostRules: [
@@ -1501,6 +1518,23 @@ describe('config/validation', () => {
   });
 
   describe('validate globalOptions()', () => {
+    it('binarySource', async () => {
+      const config = {
+        binarySource: 'invalid' as never,
+      };
+      const { warnings } = await configValidation.validateConfig(
+        'global',
+        config,
+      );
+      expect(warnings).toEqual([
+        {
+          message:
+            'Invalid value `invalid` for `binarySource`. The allowed values are docker, global, install, hermit.',
+          topic: 'Configuration Error',
+        },
+      ]);
+    });
+
     describe('validates string type options', () => {
       it('binarySource', async () => {
         const config = {
diff --git a/lib/config/validation.ts b/lib/config/validation.ts
index 62d1aa6c3cd70b8f2b55438b15be598e3a85d3a0..abdee24bffc7857e986d73d6bee5a708329bd418 100644
--- a/lib/config/validation.ts
+++ b/lib/config/validation.ts
@@ -129,6 +129,8 @@ function getDeprecationMessage(option: string): string | undefined {
     branchName: `Direct editing of branchName is now deprecated. Please edit branchPrefix, additionalBranchPrefix, or branchTopic instead`,
     commitMessage: `Direct editing of commitMessage is now deprecated. Please edit commitMessage's subcomponents instead.`,
     prTitle: `Direct editing of prTitle is now deprecated. Please edit commitMessage subcomponents instead as they will be passed through to prTitle.`,
+    logFile: `Using logFile to specify log file name is deprecated now. Please use the enviroment variable LOG_FILE instead`,
+    logFileLevel: `Using logFileLevel to specify log level for file logging is deprecated now. Please use the enviroment variable LOG_FILE_LEVEL instead`,
   };
   return deprecatedOptions[option];
 }
@@ -937,6 +939,12 @@ async function validateGlobalConfig(
   currentPath: string | undefined,
   config: RenovateConfig,
 ): Promise<void> {
+  if (getDeprecationMessage(key)) {
+    warnings.push({
+      topic: 'Deprecation Warning',
+      message: getDeprecationMessage(key)!,
+    });
+  }
   if (val !== null) {
     if (type === 'string') {
       if (is.string(val)) {
diff --git a/lib/logger/index.ts b/lib/logger/index.ts
index 54cb4ebf4e497adcecd8ff8df08abdafd86e6ad1..504e9b1a2c97e8b738238b5153eb6a284867d314 100644
--- a/lib/logger/index.ts
+++ b/lib/logger/index.ts
@@ -1,6 +1,8 @@
 import is from '@sindresorhus/is';
 import * as bunyan from 'bunyan';
+import fs from 'fs-extra';
 import { nanoid } from 'nanoid';
+import upath from 'upath';
 import cmdSerializer from './cmd-serializer';
 import configSerializer from './config-serializer';
 import errSerializer from './err-serializer';
@@ -20,16 +22,13 @@ if (is.string(process.env.LOG_LEVEL)) {
   process.env.LOG_LEVEL = process.env.LOG_LEVEL.toLowerCase().trim();
 }
 
-validateLogLevel(process.env.LOG_LEVEL);
 const stdout: bunyan.Stream = {
   name: 'stdout',
-  level:
-    (process.env.LOG_LEVEL as bunyan.LogLevel) ||
-    /* istanbul ignore next: not testable */ 'info',
+  level: validateLogLevel(process.env.LOG_LEVEL, 'info'),
   stream: process.stdout,
 };
 
-// istanbul ignore else: not testable
+// istanbul ignore if: not testable
 if (process.env.LOG_FORMAT !== 'json') {
   // TODO: typings (#9615)
   const prettyStdOut = new RenovateStream() as any;
@@ -123,6 +122,19 @@ loggerLevels.forEach((loggerLevel) => {
   logger.once[loggerLevel] = logOnceFn as never;
 });
 
+// istanbul ignore if: not easily testable
+if (is.string(process.env.LOG_FILE)) {
+  // ensure log file directory exists
+  const directoryName = upath.dirname(process.env.LOG_FILE);
+  fs.ensureDirSync(directoryName);
+
+  addStream({
+    name: 'logfile',
+    path: process.env.LOG_FILE,
+    level: validateLogLevel(process.env.LOG_FILE_LEVEL, 'debug'),
+  });
+}
+
 export function setContext(value: string): void {
   logContext = value;
 }
diff --git a/lib/logger/utils.spec.ts b/lib/logger/utils.spec.ts
index 51b3ac222616bf9f0107b2e1a01bee0d970121a7..2073782a91c1c1af7836deb605b97d1ca37ea11b 100644
--- a/lib/logger/utils.spec.ts
+++ b/lib/logger/utils.spec.ts
@@ -11,11 +11,13 @@ describe('logger/utils', () => {
   });
 
   it('checks for valid log levels', () => {
-    expect(validateLogLevel(undefined)).toBeUndefined();
-    expect(validateLogLevel('warn')).toBeUndefined();
-    expect(validateLogLevel('debug')).toBeUndefined();
-    expect(validateLogLevel('trace')).toBeUndefined();
-    expect(validateLogLevel('info')).toBeUndefined();
+    expect(validateLogLevel(undefined, 'info')).toBe('info');
+    expect(validateLogLevel('warn', 'info')).toBe('warn');
+    expect(validateLogLevel('debug', 'info')).toBe('debug');
+    expect(validateLogLevel('trace', 'info')).toBe('trace');
+    expect(validateLogLevel('info', 'info')).toBe('info');
+    expect(validateLogLevel('error', 'info')).toBe('error');
+    expect(validateLogLevel('fatal', 'info')).toBe('fatal');
   });
 
   it.each`
@@ -32,7 +34,7 @@ describe('logger/utils', () => {
       throw new Error(`process.exit: ${number}`);
     });
     expect(() => {
-      validateLogLevel(input);
+      validateLogLevel(input, 'info');
     }).toThrow();
     expect(mockExit).toHaveBeenCalledWith(1);
   });
diff --git a/lib/logger/utils.ts b/lib/logger/utils.ts
index dc49caa7f957ea4b9d02ee17f729da95455d1dc6..5fc70628769b093fce60bfdd3d48076aef419fe4 100644
--- a/lib/logger/utils.ts
+++ b/lib/logger/utils.ts
@@ -277,13 +277,16 @@ export function withSanitizer(streamConfig: bunyan.Stream): bunyan.Stream {
 }
 
 /**
- * A function that terminates exeution if the log level that was entered is
+ * A function that terminates execution if the log level that was entered is
  *  not a valid value for the Bunyan logger.
  * @param logLevelToCheck
- * @returns returns undefined when the logLevelToCheck is valid. Else it stops execution.
+ * @returns returns the logLevel when the logLevelToCheck is valid or the defaultLevel passed as argument when it is undefined. Else it stops execution.
  */
-export function validateLogLevel(logLevelToCheck: string | undefined): void {
-  const allowedValues: bunyan.LogLevel[] = [
+export function validateLogLevel(
+  logLevelToCheck: string | undefined,
+  defaultLevel: bunyan.LogLevelString,
+): bunyan.LogLevelString {
+  const allowedValues: bunyan.LogLevelString[] = [
     'trace',
     'debug',
     'info',
@@ -291,13 +294,14 @@ export function validateLogLevel(logLevelToCheck: string | undefined): void {
     'error',
     'fatal',
   ];
+
   if (
     is.undefined(logLevelToCheck) ||
     (is.string(logLevelToCheck) &&
-      allowedValues.includes(logLevelToCheck as bunyan.LogLevel))
+      allowedValues.includes(logLevelToCheck as bunyan.LogLevelString))
   ) {
     // log level is in the allowed values or its undefined
-    return;
+    return (logLevelToCheck as bunyan.LogLevelString) ?? defaultLevel;
   }
 
   const logger = bunyan.createLogger({
diff --git a/lib/workers/global/config/parse/index.spec.ts b/lib/workers/global/config/parse/index.spec.ts
index 6a4e4169525d6979387a216e704027029133eccb..d13081cc8f944055482aab7ce65547d0448cb550 100644
--- a/lib/workers/global/config/parse/index.spec.ts
+++ b/lib/workers/global/config/parse/index.spec.ts
@@ -1,13 +1,12 @@
 import upath from 'upath';
 import { mocked } from '../../../../../test/util';
-import { readSystemFile } from '../../../../util/fs';
+import { getParentDir, readSystemFile } from '../../../../util/fs';
 import getArgv from './__fixtures__/argv';
 import * as _hostRulesFromEnv from './host-rules-from-env';
 
 jest.mock('../../../../modules/datasource/npm');
 jest.mock('../../../../util/fs');
 jest.mock('./host-rules-from-env');
-jest.mock('../../config.js', () => ({}), { virtual: true });
 
 const { hostRulesFromEnv } = mocked(_hostRulesFromEnv);
 
@@ -174,9 +173,38 @@ describe('workers/global/config/parse/index', () => {
       expect(parsed).toContainEntries([['dryRun', null]]);
     });
 
+    it('initalizes file logging when logFile is set and env vars LOG_FILE is undefined', async () => {
+      jest.doMock(
+        '../../../../../config.js',
+        () => ({ logFile: 'somepath', logFileLevel: 'debug' }),
+        {
+          virtual: true,
+        },
+      );
+      const env: NodeJS.ProcessEnv = {};
+      const parsedConfig = await configParser.parseConfigs(env, defaultArgv);
+      expect(parsedConfig).not.toContain([['logFile', 'someFile']]);
+      expect(getParentDir).toHaveBeenCalledWith('somepath');
+    });
+
+    it('skips initializing file logging when logFile is set but env vars LOG_FILE is defined', async () => {
+      jest.doMock(
+        '../../../../../config.js',
+        () => ({ logFile: 'somepath', logFileLevel: 'debug' }),
+        {
+          virtual: true,
+        },
+      );
+      const env: NodeJS.ProcessEnv = {};
+      process.env.LOG_FILE = 'somepath';
+      const parsedConfig = await configParser.parseConfigs(env, defaultArgv);
+      expect(parsedConfig).not.toContain([['logFile', 'someFile']]);
+      expect(getParentDir).not.toHaveBeenCalled();
+    });
+
     it('massage onboardingNoDeps when autodiscover is false', async () => {
-      jest.mock(
-        '../../config.js',
+      jest.doMock(
+        '../../../../../config.js',
         () => ({ onboardingNoDeps: 'auto', autodiscover: false }),
         {
           virtual: true,
diff --git a/lib/workers/global/config/parse/index.ts b/lib/workers/global/config/parse/index.ts
index 5ccb5105efff5fb2a8b34a183f5db0a2074cfc4e..6d51625ace451a7bdc36799c7fe18d17a830cad0 100644
--- a/lib/workers/global/config/parse/index.ts
+++ b/lib/workers/global/config/parse/index.ts
@@ -1,3 +1,4 @@
+import is from '@sindresorhus/is';
 import * as defaultsParser from '../../../../config/defaults';
 import type { AllConfig } from '../../../../config/types';
 import { mergeChildConfig } from '../../../../config/utils';
@@ -67,8 +68,7 @@ export async function parseConfigs(
   }
 
   // Add file logger
-  // istanbul ignore if
-  if (config.logFile) {
+  if (config.logFile && is.undefined(process.env.LOG_FILE)) {
     logger.debug(
       // TODO: types (#22198)
       `Enabling ${config.logFileLevel!} logging to ${config.logFile}`,