All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m52s
User request: anh trỏ 3 subdomain mới về VPS IP 103.124.94.38:
- api.huypham.vn → api.solutions.com.vn
- admin.huypham.vn → admin.solutions.com.vn
- user.huypham.vn → eoffice.solutions.com.vn
Verified DNS: cả 3 resolve 103.124.94.38 ✓
Update 17 file repo:
FE (4): fe-admin/.env.production + fe-user/.env.production
(VITE_API_BASE_URL → https://api.solutions.com.vn)
fe-admin/src/lib/{api,realtime}.ts + fe-user equivalents (comment)
BE (1): appsettings.Production.json.example — CORS AllowedOrigins
CI/CD (1): .gitea/workflows/deploy.yml — smoke test URL
Scripts (3): setup-iis-sites (DomainApi/Admin/User), setup-ssl (3 host),
deploy-all (verify curls)
Docs (5): STATUS, HANDOFF, PROJECT-MAP, vps-setup, gotchas
Skill (1): iis-deploy-runbook — 3 site table + description
Email admin@huypham.vn giữ nguyên (Let's Encrypt contact — không phải
domain serve).
Thêm scripts/migrate-domains.ps1 — 1-shot VPS migration:
1. Pre-flight: resolve DNS 3 domain → verify IP VPS khớp
2. Add HTTP binding mới cho 3 IIS site (giữ binding cũ làm fallback)
3. Run win-acme xin 3 cert Let's Encrypt qua HTTP-01 challenge
(auto add HTTPS binding + http→https redirect)
4. Verify /health/live + /health/ready + 2 FE endpoint
5. (Optional -RemoveOld) xóa binding huypham.vn sau verify OK
Rollback: nếu fail, binding cũ vẫn active → site serve qua huypham.vn.
Anh chạy trên VPS:
cd C:\solution-erp\scripts ; .\migrate-domains.ps1
# Sau 1-2 ngày verify stable:
.\migrate-domains.ps1 -RemoveOld -SkipCert
166 lines
6.2 KiB
PowerShell
166 lines
6.2 KiB
PowerShell
# Deploy SOLUTION_ERP all-in-one on VPS Windows Server.
|
|
# Runs 4 steps: SQL -> IIS -> SSL -> Runner. Idempotent. Stops on first step fail.
|
|
#
|
|
# Usage:
|
|
# .\deploy-all.ps1 `
|
|
# -SaPassword '<SA pw>' `
|
|
# -JwtSecret '<64-char hex>' `
|
|
# -VrappPassword '<vrapp pw>' `
|
|
# -RunnerToken '<gitea runner registration token>' `
|
|
# -AdminEmail 'admin@huypham.vn'
|
|
#
|
|
# Prereq (pre-check will verify):
|
|
# - Windows Server + Admin PowerShell
|
|
# - IIS + URL Rewrite installed
|
|
# - SQL Server with login 'sa' + 'vrapp' exists
|
|
# - .NET 10 Hosting Bundle installed
|
|
# - Port 80+443 firewall open
|
|
# - DNS api/admin/eoffice.solutions.com.vn pointing to VPS
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory=$true)] [string]$SaPassword,
|
|
[Parameter(Mandatory=$true)] [string]$JwtSecret,
|
|
[Parameter(Mandatory=$true)] [string]$VrappPassword,
|
|
[Parameter(Mandatory=$true)] [string]$RunnerToken,
|
|
[string]$AdminEmail = "admin@huypham.vn",
|
|
[switch]$SkipSsl,
|
|
[switch]$SkipRunner
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$ScriptRoot = $PSScriptRoot
|
|
$StartTime = Get-Date
|
|
|
|
function Write-Banner($text) {
|
|
Write-Host ""
|
|
Write-Host ("=" * 70) -ForegroundColor Cyan
|
|
Write-Host " $text" -ForegroundColor Cyan
|
|
Write-Host ("=" * 70) -ForegroundColor Cyan
|
|
}
|
|
|
|
function Test-Prereq {
|
|
$issues = @()
|
|
|
|
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
$issues += "Not running with Admin privilege"
|
|
}
|
|
|
|
if (-not (Get-Module -ListAvailable -Name SqlServer)) {
|
|
if (-not (Get-Command sqlcmd -ErrorAction SilentlyContinue)) {
|
|
$issues += "Neither SqlServer PS module nor sqlcmd.exe available. Install-Module SqlServer -Scope AllUsers -Force"
|
|
}
|
|
}
|
|
|
|
if (-not (Get-Module -ListAvailable -Name WebAdministration)) {
|
|
$issues += "IIS WebAdministration module not installed (Install-WindowsFeature Web-Server,Web-Scripting-Tools)"
|
|
}
|
|
|
|
$dotnet10 = & dotnet --list-runtimes 2>$null | Select-String "Microsoft.AspNetCore.App 10"
|
|
if (-not $dotnet10) {
|
|
$issues += ".NET 10 Hosting Bundle not installed (https://dotnet.microsoft.com/download/dotnet/10.0)"
|
|
}
|
|
|
|
return $issues
|
|
}
|
|
|
|
Write-Banner "Pre-check prerequisites"
|
|
$issues = Test-Prereq
|
|
if ($issues.Count -gt 0) {
|
|
Write-Host "[FAIL] Issues:" -ForegroundColor Red
|
|
$issues | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
|
|
Write-Host ""
|
|
Write-Host "Fix the above then re-run." -ForegroundColor Yellow
|
|
exit 1
|
|
}
|
|
Write-Host "[OK] All prerequisites present"
|
|
|
|
# ===================== Step 1: SQL DB =====================
|
|
Write-Banner "Step 1/4: SQL Database setup"
|
|
try {
|
|
& "$ScriptRoot\setup-sql-db.ps1" -SaPassword $SaPassword
|
|
if ($LASTEXITCODE -ne 0) { throw "setup-sql-db.ps1 exit $LASTEXITCODE" }
|
|
} catch {
|
|
Write-Host "[FAIL] Step 1: $_" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
# ===================== Step 2: IIS sites =====================
|
|
Write-Banner "Step 2/4: IIS sites + app pool"
|
|
try {
|
|
& "$ScriptRoot\setup-iis-sites.ps1"
|
|
if ($LASTEXITCODE -ne 0) { throw "setup-iis-sites.ps1 exit $LASTEXITCODE" }
|
|
} catch {
|
|
Write-Host "[FAIL] Step 2: $_" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
# ===================== Step 3: appsettings.Production.json =====================
|
|
Write-Banner "Step 3/4: Write appsettings.Production.json"
|
|
$apiPath = "C:\inetpub\solution-erp\api"
|
|
$example = "$apiPath\appsettings.Production.json.example"
|
|
$prod = "$apiPath\appsettings.Production.json"
|
|
|
|
if (Test-Path $example) {
|
|
$content = (Get-Content $example -Raw) `
|
|
-replace [regex]::Escape('__SET_VIA_SECRETS__'), $VrappPassword `
|
|
-replace [regex]::Escape('__SET_VIA_USER_SECRETS_OR_ENV__minimum_64_chars_random'), $JwtSecret
|
|
Set-Content -Path $prod -Value $content -Encoding UTF8
|
|
Write-Host "[OK] Wrote $prod"
|
|
# ACL: only Administrators + app pool identity can read
|
|
icacls $prod /inheritance:r | Out-Null
|
|
icacls $prod /grant:r 'Administrators:(R,W)' 'IIS AppPool\SolutionErp-Api:(R)' | Out-Null
|
|
Write-Host " ACL restricted (Admins RW + AppPool R only)"
|
|
} else {
|
|
Write-Warning "Template not found at $example - skip (deploy may not have run yet; need Gitea Actions deploy first)"
|
|
Write-Warning "After CI deploy, re-run this step or copy manually."
|
|
}
|
|
|
|
# ===================== Step 4: SSL =====================
|
|
if (-not $SkipSsl) {
|
|
Write-Banner "Step 4/4 (SSL): win-acme Let's Encrypt"
|
|
Write-Host "WARNING: Port 80 must be reachable from Internet (Let's Encrypt HTTP-01)"
|
|
Write-Host " Test from outside: curl http://api.solutions.com.vn"
|
|
$confirm = Read-Host "Continue? (y/N)"
|
|
if ($confirm -eq 'y') {
|
|
try {
|
|
& "$ScriptRoot\setup-ssl.ps1"
|
|
} catch {
|
|
Write-Warning "SSL fail: $_ - can fix later + re-run setup-ssl.ps1"
|
|
}
|
|
} else {
|
|
Write-Host "Skip SSL - run setup-ssl.ps1 manually later"
|
|
}
|
|
} else {
|
|
Write-Host "Skip SSL (-SkipSsl flag)"
|
|
}
|
|
|
|
# ===================== Step 5: Runner =====================
|
|
if (-not $SkipRunner -and $RunnerToken -ne "SKIP") {
|
|
Write-Banner "Step 5 (Runner): Gitea Actions runner"
|
|
try {
|
|
& "$ScriptRoot\setup-gitea-runner.ps1" -RegistrationToken $RunnerToken
|
|
} catch {
|
|
Write-Warning "Runner setup fail: $_ - run setup-gitea-runner.ps1 manually later"
|
|
}
|
|
}
|
|
|
|
# ===================== Summary =====================
|
|
$duration = (Get-Date) - $StartTime
|
|
Write-Banner "[OK] DEPLOY ALL DONE - $([int]$duration.TotalMinutes)m $([int]($duration.TotalSeconds % 60))s"
|
|
Write-Host ""
|
|
Write-Host "Next:"
|
|
Write-Host " 1. Verify 3 domains:"
|
|
Write-Host " curl https://api.solutions.com.vn/health/live"
|
|
Write-Host " curl -I https://admin.solutions.com.vn"
|
|
Write-Host " curl -I https://eoffice.solutions.com.vn"
|
|
Write-Host ""
|
|
Write-Host " 2. Set remaining 2 Gitea secrets (if not done):"
|
|
Write-Host " https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/settings/actions/secrets"
|
|
Write-Host " - JWT_SECRET (update with 64-char)"
|
|
Write-Host " - IIS_PASSWORD (Windows admin password)"
|
|
Write-Host ""
|
|
Write-Host " 3. Trigger deploy: push main commit -> Gitea Actions pick up -> workflow runs"
|
|
Write-Host ""
|
|
Write-Host " 4. Change default admin password after login (security-checklist.md)"
|