.NET Core 容器化部署:Docker 最佳实践
2018.11.20
C#
3 min
1.2k 字
// 目录 · contents
把 .NET Core 应用跑在 Docker
里,是我们团队迁移到容器化部署的第一步。这篇文章记录整个过程,重点是怎么把镜像做得又小又安全。
1. 基础 Dockerfile(不推荐)
1 2 3 4 5 6 7
| FROM mcr.microsoft.com/dotnet/core/sdk:2.1 WORKDIR /app COPY . . RUN dotnet restore RUN dotnet publish -c Release -o /out EXPOSE 80 ENTRYPOINT ["dotnet", "/out/MyApp.dll"]
|
这个写法有几个问题: 1. 镜像包含 SDK(1.7GB),而运行时只需要
runtime(约 200MB) 2. 每次代码改动都会重新
dotnet restore,无法利用 Docker 层缓存
2. 多阶段构建(推荐)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| FROM mcr.microsoft.com/dotnet/core/sdk:2.1 AS build WORKDIR /src
COPY ["src/MyApp/MyApp.csproj", "src/MyApp/"] COPY ["src/MyApp.Domain/MyApp.Domain.csproj", "src/MyApp.Domain/"] RUN dotnet restore "src/MyApp/MyApp.csproj"
COPY src/ src/ WORKDIR "/src/src/MyApp" RUN dotnet publish -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/core/aspnet:2.1 AS runtime WORKDIR /app
RUN adduser --disabled-password --home /app --gecos "" appuser && \ chown -R appuser:appuser /app USER appuser
COPY --from=build /app/publish .
EXPOSE 80 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost/health || exit 1
ENTRYPOINT ["dotnet", "MyApp.dll"]
|
镜像大小对比: - 单阶段(SDK):~1.7GB - 多阶段(Runtime):~250MB -
多阶段 + 发布为 self-contained:~100MB(包含运行时,无需预装 .NET)
3. 配置管理
.NET Core 配置系统天然支持环境变量覆盖
appsettings.json:
1 2 3 4 5 6 7 8 9
| { "ConnectionStrings": { "Default": "Server=localhost;Database=myapp;..." }, "Logging": { "LogLevel": { "Default": "Information" } } }
|
1 2 3 4 5 6 7
|
docker run \ -e "ConnectionStrings__Default=Server=prod-db;..." \ -e "Logging__LogLevel__Default=Warning" \ -p 8080:80 \ myapp:latest
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| version: '3.7' services: api: image: myapp:latest ports: - "8080:80" environment: - ASPNETCORE_ENVIRONMENT=Production - ConnectionStrings__Default=${DB_CONNECTION_STRING} env_file: - .env.production healthcheck: test: ["CMD", "curl", "-f", "http://localhost/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s restart: unless-stopped
|
4. 健康检查端点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public void ConfigureServices(IServiceCollection services) { services.AddHealthChecks() .AddSqlServer(Configuration.GetConnectionString("Default"), name: "database", failureStatus: HealthStatus.Unhealthy) .AddRedis(Configuration["Redis:ConnectionString"], name: "cache"); }
public void Configure(IApplicationBuilder app) { app.UseHealthChecks("/health", new HealthCheckOptions { ResponseWriter = async (context, report) => { context.Response.ContentType = "application/json"; var result = JsonSerializer.Serialize(new { status = report.Status.ToString(), checks = report.Entries.Select(e => new { name = e.Key, status = e.Value.Status.ToString(), description = e.Value.Description }) }); await context.Response.WriteAsync(result); } });
app.UseMvc(); }
|
5. 日志配置
容器环境里日志应该输出到 stdout(Docker 会收集):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| WebHost.CreateDefaultBuilder(args) .ConfigureLogging(logging => { logging.ClearProviders(); logging.AddConsole(options => { options.TimestampFormat = "yyyy-MM-dd HH:mm:ss "; options.IncludeScopes = true; });
if (env.IsDevelopment()) logging.AddDebug(); }) .UseStartup<Startup>() .Build();
|
生产环境配合 ELK Stack 或 Seq 做集中式日志:
1 2 3 4 5
| docker run \ --log-driver=fluentd \ --log-opt fluentd-address=localhost:24224 \ myapp:latest
|
踩坑记录
部署到生产环境后,应用在高并发下偶发
SocketException: Address already in use。排查发现是
HttpClient 使用方式不对——每次请求 new 一个
HttpClient 然后 Dispose,导致 socket 进入 TIME_WAIT
状态堆积,最终耗尽可用端口。
在容器里这个问题更明显,因为容器 IP 固定,TIME_WAIT 的 socket
无法快速复用。
解决方案:使用 IHttpClientFactory(.NET Core 2.1
引入):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| services.AddHttpClient<PaymentService>(client => { client.BaseAddress = new Uri("https://payment-api.example.com"); client.DefaultRequestHeaders.Add("Accept", "application/json"); });
public class PaymentService { private readonly HttpClient _httpClient;
public PaymentService(HttpClient httpClient) { _httpClient = httpClient; } }
|
总结
.NET Core Docker 部署要点:
- 多阶段构建:SDK 用于构建,Runtime 用于运行,镜像从
1.7GB 减到 250MB
- 层缓存优化:先 COPY csproj,再 dotnet restore,最后
COPY 源码
- 非 root 用户运行:安全基线要求
- 环境变量覆盖配置:双下划线替换 JSON 层级分隔符
IHttpClientFactory:管理 HttpClient
连接池,避免 socket 泄漏
- 健康检查:容器编排(K8s/Swarm)依赖它判断服务是否就绪