C# 零基础到精通教程 - 第十八章:部署与发布——让应用上线

第十七章我们学习了 Blazor,知道了如何用 C# 构建前端应用。但代码写完了,怎么让用户真正用上?这一章要学的部署与发布,就是把你的应用放到服务器上,让全世界都能访问。


18.1 部署概述

18.1.1 部署的层次

text

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        应用部署层次                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                   1. 发布(Publish)                      │   │
│   │   将源代码编译成可执行文件                                 │   │
│   └─────────────────────────────────────────────────────────┘   │
│                              ↓                                  │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                   2. 传输(Transfer)                     │   │
│   │   将文件复制到服务器                                       │   │
│   └─────────────────────────────────────────────────────────┘   │
│                              ↓                                  │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                   3. 配置(Configure)                    │   │
│   │   配置 Web 服务器(IIS、Nginx、Apache)                    │   │
│   └─────────────────────────────────────────────────────────┘   │
│                              ↓                                  │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                   4. 运行(Run)                          │   │
│   │   启动应用,提供服务                                        │   │
│   └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

18.1.2 部署选项

部署方式 适用场景 复杂度 成本
本地运行 开发测试
IIS 部署 Windows 服务器 服务器费用
Docker 部署 容器化环境 服务器费用
Azure/AWS 云原生应用 低-中 按使用付费
Linux + Nginx Linux 服务器 服务器费用

18.2 发布应用

18.2.1 使用 dotnet publish 命令

bash

复制代码
# 基本发布命令
dotnet publish -c Release

# 指定输出目录
dotnet publish -c Release -o ./publish

# 发布为独立应用(包含 .NET 运行时)
dotnet publish -c Release -r win-x64 --self-contained true

# 发布为框架依赖应用(需要目标机器安装 .NET 运行时)
dotnet publish -c Release -r win-x64 --self-contained false

# 发布为单个文件
dotnet publish -c Release -o ./publish -p:PublishSingleFile=true

# 启用修剪(减小体积)
dotnet publish -c Release -o ./publish -p:PublishTrimmed=true

# 发布 Blazor WebAssembly(AOT 编译)
dotnet publish -c Release -o ./publish -p:RunAOTCompilation=true

18.2.2 发布不同平台

bash

复制代码
# Windows 64位
dotnet publish -c Release -r win-x64

# Windows 32位
dotnet publish -c Release -r win-x86

# Linux 64位
dotnet publish -c Release -r linux-x64

# Linux ARM(树莓派)
dotnet publish -c Release -r linux-arm

# macOS 64位
dotnet publish -c Release -r osx-x64

# macOS ARM(M1/M2)
dotnet publish -c Release -r osx-arm64

18.2.3 使用 Visual Studio 发布

  1. 右键项目 → 发布

  2. 选择目标:文件夹 / Azure / IIS / Docker

  3. 配置发布设置

  4. 点击 发布

18.2.4 发布配置文件示例

xml

复制代码
<!-- Properties/PublishProfiles/FolderProfile.pubxml -->
<Project>
  <PropertyGroup>
    <PublishProtocol>FileSystem</PublishProtocol>
    <Configuration>Release</Configuration>
    <Platform>Any CPU</Platform>
    <TargetFramework>net8.0</TargetFramework>
    <PublishDir>..\publish\</PublishDir>
    <SelfContained>false</SelfContained>
    <PublishSingleFile>true</PublishSingleFile>
    <PublishTrimmed>true</PublishTrimmed>
    <EnableReadyToRun>true</EnableReadyToRun>
  </PropertyGroup>
</Project>

18.3 部署到 IIS

18.3.1 安装 IIS 和 .NET Hosting Bundle

Windows Server / Windows 10/11:

  1. 启用 IIS:

    • 控制面板 → 程序和功能 → 启用或关闭 Windows 功能

    • 勾选 Internet Information Services

  2. 安装 .NET Hosting Bundle:

18.3.2 创建 IIS 网站

powershell

