diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.netcoreapp.cs b/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.netcoreapp.cs
index 453f92886d3e7f..d7567473b454b4 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.netcoreapp.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.netcoreapp.cs
@@ -13,7 +13,7 @@ private static void AddLifetime(IServiceCollection services)
         {
             if (!OperatingSystem.IsAndroid() && !OperatingSystem.IsBrowser() && !OperatingSystem.IsWasi() && !OperatingSystem.IsIOS() && !OperatingSystem.IsTvOS())
             {
-                services.AddSingleton<IHostLifetime, ConsoleLifetime>();
+                HostingHostBuilderExtensions.AddConsoleLifetime(services);
             }
             else
             {
diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.notnetcoreapp.cs b/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.notnetcoreapp.cs
index 4e27703be2dcb8..f100db0029d8c3 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.notnetcoreapp.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.notnetcoreapp.cs
@@ -10,7 +10,7 @@ public partial class HostBuilder
     {
         private static void AddLifetime(IServiceCollection services)
         {
-            services.AddSingleton<IHostLifetime, ConsoleLifetime>();
+            HostingHostBuilderExtensions.AddConsoleLifetime(services);
         }
     }
 }
diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/HostingHostBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Hosting/src/HostingHostBuilderExtensions.cs
index d23fd0d7903d74..88f4508355c1f8 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/HostingHostBuilderExtensions.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/HostingHostBuilderExtensions.cs
@@ -16,6 +16,7 @@
 using Microsoft.Extensions.Hosting.Internal;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.EventLog;
+using Microsoft.Extensions.Options;
 
 namespace Microsoft.Extensions.Hosting
 {
@@ -341,6 +342,26 @@ internal static ServiceProviderOptions CreateDefaultServiceProviderOptions(HostB
             };
         }
 
+        // Factory helper for registering ConsoleLifetime. Uses the internal ctor so the diagnostic
+        // log message in WaitForStartAsync can inspect the application's IConfiguration to detect
+        // a likely-misconfigured content root. Going through a factory (rather than typed
+        // registration) lets us pass IConfiguration without adding a new public ctor to the
+        // pubternal ConsoleLifetime type.
+        [UnsupportedOSPlatform("android")]
+        [UnsupportedOSPlatform("browser")]
+        [UnsupportedOSPlatform("ios")]
+        [UnsupportedOSPlatform("tvos")]
+        internal static void AddConsoleLifetime(IServiceCollection collection)
+        {
+            collection.AddSingleton<IHostLifetime>(static sp => new ConsoleLifetime(
+                sp.GetRequiredService<IOptions<ConsoleLifetimeOptions>>(),
+                sp.GetRequiredService<IHostEnvironment>(),
+                sp.GetRequiredService<IHostApplicationLifetime>(),
+                sp.GetRequiredService<IOptions<HostOptions>>(),
+                sp.GetRequiredService<ILoggerFactory>(),
+                sp.GetService<IConfiguration>()));
+        }
+
         /// <summary>
         /// Listens for Ctrl+C or SIGTERM and calls <see cref="IHostApplicationLifetime.StopApplication"/> to start the shutdown process.
         /// This will unblock extensions like RunAsync and WaitForShutdownAsync.
@@ -353,7 +374,7 @@ internal static ServiceProviderOptions CreateDefaultServiceProviderOptions(HostB
         [UnsupportedOSPlatform("tvos")]
         public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder)
         {
-            return hostBuilder.ConfigureServices(collection => collection.AddSingleton<IHostLifetime, ConsoleLifetime>());
+            return hostBuilder.ConfigureServices(AddConsoleLifetime);
         }
 
         /// <summary>
@@ -371,7 +392,7 @@ public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder, Act
         {
             return hostBuilder.ConfigureServices(collection =>
             {
-                collection.AddSingleton<IHostLifetime, ConsoleLifetime>();
+                AddConsoleLifetime(collection);
                 collection.Configure(configureOptions);
             });
         }
diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.cs
index b19ae087d23db0..a706817f8229aa 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.cs
@@ -2,9 +2,13 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.IO;
 using System.Runtime.Versioning;
+using System.Security;
 using System.Threading;
 using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.FileProviders;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
 using Microsoft.Extensions.Options;
@@ -44,6 +48,13 @@ public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironmen
         /// <param name="loggerFactory">An object to configure the logging system and create instances of <see cref="ILogger"/> from the registered <see cref="ILoggerProvider"/>.</param>
         /// <exception cref="ArgumentNullException"><paramref name="options"/> or <paramref name="environment"/> or <paramref name="applicationLifetime"/> or <paramref name="hostOptions"/> or <paramref name="loggerFactory"/> is <see langword="null"/>.</exception>
         public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, IOptions<HostOptions> hostOptions, ILoggerFactory loggerFactory)
