第十七章我们学习了 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 发布
-
右键项目 → 发布
-
选择目标:文件夹 / Azure / IIS / Docker
-
配置发布设置
-
点击 发布
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:
-
启用 IIS:
-
控制面板 → 程序和功能 → 启用或关闭 Windows 功能
-
勾选 Internet Information Services
-
-
安装 .NET Hosting Bundle:
-
安装
dotnet-hosting-8.0.x-win.exe
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 练习题
基础题
-
使用
dotnet publish发布你的应用,尝试不同的发布选项(独立、单文件、修剪)。 -
在本地 IIS 中部署一个 ASP.NET Core 应用。
-
为你的应用创建 Dockerfile 并构建镜像。
应用题
-
配置 Nginx 反向代理,让应用支持 HTTPS。
-
设置 GitHub Actions CI/CD 流水线,自动测试和部署。
-
配置 Serilog,将日志输出到文件和 Seq。
挑战题
-
实现蓝绿部署或金丝雀部署策略。
-
配置 Prometheus + Grafana 监控应用指标。
-
实现自动伸缩(Auto-scaling)配置。