In my first job as a developer, we used custom hosts for local development with IIS. This allowed us to get a bit closer to the production configuration. We had to edit our C:\Windows\System32\drivers\etc\hosts
file and add an entry for each host mapped to the IP address 127.0.0.1
. Overall, this worked well.
Nowadays, the introduction of containers into the local development flow makes this practice more complicated. Containers do not share the same hosts file as the host machine and cannot resolve custom hosts. Most developers are therefore forced to use host.docker.internal
for Docker or host.containers.internal
for Podman to access services on the host machine.
In this article, we’ll enable containers to share the host machine’s hosts file and automate this process with .NET Aspire to simplify URL management when dealing with multiple services in local development.
Why use custom hosts for local development?
Using custom hosts for local development is a fairly common practice. Relying solely on localhost
can cause some limitations, such as:
- An excessive number of cookies leading to HTTP 400 Bad Request errors Request Header Or Cookie Too Large.
- A poor development experience where you have to remember the ports used by each service.
- Difficulty testing CORS configuration.
- Inability to use subdomains if necessary.
It’s not uncommon to see developers create local domains ending with .local
, .dev
, .test
, etc. Ideally, you should use the top-level domain (TLD) .localhost
, which is reserved by the Internet Engineering Task Force (IETF). There’s no risk that this domain will ever be registered on the internet and made available for purchase, making it the safest choice.
Informing containers about custom hosts
To allow containers to resolve the host machine’s custom hosts, we need to read its hosts
file and share the custom hosts pointing to 127.0.0.1
with the containers. We can do this using the --add-host
argument of Docker or Podman. For example:
--add-host mycustomdomain.localhost:host-gateway
host-gateway
is a special hostname that allows Docker or Podman to resolve the host machine’s internal IP address from within the container.
In this example, we’re redirecting mycustomdomain.localhost
traffic from the container to the host machine.
Automating custom host sharing with .NET Aspire
We can write an extension method to automatically add the --add-host
arguments to designated ContainerResource
resources:
builder.AddContainer("myapp", "mcr.microsoft.com/dotnet/samples", "aspnetapp")
.WithLocalhostHostsFromHost();
internal static partial class ContainerResourceBuilderExtensions
{
public static IResourceBuilder<ContainerResource> WithLocalhostHostsFromHost(this IResourceBuilder<ContainerResource> builder)
{
builder.ApplicationBuilder.Services.TryAddLifecycleHook<LocalhostHostsFromHostLifecycleHook>();
return builder.WithAnnotation<LocalhostHostsFromHostAnnotation>(ResourceAnnotationMutationBehavior.Replace);
}
private sealed class LocalhostHostsFromHostAnnotation : IResourceAnnotation;
private sealed partial class LocalhostHostsFromHostLifecycleHook : IDistributedApplicationLifecycleHook
{
public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
var hostsFilePath = OperatingSystem.IsWindows()
? @"C:\Windows\System32\drivers\etc\hosts"
: "/etc/hosts";
var customLocalhostHosts = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
try
{
await foreach (var line in File.ReadLinesAsync(hostsFilePath, cancellationToken))
{
if (LocalhostHostFileLineRegex().Match(line) is { Success: true } match)
{
customLocalhostHosts.Add(match.Groups["hostname"].Value);
}
}
}
catch (OperationCanceledException)
{
// Application is shutting down
return;
}
catch (IOException ex)
{
throw new InvalidOperationException("An error occurred while reading the hosts file.", ex);
}
// Remove non-custom hostnames that may interfere with the container's network resolution
customLocalhostHosts.Remove("localhost");
customLocalhostHosts.Remove("host.docker.internal");
customLocalhostHosts.Remove("host.containers.internal");
customLocalhostHosts.Remove("gateway.docker.internal");
customLocalhostHosts.Remove("kubernetes.docker.internal");
var addHostsAnnotation = new ContainerRuntimeArgsCallbackAnnotation(args =>
{
foreach (var hostname in customLocalhostHosts)
{
args.Add("--add-host");
args.Add($"{hostname}:host-gateway");
}
});
foreach (var containerResource in appModel.Resources.OfType<ContainerResource>())
{
if (containerResource.Annotations.OfType<LocalhostHostsFromHostAnnotation>().Any())
{
containerResource.Annotations.Add(addHostsAnnotation);
}
}
}
[GeneratedRegex(@"^127\.0\.0\.1\s+(?<hostname>[a-z0-9\-\.]+)$", RegexOptions.IgnoreCase)]
private static partial Regex LocalhostHostFileLineRegex();
}
}
Given this Windows hosts
file:
# 127.0.0.1 localhost
# ::1 localhost
127.0.0.1 api.myapp.localhost
127.0.0.1 app.myapp.localhost
127.0.0.1 analytics.myapp.localhost
My mcr.microsoft.com/dotnet/samples:aspnetapp
container will be started with the --add-host
arguments:
Conclusion
In this post, we learned how to make containers aware of custom local domains on the host machine using .NET Aspire. If you combine this feature with the ability to automatically trust self-signed certificates in containers, you will have a container-based development experience that closely resembles running the services directly on your machine without the extra cost of running an IDE.