+            : this(options, environment, applicationLifetime, hostOptions, loggerFactory, configuration: null) { }
+
+        // Internal ctor accepting IConfiguration for diagnostic logging. Kept internal to avoid
+        // adding a new public API surface to a pubternal class; ConsoleLifetime is registered via
+        // a factory (see HostingHostBuilderExtensions.AddConsoleLifetime) so DI doesn't need
+        // to pick this ctor automatically.
+        internal ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, IOptions<HostOptions> hostOptions, ILoggerFactory loggerFactory, IConfiguration? configuration)
         {
             ArgumentNullException.ThrowIfNull(options?.Value, nameof(options));
             ArgumentNullException.ThrowIfNull(applicationLifetime);
@@ -55,6 +66,7 @@ public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironmen
             Environment = environment;
             ApplicationLifetime = applicationLifetime;
             HostOptions = hostOptions.Value;
+            Configuration = configuration;
             Logger = loggerFactory.CreateLogger("Microsoft.Hosting.Lifetime");
         }
 
@@ -66,6 +78,8 @@ public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironmen
 
         private HostOptions HostOptions { get; }
 
+        private IConfiguration? Configuration { get; }
+
         private ILogger Logger { get; }
 
         /// <summary>
@@ -77,6 +91,16 @@ public Task WaitForStartAsync(CancellationToken cancellationToken)
         {
             if (!Options.SuppressStatusMessages)
             {
+                // Log a diagnostic when the content root is the current working directory and looks
+                // suspicious. Logging here (rather than in OnApplicationStarted) ensures the message
+                // is still emitted when the host fails to start (e.g. a hosted service can't find
+                // appsettings.json because the working directory is unintentionally "/" in a
+                // container without WORKDIR or when launched by systemd without WorkingDirectory).
+                if (Logger.IsEnabled(LogLevel.Information) && ShouldWarnAboutContentRoot())
+                {
+                    Logger.LogInformation("Content root path is the current working directory ({ContentRoot}). To override, set the content root explicitly.", Environment.ContentRootPath);
+                }
+
                 _applicationStartedRegistration = ApplicationLifetime.ApplicationStarted.Register(state =>
                 {
                     ((ConsoleLifetime)state!).OnApplicationStarted();
@@ -95,6 +119,106 @@ public Task WaitForStartAsync(CancellationToken cancellationToken)
             return Task.CompletedTask;
         }
 
+        private bool ShouldWarnAboutContentRoot()
+        {
+            try
+            {
+                string contentRootPath = Environment.ContentRootPath;
+
+                // Hosting does not normalize ContentRootPath, so an exact match
+                // indicates the working directory is being used as the content root.
+                // A user-specified content root that happens to expand to the same directory
+                // but as a different string (e.g. with a trailing separator, or via "./")
+                // is assumed to be intentional.
+                if (!string.Equals(contentRootPath, Directory.GetCurrentDirectory(), StringComparison.Ordinal))
+                {
+                    return false;
+                }
+
+                // Case 1: the content root is a filesystem root (e.g. "/" or "C:\").
+                // Almost certainly not where the user intended their app to be rooted - log
+                // regardless of how it got set.
+                if (string.Equals(Path.GetPathRoot(contentRootPath), contentRootPath, StringComparison.Ordinal))
+                {
+                    return true;
+                }
+
+                // Case 2: at least one file-based configuration source is rooted at the content
+                // root but none of those files exist on disk. This typically means appsettings.json
+                // was expected (hosting defaults registered it) but the working directory doesn't
+                // actually contain it - a sign the working directory is not the intended app
+                // directory.
+                return AllContentRootFileSourcesAreMissing(contentRootPath);
+            }
+            catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or SecurityException or ArgumentException)
+            {
+                // This diagnostic is a heuristic. I/O and security failures from
+                // querying the current directory or probing file-based configuration providers
+                // should simply skip the diagnostic.
+                return false;
+            }
+        }
+
+        private bool AllContentRootFileSourcesAreMissing(string contentRootPath)
+        {
+            if (Configuration is not IConfigurationRoot configRoot)
+            {
+                return false;
+            }
+
+            bool sawContentRootSource = false;
+
+            foreach (IConfigurationProvider provider in configRoot.Providers)
+            {
+                if (provider is not FileConfigurationProvider fileProvider)
+                {
+                    continue;
+                }
+
+                FileConfigurationSource source = fileProvider.Source;
+                if (source.FileProvider is not PhysicalFileProvider physicalProvider)
+                {
+                    // We can only compare paths against the content root for physical providers.
+                    continue;
+                }
+
+                if (!TrimTrailingDirectorySeparator(physicalProvider.Root).Equals(contentRootPath.AsSpan(), StringComparison.Ordinal))
+                {
+                    continue;
+                }
+
+                if (source.Path is not string sourcePath)
+                {
+                    continue;
+                }
+
+                sawContentRootSource = true;
+
+                if (source.FileProvider.GetFileInfo(sourcePath).Exists)
+                {
+                    return false;
+                }
+            }
+
+            return sawContentRootSource;
+        }
+
+        private static ReadOnlySpan<char> TrimTrailingDirectorySeparator(string path)
+        {
+            if (path.Length <= 1)
+            {
+                return path;
+            }
+
+            char last = path[path.Length - 1];
+            if (last == Path.DirectorySeparatorChar || last == Path.AltDirectorySeparatorChar)
+            {
+                return path.AsSpan(0, path.Length - 1);
+            }
+
+            return path;
+        }
+
         private partial void RegisterShutdownHandlers();
 
         private void OnApplicationStarted()
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/ConsoleLifetimeTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/ConsoleLifetimeTests.cs
new file mode 100644
index 00000000000000..731a1f8850dfdd
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/ConsoleLifetimeTests.cs
@@ -0,0 +1,221 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.DotNet.RemoteExecutor;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Xunit;
+
+namespace Microsoft.Extensions.Hosting.Tests
+{
+    public class ConsoleLifetimeTests
+    {
+        private const string ContentRootDiagnosticMessagePrefix = "Content root path is the current working directory";
+        private const string ApplicationStartedMessage = "Application started. Press Ctrl+C to shut down.";
+
+        [Fact]
+        public async Task LogsBasicStartupMessages()
+        {
+            string cwd = Directory.GetCurrentDirectory();
+            string[] messages = await RunWithDefaultsAsync(
+                contentRootPath: cwd,
+                environmentName: "Production");
+
+            Assert.Contains(ApplicationStartedMessage, messages);
+            Assert.Contains("Hosting environment: Production", messages);
+            Assert.Contains($"Content root path: {cwd}", messages);
+        }
+
+        [Fact]
+        public async Task DoesNotLogStartupMessages_WhenSuppressed()
+        {
+            string[] messages = await RunWithDefaultsAsync(
+                contentRootPath: Directory.GetCurrentDirectory(),
+                configureLifetime: o => o.SuppressStatusMessages = true);
+
+            Assert.Empty(messages);
+        }
+
+        [Fact]
+        public async Task DoesNotLogContentRootDiagnostic_WhenContentRootDiffersFromCwd()
+        {
+            using var differentDirectory = new TempDirectory();
+
+            string[] messages = await RunWithDefaultsAsync(contentRootPath: differentDirectory.Path);
+
+            Assert.DoesNotContain(messages, m => m.StartsWith(ContentRootDiagnosticMessagePrefix));
+        }
+
+        // The tests below mutate the process-global current working directory, so they run in
+        // their own remote process.
+
+        [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        public void DoesNotLogContentRootDiagnostic_WhenAppSettingsExists()
+        {
+            RemoteExecutor.Invoke(static async () =>
+            {
+                using var dir = new CwdTempDirectory();
+                File.WriteAllText(Path.Combine(dir.ResolvedPath, "appsettings.json"), "{}");
+
+                string[] messages = await RunWithDefaultsAsync(contentRootPath: dir.ResolvedPath);
+
+                Assert.DoesNotContain(messages, m => m.StartsWith(ContentRootDiagnosticMessagePrefix));
+            }).Dispose();
+        }
+
+        [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        public void LogsContentRootDiagnostic_WhenDefaultsAppliedAndAppSettingsMissing()
+        {
+            RemoteExecutor.Invoke(static async () =>
+            {
+                using var dir = new CwdTempDirectory();
+                Assert.False(File.Exists(Path.Combine(dir.ResolvedPath, "appsettings.json")));
+
+                string[] messages = await RunWithDefaultsAsync(contentRootPath: dir.ResolvedPath);
+
+                Assert.Contains(messages, m => m.StartsWith(ContentRootDiagnosticMessagePrefix));
+            }).Dispose();
+        }
+
+        [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        public void DoesNotLogContentRootDiagnostic_WhenNoFileSourceRegistered()
+        {
+            // No file-based configuration sources rooted at the content root means the user opted
+            // out of file-based config; absence of appsettings.json is not a signal.
+            RemoteExecutor.Invoke(static async () =>
+            {
+                using var dir = new CwdTempDirectory();
+                Assert.False(File.Exists(Path.Combine(dir.ResolvedPath, "appsettings.json")));
+
+                string[] messages = await RunWithoutDefaultsAsync(contentRootPath: dir.ResolvedPath);
+
+                Assert.DoesNotContain(messages, m => m.StartsWith(ContentRootDiagnosticMessagePrefix));
+            }).Dispose();
+        }
+
+        [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        public void LogsContentRootDiagnostic_WhenContentRootIsFilesystemRoot()
+        {
+            RemoteExecutor.Invoke(static async () =>
+            {
+                string root = Path.GetPathRoot(Directory.GetCurrentDirectory());
+                Assert.False(string.IsNullOrEmpty(root));
+                Directory.SetCurrentDirectory(root);
+
+                // Even with no file sources registered, a filesystem-root content root is
+                // suspicious enough to always log.
+                string[] messages = await RunWithoutDefaultsAsync(contentRootPath: Directory.GetCurrentDirectory());
+
+                Assert.Contains(messages, m => m.StartsWith(ContentRootDiagnosticMessagePrefix));
+            }).Dispose();
+        }
+
+        [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        public void LogsContentRootDiagnostic_EvenWhenApplicationFailsToStart()
+        {
+            // The diagnostic should fire when the lifetime starts waiting, before any hosted
+            // services run, so users still see it when the host fails during startup.
+            RemoteExecutor.Invoke(static async () =>
+            {
+                using var dir = new CwdTempDirectory();
+
+                var loggerProvider = new TestLoggerProvider();
+                IHostBuilder builder = new HostBuilder()
+                    .ConfigureDefaults(Array.Empty<string>())
+                    .UseContentRoot(dir.ResolvedPath)
+                    .ConfigureLogging(logging => logging.AddProvider(loggerProvider))
+                    .ConfigureServices(services => services.AddHostedService<ThrowingHostedService>());
+
+                using IHost host = builder.Build();
+                await Assert.ThrowsAsync<InvalidOperationException>(() => host.StartAsync());
+
+                string[] messages = loggerProvider.GetEvents().Select(e => e.Message).ToArray();
+                Assert.Contains(messages, m => m.StartsWith(ContentRootDiagnosticMessagePrefix));
+                Assert.DoesNotContain(ApplicationStartedMessage, messages);
+            }).Dispose();
+        }
+
+        private static async Task<string[]> RunWithDefaultsAsync(
+            string contentRootPath,
+            string environmentName = "Production",
+            Action<ConsoleLifetimeOptions> configureLifetime = null)
+        {
+            var loggerProvider = new TestLoggerProvider();
+            IHostBuilder builder = new HostBuilder()
+                .ConfigureDefaults(Array.Empty<string>())
+                .UseContentRoot(contentRootPath)
+                .UseEnvironment(environmentName)
+                .ConfigureLogging(logging => logging.AddProvider(loggerProvider));
+
+            if (configureLifetime is not null)
+            {
+                builder = builder.UseConsoleLifetime(configureLifetime);
+            }
+
+            using IHost host = builder.Build();
+            await host.StartAsync();
+            await host.StopAsync();
+
+            return loggerProvider.GetEvents().Select(e => e.Message).ToArray();
+        }
+
+        private static async Task<string[]> RunWithoutDefaultsAsync(string contentRootPath)
+        {
+            var loggerProvider = new TestLoggerProvider();
+            using IHost host = new HostBuilder()
+                .UseContentRoot(contentRootPath)
+                .ConfigureLogging(logging => logging.AddProvider(loggerProvider))
+                .UseConsoleLifetime()
+                .Build();
+
+            await host.StartAsync();
+            await host.StopAsync();
+
+            return loggerProvider.GetEvents().Select(e => e.Message).ToArray();
+        }
+
+        // TempDirectory that also sets the current working directory to its path for the lifetime
+        // of the instance. Always used inside RemoteExecutor.Invoke to keep the CWD change
+        // isolated from the test runner process.
+        //
+        // On macOS, Path.GetTempPath() returns a path under /tmp while Directory.GetCurrentDirectory()
+        // returns the resolved path (under /private/tmp). Capture the resolved CWD so callers can use
+        // a path that matches what ConsoleLifetime sees at runtime.
+        private sealed class CwdTempDirectory : TempDirectory
+        {
+            private readonly string _previousCwd;
+
+            /// <summary>The resolved current directory after this instance's <c>SetCurrentDirectory</c> call.</summary>
+            public string ResolvedPath { get; }
+
+            public CwdTempDirectory()
+            {
+                _previousCwd = Directory.GetCurrentDirectory();
+                Directory.SetCurrentDirectory(Path);
+                ResolvedPath = Directory.GetCurrentDirectory();
+            }
+
+            protected override void DeleteDirectory()
+            {
+                // Restore the CWD first - on Windows, a directory can't be deleted while it is
+                // the process's current working directory.
+                try { Directory.SetCurrentDirectory(_previousCwd); }
+                catch { }
+                base.DeleteDirectory();
+            }
+        }
+
+        private sealed class ThrowingHostedService : IHostedService
+        {
+            public Task StartAsync(CancellationToken cancellationToken) =>
+                throw new InvalidOperationException("startup failed");
+
+            public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+        }
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs
index c345548c3a44c3..f43ffa4edc4844 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs
@@ -1453,9 +1453,8 @@ public async Task HostedServiceFactoryExceptionGetsLogged()
             await Assert.ThrowsAsync<InvalidOperationException>(() => host.StartAsync());
 
             LogEvent[] events = logger.GetEvents();
-            Assert.Single(events);
-            Assert.Equal(LogLevel.Error, events[0].LogLevel);
-            Assert.Equal("HostedServiceStartupFaulted", events[0].EventId.Name);
+            LogEvent errorEvent = Assert.Single(events, e => e.LogLevel == LogLevel.Error);
+            Assert.Equal("HostedServiceStartupFaulted", errorEvent.EventId.Name);
         }
 
         [Fact]