复制代码
# 使用 PowerShell 创建网站
New-WebSite -Name "MyApp" -Port 80 -PhysicalPath "C:\inetpub\MyApp" -ApplicationPool "MyAppPool"
New-WebAppPool -Name "MyAppPool"
Set-ItemProperty -Path "IIS:\AppPools\MyAppPool" -Name "processModel" -Value "ApplicationPoolIdentity"

18.3.3 配置 web.config

xml

复制代码
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="dotnet" 
                  arguments=".\MyApp.dll" 
                  stdoutLogEnabled="true" 
                  stdoutLogFile=".\logs\stdout"
                  hostingModel="inprocess">
        <environmentVariables>
          <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
        </environmentVariables>
      </aspNetCore>
    </system.webServer>
  </location>
</configuration>

18.3.4 部署步骤

bash

复制代码
# 1. 发布应用
dotnet publish -c Release -o ./publish

# 2. 复制到 IIS 目录
xcopy /e /y ./publish C:\inetpub\MyApp

# 3. 设置权限
icacls C:\inetpub\MyApp /grant "IIS_IUSRS:(OI)(CI)RX"

# 4. 重启 IIS
iisreset

18.4 部署到 Linux + Nginx

18.4.1 安装 .NET SDK/Runtime

bash

复制代码
# Ubuntu/Debian
wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt-get update
sudo apt-get install -y aspnetcore-runtime-8.0

# 或安装 SDK
sudo apt-get install -y dotnet-sdk-8.0

18.4.2 创建 systemd 服务

bash

复制代码
# 创建服务文件
sudo nano /etc/systemd/system/myapp.service

ini

复制代码
[Unit]
Description=My ASP.NET Core Application
After=network.target

[Service]
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/dotnet /var/www/myapp/MyApp.dll
Restart=always
RestartSec=10
SyslogIdentifier=myapp
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production

[Install]
WantedBy=multi-user.target

bash

复制代码
# 启动服务
sudo systemctl enable myapp.service
sudo systemctl start myapp.service
sudo systemctl status myapp.service

18.4.3 安装和配置 Nginx

bash

复制代码
# 安装 Nginx
sudo apt-get install -y nginx

# 创建站点配置
sudo nano /etc/nginx/sites-available/myapp

nginx

