容器化与 Docker 部署
大约 7 分钟约 2186 字
容器化与 Docker 部署
简介
容器化是微服务部署的基础,.NET 应用通过 Docker 镜像实现一致的构建和运行环境。理解多阶段构建、镜像优化、Docker Compose 编排和 Kubernetes 部署,有助于构建高效的 CI/CD 流水线。
特点
Dockerfile 最佳实践
多阶段构建
# .NET 8 多阶段构建 Dockerfile
# 阶段 1: 构建
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# 先复制项目文件,利用 Docker 缓存
COPY ["MyApp.Api/MyApp.Api.csproj", "MyApp.Api/"]
COPY ["MyApp.Application/MyApp.Application.csproj", "MyApp.Application/"]
COPY ["MyApp.Domain/MyApp.Domain.csproj", "MyApp.Domain/"]
COPY ["MyApp.Infrastructure/MyApp.Infrastructure.csproj", "MyApp.Infrastructure/"]
# 还原依赖(依赖不变时使用缓存)
RUN dotnet restore "MyApp.Api/MyApp.Api.csproj"
# 复制源代码
COPY . .
# 构建和发布
RUN dotnet publish "MyApp.Api/MyApp.Api.csproj" \
-c Release \
-o /app/publish \
--no-restore \
-p:PublishTrimmed=true \
-p:PublishSingleFile=false
# 阶段 2: 运行时(小体积)
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime
# 安全:使用非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# 设置时区
RUN apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone && \
apk del tzdata
WORKDIR /app
# 从构建阶段复制发布文件
COPY --from=build /app/publish .
# 设置环境变量
ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production
ENV DOTNET_EnableDiagnostics=0
ENV DOTNET_gcServer=1
EXPOSE 8080
EXPOSE 8081
# 切换到非 root 用户
USER appuser
ENTRYPOINT ["dotnet", "MyApp.Api.dll"]Native AOT 镜像
# Native AOT Dockerfile(更小的镜像)
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyApp.Api/MyApp.Api.csproj", "MyApp.Api/"]
RUN dotnet restore "MyApp.Api/MyApp.Api.csproj"
COPY . .
# AOT 发布
RUN dotnet publish "MyApp.Api/MyApp.Api.csproj" \
-c Release \
-o /app/publish \
-r linux-musl-x64 \
--self-contained \
-p:PublishAot=true \
-p:StripSymbols=true
# 运行时(极小体积)
FROM alpine:3.19 AS runtime
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=build /app/publish .
RUN apk add --no-cache libgcc libstdc++
USER appuser
ENTRYPOINT ["./MyApp.Api"]Docker Compose 编排
本地开发环境
# docker-compose.yml
version: '3.8'
services:
# API 网关
gateway:
build:
context: .
dockerfile: src/Gateway/Dockerfile
ports:
- "8080:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ReverseProxy__Clusters__user-cluster__Destinations__dest__Address=http://user-api:8080
- ReverseProxy__Clusters__order-cluster__Destinations__dest__Address=http://order-api:8080
depends_on:
user-api:
condition: service_healthy
order-api:
condition: service_healthy
# 用户服务
user-api:
build:
context: .
dockerfile: src/UserService/Dockerfile
ports:
- "8081:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__Default=Host=postgres;Database=users;Username=postgres;Password=postgres
- Redis__Connection=redis:6379
- Consul__Address=http://consul:8500
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
consul:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
# 订单服务
order-api:
build:
context: .
dockerfile: src/OrderService/Dockerfile
ports:
- "8082:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__Default=Host=postgres;Database=orders;Username=postgres;Password=postgres
- RabbitMQ__Host=rabbitmq
- Consul__Address=http://consul:8500
depends_on:
postgres:
condition: service_healthy
rabbitmq:
condition: service_healthy
# PostgreSQL
postgres:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_MULTIPLE_DATABASES=users,orders
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
# Redis
redis:
image: redis:7-alpine
ports:
- "6379:6379"
command: redis-server --appendonly yes
volumes:
- redis_data:/data
# RabbitMQ
rabbitmq:
image: rabbitmq:3-management-alpine
ports:
- "5672:5672"
- "15672:15672"
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=admin
volumes:
- rabbitmq_data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "check_running"]
interval: 10s
timeout: 5s
retries: 5
# Consul
consul:
image: hashicorp/consul:1.15
ports:
- "8500:8500"
command: agent -dev -ui -client=0.0.0.0
volumes:
- consul_data:/consul/data
# Jaeger(链路追踪)
jaeger:
image: jaegertracing/all-in-one:1.52
ports:
- "16686:16686" # UI
- "4317:4317" # OTLP gRPC
environment:
- COLLECTOR_OTLP_ENABLED=true
volumes:
postgres_data:
redis_data:
rabbitmq_data:
consul_data:开发用 Docker Compose Override
# docker-compose.override.yml(开发环境覆盖)
version: '3.8'
services:
user-api:
build:
target: build
volumes:
- ./src/UserService:/src
command: dotnet watch --project src/UserService run
environment:
- ASPNETCORE_ENVIRONMENT=Development
- DOTNET_USE_POLLING_FILE_WATCHER=1
order-api:
build:
target: build
volumes:
- ./src/OrderService:/src
command: dotnet watch --project src/OrderService runKubernetes 部署
生产部署清单
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-api
labels:
app: user-api
version: v1
spec:
replicas: 3
strategy:
rollingUpdate:
maxSurge: 1 # 滚动更新时最多多 1 个 Pod
maxUnavailable: 0 # 不允许不可用
type: RollingUpdate
selector:
matchLabels:
app: user-api
template:
metadata:
labels:
app: user-api
version: v1
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
# 优雅关闭
terminationGracePeriodSeconds: 60
containers:
- name: api
image: registry.example.com/user-api:latest
ports:
- containerPort: 8080
name: http
- containerPort: 8081
name: grpc
# 环境变量
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
- name: DOTNET_gcServer
value: "1"
# 从 Secret 注入敏感配置
envFrom:
- secretRef:
name: user-api-secrets
# 资源限制
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
# 启动探针
startupProbe:
httpGet:
path: /health/startup
port: 8080
failureThreshold: 30
periodSeconds: 10
# 存活探针
livenessProbe:
httpGet:
path: /health/live
port: 8080
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
# 就绪探针
readinessProbe:
httpGet:
path: /health/ready
port: 8080
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# 生命周期钩子
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
# 挂载配置
volumeMounts:
- name: config
mountPath: /app/config
volumes:
- name: config
configMap:
name: user-api-config
---
# Service
apiVersion: v1
kind: Service
metadata:
name: user-api
spec:
selector:
app: user-api
ports:
- port: 80
targetPort: 8080
name: http
- port: 81
targetPort: 8081
name: grpc
---
# HPA(自动扩缩容)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: user-api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: user-api
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80镜像安全
安全扫描与最佳实践
# 安全最佳实践
# 1. 使用特定版本标签(不用 latest)
FROM mcr.microsoft.com/dotnet/aspnet:8.0.1-alpine3.19
# 2. 非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# 3. 最小化安装
RUN apk add --no-cache \
ca-certificates \
tzdata \
&& rm -rf /var/cache/apk/*
# 4. 健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# 5. 只读文件系统
# docker run --read-only --tmpfs /tmp ...优点
缺点
总结
Docker 多阶段构建显著减小镜像体积:构建阶段使用 SDK 镜像编译,运行时使用 Alpine 镜像。安全最佳实践包括使用非 root 用户、特定版本标签、只读文件系统。Docker Compose 适合本地开发编排,depends_on + healthcheck 控制启动顺序。Kubernetes 部署通过三探针(Startup/Liveness/Readiness)实现自动故障恢复,HPA 实现自动扩缩容。建议 CI/CD 中集成镜像安全扫描(Trivy)。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 部署主题通常要同时看镜像、容器、卷、网络和宿主机资源。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
- 固定镜像标签,记录端口、挂载目录、环境变量和自启动策略。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 使用 latest 导致结果不可复现。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 继续补齐 Compose 编排、镜像瘦身、安全扫描和镜像仓库治理。
适用场景
- 当你准备把《容器化与 Docker 部署》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《容器化与 Docker 部署》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《容器化与 Docker 部署》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《容器化与 Docker 部署》最大的收益和代价分别是什么?
