雲端部署實戰
部署選項比較
平台 難度 價格 適合場景
──────────────────────────────────────────────────
Railway 低 免費方案 個人專案、學習用(我們實際在用!)
Azure App Service 中 免費方案 .NET 專案、企業級
AWS EC2 高 按用量 完全自訂、大規模
Heroku 低 免費方案 快速原型
DigitalOcean 中 固定月費 中小型專案
Railway 部署(我們實際用的!)
💡 Railway 是一個簡單的雲端平台,非常適合學習和小型專案。 支援從 GitHub 自動部署,幾乎不用設定就能上線。
部署步驟
1. 到 railway.app 註冊帳號(用 GitHub 登入)
2. 點「New Project」
3. 選「Deploy from GitHub repo」
4. 選擇你的 Repository
5. Railway 自動偵測 Dockerfile 或 .NET 專案
6. 設定環境變數
7. 部署完成!自動產生 URL
Railway 環境變數設定
在 Railway Dashboard 設定:
變數名稱 值
─────────────────────────────────────────────────────
ASPNETCORE_ENVIRONMENT Production
ConnectionStrings__Default Server=...;Database=...
JWT_KEY your-production-jwt-key
PORT 8080(Railway 自動設定)
Railway 用的 Dockerfile
# 階段 1:建置
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# 複製專案檔案並還原套件
COPY *.csproj ./
RUN dotnet restore
# 複製所有原始碼並發佈
COPY . .
RUN dotnet publish -c Release -o /app/publish
# 階段 2:執行
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
# 從建置階段複製成品
COPY --from=build /app/publish .
# Railway 使用 PORT 環境變數
ENV ASPNETCORE_URLS=http://+:${PORT:-8080}
# 啟動應用程式
ENTRYPOINT ["dotnet", "MyApp.dll"]
Azure App Service 部署
使用 Azure CLI 部署
# 安裝 Azure CLI 後登入
az login
# 建立 Resource Group(資源群組)
az group create --name myapp-rg --location eastasia
# 建立 App Service Plan(選擇免費方案)
az appservice plan create \
--name myapp-plan \
--resource-group myapp-rg \
--sku F1 \
--is-linux
# 建立 Web App
az webapp create \
--name myapp-unique-name \
--resource-group myapp-rg \
--plan myapp-plan \
--runtime "DOTNETCORE:8.0"
# 設定環境變數
az webapp config appsettings set \
--name myapp-unique-name \
--resource-group myapp-rg \
--settings ASPNETCORE_ENVIRONMENT=Production
# 部署程式碼
az webapp up \
--name myapp-unique-name \
--resource-group myapp-rg
GitHub Actions 部署到 Azure
# .github/workflows/azure-deploy.yml
name: Deploy to Azure
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# 拉取程式碼
- uses: actions/checkout@v4
# 安裝 .NET
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
# 建置和發佈
- name: Build and Publish
run: dotnet publish -c Release -o ./publish
# 部署到 Azure App Service
- name: Deploy to Azure
uses: azure/webapps-deploy@v2
with:
# App Service 的名稱
app-name: myapp-unique-name
# 從 GitHub Secrets 讀取發佈設定
publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE }}
# 指定發佈目錄
package: ./publish
反向代理(Reverse Proxy)
💡 比喻:公司總機 客戶打電話到公司總機(反向代理), 總機根據需求轉接到不同部門(後端伺服器)。 客戶不需要知道每個部門的分機號碼。
Nginx 基本設定
# /etc/nginx/sites-available/myapp
# Nginx 反向代理設定
server {
# 監聽 80 port(HTTP)
listen 80;
# 網域名稱
server_name myapp.example.com;
# 轉址到 HTTPS
return 301 https://$server_name$request_uri;
}
server {
# 監聽 443 port(HTTPS)
listen 443 ssl;
server_name myapp.example.com;
# SSL 憑證路徑
ssl_certificate /etc/letsencrypt/live/myapp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem;
# 把所有請求轉發到 .NET 應用程式
location / {
# 轉發到本機的 5000 port(Kestrel)
proxy_pass http://localhost:5000;
# 傳遞原始的 Host 標頭
proxy_set_header Host $host;
# 傳遞使用者的真實 IP
proxy_set_header X-Real-IP $remote_addr;
# 傳遞轉發鏈的 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 告訴後端使用了 HTTPS
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Docker Compose 中使用 Nginx
# docker-compose.yml 中加入 Nginx
services:
# Nginx 反向代理
nginx:
image: nginx:alpine
ports:
# 對外只開 80 和 443
- "80:80"
- "443:443"
volumes:
# 掛載 Nginx 設定檔
- ./nginx.conf:/etc/nginx/conf.d/default.conf
# 掛載 SSL 憑證
- ./certs:/etc/nginx/certs
depends_on:
- web
# .NET 應用程式(不直接對外)
web:
build: .
# 不需要對外開 port,Nginx 會轉發
expose:
- "8080"
SSL/TLS 設定
Let's Encrypt 免費 SSL 憑證
# 安裝 Certbot(Let's Encrypt 的工具)
sudo apt install certbot python3-certbot-nginx
# 自動取得和設定 SSL 憑證
sudo certbot --nginx -d myapp.example.com
# 憑證自動更新(Let's Encrypt 憑證 90 天過期)
sudo certbot renew --dry-run
# 設定自動更新排程(每天檢查一次)
# sudo crontab -e
# 0 0 * * * certbot renew --quiet
監控與 Logging
Serilog 結構化日誌
// 安裝套件:
// dotnet add package Serilog.AspNetCore
// dotnet add package Serilog.Sinks.Console
// dotnet add package Serilog.Sinks.File
// Program.cs 設定 Serilog
using Serilog;
// 建立 Serilog Logger
Log.Logger = new LoggerConfiguration()
// 最低日誌等級
.MinimumLevel.Information()
// 輸出到 Console(結構化格式)
.WriteTo.Console()
// 輸出到檔案(每天一個新檔案)
.WriteTo.File(
"logs/myapp-.log",
rollingInterval: RollingInterval.Day, // 每天產生新檔案
retainedFileCountLimit: 30 // 保留 30 天
)
.CreateLogger();
// 在 ASP.NET Core 中使用 Serilog
builder.Host.UseSerilog();
// 在程式碼中記錄日誌
app.MapGet("/api/users/{id}", (int id, ILogger<Program> logger) =>
{
// 結構化日誌:用 {} 包裹參數名稱
logger.LogInformation("查詢使用者 {UserId}", id);
try
{
// 處理邏輯...
return Results.Ok();
}
catch (Exception ex)
{
// 記錄錯誤日誌(包含例外堆疊)
logger.LogError(ex, "查詢使用者 {UserId} 時發生錯誤", id);
return Results.StatusCode(500);
}
});
Health Endpoint(健康檢查端點)
// Program.cs 設定完整的健康檢查
builder.Services.AddHealthChecks()
// 檢查資料庫
.AddSqlServer(
builder.Configuration.GetConnectionString("Default")!,
name: "database",
timeout: TimeSpan.FromSeconds(5))
// 自訂健康檢查
.AddCheck("disk_space", () =>
{
// 檢查磁碟空間
var drive = new DriveInfo("C");
var freeSpacePercent = (double)drive.AvailableFreeSpace / drive.TotalSize * 100;
// 磁碟空間低於 10% 就回報不健康
return freeSpacePercent > 10
? HealthCheckResult.Healthy($"磁碟空間:{freeSpacePercent:F1}%")
: HealthCheckResult.Unhealthy($"磁碟空間不足:{freeSpacePercent:F1}%");
});
// 設定健康檢查路由
app.MapHealthChecks("/health", new HealthCheckOptions
{
// 自訂回應格式(JSON)
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
// 組合所有檢查結果
var result = new
{
status = report.Status.ToString(),
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
description = e.Value.Description
})
};
// 序列化成 JSON 回傳
await context.Response.WriteAsJsonAsync(result);
}
});
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:把連線字串寫死在程式碼中
// ❌ 錯誤:連線字串寫死在程式碼中
var connectionString = "Server=myserver.database.windows.net;Database=mydb;User=admin;Password=P@ssw0rd123";
// 推上 Git 就洩漏了!
// ✅ 正確:從環境變數或設定檔讀取
// 開發環境:User Secrets
// dotnet user-secrets set "ConnectionStrings:Default" "Server=..."
// 正式環境:環境變數
// Railway/Azure 的 Dashboard 設定環境變數
var connectionString2 = builder.Configuration.GetConnectionString("Default");
❌ 錯誤 2:沒有健康檢查端點
❌ 問題:應用程式掛了但不知道
沒有健康檢查的後果:
├── 使用者反應「網站掛了」才知道有問題
├── 不知道是應用程式掛了還是資料庫掛了
├── Docker/K8s 無法自動重啟不健康的容器
└── 負載平衡器不知道要把流量導到哪裡
✅ 正確:設定 /health 端點
├── 檢查應用程式是否活著
├── 檢查資料庫連線是否正常
├── 檢查外部服務是否可用
├── Docker healthcheck 會定期呼叫
└── 監控系統可以監測並告警
❌ 錯誤 3:沒有設定 Logging
// ❌ 錯誤:用 Console.WriteLine 當日誌
Console.WriteLine("使用者登入了"); // 沒有時間戳、等級、結構化
Console.WriteLine($"錯誤:{ex.Message}"); // 沒有堆疊追蹤
// ✅ 正確:使用結構化日誌(Serilog)
// 有時間戳、日誌等級、結構化參數
logger.LogInformation("使用者 {UserId} 於 {LoginTime} 登入", userId, DateTime.UtcNow);
// 錯誤日誌包含完整的例外堆疊
logger.LogError(ex, "使用者 {UserId} 登入失敗", userId);
// Serilog 的好處:
// ├── 結構化日誌(可以搜尋、過濾)
// ├── 自動包含時間戳和日誌等級
// ├── 可以輸出到多個目標(Console、檔案、Seq、Elasticsearch)
// └── 效能優秀(非同步寫入)
💡 重點整理
| 概念 | 說明 |
|---|---|
| Railway | 簡單的雲端平台,適合學習和小型專案 |
| Azure App Service | 微軟的雲端應用程式代管服務 |
| Reverse Proxy | 反向代理(Nginx),轉發請求到後端 |
| SSL/TLS | Let's Encrypt 提供免費 SSL 憑證 |
| Serilog | .NET 的結構化日誌框架 |
| Health Check | 健康檢查端點,讓監控系統知道服務是否正常 |
| 環境變數 | 不同環境用不同的設定,不寫死在程式碼中 |