复制代码
server {
    listen 80;
    server_name mydomain.com www.mydomain.com;
    
    location / {
        proxy_pass http://localhost:5000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection keep-alive;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
    
    # 静态文件
    location /static/ {
        alias /var/www/myapp/wwwroot/;
        expires 30d;
    }
    
    # 日志
    access_log /var/log/nginx/myapp-access.log;
    error_log /var/log/nginx/myapp-error.log;
}

bash

复制代码
# 启用站点
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

18.4.4 配置 HTTPS(Let's Encrypt)

bash

复制代码
# 安装 Certbot
sudo apt-get install -y certbot python3-certbot-nginx

# 获取证书
sudo certbot --nginx -d mydomain.com -d www.mydomain.com

# 自动续期
sudo certbot renew --dry-run

18.5 部署到 Docker

18.5.1 创建 Dockerfile

dockerfile

复制代码
# 多阶段构建 Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# 复制项目文件并还原依赖
COPY ["MyApp.csproj", "."]
RUN dotnet restore

# 复制源代码并发布
COPY . .
RUN dotnet publish -c Release -o /app/publish

# 运行阶段
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
EXPOSE 80
EXPOSE 443

# 从构建阶段复制发布文件
COPY --from=build /app/publish .

# 设置环境变量
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:80

ENTRYPOINT ["dotnet", "MyApp.dll"]

18.5.2 构建和运行 Docker 镜像

bash

复制代码
# 构建镜像
docker build -t myapp:latest .

# 运行容器
docker run -d \
  --name myapp \
  -p 8080:80 \
  -e ASPNETCORE_ENVIRONMENT=Production \
  -v /data/logs:/app/logs \
  myapp:latest

# 查看日志
docker logs -f myapp

# 停止和删除
docker stop myapp
docker rm myapp

18.5.3 使用 Docker Compose

yaml

复制代码
# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - "8080:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ConnectionStrings__DefaultConnection=Server=db;Database=MyApp;User=sa;Password=Your_password123;
    depends_on:
      - db
    volumes:
      - ./logs:/app/logs
    restart: unless-stopped

  db:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=Your_password123
      - MSSQL_PID=Express
    ports:
      - "1433:1433"
    volumes:
      - sql_data:/var/opt/mssql
    restart: unless-stopped

volumes:
  sql_data:

bash

复制代码
# 启动所有服务
docker-compose up -d

# 查看状态
docker-compose ps

# 查看日志
docker-compose logs -f

# 停止服务
docker-compose down

# 停止并删除数据卷
docker-compose down -v

18.6 部署到 Azure

18.6.1 使用 Azure CLI 部署

bash

复制代码
# 登录 Azure
az login

# 创建资源组
az group create --name MyAppRG --location eastus

# 创建 App Service 计划
az appservice plan create \
  --name MyAppPlan \
  --resource-group MyAppRG \
  --sku F1 \  # 免费层
  --is-linux

# 创建 Web App
az webapp create \
  --name myapp-unique-name \
  --resource-group MyAppRG \
  --plan MyAppPlan \
  --runtime "DOTNET|8.0"

# 部署应用
az webapp deployment source config-zip \
  --resource-group MyAppRG \
  --name myapp-unique-name \
  --src ./publish.zip

# 设置环境变量
az webapp config appsettings set \
  --resource-group MyAppRG \
  --name myapp-unique-name \
  --settings ASPNETCORE_ENVIRONMENT=Production

# 查看日志
az webapp log tail \
  --resource-group MyAppRG \
  --name myapp-unique-name

18.6.2 使用 GitHub Actions CI/CD

yaml

复制代码
# .github/workflows/deploy.yml
name: Deploy to Azure

on:
  push:
    branches: [ main ]

env:
  AZURE_WEBAPP_NAME: myapp-unique-name
  AZURE_WEBAPP_PACKAGE_PATH: './publish'

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: '8.0'
    
    - name: Restore dependencies
      run: dotnet restore
    
    - name: Build
      run: dotnet build --configuration Release --no-restore
    
    - name: Test
      run: dotnet test --configuration Release --no-build
    
    - name: Publish
      run: dotnet publish --configuration Release --output ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}
    
    - name: Deploy to Azure
      uses: azure/webapps-deploy@v2
      with:
        app-name: ${{ env.AZURE_WEBAPP_NAME }}
        publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
        package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}

18.7 环境配置

18.7.1 多环境配置

json

复制代码
// appsettings.json(基础配置)
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

// appsettings.Development.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "Microsoft.AspNetCore": "Information"
    }
  },
  "Database": {
    "ConnectionString": "Server=localhost;Database=DevDb;"
  }
}

// appsettings.Production.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Error",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "Database": {
    "ConnectionString": "Server=prod-server;Database=ProdDb;"
  }
}

18.7.2 使用环境变量

csharp

复制代码
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// 加载配置
builder.Configuration
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
    .AddEnvironmentVariables();

// 使用环境变量覆盖配置
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

// 设置环境变量(Linux)
// export ASPNETCORE_ENVIRONMENT=Production

// Windows
// set ASPNETCORE_ENVIRONMENT=Production

18.7.3 使用 User Secrets(开发环境)

bash

复制代码
# 初始化 User Secrets
dotnet user-secrets init

# 设置秘密
dotnet user-secrets set "Database:Password" "MySecretPassword"
dotnet user-secrets set "ApiKey" "abc123"

# 列出所有秘密
dotnet user-secrets list

# 移除秘密
dotnet user-secrets remove "Database:Password"

# 清除所有秘密
dotnet user-secrets clear

csharp

复制代码
// Program.cs
if (builder.Environment.IsDevelopment())
{
    builder.Configuration.AddUserSecrets<Program>();
}

18.8 性能优化

18.8.1 启用响应压缩

csharp

复制代码
// Program.cs
builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
    options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
    {
        "image/svg+xml",
        "application/json"
    });
});

app.UseResponseCompression();

18.8.2 启用缓存

csharp

复制代码
// Program.cs
builder.Services.AddResponseCaching();

app.UseResponseCaching();

