Backend production infra:
- Packages: Serilog.Sinks.File, HealthChecks.EntityFrameworkCore (RateLimiting built-in .NET 10)
- appsettings.Production.json MOI: placeholder __SET_VIA_SECRETS__, AllowedOrigins, Serilog File sink rolling daily retention 30d, RateLimit config
- appsettings.json + Development.json: them Serilog WriteTo Console
- Program.cs REWRITE:
- Serilog ReadFrom.Configuration (prod file / dev console)
- Rate limiter: policy auth-login 5/min/IP (AuthController.Login) + GlobalLimiter 300/min/IP
- Health checks: /health/live liveness (empty predicate) + /health/ready DB probe (AddDbContextCheck)
- HSTS production 1 year
- CORS origins from config AllowedOrigins (default dev 2 localhost)
- AuthController.Login gắn [EnableRateLimiting("auth-login")]
Deploy scripts:
- scripts/deploy-iis.ps1: stop pool → backup current → clean+extract artifact → start pool → health check loop 30s timeout → rollback instruction if fail
- scripts/backup-sql.ps1: BACKUP DATABASE voi INIT+COMPRESSION+CHECKSUM + retention 30d auto cleanup
- .gitea/workflows/deploy.yml MOI: 4 job build BE (Windows) + build 2 FE (Ubuntu, pin .nvmrc 20) + deploy-iis qua WinRM PSSession (secrets IIS_HOST/USER/PASSWORD/JWT_SECRET/DB_CONNECTION)
Docs guides MOI (4 file):
- deployment-iis.md: prereqs (IIS features, Hosting Bundle, SQL, WinRM) + setup lan dau (app pool, 3 site, HTTPS win-acme, user-secrets) + deploy hang ngay (CI/CD + manual) + rollback + monitoring + troubleshooting + SPA web.config sample
- cicd.md: pipeline overview 4 job, secrets setup, runner Windows+Ubuntu, branch strategy, build optimizations, common CI/CD issues
- security-checklist.md: OWASP top 10 2021 mapping voi status + pre go-live checklist + incident response
- runbook.md: daily ops (health/logs), restart/rollback, DB backup/restore/migration revert, user management (reset password, unlock, disable), monitoring (CPU/disk/connection pool), deployment checklist, common gotcha
Frontend refresh token (ca 2 app fe-admin + fe-user):
- lib/api.ts REWRITE: them REFRESH_KEY, axios response interceptor 401 → POST /auth/refresh → retry request goc. Queue pattern cho nhieu request song song chi 1 refresh call chay. Skip retry /auth/login + /auth/refresh tranh infinite loop. _retry flag tren original config.
- contexts/AuthContext.tsx: luu+xoa REFRESH_KEY trong login/logout
E2E verified:
- GET /health/live → 200 Healthy
- GET /health/ready → 200 Healthy (DB probe)
- Rate limit flood 7 POST /auth/login → #1-5 HTTP 400 (cred sai) + #6-7 HTTP 429 Too Many Requests ✅
- TS check fe-admin + fe-user → pass
- dotnet build → 0 errors
Docs updates:
- docs/STATUS.md: Phase 5 prep done, next Phase 5 deploy production + Phase 5.1 security hardening, cumulative stats 8 commits
- docs/HANDOFF.md: phase table them Phase 5 prep row, file tree update voi guides + scripts + workflows, git state commit 8
- docs/changelog/migration-todos.md: tick Phase 5 prep items (12 items done) + Phase 5 deploy items remaining + Phase 5.1 security hardening list
- docs/changelog/sessions/2026-04-21-1530-phase5-prep.md: session log chi tiet
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.7 KiB
7.7 KiB
Deployment — Windows Server + IIS
Step-by-step setup lần đầu + deploy hàng ngày. Test với Windows Server 2019/2022 + IIS 10.
1. Prerequisites trên target server
OS + IIS
- Windows Server 2019 / 2022 (hoặc Windows 10/11 Pro cho staging)
- IIS với features: Static Content, HTTP Redirection, Application Initialization, WebSocket Protocol, Management Console, Windows Auth (optional)
- URL Rewrite Module 2.1+: https://www.iis.net/downloads/microsoft/url-rewrite
- Application Request Routing (ARR) 3.0+ (nếu dùng reverse proxy): https://www.iis.net/downloads/microsoft/application-request-routing
.NET 10 Hosting Bundle
https://dotnet.microsoft.com/en-us/download/dotnet/10.0
→ .NET 10 Hosting Bundle (không phải SDK, runtime + ASP.NET Core Module)
Sau khi cài → restart IIS: iisreset trong cmd elevated.
SQL Server
- SQL Server 2019+ Express / Standard / Enterprise
- Tạo DB
SolutionErp+ SQL usersolutionerp_appvới db_owner - Bật Named Pipes hoặc TCP/IP protocol (SQL Server Configuration Manager)
WinRM (cho CI/CD deploy từ xa)
# Run as admin trên target server
Enable-PSRemoting -Force
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*" -Force
winrm quickconfig -Transport:HTTPS
2. Setup lần đầu
2.1 Tạo app pool
Import-Module WebAdministration
New-WebAppPool -Name "SolutionErpApi"
Set-ItemProperty IIS:\AppPools\SolutionErpApi -Name managedRuntimeVersion -Value "" # .NET CORE (no CLR)
Set-ItemProperty IIS:\AppPools\SolutionErpApi -Name startMode -Value "AlwaysRunning"
Set-ItemProperty IIS:\AppPools\SolutionErpApi -Name processModel.identityType -Value "ApplicationPoolIdentity"
2.2 Tạo site (3 site hoặc 1 site với 3 path)
Recommended: 3 site riêng để quản lý binding dễ:
# Api
New-WebSite -Name "SolutionErp-Api" -Port 443 -HostHeader "api.solutionerp.local" `
-PhysicalPath "C:\inetpub\solution-erp\api" -ApplicationPool "SolutionErpApi" -Ssl
# Admin FE
New-WebSite -Name "SolutionErp-Admin" -Port 443 -HostHeader "admin.solutionerp.local" `
-PhysicalPath "C:\inetpub\solution-erp\fe-admin" -Ssl
# User FE
New-WebSite -Name "SolutionErp-User" -Port 443 -HostHeader "app.solutionerp.local" `
-PhysicalPath "C:\inetpub\solution-erp\fe-user" -Ssl
2.3 HTTPS certificate
Option A — win-acme (Let's Encrypt free)
# Download từ https://www.win-acme.com/
wacs.exe
# Menu: N (new cert) → chọn sites → auto-renew via scheduled task
Option B — Self-signed (dev/staging only)
$cert = New-SelfSignedCertificate -DnsName "api.solutionerp.local", "admin.solutionerp.local", "app.solutionerp.local" `
-CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(5)
$bytes = $cert.GetCertHash()
2.4 Secrets cho Api
KHÔNG commit appsettings.Production.json với secret thật. Dùng user-secrets:
# Trên target server, với identity app pool
cd C:\inetpub\solution-erp\api
dotnet user-secrets set "Jwt:Secret" "__RANDOM_64_CHAR_STRING__"
dotnet user-secrets set "ConnectionStrings:Default" "Server=.;Database=SolutionErp;User Id=solutionerp_app;Password=..."
Hoặc dùng env vars (set qua IIS app pool advanced settings):
Jwt__Secret=...ConnectionStrings__Default=...
2.5 FE build — adjust Vite base URL + API proxy
fe-admin/vite.config.ts production build không proxy, FE gọi trực tiếp https://api.solutionerp.local. Thêm env:
# fe-admin/.env.production
VITE_API_BASE_URL=https://api.solutionerp.local
Update src/lib/api.ts sử dụng import.meta.env.VITE_API_BASE_URL ?? '/api'.
2.6 Init DB
# Run trên app server với .NET SDK installed (KHÔNG chỉ runtime)
cd C:\Deploy\staging
dotnet ef database update --project src\Backend\SolutionErp.Infrastructure --startup-project src\Backend\SolutionErp.Api
Hoặc tự động khi app khởi động lần đầu (đã có trong DbInitializer).
3. Deploy hàng ngày
3.1 Qua CI/CD (Gitea Actions)
Push code vào main → .gitea/workflows/deploy.yml auto:
- Build BE + 2 FE
- Upload artifact
- WinRM tới IIS host
- Run
scripts/deploy-iis.ps1
3.2 Manual deploy
# Trên dev machine
dotnet publish src/Backend/SolutionErp.Api --configuration Release -o .\publish\api
cd fe-admin && npm ci && npm run build # dist/
cd fe-user && npm ci && npm run build # dist/
Compress-Archive .\publish\api\* api.zip
Compress-Archive .\fe-admin\dist\* fe-admin.zip
Compress-Archive .\fe-user\dist\* fe-user.zip
# Copy lên target server
Copy-Item api.zip, fe-admin.zip, fe-user.zip -Destination \\server\C$\Deploy\ -Force
# Trên target server
.\scripts\deploy-iis.ps1 -Artifact C:\Deploy\api.zip -Site SolutionErpApi
Expand-Archive C:\Deploy\fe-admin.zip C:\inetpub\solution-erp\fe-admin -Force
Expand-Archive C:\Deploy\fe-user.zip C:\inetpub\solution-erp\fe-user -Force
3.3 Rollback
# Deploy script tự backup vào C:\inetpub\solution-erp\backups\api-{timestamp}
Stop-WebAppPool SolutionErpApi
Copy-Item C:\inetpub\solution-erp\backups\api-20260421-0930\* `
C:\inetpub\solution-erp\api\ -Recurse -Force
Start-WebAppPool SolutionErpApi
# Nếu migration hỏng → revert DB:
dotnet ef database update <PreviousMigrationName> --project ...
4. Monitoring
4.1 Health check
/health/live— liveness probe (IIS ping)/health/ready— readiness probe (DB reachable)
4.2 Logs
- Serilog →
C:\inetpub\solution-erp\api\logs\solution-erp-{yyyyMMdd}.log - Retention 30 ngày auto
- IIS request log:
C:\inetpub\logs\LogFiles\ - Application event log:
eventvwr→ Windows Logs → Application → Source: IIS Express / .NET Runtime
4.3 SQL backup
Task Scheduler trigger scripts\backup-sql.ps1 daily 2:00 AM. Retention 30 ngày .bak files.
5. Troubleshooting
| Triệu chứng | Check |
|---|---|
| HTTP 500.30 ANCM startup | event log Application, logs folder có được tạo không, permission app pool identity |
| HTTP 502.5 ANCM Process failure | .NET 10 Hosting Bundle đã cài chưa, app pool CLR = "" |
| 500 khi login | secrets config đúng chưa (Jwt__Secret + ConnectionStrings__Default) |
| CORS fail | appsettings.Production.json → AllowedOrigins match domain FE |
| Slow query | check index (xem database/schema-diagram.md §4) |
| App pool crash loop | disable rapid fail protection tạm thời khi debug: Set-ItemProperty IIS:\AppPools\SolutionErpApi -Name failure.rapidFailProtection -Value false |
| FE 404 routes (vd /contracts/123) | IIS URL Rewrite config SPA fallback — tạo web.config FE folder với rule rewrite .* → index.html |
web.config cho SPA FE (sample)
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="SPA Routes" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
<add input="{REQUEST_URI}" pattern="^/api" negate="true" />
</conditions>
<action type="Rewrite" url="/" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
6. Liên quan
cicd.md— Gitea Actions workflow chi tiếtsecurity-checklist.md— OWASP top 10runbook.md— operations (restart, restore, common tasks)../database/database-guide.md— DB backup/restore chi tiết