Documentation
adrs/020-container-aware-configuration.md
ADR-020: Container-Aware Configuration
Status
Accepted
Context
Dynaplex services need to behave differently depending on their execution environment. Local development, Docker containers, and production Kubernetes deployments have different requirements for configuration, networking, and security. We need a systematic approach to detect the runtime environment and adjust behavior accordingly.
Key differences between environments:
- CORS requirements (development vs production)
- Service discovery mechanisms
- Health check endpoints
- Configuration sources
- Security policies
- Logging verbosity
- Debug endpoints availability
Decision
We will implement container-aware configuration that detects the runtime environment using the DOTNET_RUNNING_IN_CONTAINER environment variable and other indicators to automatically adjust service behavior.
Pattern:
if (Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true")
{
// Container-specific configuration
}
Consequences
Positive
- Automatic Configuration: Services self-configure based on environment
- Development Experience: Simplified local development setup
- Security: Different security policies per environment
- Flexibility: Easy to add environment-specific features
- Debugging: Enhanced debugging in development
- Deployment Simplicity: Same image works in multiple environments
Negative
- Hidden Behavior: Configuration not always explicit
- Testing Complexity: Must test multiple environment configurations
- Debugging Issues: Environment-specific bugs harder to reproduce
- Documentation: Must document environment-specific behaviors
Neutral
- Environment Detection: Relies on environment variables
- Configuration Precedence: Environment overrides defaults
- Runtime Decisions: Some configuration happens at runtime
Implementation Notes
Environment Detection
public static class EnvironmentDetector
{
public static bool IsRunningInContainer =>
Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true";
public static bool IsKubernetes =>
Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST") != null;
public static bool IsDocker =>
IsRunningInContainer && !IsKubernetes;
public static bool IsAzureContainerInstance =>
Environment.GetEnvironmentVariable("ACI_NAME") != null;
public static bool IsDevelopment =>
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development";
}
CORS Configuration
// In Program.cs
if (EnvironmentDetector.IsRunningInContainer)
{
// Enable CORS for NextJS UI in containers
builder.Services.AddCors(options =>
{
options.AddPolicy("ContainerCors", policy =>
{
policy.WithOrigins(
"http://localhost:3000", // Local NextJS
"http://assettrak-ui:3000", // Docker service name
"https://ui.dynaplex.local" // Production URL
)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
// Use the CORS policy
app.UseCors("ContainerCors");
}
else
{
// Development: Allow all origins
if (app.Environment.IsDevelopment())
{
builder.Services.AddCors(options =>
{
options.AddPolicy("DevCors", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
app.UseCors("DevCors");
}
}
Service Discovery
public static class ServiceConfiguration
{
public static void ConfigureServiceDiscovery(this IServiceCollection services, IConfiguration configuration)
{
if (EnvironmentDetector.IsKubernetes)
{
// Kubernetes: Use DNS service discovery
services.AddHttpClient("catalog", client =>
{
client.BaseAddress = new Uri("http://catalog-service:80");
});
}
else if (EnvironmentDetector.IsDocker)
{
// Docker Compose: Use container names
services.AddHttpClient("catalog", client =>
{
client.BaseAddress = new Uri("http://catalog:5000");
});
}
else
{
// Local development: Use localhost with ports
services.AddHttpClient("catalog", client =>
{
var port = configuration["Services:Catalog:Port"] ?? "5001";
client.BaseAddress = new Uri($"http://localhost:{port}");
});
}
}
}
Health Checks
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddSqlServer(
connectionString,
name: "database",
tags: EnvironmentDetector.IsRunningInContainer
? new[] { "ready", "live" } // Kubernetes probes
: new[] { "health" }); // Simple health check
if (EnvironmentDetector.IsKubernetes)
{
// Kubernetes-specific health endpoints
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live")
});
}
else
{
// Standard health endpoint
app.MapHealthChecks("/health");
}
Logging Configuration
builder.Logging.ClearProviders();
if (EnvironmentDetector.IsRunningInContainer)
{
// Container: JSON logging for structured logs
builder.Logging.AddJsonConsole(options =>
{
options.IncludeScopes = true;
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff";
options.JsonWriterOptions = new JsonWriterOptions
{
Indented = false // Compact JSON in containers
};
});
// Set appropriate log levels
builder.Logging.SetMinimumLevel(
EnvironmentDetector.IsKubernetes
? LogLevel.Information
: LogLevel.Debug);
}
else
{
// Development: Human-readable console logging
builder.Logging.AddConsole(options =>
{
options.IncludeScopes = true;
options.TimestampFormat = "HH:mm:ss ";
});
builder.Logging.SetMinimumLevel(LogLevel.Debug);
}
Database Configuration
services.AddDbContext<CatalogDb>(options =>
{
var connectionString = configuration.GetConnectionString("catalog");
if (EnvironmentDetector.IsRunningInContainer)
{
// Container: Use environment variable override if present
connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING")
?? connectionString;
// Add resiliency for container networking
options.UseSqlServer(connectionString, sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null);
});
}
else
{
// Local development: Use local connection
options.UseSqlServer(connectionString);
}
});
Debug Endpoints
if (!EnvironmentDetector.IsRunningInContainer || EnvironmentDetector.IsDevelopment)
{
// Development/debug endpoints
app.MapGet("/debug/config", (IConfiguration config) =>
{
return config.AsEnumerable()
.Where(c => !c.Key.Contains("Password") && !c.Key.Contains("Secret"))
.ToDictionary(c => c.Key, c => c.Value);
}).ExcludeFromDescription(); // Don't show in OpenAPI
app.MapGet("/debug/environment", () =>
{
return new
{
IsContainer = EnvironmentDetector.IsRunningInContainer,
IsKubernetes = EnvironmentDetector.IsKubernetes,
IsDocker = EnvironmentDetector.IsDocker,
Environment = app.Environment.EnvironmentName,
Variables = Environment.GetEnvironmentVariables()
};
}).ExcludeFromDescription();
}
Configuration Sources Priority
var builder = WebApplication.CreateBuilder(args);
// Base configuration
builder.Configuration
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true);
if (EnvironmentDetector.IsRunningInContainer)
{
// Container: Environment variables take precedence
builder.Configuration
.AddJsonFile("/config/appsettings.json", optional: true) // Mounted config
.AddEnvironmentVariables("DYNAPLEX_"); // Prefixed env vars
}
else
{
// Development: User secrets and local settings
builder.Configuration
.AddUserSecrets<Program>()
.AddJsonFile("appsettings.Local.json", optional: true);
}
// Command line args always win
builder.Configuration.AddCommandLine(args);
Dockerfile ENV Variables
FROM mcr.microsoft.com/dotnet/aspnet:9.0
ENV DOTNET_RUNNING_IN_CONTAINER=true
ENV ASPNETCORE_URLS=http://+:80
ENV ASPNETCORE_ENVIRONMENT=Production
EXPOSE 80
Best Practices
- Always check environment before applying environment-specific config
- Document environment variables required for each environment
- Provide sensible defaults that work without configuration
- Log environment detection at startup for debugging
- Test all environments in CI/CD pipeline
- Use configuration hierarchy with clear precedence rules
- Avoid production secrets in environment detection logic
Related ADRs
- ADR-007: Migration to .NET Aspire Microservices (container deployment)
- ADR-019: OpenTelemetry Integration (environment-specific telemetry)
- ADR-022: Scalar API Documentation (environment-specific API docs)