// 在控制器中使用
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Client)]
[HttpGet("data")]
public IActionResult GetData()
{
    return Ok(new { timestamp = DateTime.Now });
}

18.8.3 数据库连接池优化

csharp

复制代码
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection"),
        sqlOptions =>
        {
            sqlOptions.MaxBatchSize(100);
            sqlOptions.CommandTimeout(30);
            sqlOptions.EnableRetryOnFailure(3);
        });
    options.EnableSensitiveDataLogging(builder.Environment.IsDevelopment());
    options.EnableDetailedErrors(builder.Environment.IsDevelopment());
});

18.8.4 静态文件优化

csharp

复制代码
// Program.cs
app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        // 缓存静态文件 1 年
        ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=31536000");
        ctx.Context.Response.Headers.Append("Expires", DateTime.UtcNow.AddYears(1).ToString("R"));
    }
});

18.8.5 使用 CDN

html

复制代码
<!-- _Layout.cshtml -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" />
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>

<!-- 本地回退 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
    window.addEventListener('load', function() {
        if (typeof bootstrap === 'undefined') {
            var script = document.createElement('script');
            script.src = '/lib/bootstrap/js/bootstrap.bundle.min.js';
            document.body.appendChild(script);
        }
    });
</script>

18.9 日志和监控

18.9.1 配置日志

csharp

复制代码
// Program.cs
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
builder.Logging.AddEventLog();  // Windows only
builder.Logging.AddApplicationInsights();

// 设置最小日志级别
builder.Logging.SetMinimumLevel(LogLevel.Information);

// 按类别配置
builder.Logging.AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning);
builder.Logging.AddFilter("System.Net.Http", LogLevel.Error);

18.9.2 使用 Serilog

bash

复制代码
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq

csharp

复制代码
// Program.cs
using Serilog;

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .WriteTo.Console()
    .WriteTo.File("logs/app-.txt", 
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 30)
    .WriteTo.Seq("http://localhost:5341")
    .CreateLogger();

builder.Host.UseSerilog();

// 使用
app.UseSerilogRequestLogging();

18.9.3 健康检查

csharp

复制代码
// Program.cs
builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>()
    .AddUrlGroup(new Uri("https://api.example.com"), "External API")
    .AddCheck<CustomHealthCheck>("Custom");

app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = _ => true
});

app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false  // 只检查是否运行
});

18.9.4 应用洞察(Application Insights)

bash

复制代码
dotnet add package Microsoft.ApplicationInsights.AspNetCore

csharp

复制代码
// Program.cs
builder.Services.AddApplicationInsightsTelemetry();

// appsettings.json
{
  "ApplicationInsights": {
    "ConnectionString": "InstrumentationKey=your-key;IngestionEndpoint=https://..."
  }
}

18.10 综合示例:完整的生产部署脚本

18.10.1 Linux 部署脚本

bash

复制代码
#!/bin/bash
# deploy.sh

set -e

APP_NAME="MyApp"
DEPLOY_DIR="/var/www/$APP_NAME"
BACKUP_DIR="/var/www/backups/$APP_NAME-$(date +%Y%m%d_%H%M%S)"

echo "=== 部署 $APP_NAME ==="

# 1. 备份当前版本
echo "1. 备份当前版本..."
if [ -d "$DEPLOY_DIR" ]; then
    mkdir -p "$BACKUP_DIR"
    cp -r "$DEPLOY_DIR" "$BACKUP_DIR"
    echo "备份到: $BACKUP_DIR"
fi

# 2. 发布新版本
echo "2. 发布新版本..."
dotnet publish -c Release -o ./publish

# 3. 停止服务
echo "3. 停止服务..."
sudo systemctl stop "$APP_NAME.service"

# 4. 部署文件
echo "4. 部署文件..."
sudo rm -rf "$DEPLOY_DIR"
sudo mkdir -p "$DEPLOY_DIR"
sudo cp -r ./publish/* "$DEPLOY_DIR"

# 5. 设置权限
echo "5. 设置权限..."
sudo chown -R www-data:www-data "$DEPLOY_DIR"
sudo chmod -R 755 "$DEPLOY_DIR"

# 6. 运行数据库迁移
echo "6. 运行数据库迁移..."
cd "$DEPLOY_DIR"
dotnet ef database update --connection "Server=localhost;Database=MyApp;"

# 7. 启动服务
echo "7. 启动服务..."
sudo systemctl start "$APP_NAME.service"

# 8. 检查状态
echo "8. 检查服务状态..."
sleep 5
if systemctl is-active --quiet "$APP_NAME.service"; then
    echo "✅ 部署成功!"
else
    echo "❌ 部署失败!正在回滚..."
    if [ -d "$BACKUP_DIR" ]; then
        sudo rm -rf "$DEPLOY_DIR"
        sudo cp -r "$BACKUP_DIR" "$DEPLOY_DIR"
        sudo systemctl restart "$APP_NAME.service"
    fi
    exit 1
fi

# 9. 清理旧备份
echo "9. 清理旧备份(保留最近5个)..."
ls -dt /var/www/backups/$APP_NAME-* | tail -n +6 | xargs -r rm -rf

echo "=== 部署完成 ==="

18.10.2 GitHub Actions 完整工作流

yaml

复制代码
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
    paths-ignore:
      - '**.md'
      - 'docs/**'
  pull_request:
    branches: [ main ]

env:
  DOTNET_VERSION: '8.0.x'
  PROJECT_PATH: 'src/MyApp/MyApp.csproj'
  TEST_PATH: 'tests/MyApp.Tests/MyApp.Tests.csproj'

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: ${{ env.DOTNET_VERSION }}
    
    - name: Restore dependencies
      run: dotnet restore ${{ env.PROJECT_PATH }}
    
    - name: Build
      run: dotnet build ${{ env.PROJECT_PATH }} --configuration Release --no-restore
    
    - name: Run tests
      run: dotnet test ${{ env.TEST_PATH }} --configuration Release --no-build --verbosity normal
    
    - name: Code coverage
      run: |
        dotnet tool install --global dotnet-coverage
        dotnet-coverage collect "dotnet test ${{ env.TEST_PATH }}" -f xml -o coverage.xml
    
    - name: Upload coverage
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage.xml

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: ${{ env.DOTNET_VERSION }}
    
    - name: Publish
      run: dotnet publish ${{ env.PROJECT_PATH }} -c Release -o publish
    
    - name: Upload artifact
      uses: actions/upload-artifact@v3
      with:
        name: app-publish
        path: publish/

    - name: Log in to Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}
    
    - name: Build Docker image
      run: |
        docker build -t myapp:${{ github.sha }} .
        docker tag myapp:${{ github.sha }} myapp:latest
    
    - name: Push Docker image
      run: |
        docker push myapp:${{ github.sha }}
        docker push myapp:latest

  deploy-staging:
    needs: build-and-push
    runs-on: ubuntu-latest
    environment: staging
    
    steps:
    - name: Deploy to staging
      run: |
        ssh ${{ secrets.STAGING_HOST }} "
          cd /var/www/myapp &&
          docker pull myapp:${{ github.sha }} &&
          docker-compose down &&
          docker-compose up -d &&
          docker system prune -f
        "

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production
    if: github.ref == 'refs/heads/main'
    
    steps:
    - name: Approval required
      uses: trstringer/manual-approval@v1
      with:
        secret: ${{ secrets.GITHUB_TOKEN }}
        approvers: admin,lead-developer
    
    - name: Deploy to production
      run: |
        ssh ${{ secrets.PRODUCTION_HOST }} "
          cd /var/www/myapp &&
          docker pull myapp:${{ github.sha }} &&
          docker-compose down &&
          docker-compose up -d &&
          docker system prune -f &&
          curl -X POST https://api.rollbar.com/api/1/deploy/ \
            -H 'X-Rollbar-Access-Token: ${{ secrets.ROLLBAR_TOKEN }}' \
            -d 'environment=production&revision=${{ github.sha }}'
        "
  
  notify:
    needs: deploy-production
    runs-on: ubuntu-latest
    
    steps:
    - name: Send Slack notification
      uses: rtCamp/action-slack-notify@v2
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        SLACK_TITLE: "Deployment Successful"
        SLACK_MESSAGE: "Version ${{ github.sha }} deployed to production"
        SLACK_COLOR: good

18.10.3 Docker Compose 生产配置

yaml

复制代码
# docker-compose.prod.yml
version: '3.8'

services:
  web:
    image: myapp:${TAG:-latest}
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ConnectionStrings__DefaultConnection=Server=db;Database=MyApp;User=sa;Password=${DB_PASSWORD}
      - Redis__ConnectionString=redis:6379
    volumes:
      - app_logs:/app/logs
      - /etc/letsencrypt:/etc/letsencrypt:ro
    depends_on:
      - db
      - redis
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 1G
        reservations:
          cpus: '0.5'
          memory: 512M

  db:
    image: mcr.microsoft.com/mssql/server:2022-latest
    restart: unless-stopped
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=${DB_PASSWORD}
      - MSSQL_PID=Standard
    volumes:
      - sql_data:/var/opt/mssql
      - sql_backup:/var/opt/mssql/backup
    healthcheck:
      test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-S", "localhost", "-U", "sa", "-P", "${DB_PASSWORD}", "-Q", "SELECT 1"]
      interval: 30s
      timeout: 10s
      retries: 5
    deploy:
      resources:
        limits:
          cpus: '4'
          memory: 8G
        reservations:
          cpus: '1'
          memory: 2G

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 1G
        reservations:
          cpus: '0.25'
          memory: 256M

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
    depends_on:
      - web
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost"]
      interval: 30s
      timeout: 10s
      retries: 3

  seq:
    image: datalust/seq:latest
    restart: unless-stopped
    environment:
      - ACCEPT_EULA=Y
    volumes:
      - seq_data:/data
    ports:
      - "5341:80"
    deploy:
      resources:
        limits:
          memory: 512M

volumes:
  sql_data:
  sql_backup:
  redis_data:
  app_logs:
  seq_data:

18.10.4 Nginx 反向代理配置(生产)

nginx

复制代码
# /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
worker_rlimit_nofile 65535;

events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}

http {
    # 基础配置
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    server_tokens off;
    
    # MIME 类型
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # 日志格式
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" '
                    'rt=$request_time uct="$upstream_connect_time" uht="$upstream_header_time" urt="$upstream_response_time"';
    
    access_log /var/log/nginx/access.log main buffer=32k flush=5s;
    error_log /var/log/nginx/error.log warn;
    
    # Gzip 压缩
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml image/svg+xml;
    gzip_min_length 1000;
    
    # 限流配置
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_conn_zone $binary_remote_addr zone=conn:10m;
    
    # 上游服务器(Blazor Server)
    upstream blazor_server {
        server web:80;
        keepalive 32;
    }
    
    # HTTP 重定向到 HTTPS
    server {
        listen 80 default_server;
        listen [::]:80 default_server;
        server_name _;
        return 301 https://$host$request_uri;
    }
    
    # HTTPS 主配置
    server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        server_name mydomain.com;
        
        # SSL 证书
        ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;
        
        # SSL 安全配置
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 10m;
        
        # HSTS
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
        
        # 安全头
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        
        # 健康检查
        location /health {
            proxy_pass http://blazor_server;
            access_log off;
        }
        
        # WebSocket 支持(Blazor Server 需要)
        location /_blazor {
            proxy_pass http://blazor_server;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_read_timeout 300s;
            proxy_connect_timeout 75s;
        }
        
        # API
        location /api/ {
            proxy_pass http://blazor_server;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            
            # 限流
            limit_req zone=api burst=20 nodelay;
            limit_conn conn 10;
        }
        
        # 静态文件
        location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
            proxy_pass http://blazor_server;
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
        
        # 默认路由
        location / {
            proxy_pass http://blazor_server;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_buffer_size 128k;
            proxy_buffers 4 256k;
            proxy_busy_buffers_size 256k;
        }
    }
}

18.11 常见问题与解决方案

问题1:部署后显示 500 错误

bash

复制代码
# 检查日志
# Windows (IIS)
C:\inetpub\logs\LogFiles

# Linux (systemd)
journalctl -u myapp.service -n 50

# Docker
docker logs myapp --tail 50

# 常见原因:
# 1. 数据库连接字符串错误
# 2. 缺少依赖(如 SQL Server 客户端)
# 3. 权限问题
# 4. 端口被占用

问题2:内存泄漏

bash

复制代码
# 检查内存使用
# Linux
top -p $(pgrep dotnet)
# 或
docker stats

# 启用 GC 日志
# 环境变量:DOTNET_gcServer=1
# 环境变量:DOTNET_gcConcurrent=1

问题3:数据库连接池耗尽

csharp

复制代码
// 增加连接池大小
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString, sqlOptions =>
    {
        sqlOptions.MaxPoolSize(200);
        sqlOptions.MinPoolSize(10);
    });
});

// 确保正确释放 DbContext
// 使用 using 或依赖注入(Scoped 生命周期)

问题4:Blazor Server 连接中断

csharp

复制代码
// Program.cs
builder.Services.AddServerSideBlazor(options =>
{
    options.DisconnectedRetryCount = 5;
    options.DisconnectedRetryInterval = TimeSpan.FromSeconds(3);
});

// 添加重连组件
// 在 _Layout.cshtml 中添加:
// <script src="_framework/blazor.server.js" autostart="false"></script>
// <script>
//   Blazor.start({
//     reconnectionOptions: {
//       maxRetries: 10,
//       retryIntervalMilliseconds: 2000
//     }
//   });
// </script>

18.12 本章总结

部署决策树

text

复制代码
                    开始部署
                       │
                       ▼
              应用类型是什么?
          ┌──────────┴──────────┐
          │                     │
       Web API               Blazor
          │                     │
          ▼                     ▼
   需要容器化?           需要容器化?
    ┌───┴───┐               ┌───┴───┐
   是      否              是      否
    │       │               │       │
    ▼       ▼               ▼       ▼
 Docker   IIS/       Docker   Blazor
          Nginx              Server vs WASM

部署检查清单

  • 连接字符串配置正确

  • 环境变量已设置

  • 数据库迁移已执行

  • 日志目录有写入权限

  • 防火墙端口已开放

  • SSL 证书已配置

  • 健康检查端点可访问

  • 备份策略已制定

  • 监控告警已配置


18.13 练习题

基础题

  1. 使用 dotnet publish 发布你的应用,尝试不同的发布选项(独立、单文件、修剪)。

  2. 在本地 IIS 中部署一个 ASP.NET Core 应用。

  3. 为你的应用创建 Dockerfile 并构建镜像。

应用题

  1. 配置 Nginx 反向代理,让应用支持 HTTPS。

  2. 设置 GitHub Actions CI/CD 流水线,自动测试和部署。

  3. 配置 Serilog,将日志输出到文件和 Seq。

挑战题

  1. 实现蓝绿部署或金丝雀部署策略。

  2. 配置 Prometheus + Grafana 监控应用指标。

  3. 实现自动伸缩(Auto-scaling)配置。

相关推荐
xiaoshuaishuai842 分钟前
C# AvaloniaUI 资源找不到报错
java·服务器·前端·windows·c#
思麟呀1 小时前
C++11并发编程:call_once一次性执行+atomic原子类型+CAS无锁编程+自旋锁
linux·开发语言·jvm·c++·windows
码不停蹄的玄黓2 小时前
Java 生产者-消费者模型详解
java·开发语言·python
爱讲故事的2 小时前
操作系统第一讲复习:为什么学习操作系统,以及操作系统到底在做什么?
linux·开发语言·windows·学习·ubuntu·c#
笨蛋不要掉眼泪2 小时前
Java并发编程:Executors框架类深度解析
java·开发语言·并发
_童年的回忆_2 小时前
【php】在linux下PHP安装amqp扩展
linux·开发语言·php
JaydenAI3 小时前
[MAF预定义的AIContextProvider-03]ChatHistoryMemoryProvider——赋予Agent从经验中学习的能力
ai·c#·agent·memory·maf
AIMath~3 小时前
python中的uv命令揭秘
开发语言·python·uv
弹简特3 小时前
【零基础学Python】06-Python模块和包、异常处理、文件常用操作
开发语言·python