Build APM บน Windows ตอนที่ 7: Deploy รัน และอัปเดต security patch
ของที่ build เสร็จยังอยู่ใน stage\ ซึ่งเป็นโฟลเดอร์ชั่วคราว ตอนนี้เอามันลง C:\APM สตาร์ตให้เห็นกับตา และเก็บขั้นตอนอัปเดตครั้งต่อไปไว้ให้ทำซ้ำได้โดยไม่ต้องจำ
พอดีผมมาถึงตอนสุดท้ายของซีรีส์ และเป็นตอนที่ผมตั้งใจเขียนให้ใช้ซ้ำได้มากที่สุด เพราะเหตุผลทั้งหมดที่ผมลงมือ build Apache, PHP และ MariaDB เองคือเมื่อ PHP ปล่อย 8.4.27 แก้ RCE สองตัวเมื่อ 21 ตุลาคม 2026 (เรื่องเต็มอยู่ในข่าว php2shell) ผมอยากแก้เลขเวอร์ชันไฟล์เดียวแล้ว build ใหม่ได้ภายในวัน ไม่ต้องรอ bundle อย่าง XAMPP ที่ในเครื่องผมยังค้างที่ PHP 8.2.12 จากปี 2023
ตอนนี้มีสามงาน งานแรกคือเอาของใน stage\ ลง C:\APM ด้วย deploy.cmd งานที่สองคือสตาร์ต MariaDB กับ Apache ครั้งแรก ทดสอบ แล้วปิด ซึ่งผมเจอบั๊กจริงหนึ่งตัวตรงนี้และจะเล่าให้ครบ งานที่สามคือขั้นตอนอัปเดตเมื่อมี security release ครั้งหน้า ทุกขั้นตอนมีเลขกำกับ บอกว่ารันที่ terminal ไหน ควรเห็นอะไร และตรวจอย่างไร นอกจากสามงานนี้ ตอนนี้มีหัวข้อเสริมอีกสองเรื่อง คือการเปิดให้เครื่องใน LAN เข้า และบั๊กอีกห้าตัวที่ผมเจอหลังซีรีส์ออก
ใช้ PowerShell 7 (pwsh) แบบ user ปกติ เสมอ ไม่ต้องเปิดแบบ admin คำสั่งที่รันจากโฟลเดอร์ build ให้อยู่ที่ C:\apm-build ส่วนคำสั่งที่เรียกโปรแกรมใน C:\APM ใช้พาธเต็มทุกครั้ง รันจากโฟลเดอร์ไหนก็ได้ ตัวพิมพ์ใหญ่เล็กของ APM กับ apm-build ต่างกัน ระวังอย่าสลับ
หลังซีรีส์ออก ผมแก้สคริปต์ deploy กับ template ไปหลายจุด คำสั่ง สคริปต์ และตารางทั้งหมดในตอนนี้ตรงกับ kit รุ่นใหม่แล้ว เพิ่มของใหม่คือ -Lan, trust-ca.ps1, new-cert.ps1, firewall-lan.ps1 และ set-path.ps1 ใครโหลด kit ก่อนหน้านั้น อ่านวิธีอัปเดตในหัวข้อ "บั๊กที่เจอหลังซีรีส์ออก" ด้านล่าง
ก่อน deploy ตรวจของและเลือกพอร์ต
deploy.cmd ไม่สตาร์ตอะไรเลย มันแค่คัดลอกและสร้าง config เลยปลอดภัยที่จะรันซ้ำ แต่มีเงื่อนไขเริ่มต้นสี่ข้อที่ต้องเช็กก่อน
ขั้นที่ 1: เปิด terminal แล้วไปที่โฟลเดอร์ build
cd C:\apm-build
ขั้นที่ 2: ตรวจว่า stage มีของครบสามส่วน
Test-Path C:\apm-build\stage\apache\bin\httpd.exe
Test-Path C:\apm-build\stage\php\php.exe
Test-Path C:\apm-build\stage\mariadb\bin\mariadbd.exe
ผลที่ควรเห็นคือ True สามบรรทัด
True
True
True
ถ้าบรรทัดไหนเป็น False ให้กลับไปทำตอนที่ 3 (Apache), 4 (MariaDB) หรือ 6 (PHP) ให้ครบก่อน ถ้าข้ามไป deploy.cmd จะหยุดเองด้วยข้อความ [ERR] ...\stage\apache missing - build first (ชื่อโฟลเดอร์จะเปลี่ยนตามส่วนที่ขาด)
ขั้นที่ 3: ตรวจว่าไม่มี APM ตัวเก่ารันอยู่
สคริปต์ปฏิเสธการ deploy ถ้ามี httpd, mariadbd หรือ php-cgi ที่มาจาก C:\APM ทำงานอยู่ เพราะ Windows ไม่ยอมให้เขียนทับไฟล์ที่กำลังถูกใช้ และรุ่นใหม่ปฏิเสธด้วยถ้ามี Windows service APM-Apache หรือ APM-MariaDB รันอยู่ ตรวจทั้งสองอย่างด้วยคำสั่งนี้
Get-Process httpd,mariadbd,php-cgi -ErrorAction SilentlyContinue | Where-Object { $_.Path -like 'C:\APM\*' }
Get-Service APM-Apache,APM-MariaDB -ErrorAction SilentlyContinue
ผลที่ควรเห็นคือ ไม่มีอะไรพิมพ์ออกมาเลย สองคำสั่งนี้ ถ้ามีตารางโผล่จากคำสั่งแรก แปลว่ามี process ค้างอยู่ ให้ไปทำขั้นหยุด APM ในหัวข้อสตาร์ตครั้งแรกก่อน แล้วค่อยกลับมา ส่วนคำสั่งที่สองจะมีแถวโผล่ก็ต่อเมื่อเคยติดตั้ง APM แบบพร้อมใช้ที่ลงเป็น service ไว้ ถ้ามีแถวและสถานะไม่ใช่ Stopped ให้หยุดมันก่อน (Stop-Service ต้องใช้ pwsh แบบ admin) ที่ต้องมีข้อตรวจแยก เพราะ service รันเป็นบัญชี LocalService ซึ่ง Get-Process มองไม่เห็น .Path ของมัน ถ้าลืม สคริปต์ deploy จะหยุดด้วย [ERR] service APM-Apache is running - stop APM before deploying
ขั้นที่ 4: ตรวจว่าพอร์ตที่จะใช้ว่าง
ค่าเริ่มต้นของสคริปต์ตั้งใจเลี่ยงพอร์ตที่ XAMPP ใช้ จะได้ติดตั้งคู่กันได้
| ตัวเลือก | ค่าเริ่มต้น | XAMPP ใช้ | ใช้ทำอะไร |
|---|---|---|---|
-HttpPort |
8080 | 80 | HTTP ของ Apache |
-HttpsPort |
8443 | 443 | HTTPS ของ Apache |
-DbPort |
3307 | 3306 | MariaDB |
-Target |
C:\APM |
- | โฟลเดอร์ติดตั้ง |
-Lan |
ปิด | - | Apache ฟังทุก interface (0.0.0.0) แทน 127.0.0.1 และใบรับรองครอบชื่อเครื่องกับ IP ของ LAN (MariaDB ยังอยู่ที่ 127.0.0.1 เสมอ) ดูหัวข้อ LAN |
-ResetConfig |
ปิด | - | สร้าง config ใหม่จาก templates\ ทับของเดิม ออกใบรับรองเซิร์ฟเวอร์ใหม่ และ sync data\mysql\my.ini |
ตรวจว่าสามพอร์ตนี้ยังไม่มีใครใช้
Get-NetTCPConnection -LocalPort 8080,8443,3307 -ErrorAction SilentlyContinue
ผลที่ควรเห็นคือไม่มีอะไรพิมพ์ออกมา ถ้ามีแถวโผล่แปลว่ามีโปรแกรมอื่นใช้พอร์ตอยู่ ให้เลือกพอร์ตอื่นโดยใส่ตัวเลือกตอนรัน deploy.cmd เช่น -HttpPort 8081 ในซีรีส์นี้ผมใช้ค่าเริ่มต้นทั้งหมด ขนาดที่ใช้ลงดิสก์ประมาณ 650 MB (apache 20, php 124, mariadb 201, data 302 โดยประมาณ)
90-deploy.ps1 ทำอะไรบ้างทีละช่วง
ตัวห่อ deploy.cmd สั้นมาก ตรวจว่ามี pwsh แล้วเรียกสคริปต์พร้อมส่งอาร์กิวเมนต์ต่อ (ไฟล์นี้ไม่เปลี่ยนจากรุ่นแรก ตัวเลือกใหม่อย่าง -Lan ผ่านไปที่สคริปต์ผ่าน %* เอง)
@echo off
rem deploy.cmd - install stage\ into C:\APM (does not start anything)
rem deploy.cmd defaults: C:\APM, http 8080, https 8443, db 3307
rem deploy.cmd -Target D:\APM -HttpPort 80 -HttpsPort 443 -DbPort 3306
rem deploy.cmd -ResetConfig re-render config files from templates\
setlocal
where pwsh >nul 2>&1 || (echo [ERR] PowerShell 7 ^(pwsh^) not found - see https://linuxthai.org/article/apm-build-windows-1-prepare-tools & exit /b 1)
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\90-deploy.ps1" %*
exit /b %ERRORLEVEL%
ตัวสคริปต์จริงคือไฟล์นี้ เป็นรุ่นใหม่ของ kit ผมวางทั้งไฟล์ไม่ตัดบรรทัด
# 90-deploy.ps1 - install the built stage\ into a runtime folder (default C:\APM). Does NOT start anything.
# pwsh -File scripts\90-deploy.ps1 [-Target C:\APM] [-HttpPort 8080] [-HttpsPort 8443] [-DbPort 3307] [-Lan]
# Re-running upgrades binaries in place; existing config files, data, www, ssl and logs are kept
# (use -ResetConfig to re-render config from templates\).
# -Lan: Apache listens on all interfaces + the cert covers this PC's name and LAN IPs. MariaDB always
# stays on 127.0.0.1 (root has no password). Windows Firewall still needs scripts\firewall-lan.ps1 (admin).
param(
[string]$Target = 'C:\APM',
[int]$HttpPort = 8080, [int]$HttpsPort = 8443, [int]$DbPort = 3307, # XAMPP keeps 80/443/3306 meanwhile
[switch]$Lan,
[switch]$ResetConfig
)
$Bind = if ($Lan) { '0.0.0.0' } else { '127.0.0.1' }
$LanIPs = @()
if ($Lan) {
$LanIPs = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object { $_.IPAddress -notlike '127.*' -and $_.IPAddress -notlike '169.254.*' -and $_.AddressState -eq 'Preferred' } |
ForEach-Object IPAddress
}
$ErrorActionPreference = 'Stop'
$Root = Split-Path $PSScriptRoot -Parent
$Stage = "$Root\stage"
foreach ($c in 'apache','php','mariadb') { if (-not (Test-Path "$Stage\$c")) { throw "[ERR] $Stage\$c missing - build first" } }
foreach ($exe in 'httpd','mariadbd','php-cgi') {
$p = Get-Process $exe -ErrorAction SilentlyContinue | Where-Object { $_.Path -like "$Target\*" }
if ($p) { throw "[ERR] $exe from $Target is running - stop APM before deploying" }
}
# services run as LocalService: their processes are not visible to Get-Process .Path -> ask the SCM
$svc = Get-Service 'APM-Apache','APM-MariaDB' -ErrorAction SilentlyContinue | Where-Object Status -ne 'Stopped'
if ($svc) { throw "[ERR] service $($svc.Name -join ', ') is running - stop APM before deploying" }
# 1. binaries (robocopy /MIR per component, minus debug symbols and dev files)
# /XD and /XF match names on both sides, so they also keep /MIR from deleting the target's conf/logs/ini
$skip = @{
apache = @('/XD', 'include', 'lib', 'conf', 'logs', '/XF', '*.pdb', '*.lib', '*.exp')
php = @('/XF', '*.pdb', '*.lib', '*.exp', 'php.ini')
# NOT 'lib' here: mariadb\lib\plugin holds the plugin DLLs (ha_archive, auth_ed25519, ...); *.lib files are dropped by /XF
mariadb = @('/XD', 'include', 'symbols', 'sql-bench', '/XF', '*.pdb', '*.lib', '*.exp', 'my.ini')
}
foreach ($c in 'apache','php','mariadb') {
robocopy "$Stage\$c" "$Target\$c" /MIR /R:1 /W:1 /NFL /NDL /NJH /NJS /NP @($skip[$c]) | Out-Null
if ($LASTEXITCODE -ge 8) { throw "[ERR] copy $c failed (robocopy rc=$LASTEXITCODE)" }
Write-Host "[OK] $c -> $Target\$c"
}
# templates + tools travel with the install (re-render config later, new-cert.ps1)
foreach ($d in 'templates','tools') {
robocopy "$Root\$d" "$Target\$d" /MIR /R:1 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Null
if ($LASTEXITCODE -ge 8) { throw "[ERR] copy $d failed" }
}
# license of the build scripts (MIT + Commons Clause)
Copy-Item "$Root\LICENSE" "$Target\LICENSE.txt" -Force
# Apache conf data files (mime.types, magic ...) - copy, but never overwrite an edited httpd.conf
New-Item -ItemType Directory -Force "$Target\apache\conf\extra", "$Target\apache\conf\vhosts" | Out-Null
Get-ChildItem "$Stage\apache\conf" -File | Where-Object Name -ne 'httpd.conf' | Copy-Item -Destination "$Target\apache\conf" -Force
# 2. runtime folders
foreach ($d in 'www\default','logs\apache','logs\php','logs\mariadb','tmp\sessions','tmp\uploads','tmp\opcache','ssl','data') {
New-Item -ItemType Directory -Force "$Target\$d" | Out-Null
}
# 3. config from templates (only if missing, or -ResetConfig)
# CACHE_ID: per-folder OPcache segment name, derived from the install path
$sha1 = [Security.Cryptography.SHA1]::Create().ComputeHash([Text.Encoding]::UTF8.GetBytes(($Target -replace '\\','/').ToLower()))
$cacheId = 'apm' + (-join ($sha1[0..3] | ForEach-Object { $_.ToString('x2') }))
$vars = @{ APM_ROOT = ($Target -replace '\\','/'); HTTP_PORT = $HttpPort; HTTPS_PORT = $HttpsPort; DB_PORT = $DbPort; BIND = $Bind; CACHE_ID = $cacheId }
function Render($tpl, $dest, [switch]$WinPaths) {
if ((Test-Path $dest) -and -not $ResetConfig) { Write-Host "[KEEP] $dest"; return }
$t = Get-Content "$Root\templates\$tpl" -Raw
foreach ($k in $vars.Keys) {
$v = if ($WinPaths -and $k -eq 'APM_ROOT') { $Target } else { "$($vars[$k])" }
$t = $t.Replace("{{$k}}", $v)
}
Set-Content $dest $t -Encoding ascii -NoNewline
Write-Host "[OK] $dest"
}
Render 'httpd.conf' "$Target\apache\conf\httpd.conf"
Render 'apm-ssl.conf' "$Target\apache\conf\extra\apm-ssl.conf"
Render 'php.ini' "$Target\php\php.ini" -WinPaths
Render 'my.ini' "$Target\mariadb\my.ini"
Copy-Item "$Root\versions.json" "$Target\versions.json" -Force
# 4. CA bundle for curl/openssl in PHP (Mozilla bundle from curl.se, sha256-checked)
$ca = "$Target\ssl\cacert.pem"
if (-not (Test-Path $ca) -or $ResetConfig) {
curl.exe -fsSL -o "$ca.part" https://curl.se/ca/cacert.pem
$want = ((curl.exe -fsSL https://curl.se/ca/cacert.pem.sha256) -split '\s+')[0]
if ((Get-FileHash "$ca.part" -Algorithm SHA256).Hash -ne $want) { Remove-Item "$ca.part"; throw '[ERR] cacert.pem sha256 mismatch' }
Move-Item -Force "$ca.part" $ca
Write-Host "[OK] $ca"
}
# 5. local CA + localhost certificate (generated with APM's own php.exe). The CA is created once and
# kept; the server cert is re-issued on -ResetConfig/-Lan so its SAN matches how the site is reached.
if (-not (Test-Path "$Target\ssl\localhost.crt") -or $ResetConfig -or $Lan) {
$certHosts = @('localhost', '127.0.0.1', '::1')
if ($Lan) { $certHosts += @($env:COMPUTERNAME.ToLower()) + $LanIPs }
& "$Target\php\php.exe" -n -d "extension_dir=`"$Target\php\ext`"" -d extension=openssl `
"$Root\tools\mkcert.php" "$Target\ssl" @certHosts
if ($LASTEXITCODE) { throw '[ERR] certificate generation failed' }
}
# 6. default page
if (-not (Test-Path "$Target\www\default\index.php")) {
Copy-Item "$Root\templates\index.php" "$Target\www\default\index.php"
}
# 7. MariaDB datadir (bootstrap only - no network listener is opened)
if (-not (Test-Path "$Target\data\mysql\mysql")) {
& "$Target\mariadb\bin\mariadb-install-db.exe" --datadir="$Target\data\mysql" --port=$DbPort --config="$Target\mariadb\my.ini" --silent
if ($LASTEXITCODE) { throw '[ERR] mariadb-install-db failed' }
Write-Host "[OK] datadir $Target\data\mysql (root@localhost, no password)"
} elseif ($ResetConfig) {
# the server runs with <datadir>\my.ini (written once by mariadb-install-db) -> keep it in sync
Copy-Item "$Target\mariadb\my.ini" "$Target\data\mysql\my.ini" -Force
Write-Host "[OK] $Target\data\mysql\my.ini (synced)"
}
# 8. config syntax check (parses only, binds nothing)
& "$Target\apache\bin\httpd.exe" -t
if ($LASTEXITCODE) { throw '[ERR] httpd -t failed' }
$live = Select-String -Path "$Target\apache\conf\httpd.conf" -Pattern '^Listen\s+(\S+)' | ForEach-Object { $_.Matches[0].Groups[1].Value }
Write-Host "[OK] APM deployed to $Target (http :$HttpPort, https :$HttpsPort, db 127.0.0.1:$DbPort) - not started"
Write-Host " httpd.conf Listen: $live"
if ($Lan) {
if ($live -like '127.0.0.1:*') { Write-Host '[WARN] httpd.conf was kept and still binds 127.0.0.1 - add -ResetConfig to switch it to LAN' }
foreach ($ip in $LanIPs) { Write-Host " LAN: http://${ip}:$HttpPort/ https://${ip}:$HttpsPort/" }
Write-Host ' Firewall: run scripts\firewall-lan.ps1 once as Administrator'
}
(บล็อกแรกเป็นไฟล์ batch แต่ผมใช้ fence powershell เพราะเว็บนี้ไม่มีตัวไฮไลต์สำหรับ batch)
ผมแบ่งสคริปต์เป็นแปดช่วงตามคอมเมนต์ในไฟล์ บวกส่วนหัวที่เพิ่มมาอีกหนึ่งส่วน ไล่ตามลำดับที่มันรัน
ส่วนหัว: ตัวเลือก -Lan และตัวแปรที่ตั้งก่อน
param มีตัวเลือกใหม่หนึ่งตัวคือ -Lan ใต้ param สคริปต์ตั้ง $Bind เป็น 0.0.0.0 เมื่อมี -Lan และ 127.0.0.1 เมื่อไม่มี ค่านี้ไปเป็น {{BIND}} ใน template ของ Apache ภายหลัง ถ้ามี -Lan มันยังเก็บรายการ IPv4 ของเครื่องใน $LanIPs ด้วย โดยเอาที่อยู่ที่ไม่ขึ้นต้น 127. ไม่ขึ้นต้น 169.254. (ที่อยู่ที่เครื่องตั้งให้ตัวเองเมื่อไม่ได้ DHCP) และสถานะเป็น Preferred ไว้ใช้สองที่ คือใส่ใน SAN ของใบรับรอง และพิมพ์ URL บอกตอนจบ
ช่วงตรวจเงื่อนไข (บรรทัดบนสุด)
มีสามอย่างที่สคริปต์ตรวจ ถ้าล้มจะ throw ทันทีก่อนแตะไฟล์ใดๆ
stage\apache,stage\php,stage\mariadbต้องมี ไม่งั้นพิมพ์[ERR] <พาธ>\stage\<ส่วน> missing - build first- ไม่มี
httpd,mariadbd,php-cgiจากC:\APMรันอยู่ ไม่งั้นพิมพ์[ERR] <ชื่อ exe> from C:\APM is running - stop APM before deploying - ใหม่: ไม่มี Windows service
APM-ApacheหรือAPM-MariaDBที่ไม่ได้อยู่ในสถานะ Stopped ไม่งั้นพิมพ์[ERR] service APM-Apache is running - stop APM before deploying(ชื่อ service ที่รันอยู่จะถูกเรียงต่อกันด้วยจุลภาค) เหตุผลที่ต้องมีข้อนี้แยก คือ service รันเป็นบัญชีLocalServiceซึ่งGet-Processอ่าน.Pathของมันจากบัญชีเราไม่ได้ ข้อตรวจแรกเลยมองไม่เห็น สคริปต์เลยถามตรงกับ Service Control Manager แทน service ชื่อนี้คือ service ที่ APM แบบพร้อมใช้ (ที่ผมแจกแยกต่างหาก ดูกล่อง "ไม่อยาก build เอง" ท้ายตอน) สร้างไว้ ถ้าคุณ build และ deploy เองตามซีรีส์นี้ ข้อนี้ผ่านเสมอ
ช่วงที่ 1: คัดลอก binary ด้วย robocopy
robocopy /MIR ทำให้ปลายทางเหมือนต้นทางเป๊ะ รวมถึงลบไฟล์ที่ต้นทางไม่มีด้วย เลยอันตรายถ้าไม่ระวัง สคริปต์เลยใส่ตัวกันไว้สองชั้น
/XD(ข้ามโฟลเดอร์) และ/XF(ข้ามไฟล์) ทำงานทั้งฝั่งต้นทางและปลายทาง ดังนั้นconf,logsของ Apache,php.iniของ PHP และmy.iniของ MariaDB ที่อยู่ปลายทางเลยไม่ถูก/MIRลบหรือทับ- ไม่คัดลอก
*.pdb,*.lib,*.expและโฟลเดอร์include(ไฟล์สำหรับนักพัฒนา) ทำให้ไบนารีที่ติดตั้งเล็กกว่า stage
จุดที่แก้จากรุ่นแรก: ในรายการของ mariadb เดิมมีคำว่า lib อยู่ใน /XD ด้วย ตอนนี้เอาออกแล้ว และมีคอมเมนต์กำกับในสคริปต์ว่าทำไม เรื่องนี้ผมเล่าเต็มๆ ในหัวข้อบั๊กด้านล่าง ย่อๆ คือ /XD lib ตัดโฟลเดอร์ชื่อ lib ทุกตัวในทุกระดับ รวม mariadb\lib\plugin ที่เก็บ DLL ของ plugin ไปด้วย ตอนนี้ปล่อยโฟลเดอร์ lib ผ่าน และไฟล์ *.lib ถูกตัดด้วย /XF อยู่แล้ว ส่วนของ apache ยังมี lib ใน /XD ตามเดิม เพราะ stage\apache\lib มีแต่ไฟล์ .lib ของ APR กับ httpd ที่ใช้ตอน build ไม่มีอะไรที่ต้องใช้ตอนรัน
ตัวเลือกที่เหลือ: /R:1 /W:1 ลองซ้ำหนึ่งครั้งรอหนึ่งวินาทีเมื่อไฟล์ติด, /NFL /NDL /NJH /NJS /NP ปิดการพิมพ์รายชื่อไฟล์ ส่วนรหัสออกของ robocopy ตั้งแต่ 0 ถึง 7 ถือว่าสำเร็จ (บอกว่าคัดลอกอะไรไปบ้าง) ตั้งแต่ 8 ขึ้นไปคือล้มจริง สคริปต์เลยเช็ก -ge 8
หลังลูปของสามส่วนนี้มีของที่เพิ่มมาใหม่สามอย่าง
templates\และtools\ถูก mirror ไปที่C:\APM\templatesและC:\APM\toolsด้วย robocopy/MIRเหมือนกัน (ล้มเมื่อรหัสออก 8 ขึ้นไปด้วย[ERR] copy templates failed) เหตุผลคือให้C:\APMพกต้นแบบ config กับmkcert.phpติดตัวไปด้วย จะได้เรนเดอร์ config ใหม่หรือออกใบรับรองเพิ่มทีหลังโดยไม่ต้องพึ่งโฟลเดอร์ buildLICENSEถูกคัดลอกเป็นC:\APM\LICENSE.txtใบอนุญาตของสคริปต์ build ที่แจกใน kit เป็น MIT พร้อมเงื่อนไข Commons Clause (ไฟล์เต็มอยู่ใน kit ชื่อLICENSE) ตัวโปรแกรม Apache, PHP และ MariaDB ที่ build ออกมาใช้ใบอนุญาตของตัวเองตามต้นทาง- ไฟล์ข้อมูลของ Apache (
mime.types,magic,charset.convฯลฯ) ถูกคัดลอกเหมือนเดิม ยกเว้นhttpd.confเพื่อไม่ทับไฟล์ที่คุณแก้เอง
ช่วงที่ 2: สร้างโฟลเดอร์ใช้งาน
www\default, logs\{apache,php,mariadb}, tmp\{sessions,uploads,opcache}, ssl, data ถ้ามีอยู่แล้ว New-Item -Force ไม่ทำอะไรเสียหาย ตัวใหม่คือ tmp\opcache ซึ่ง PHP ใช้เป็นที่เก็บไฟล์แคชของ OPcache (ดูเรื่อง ASLR ด้านล่าง) ข้อสังเกตที่เกี่ยวกับบั๊ก PidFile คือ ไม่มี apache\logs อยู่ในรายการนี้ โฟลเดอร์ log ของ Apache อยู่ที่ C:\APM\logs\apache ต่างหาก
ช่วงที่ 3: เรนเดอร์ config จาก templates
ฟังก์ชัน Render อ่านไฟล์จาก templates\ แทนค่า placeholder แล้วเขียนลงปลายทาง ตอนนี้มีหกตัว ได้แก่ {{APM_ROOT}}, {{HTTP_PORT}}, {{HTTPS_PORT}}, {{DB_PORT}} และสองตัวใหม่ {{BIND}}, {{CACHE_ID}} ข้อสำคัญคือ ถ้าปลายทางมีอยู่แล้วและไม่ได้ใส่ -ResetConfig มันพิมพ์ [KEEP] แล้วข้าม ไฟล์ที่คุณแก้เลยปลอดภัย ข้อควรจำที่ตามมาคือถ้าผมแก้ template ในรุ่นใหม่ของ kit config ที่ deploy ไปแล้วจะไม่ได้รับการแก้ตามอัตโนมัติ (บั๊ก PidFile, FcgidWrapper และ OPcache ด้านล่างคือตัวอย่าง) และ -Lan ที่มาทีหลังก็ไม่เปลี่ยน Listen ของ httpd.conf เดิมให้ ต้องคู่กับ -ResetConfig
APM_ROOTเป็นC:/APM(ทับตรง) สำหรับ Apache และ MariaDB ซึ่งรับ/ได้ แต่ส่วนของphp.iniใช้C:\APMแบบแบ็กสแลช (-WinPaths)BINDคือ$Bindจากส่วนหัว ใช้ในListenของhttpd.confและapm-ssl.confเท่านั้นmy.iniของ MariaDB ไม่ใช้ตัวแปรนี้ และตายตัวที่bind-address = 127.0.0.1เสมอCACHE_IDคือชื่อเฉพาะของแคช OPcache ต่อหนึ่งโฟลเดอร์ติดตั้ง สคริปต์เอาพาธติดตั้ง (แปลง\เป็น/และเป็นตัวพิมพ์เล็ก) ไปทำ SHA1 แล้วเอา 4 ไบต์แรกมาเป็นเลขฐานสิบหก 8 ตัว ต่อท้ายคำว่าapmได้ชื่อยาว 11 ตัวอักษร พาธเดียวกันได้ชื่อเดิมทุกครั้ง พาธต่างกันได้ชื่อต่างกัน- เขียนไฟล์เป็น ascii ล้วน ไม่มี BOM และไม่เติมบรรทัดท้าย
ช่วงที่ 4: CA bundle ของ Mozilla
ดาวน์โหลด cacert.pem จาก curl.se ลง ssl\cacert.pem พร้อมเทียบ sha256 กับค่าที่เว็บเดียวกันประกาศ ถ้าไม่ตรงลบไฟล์ทิ้งแล้ว throw '[ERR] cacert.pem sha256 mismatch' ไฟล์นี้ให้ cURL และ OpenSSL ของ PHP เชื่อใบรับรองของเว็บภายนอก (ตั้งใน php.ini ที่ curl.cainfo และ openssl.cafile) ทำเฉพาะเมื่อยังไม่มีไฟล์ หรือสั่ง -ResetConfig
ช่วงที่ 5: CA ของเครื่องและใบรับรอง localhost
APM ไม่มี openssl.exe ให้ใช้ ผมเลยเขียน tools\mkcert.php ให้ใช้ extension openssl ของ PHP ที่เพิ่ง build ออกใบรับรองแทน สคริปต์เรียก php.exe ของ APM เอง ด้วย -n (ไม่โหลด php.ini เพราะ deploy ยังไม่เสร็จ) และ -d extension=openssl โหลดเฉพาะตัวที่ต้องใช้ ส่วนค่าของ -d extension_dir= ตอนนี้ครอบด้วยเครื่องหมายคำพูดอีกชั้นแล้ว (ดูบรรทัด & "$Target\php\php.exe" -n ... ในสคริปต์) เหตุผลอยู่ในเรื่องพาธมีวงเล็บด้านล่าง
เงื่อนไขของช่วงนี้เปลี่ยนไป รุ่นแรกออกใบรับรองเฉพาะเมื่อ localhost.crt ยังไม่มี รุ่นนี้ออกใหม่เมื่อ ยังไม่มีไฟล์ หรือสั่ง -ResetConfig หรือสั่ง -Lan เหตุผลคือ SAN ของใบรับรองต้องตรงกับวิธีที่เปิดเว็บ ถ้าเปิดทางชื่อเครื่องหรือ IP ของ LAN แล้วใบรับรองมีแต่ localhost browser ของเครื่องอื่นจะฟ้อง รายชื่อที่ใส่คือ localhost, 127.0.0.1, ::1 และเมื่อมี -Lan เพิ่มชื่อเครื่อง (ตัวพิมพ์เล็ก) กับ IP ใน $LanIPs
สิ่งที่ mkcert.php ทำ
- สร้าง CA ชื่อ
APM Local Dev CAอายุ 10 ปี ครั้งเดียวเท่านั้น (ถ้ามีapm-ca.keyและapm-ca.crtอยู่แล้วจะใช้ของเดิม) การออกใบรับรองเซิร์ฟเวอร์ใหม่เลยไม่ทำให้ browser ที่เชื่อ CA แล้วต้องนำเข้าอีก - ออกใบรับรองเซิร์ฟเวอร์
localhost.crt(ชื่อไฟล์มาจากโฮสต์ตัวแรก) อายุ 825 วัน SAN เป็นรายชื่อข้างบนทั้งหมด กุญแจเป็น EC (เส้นโค้ง prime256v1) - ใช้ไฟล์ตั้งค่า OpenSSL ที่เขียนขึ้นเองทั้งไฟล์ เพราะตอนแรกผมยืม
openssl.cnfจาก deps แล้วเจอError loading extension section v3_caไฟล์นั้นไม่มี section ที่ต้องใช้ - ใหม่: เช็กตั้งแต่ต้นว่า extension openssl โหลดได้จริง ถ้าไม่ได้พิมพ์
[ERR] PHP openssl extension not loaded (check -d extension_dir="..." - quote it, folder names may contain ( ) or spaces)แล้วออกด้วยรหัส 1 ข้อความนี้คือผลจากบั๊กพาธมีวงเล็บ - เมื่อใบรับรองใกล้หมดอายุ (825 วัน) วิธีที่ง่ายที่สุดตอนนี้คือ
deploy.cmd -ResetConfigมันออกใบใหม่จาก CA เดิม (แต่ทับ config ที่แก้ไว้ด้วย) หรือจะลบlocalhost.crtกับlocalhost.keyแล้วรันdeploy.cmdเฉยๆ ก็ได้ ข้อนี้ผมอนุมานจากตรรกะของสคริปต์ ยังไม่ได้ลองจนใบหมดอายุจริง
ช่วงที่ 6 และ 7: หน้าแรกและ datadir ของ MariaDB
คัดลอก templates\index.php เป็น www\default\index.php ถ้ายังไม่มี หน้านี้เปลี่ยนไปเล็กน้อยในรุ่นใหม่ (ดูหัวข้อ templates ข้างล่าง) จากนั้นถ้ายังไม่มี data\mysql\mysql (ตารางระบบ) จะรัน mariadb-install-db.exe --datadir=... --port=... --config=<my.ini> --silent ซึ่งเป็นการ bootstrap ไม่เปิดพอร์ต ได้ user root@localhost แบบไม่มีรหัสผ่าน เหมือน XAMPP
จุดที่คนมักพลาด: mariadb-install-db คัดลอก my.ini ไปไว้ใน data\mysql\my.ini และ ไฟล์นั้นคือตัวที่ server อ่านจริงตอนรัน (เหมือนที่ผมใช้ตอนสตาร์ตด้านล่าง) ไฟล์ C:\APM\mariadb\my.ini เป็นแค่ต้นแบบ รุ่นแรกของสคริปต์ไม่แตะไฟล์ใน datadir อีกเลยหลังสร้างครั้งแรก รุ่นนี้เพิ่มกิ่ง elseif ($ResetConfig) เข้ามา คือถ้า datadir มีอยู่แล้วและสั่ง -ResetConfig สคริปต์คัดลอก C:\APM\mariadb\my.ini (ที่เพิ่งเรนเดอร์ใหม่) ทับ data\mysql\my.ini แล้วพิมพ์ [OK] C:\APM\data\mysql\my.ini (synced) ผลคือเปลี่ยน -DbPort ด้วย -ResetConfig ได้จริง ข้อที่ต้องระวังคือถ้าคุณแก้ data\mysql\my.ini ด้วยมือไว้ การ sync จะทับการแก้นั้น
ช่วงที่ 8: ตรวจ syntax และสรุปผล
httpd.exe -t อ่าน config อย่างเดียวไม่เปิดพอร์ต ถ้ามี directive ผิดหรือ module โหลดไม่ได้จะล้มที่นี่ก่อนที่คุณจะได้สตาร์ต หลังจากนั้นรุ่นใหม่ทำเพิ่มอีกสองอย่าง
- อ่านบรรทัด
Listenในไฟล์httpd.confที่ใช้งานอยู่จริง (ไม่ใช่ค่าที่เพิ่งคำนวณ) แล้วพิมพ์httpd.conf Listen: <ค่า>ให้เห็นว่า Apache จะฟังที่ไหน บรรทัดสรุปด้านบนก็เปลี่ยนจากdb :3307เป็นdb 127.0.0.1:3307เพราะ MariaDB ฟังที่นั่นเสมอ - ถ้าสั่ง
-Lanและพบว่าhttpd.confที่ถูก[KEEP]ไว้ยังขึ้นต้นด้วย127.0.0.1:จะพิมพ์[WARN] httpd.conf was kept and still binds 127.0.0.1 - add -ResetConfig to switch it to LANแล้วพิมพ์ URL ของ LAN ทุก IP พร้อมเตือนให้รันscripts\firewall-lan.ps1อย่างน้อยหนึ่งครั้งในฐานะ Administrator
ของที่เปลี่ยนใน templates\ ของ kit รุ่นใหม่
บั๊กที่ผมแก้ทั้งหมดในรอบนี้ไปลงที่ template สี่ไฟล์ ผมวางเฉพาะบรรทัดที่เปลี่ยน ที่เหลือของไฟล์เหมือนเดิมตามที่เล่าในตอนที่ 3, 4 และ 6
httpd.conf: PidFile และ Listen ที่เลือกได้
Define APM "{{APM_ROOT}}"
ServerRoot "${APM}/apache"
# Default is <ServerRoot>/logs/httpd.pid, but deploy keeps logs in ${APM}/logs -> without this line
# httpd exits right after start with AH00099 (could not create pid file)
PidFile "${APM}/logs/apache/httpd.pid"
Listen {{BIND}}:{{HTTP_PORT}}
PidFile ย้ายขึ้นมาอยู่บนสุดพร้อมคอมเมนต์อธิบาย (รุ่นแรกอยู่ท้ายไฟล์ใกล้ ErrorLog) ส่วน Listen เดิมเขียนตายตัวเป็น 127.0.0.1: ตอนนี้เป็น {{BIND}}: ค่าเริ่มต้นเลยยังเป็น 127.0.0.1:8080 เหมือนเดิม และเป็น 0.0.0.0:8080 เมื่อสั่ง -Lan
apm-ssl.conf: Listen ของ HTTPS
Listen {{BIND}}:{{HTTPS_PORT}}
หลักการเดียวกัน บรรทัดนี้คือบรรทัดที่สามของไฟล์ ถ้าแก้แค่ httpd.conf แต่ไม่แก้ไฟล์นี้ HTTP จะเปิด LAN แต่ HTTPS ยังอยู่ที่ 127.0.0.1 -ResetConfig เรนเดอร์ทั้งสองไฟล์พร้อมกันเลยไม่ต้องห่วง
httpd.conf: FcgidWrapper ที่มีเครื่องหมายคำพูดซ้อน
# inner \" quotes: mod_fcgid splits the wrapper command line on spaces (apr_tokenize_to_argv), so a path
# like "D:/APM test/php/php-cgi.exe" must be quoted inside the directive argument as well
FcgidWrapper "\"${APM}/php/php-cgi.exe\"" .php
รุ่นแรกเป็น FcgidWrapper "${APM}/php/php-cgi.exe" .php ธรรมดา ใช้ได้เมื่อพาธไม่มีช่องว่าง (C:\APM ไม่มี) เรื่องเต็มอยู่ในหัวข้อบั๊กด้านล่าง
php.ini: บล็อก OPcache ที่เพิ่ม
; Windows: processes with the same cache_id share one OPcache segment. Two APM folders running at once
; (or any other PHP of the same build) would collide -> "Opcode handlers are unusable due to ASLR" and
; every request 500s. A per-folder id keeps them apart; the file cache is the fallback if mapping fails.
opcache.cache_id = "{{CACHE_ID}}"
opcache.file_cache = "{{APM_ROOT}}\tmp\opcache"
opcache.file_cache_fallback = 1
สามบรรทัดนี้ต่อท้าย opcache.jit_buffer_size ในส่วน [opcache] ที่เหลือของบล็อก (memory_consumption = 512, jit = tracing ฯลฯ) ไม่เปลี่ยน
index.php: phpinfo เฉพาะเครื่องตัวเอง
// APM default page. Shows what is running; phpinfo() with ?info=1 (this machine only - APM may be on the LAN)
$isLocal = in_array($_SERVER['REMOTE_ADDR'] ?? '', ['127.0.0.1', '::1'], true);
if (isset($_GET['info']) && $isLocal) { phpinfo(); exit; }
และท้ายหน้ามีลิงก์ที่แสดงเฉพาะเมื่อ $isLocal เป็นจริง
<?php if ($isLocal): ?><p><a href="?info=1">phpinfo()</a></p><?php endif; ?>
เหตุผลคือ phpinfo() เปิดเผย path ของเครื่อง รายชื่อ extension และค่า config ทั้งหมด ซึ่งไม่ควรให้เครื่องอื่นใน LAN เห็นเมื่อเปิด -Lan เครื่องอื่นที่ขอ ?info=1 จะได้หน้าปกติแทน (ผมทดสอบแล้ว ผลอยู่ในหัวข้อ LAN)
หมายเหตุเรื่องไฟล์: index.php คัดลอกไป www\default เฉพาะตอนที่ยังไม่มีไฟล์ ใครที่ deploy ไปแล้วจะยังได้หน้าแรกรุ่นเก่า ถ้าต้องการรุ่นใหม่ให้คัดลอกทับเอง Copy-Item C:\APM\templates\index.php C:\APM\www\default\index.php -Force (ไฟล์ต้นทางอยู่ใน C:\APM\templates เพราะ deploy รุ่นใหม่ mirror มาให้แล้ว)
รัน deploy.cmd และดูของที่ได้
ขั้นที่ 5: สั่ง deploy
ที่ C:\apm-build ใน pwsh ต้องมี .\ นำหน้า (PowerShell ไม่รันไฟล์จากโฟลเดอร์ปัจจุบันถ้าไม่ระบุ)
.\deploy.cmd
สคริปต์ใช้เวลาไม่นานและพิมพ์สถานะเป็นบรรทัดๆ ผมไม่ได้จับเวลาขั้นนี้ ถ้าต้องการพอร์ตอื่นใส่ตัวเลือกต่อท้าย เช่น .\deploy.cmd -HttpPort 8081 ส่วนการเปิดให้เครื่องอื่นใน LAN เข้า (-Lan) แยกไปอยู่ในหัวข้อของมันด้านล่าง ขั้นนี้ยังเป็นแบบ local อย่างเดียว
ขั้นที่ 6: อ่านผลที่ควรเห็น (deploy ครั้งแรก)
[OK] apache -> C:\APM\apache
[OK] php -> C:\APM\php
[OK] mariadb -> C:\APM\mariadb
[OK] C:\APM\apache\conf\httpd.conf
[OK] C:\APM\apache\conf\extra\apm-ssl.conf
[OK] C:\APM\php\php.ini
[OK] C:\APM\mariadb\my.ini
[OK] C:\APM\ssl\cacert.pem
[OK] new CA: C:\APM\ssl/apm-ca.crt
[OK] cert C:\APM\ssl/localhost.crt SAN=DNS:localhost,IP:127.0.0.1,IP:::1
[OK] datadir C:\APM\data\mysql (root@localhost, no password)
Syntax OK
[OK] APM deployed to C:\APM (http :8080, https :8443, db 127.0.0.1:3307) - not started
httpd.conf Listen: 127.0.0.1:8080
บล็อกนี้ผมเอาผลจริงที่บันทึกจาก deploy รุ่นแรก มาปรับสามจุดตามโค้ดของรุ่นใหม่ คือ SAN ที่มี IP:::1 เพิ่ม, db 127.0.0.1:3307 ในบรรทัดสรุป และบรรทัด httpd.conf Listen: ท้ายสุด ผมยังไม่ได้ deploy ซ้ำด้วยรุ่นนี้บนเครื่องว่างเพื่อเทียบตัวอักษรต่อตัวอักษร ถ้าบรรทัดของคุณต่างจากนี้เล็กน้อยให้ดูความหมายของมัน
ข้อสังเกต: พาธของ CA แสดงเป็น C:\APM\ssl/apm-ca.crt มี / ปนอยู่ ไม่ผิด เป็นแค่รูปแบบการต่อสตริงของ mkcert.php ส่วนบรรทัดสรุปต้องเขียนว่า not started เสมอ เพราะสคริปต์ไม่เปิดอะไรทั้งนั้น และ httpd.conf Listen: ต้องเป็น 127.0.0.1:8080 ตราบที่คุณไม่ได้ใช้ -Lan
ขั้นที่ 7: ตรวจโฟลเดอร์ที่ได้
Get-ChildItem C:\APM -Name
ผลที่ควรเห็นต้องมีชื่อเหล่านี้ (ลำดับอาจต่างเล็กน้อย)
apache
data
LICENSE.txt
logs
mariadb
php
ssl
templates
tmp
tools
versions.json
www
โครงสร้างและขนาดจริงจากเครื่องผม (ขนาดวัดจากรุ่นแรก บรรทัดที่เพิ่มใหม่ไม่ได้ทำให้ขนาดรวมเปลี่ยนในระดับที่ผมสังเกตได้ และผมไม่ได้วัดซ้ำ)
C:\APM\
apache\ 20 MB bin\ modules\ conf\ error\ icons\
php\ 124 MB php.exe php-cgi.exe ext\ + DLL ของ deps และ ImageMagick, php.ini
mariadb\ 201 MB bin\ lib\plugin\ share\ my.ini
data\ 302 MB mysql\ (datadir + my.ini ที่ server ใช้ตอนรันจริง)
ssl\ apm-ca.crt/.key, localhost.crt/.key, cacert.pem
www\default\ index.php
templates\ tools\ ต้นแบบ config กับ mkcert.php (คัดลอกมาจาก kit)
logs\ tmp\ tmp\ มี sessions\ uploads\ opcache\
LICENSE.txt MIT + Commons Clause ของสคริปต์ build
versions.json เวอร์ชันที่ติดตั้งอยู่
ตรวจให้แน่ใจอีกข้อว่า plugin ของ MariaDB มาครบ (ข้อนี้คือบั๊กที่รุ่นแรกพลาด)
Get-ChildItem C:\APM\mariadb\lib\plugin -Filter *.dll | Measure-Object | Select-Object -ExpandProperty Count
ผลที่ควรเห็นคือตัวเลขมากกว่า 0 ผมไม่ได้บันทึกจำนวนไฟล์ที่แน่นอน ถ้าคำสั่งฟ้องว่าไม่พบพาธหรือได้ 0 แปลว่ายังใช้ kit รุ่นเก่า ให้ดูหัวข้อบั๊กด้านล่าง
เมื่อรัน deploy ซ้ำ จะเห็นอะไร
การรันซ้ำคือวิธีอัปเกรด สิ่งที่ต่างจากครั้งแรกคือบรรทัด config กลายเป็น [KEEP] และไม่มีบรรทัด CA, cert หรือ datadir เพราะมีอยู่แล้ว (ผลจากตรรกะของสคริปต์ที่อ่านข้างบน)
[OK] apache -> C:\APM\apache
[OK] php -> C:\APM\php
[OK] mariadb -> C:\APM\mariadb
[KEEP] C:\APM\apache\conf\httpd.conf
[KEEP] C:\APM\apache\conf\extra\apm-ssl.conf
[KEEP] C:\APM\php\php.ini
[KEEP] C:\APM\mariadb\my.ini
Syntax OK
[OK] APM deployed to C:\APM (http :8080, https :8443, db 127.0.0.1:3307) - not started
httpd.conf Listen: 127.0.0.1:8080
-ResetConfig เขียน httpd.conf, apm-ssl.conf, php.ini, my.ini (ในโฟลเดอร์ mariadb) ใหม่จาก template ดาวน์โหลด cacert.pem ใหม่ ออกใบรับรองของเซิร์ฟเวอร์ใหม่ (CA เดิมยังอยู่) และ คัดลอก my.ini ใหม่ทับ data\mysql\my.ini ให้ด้วย ข้อมูลฐานข้อมูลไม่ถูกแตะ ก่อนใช้ ให้คัดลอกไฟล์ config ที่คุณแก้ไว้เก็บก่อน รวมถึง data\mysql\my.ini ถ้าคุณเคยแก้ด้วยมือ การเปลี่ยน -DbPort ภายหลังใช้ได้แล้วด้วยคำสั่งเดียว .\deploy.cmd -DbPort 3306 -ResetConfig (ต้องหยุด APM ก่อน) ข้อสรุปนี้มาจากการอ่านสคริปต์ ผมยังไม่ได้ทดสอบการเปลี่ยนพอร์ตหลัง deploy ครบวงจร
ให้ browser เชื่อ CA ออกใบรับรองเพิ่ม และใส่ PATH
ใบรับรองของ https://localhost:8443 ออกโดย CA ที่เครื่องคุณสร้างเอง browser ยังไม่เชื่อ เลยฟ้องเตือนจนกว่าจะนำเข้า CA นี้ ทำครั้งเดียวต่อ user มีสองทาง ทางแรกเป็นสคริปต์บรรทัดเดียว (ที่ผมแนะนำ) ทางที่สองเป็นการคลิกเองใน Windows ผลเหมือนกัน
ขั้นที่ 8: ทางลัดด้วย trust-ca.ps1 (แนะนำ)
เปิด pwsh แบบ user ปกติ (ไม่ต้อง admin) แล้วรันจากโฟลเดอร์ kit เพราะ scripts\ ไม่ได้ถูกคัดลอกไป C:\APM
pwsh -NoProfile -File C:\apm-build\scripts\trust-ca.ps1
สคริปต์อ่าน C:\APM\ssl\apm-ca.crt (ถ้าไม่มีจะหยุดด้วย [ERR] C:\APM\ssl\apm-ca.crt missing - run deploy.cmd first) เปิดที่เก็บ Trusted Root Certification Authorities ของ user ปัจจุบัน แล้วเพิ่ม CA เข้าไปถ้ายังไม่มี thumbprint นี้ Windows จะเด้งหน้าต่างเตือนความปลอดภัยถามยืนยันหนึ่งครั้ง ให้กด Yes เหมือนที่ทำมือในขั้นที่ 12 ด้านล่าง สิ่งที่พิมพ์ตอนสำเร็จมีรูปแบบนี้ (ชื่อ, thumbprint และวันหมดอายุเป็นของ CA ที่เครื่องคุณสร้าง)
[OK] trusted: <ชื่อของ CA> (<thumbprint>), valid until <yyyy-MM-dd>
ถ้ารันซ้ำจะได้ [OK] already trusted: ... และถ้าอยากเลิกเชื่อ ใส่ -Remove ท้ายคำสั่ง จะพิมพ์ [OK] removed ... (หรือ [OK] not trusted - nothing to remove) ตัวเลือก -Target ใช้ชี้โฟลเดอร์ติดตั้งอื่นนอกจาก C:\APM
ตรวจว่า CA เข้าที่เก็บแล้ว
Get-ChildItem Cert:\CurrentUser\Root | Where-Object Subject -like '*APM Local Dev CA*'
ผลที่ควรเห็นคือหนึ่งแถวที่มี thumbprint ตรงกับที่สคริปต์พิมพ์ จากนั้นปิดแล้วเปิด browser ใหม่ แล้วเข้า https://localhost:8443/ ตอนที่ Apache รันแล้ว (ดูขั้นที่ 18) ต้องไม่ขึ้นคำเตือน
หมายเหตุเรื่อง Firefox: ตามคอมเมนต์ในสคริปต์ Firefox อ่านที่เก็บของ Windows ผ่านค่า security.enterprise_roots ซึ่งเปิดเป็นค่าเริ่มต้นใน Firefox รุ่นปัจจุบัน ผมไม่ได้ทดสอบกับ Firefox เอง ถ้ายังขึ้นคำเตือนให้นำเข้าในหน้า Settings ของ Firefox แยกต่างหาก
เครื่องอื่นใน LAN: คัดลอกเฉพาะ apm-ca.crt ไปที่เครื่องนั้น (ไม่ใช่ apm-ca.key เด็ดขาด) แล้วดับเบิลคลิกนำเข้าตามขั้นที่ 9 ถึง 12 ด้านล่าง
ทางเลือก: นำเข้าเองด้วยมือ (ขั้นที่ 9 ถึง 12)
ใครไม่อยากรันสคริปต์ หรืออยากเห็นว่า Windows ทำอะไร ใช้ขั้นเหล่านี้แทน
ขั้นที่ 9: เปิดไฟล์ CA
ใน File Explorer ไปที่โฟลเดอร์ C:\APM\ssl ดับเบิลคลิกไฟล์ apm-ca.crt จะเห็นหน้าต่างรายละเอียดใบรับรอง ชื่อผู้ออกคือ APM Local Dev CA
ขั้นที่ 10: เริ่มติดตั้ง
กดปุ่ม Install Certificate... เลือก Current User แล้วกด Next
ขั้นที่ 11: เลือกที่เก็บให้ถูก
เลือก Place all certificates in the following store กด Browse เลือก Trusted Root Certification Authorities กด OK แล้ว Next และ Finish
ขั้นที่ 12: ยืนยันคำเตือนของ Windows
Windows จะถามว่าจะติดตั้ง CA นี้จริงหรือไม่ กด Yes หน้าต่างสุดท้ายแจ้งว่านำเข้าสำเร็จ
ผลที่ควรเห็นคือ ตอนที่ Apache รันแล้ว การเปิด https://localhost:8443/ ใน browser (Edge หรือ Chrome) ไม่ขึ้นคำเตือนใบรับรอง ผมยืนยันขั้นนี้ด้วยการทดสอบแบบข้ามการตรวจใบรับรอง (curl -k) ในหัวข้อถัดไป แต่ไม่ได้บันทึกผลของ browser ที่เชื่อ CA แล้ว
ไฟล์ C:\APM\ssl\apm-ca.key คือกุญแจที่ออกใบรับรองให้เว็บไหนก็ได้ที่ browser ของคุณเชื่อ ห้ามคัดลอกออกจากเครื่อง ห้ามแชร์โฟลเดอร์ ssl และห้ามนำ apm-ca.crt ไปติดตั้งบนเครื่องของคนอื่นโดยไม่บอกเขา
ขั้นที่ 13: ออกใบรับรองเพิ่มสำหรับ vhost ด้วย new-cert.ps1
ถ้าคุณทำหลายเว็บบนเครื่องเดียวและอยากให้แต่ละเว็บมี HTTPS ของตัวเอง ออกใบรับรองเพิ่มจาก CA เดิมได้ ไม่ต้องนำเข้า CA ใหม่
pwsh -NoProfile -File C:\apm-build\scripts\new-cert.ps1 -Hosts myapp.localhost
สคริปต์เรียก mkcert.php ตัวเดียวกับที่ deploy ใช้ ผ่าน php.exe ของ C:\APM ใส่หลายชื่อได้โดยคั่นด้วยจุลภาค เช่น -Hosts myapp.localhost,myapp.test ชื่อแรกใช้ตั้งชื่อไฟล์ ผลที่ได้คือไฟล์ C:\APM\ssl\myapp.localhost.crt กับ .key และบรรทัดสถานะตามโค้ดของ mkcert.php
[OK] cert C:\APM\ssl/myapp.localhost.crt SAN=DNS:myapp.localhost
ชื่อที่ลงท้าย .localhost ใช้สะดวกที่สุด เพราะ Chrome, Edge และ Firefox แปลงชื่อพวกนี้เป็น 127.0.0.1 เองโดยไม่ต้องแก้ไฟล์ hosts ส่วนชื่ออื่นอย่าง .test ต้องเพิ่มบรรทัดใน C:\Windows\System32\drivers\etc\hosts เอง ถ้าใส่ชื่อซ้ำกับของเดิมไฟล์ .crt และ .key ของชื่อนั้นจะถูกเขียนทับ
เอาไปใช้ใน vhost โดยสร้างไฟล์ใหม่ในโฟลเดอร์ C:\APM\apache\conf\vhosts (httpd.conf มี IncludeOptional conf/vhosts/*.conf อยู่แล้ว) ตัวอย่างข้างล่างผมเขียนตามรูปแบบของ apm-ssl.conf ยังไม่ได้ทดสอบ สองบรรทัด SSLCertificate... มาจากคอมเมนต์ในสคริปต์
<VirtualHost *:8443>
ServerName myapp.localhost
DocumentRoot "${APM}/www/myapp"
SSLEngine on
SSLCertificateFile "${APM}/ssl/myapp.localhost.crt"
SSLCertificateKeyFile "${APM}/ssl/myapp.localhost.key"
</VirtualHost>
เรียก C:\APM\apache\bin\httpd.exe -t ตรวจก่อนสตาร์ตเสมอ
ขั้นที่ 14: ใส่ php กับ mariadb ลง PATH ด้วย set-path.ps1
ถ้าอยากพิมพ์ php -v หรือ mariadb ได้จากทุกที่ (และให้ VS Code, composer, artisan เจอ) ใส่โฟลเดอร์ของ APM ลง PATH ได้ด้วยสคริปต์นี้ มีสองระดับ
pwsh -NoProfile -File C:\apm-build\scripts\set-path.ps1 -Scope User
-Scope User ไม่ต้อง admin แก้เฉพาะ PATH ของคุณ ถ้าไม่ใส่ตัวเลือก จะแก้ PATH ของทั้งเครื่อง ต้องเปิด pwsh แบบ Administrator (ถ้าไม่ใช่ admin จะหยุดด้วย [ERR] Machine scope needs Administrator (or use -Scope User)) สคริปต์ทำสามอย่าง คือ ลบรายการ C:\xampp\... ออกจาก PATH, ใส่ C:\APM\php กับ C:\APM\mariadb\bin ไว้ตรงตำแหน่งที่รายการ XAMPP รายการแรกเคยอยู่ (ลำดับความสำคัญเทียบกับโปรแกรมอื่นเลยไม่เปลี่ยน) แล้วแจ้ง Windows ว่า environment เปลี่ยนแล้ว ผลที่พิมพ์
[OK] User PATH updated (REG_EXPAND_SZ)
- <รายการที่ถูกถอดออก>
+ C:\APM\php
+ C:\APM\mariadb\bin
restart VS Code / terminals to pick it up
เตือนไว้ก่อน: ถ้าคุณยังใช้ XAMPP คู่กัน การรันสคริปต์นี้ถอด php ของ XAMPP ออกจาก PATH ด้วย ใช้ -Remove เมื่ออยากเอารายการของ APM ออกอีกที โปรแกรมที่เปิดค้างอยู่ (VS Code, terminal) ต้องปิดแล้วเปิดใหม่ถึงจะเห็น
ตรวจว่าใช้ได้ในหน้าต่าง pwsh ใหม่
php -v
บรรทัดแรกต้องเป็น PHP 8.4.27 (cli) ... ตามรุ่นที่คุณ build
เรื่องที่ผมพลาดกับ PATH: ทำไมสคริปต์แก้ registry ตรงๆ
ตอนแรกผมแก้ PATH ด้วย [Environment]::SetEnvironmentVariable ปรากฏว่ามันขยาย %SystemRoot% เป็น C:\WINDOWS แล้วเขียนกลับเป็น REG_SZ ถาวร ค่า PATH ใน registry ควรเป็นชนิด REG_EXPAND_SZ ที่เก็บข้อความดิบอย่าง %SystemRoot%\system32 ไว้ให้ Windows ขยายตอนใช้ พอกลายเป็น REG_SZ ข้อความธรรมดา รายการพวกนั้นก็ถูกแทนด้วยค่าตายตัวไปตลอด ไม่ขยายตามเครื่องอีก ตอนนี้ set-path.ps1 เปิด registry key ตรงๆ อ่านค่าด้วยตัวเลือก DoNotExpandEnvironmentNames เพื่อได้ข้อความดิบ แก้เฉพาะรายการของเรา แล้วเขียนกลับเป็น REG_EXPAND_SZ ใครเขียนสคริปต์แก้ PATH เองให้ระวังจุดนี้
ตรวจชนิดของค่าหลังรัน (ถ้าใช้ -Scope User)
(Get-Item -Path HKCU:\Environment).GetValueKind('Path')
ผลที่ควรเห็นคือ ExpandString ซึ่งคือ REG_EXPAND_SZ ผมระบุชื่อชนิดตามเอกสารของ .NET ไม่ได้บันทึกผลจริงจากคำสั่งนี้
สตาร์ตครั้งแรก MariaDB Apache ทดสอบ แล้วปิด
ส่วนนี้ใช้ผลที่ผมวัดจริงเมื่อ 2026-10-22 บน Windows 11 x64 ทั้งหมด หน้าต่างที่ใช้คือ pwsh ตัวใหม่ (user ปกติ) โฟลเดอร์ไหนก็ได้ เพราะทุกคำสั่งใช้พาธเต็ม
ขั้นที่ 15: สตาร์ต MariaDB
Start-Process C:\APM\mariadb\bin\mariadbd.exe -ArgumentList '--defaults-file=C:\APM\data\mysql\my.ini'
คำสั่งคืน prompt ทันทีโดยไม่พิมพ์อะไร อาจมีหน้าต่างคอนโซลของ mariadbd โผล่ขึ้นมา อย่าปิดหน้าต่างนั้น เพราะการปิดคือการฆ่า server ใช้ --defaults-file ชี้ไปที่ my.ini ใน datadir ตามที่อธิบายในช่วงที่ 7
ขั้นที่ 16: รอให้ MariaDB พร้อม
Start-Sleep 6
หกวินาทีพอสำหรับ datadir ที่เพิ่งสร้างบนเครื่องผม ถ้าเครื่องช้ากว่านี้ให้รอนานขึ้นก่อนทำขั้นถัดไป
ขั้นที่ 17: ถามเวอร์ชันและค่า collation
C:\APM\mariadb\bin\mariadb.exe -uroot --port=3307 --protocol=tcp -e "select version(), @@collation_server, @@have_ssl, @@bind_address"
ผลที่ควรเห็น (ผลจริง)
version() @@collation_server @@have_ssl @@bind_address
11.8.9-MariaDB-log utf8mb4_unicode_ci YES 127.0.0.1
อ่านผลทีละคอลัมน์: เวอร์ชัน 11.8.9, collation เป็น utf8mb4_unicode_ci ตามที่ล็อกไว้ใน my.ini (ตอนที่ 4 อธิบายเหตุผล), have_ssl เป็น YES แปลว่า wolfSSL ที่มากับ source ใช้ได้, และ bind อยู่ที่ 127.0.0.1 ตามต้องการ
ขั้นที่ 18: สตาร์ต Apache
Start-Process C:\APM\apache\bin\httpd.exe
คืน prompt ทันทีเช่นกัน ถ้าคุณใช้ kit รุ่นล่าสุด Apache จะขึ้นปกติ ถ้าไม่ขึ้นและเคย deploy ด้วย template รุ่นเก่า ให้ข้ามไปอ่านเรื่อง PidFile ด้านล่าง
ขั้นที่ 19: ทดสอบ HTTP
curl.exe -s -o NUL -w "%{http_code} %{content_type}`n" http://127.0.0.1:8080/
ผลที่ควรเห็น
200 text/html; charset=UTF-8
200 แปลว่า Apache ตอบ และ text/html; charset=UTF-8 ที่ได้จากหน้า index.php แปลว่า PHP ผ่าน mod_fcgid ทำงานแล้วด้วย (ไม่ใช่ไฟล์ static)
ขั้นที่ 20: ทดสอบ HTTPS
curl.exe -sk -o NUL -w "%{http_code}`n" https://localhost:8443/
ผลที่ควรเห็น
200
-k บอก curl ให้ข้ามการตรวจใบรับรอง ผลนี้เลยยืนยันแค่ว่า TLS ทำงานและ Apache ตอบ ไม่ได้ยืนยันว่า CA ถูกเชื่อ ถ้านำเข้า CA แล้วและอยากตรวจโดยไม่ข้ามการตรวจ ให้ลบ -k ออก ควรได้ 200 เหมือนกัน แต่ผมไม่ได้บันทึกผลของการรันแบบนั้นไว้
ขั้นที่ 21: เปิดหน้าแรกใน browser
เปิด http://127.0.0.1:8080/ ในเบราว์เซอร์ คุณจะเห็นตารางสั้นๆ ที่ผม (ในฐานะผู้เขียนหน้านี้) ให้บอกสิ่งที่รันอยู่
| แถว | ค่าที่ผมเห็น |
|---|---|
| Server | Apache (ServerTokens Prod ตัดเลขเวอร์ชันออก) |
| Protocol | HTTP/1.1 และต่อท้ายด้วย TLS เมื่อเข้าทาง https |
| PHP | 8.4.27 cgi-fcgi, OPcache JIT on |
| MariaDB | เลขเวอร์ชันของ MariaDB ถ้ารันอยู่ หรือ not reachable (...) ถ้ายังไม่ได้สตาร์ต |
| Extensions | รายชื่อ extension ที่โหลด รวม igbinary redis imagick gd intl mbstring opcache |
ลิงก์ phpinfo() ใต้ตารางแสดงเฉพาะเมื่อเปิดหน้านี้จากเครื่อง APM เอง (127.0.0.1 หรือ ::1) ถ้าแถว MariaDB ขึ้น not reachable ทั้งที่สตาร์ตแล้ว ดูตารางปัญหาท้ายตอน หน้าแรกเชื่อมต่อด้วย user root ไม่มีรหัสผ่านที่พอร์ตของ mysqli.default_port ใน php.ini ถ้าคุณตั้งรหัสผ่านให้ root ภายหลัง แถวนี้จะรายงานว่าเข้าไม่ได้ ซึ่งเป็นพฤติกรรมที่คาดไว้
ขั้นที่ 22: ดู phpinfo
เปิด http://127.0.0.1:8080/?info=1 ได้หน้า phpinfo() เต็ม (รุ่นใหม่ของ index.php ให้เฉพาะเครื่อง APM เอง ผ่าน 127.0.0.1 หรือ ::1 เครื่องอื่นที่ขอ ?info=1 ได้หน้าปกติ) ใช้ดูว่าไฟล์ ini ที่ถูกโหลดคือไฟล์ของ APM และดูรายละเอียด extension หน้า header ที่ Apache ส่งกลับ มีแค่ Server: Apache ไม่มีเลขเวอร์ชัน
เรื่องที่ผมเจอตอนสตาร์ต Apache ครั้งแรก
ตอนที่ผมทดสอบรอบแรก ผมสั่ง Start-Process httpd.exe แล้ว curl ไปที่พอร์ต 8080 ไม่ได้อะไรกลับมา ตรวจ process พบว่า httpd ตายทันทีหลังสตาร์ต ไม่มีหน้าต่าง error ให้เห็น เพราะ Start-Process ซ่อนทุกอย่าง
ผมรัน C:\APM\apache\bin\httpd.exe ตรงๆ ในคอนโซล และเห็นบรรทัดนี้
AH00099: could not create C:/APM/apache/logs/httpd.pid
สาเหตุอยู่ที่ช่วงที่ 2 ของสคริปต์ deploy ที่ผมชี้ให้ดูข้างบน Apache ใช้ค่าเริ่มต้นของ PidFile คือ logs/httpd.pid เทียบกับ ServerRoot (C:/APM/apache) แต่สคริปต์ deploy สร้างโฟลเดอร์ log ที่ C:\APM\logs\apache ไม่ได้สร้าง C:\APM\apache\logs Apache เลยเขียน pid file ไม่ได้และปฏิเสธจะสตาร์ต ผมไม่ได้ตรวจว่าบรรทัดนี้ถูกเขียนลง error.log ด้วยหรือไม่ เห็นแน่ๆ แค่ในคอนโซล
ทางแก้ที่ผมใส่ไว้ใน kit คือบอก Apache ชัดๆ ใน templates\httpd.conf ว่า pid file อยู่ในโฟลเดอร์ log ที่มีจริง
PidFile "${APM}/logs/apache/httpd.pid"
ErrorLog "${APM}/logs/apache/error.log"
kit ที่คุณดาวน์โหลดมีบรรทัดนี้แล้ว แต่ ถ้าคุณ deploy ไปก่อนด้วย template เก่า ไฟล์ httpd.conf ใน C:\APM ถูก [KEEP] ไว้ ไม่ได้รับการแก้ตาม ให้ตรวจและแก้ตามขั้นเหล่านี้
ขั้นที่ 23: ตรวจว่าไฟล์ของคุณมีบรรทัด PidFile หรือยัง
Get-Content C:\APM\apache\conf\httpd.conf | Select-String -Pattern '^PidFile'
ถ้ามีบรรทัดนี้โผล่ แปลว่าแก้แล้ว ไม่ต้องทำต่อ
PidFile "${APM}/logs/apache/httpd.pid"
ถ้าไม่มีอะไรพิมพ์ออกมา ให้เลือกทางแก้ทางใดทางหนึ่งในสองขั้นถัดไป
ขั้นที่ 24 (ทางเลือก ก): เติมบรรทัดเอง
เปิดไฟล์ด้วย Notepad
notepad C:\APM\apache\conf\httpd.conf
หาบรรทัด ErrorLog "${APM}/logs/apache/error.log" แล้วเพิ่มบรรทัด PidFile "${APM}/logs/apache/httpd.pid" ไว้เหนือมัน บันทึกไฟล์ แล้วตรวจ syntax
C:\APM\apache\bin\httpd.exe -t
ผลที่ควรเห็น
Syntax OK
ขั้นที่ 25 (ทางเลือก ข): ให้ deploy เรนเดอร์ config ใหม่
ทางนี้ง่ายกว่าแต่ ทับ config ที่คุณแก้ไว้ทั้งสี่ไฟล์ ให้สำรองก่อนถ้ามีการแก้ จากนั้นรันที่ C:\apm-build (ใช้ได้เมื่อไม่มี process ของ APM รันอยู่)
.\deploy.cmd -ResetConfig
หลังทำเสร็จ ย้อนกลับไปทำขั้นที่ 18 (สตาร์ต Apache) อีกครั้ง
หยุดทุกอย่างให้เรียบร้อย
ขั้นที่ 26: หยุด Apache
Apache ที่สตาร์ตด้วย Start-Process ไม่มีหน้าต่างให้กด Ctrl+C ต้องหยุดด้วยคำสั่ง (ถ้าคุณรัน httpd.exe ในคอนโซลของตัวเอง ใช้ Ctrl+C ในหน้าต่างนั้นได้เลย)
Get-Process httpd | Where-Object Path -like 'C:\APM\*' | Stop-Process
คำสั่งนี้ไม่พิมพ์อะไรเมื่อสำเร็จ กรองด้วย Path เพื่อไม่ไปฆ่า httpd ของโปรแกรมอื่น เช่น XAMPP ที่อาจรันอยู่ การเรียกให้เจอ process มากกว่าหนึ่งตัวเป็นเรื่องปกติของ Apache บน Windows (มีตัวแม่กับตัวลูก) คำสั่งนี้หยุดทุกตัว
ขั้นที่ 27: หยุด php-cgi ที่ค้าง
Get-Process php-cgi -ErrorAction SilentlyContinue | Where-Object Path -like 'C:\APM\*' | Stop-Process
mod_fcgid เป็นคนสร้าง php-cgi.exe ไว้รอรับงาน เมื่อ Apache ตายมันอาจค้างอยู่ได้ ถ้าไม่มี process ให้หยุด คำสั่งนี้ไม่พิมพ์อะไร
ขั้นที่ 28: ปิด MariaDB อย่างถูกวิธี
C:\APM\mariadb\bin\mariadb-admin.exe -uroot --port=3307 --protocol=tcp shutdown
คำสั่งนี้ไม่พิมพ์อะไรเมื่อสำเร็จ และให้ MariaDB flush ข้อมูลลงดิสก์ก่อนจบ ดีกว่าการฆ่า process ทิ้ง หน้าต่างคอนโซลของ mariadbd จะปิดเอง
ขั้นที่ 29: ยืนยันว่าไม่มีอะไรค้าง
Get-Process httpd,php-cgi,mariadbd -ErrorAction SilentlyContinue | Where-Object { $_.Path -like 'C:\APM\*' }
ผลที่ควรเห็นคือไม่มีอะไรพิมพ์ออกมา ตอนนี้ APM ปิดสนิท พร้อมสำหรับ deploy.cmd รอบถัดไป
ซีรีส์นี้ครอบคลุมการ build และ deploy ส่วนตัวควบคุมสำหรับ start, stop และติดตั้งเป็น Windows Service (apmctl) ผมจะเขียนแยกเป็นบทความต่างหากภายหลัง ตอนนี้ยังไม่มีคำสั่งของ apmctl ให้ใช้ ใช้วิธีสตาร์ตและหยุดด้วยมือตามที่เขียนไว้ข้างบน
เปิดให้เครื่องใน LAN เข้า
ค่าเริ่มต้นของ APM คือฟังที่ 127.0.0.1 ล้วน มีแต่เครื่องที่ติดตั้งเท่านั้นที่เข้าได้ ซึ่งตรงกับจุดประสงค์ของเครื่องพัฒนา แต่บางทีผมอยากเปิดเว็บที่กำลังทำให้เพื่อนร่วมทีมในออฟฟิศเดียวกันดู หรือเปิดจากมือถือที่ต่อ Wi-Fi เดียวกัน kit รุ่นใหม่เลยมีตัวเลือก -Lan ที่เปิดเฉพาะ Apache ให้ LAN เข้า และคง MariaDB ไว้ที่ 127.0.0.1 เสมอ
เปิด -Lan แล้วทุกเครื่องในซับเน็ตเดียวกันเปิดเว็บใน C:\APM\www ได้ ถ้าเว็บที่คุณวางไว้มีหน้า admin หรือข้อมูลจริง ใครในวงเดียวกันก็เห็น อย่าเปิดในเครือข่ายสาธารณะ (ร้านกาแฟ โรงแรม) และอย่า port forward จากเราเตอร์ออกอินเทอร์เน็ต APM เป็นเครื่องพัฒนา ไม่ใช่เซิร์ฟเวอร์ production
ขั้นตอนมีห้าขั้น ทำต่อจากที่ APM หยุดสนิทแล้ว (ขั้นที่ 29)
ขั้นที่ 30: deploy ใหม่ด้วย -Lan และ -ResetConfig
ที่ C:\apm-build ใน pwsh แบบ user ปกติ
.\deploy.cmd -Lan -ResetConfig
ต้องมี -ResetConfig ด้วยเมื่อคุณ deploy ไปแล้วก่อนหน้านี้ เพราะ httpd.conf และ apm-ssl.conf เดิมถูก [KEEP] ไว้และยังเขียน Listen 127.0.0.1:... อยู่ ถ้าลืม สคริปต์เตือนด้วย [WARN] httpd.conf was kept and still binds 127.0.0.1 - add -ResetConfig to switch it to LAN ส่วนครั้งแรกที่ยังไม่มี config ใส่ -Lan อย่างเดียวพอ ตัวเลือกพอร์ตใช้ร่วมกันได้ ตอนที่ผมทดสอบ ผมใช้ deploy.cmd -HttpPort 80 -HttpsPort 443 -DbPort 3306 -Lan -ResetConfig (พอร์ตมาตรฐาน เพราะเครื่องทดสอบไม่มี XAMPP) ใครยังมี XAMPP รันอยู่ ให้ใช้พอร์ตค่าเริ่มต้นของซีรีส์นี้ตามที่เขียนด้านบน
ข้อเตือน: -ResetConfig ทับ config ที่คุณแก้ไว้ทั้งสี่ไฟล์ ตามที่เตือนในหัวข้อ deploy ซ้ำ
ขั้นที่ 31: อ่านผลว่า Listen เปลี่ยนแล้ว
ผลที่ต่างจากการ deploy แบบ local มีสามจุด ผมแสดงรูปแบบที่สคริปต์พิมพ์ (ชื่อเครื่องกับ IP เป็นตัวอย่าง)
[OK] cert C:\APM\ssl/localhost.crt SAN=DNS:localhost,IP:127.0.0.1,IP:::1,DNS:pc-name,IP:192.168.10.x
[OK] C:\APM\data\mysql\my.ini (synced)
Syntax OK
[OK] APM deployed to C:\APM (http :8080, https :8443, db 127.0.0.1:3307) - not started
httpd.conf Listen: 0.0.0.0:8080
LAN: http://192.168.10.x:8080/ https://192.168.10.x:8443/
Firewall: run scripts\firewall-lan.ps1 once as Administrator
จุดแรก SAN ของใบรับรองมีชื่อเครื่องและ IP ของ LAN เพิ่ม จุดที่สอง Listen เป็น 0.0.0.0:8080 จุดที่สามมีบรรทัด LAN: หนึ่งบรรทัดต่อหนึ่ง IP ที่เครื่องมี (ถ้าเครื่องมีทั้งสาย LAN และ Wi-Fi จะเห็นสองบรรทัด) และเตือนเรื่อง firewall ถ้า IP ของคุณเปลี่ยนภายหลัง (DHCP แจกใหม่) ใบรับรองจะไม่ครอบ IP ใหม่ ให้รันขั้นนี้ซ้ำ
ขั้นที่ 32: เปิด firewall ครั้งเดียวด้วย firewall-lan.ps1
Windows Firewall ปิดกั้นขาเข้าเป็นค่าเริ่มต้น -Lan อย่างเดียวเลยยังไม่พอ ต้องเปิดกฎให้ httpd.exe ขั้นนี้ต้องใช้สิทธิ์ผู้ดูแล คลิกขวาที่ pwsh เลือก Run as administrator แล้วรัน
pwsh -NoProfile -File C:\apm-build\scripts\firewall-lan.ps1
ผลที่ควรเห็น (พอร์ตตามที่คุณ deploy)
[OK] firewall rule 'APM Apache (LAN)': TCP 8080,8443 from LocalSubnet -> C:\APM\apache\bin\httpd.exe
ถ้าไม่ได้รันแบบ admin จะหยุดด้วย [ERR] run as Administrator กฎที่สร้างมีขอบเขตแคบโดยตั้งใจ
- ขาเข้า (Inbound) TCP เท่านั้น
- ผูกกับโปรแกรม
C:\APM\apache\bin\httpd.exeตัวเดียว ไม่ใช่เปิดพอร์ตลอย - รับเฉพาะ
LocalSubnetคือเครื่องในซับเน็ตเดียวกัน ไม่ใช่ทั้งอินเทอร์เน็ต - ใช้กับทุกโปรไฟล์เครือข่าย (
Any) เพราะ Wi-Fi ที่บ้านบางทีถูก Windows จัดเป็น Public - พอร์ตอ่านจากบรรทัด
Listenในhttpd.confกับapm-ssl.confจริง ไม่ได้เดาจากค่าเริ่มต้น ถ้าไม่เจอพอร์ตเลยจะหยุดด้วย[ERR] no Listen ports found in ...
รันซ้ำได้ มันลบกฎเดิมชื่อเดียวกันก่อนแล้วสร้างใหม่ เลยไม่เกิดกฎซ้ำซ้อน
ขั้นที่ 33: สตาร์ตแล้วทดสอบจากอีกเครื่อง
สตาร์ต MariaDB กับ Apache ตามขั้นที่ 15 ถึง 18 บนเครื่อง APM แล้วที่เครื่องอื่นในซับเน็ตเดียวกัน (ตัวอย่างใช้ IP 192.168.10.x แทน IP จริงของเครื่อง APM) ลองสองคำสั่งนี้
curl.exe -s -o NUL -w "%{http_code}`n" http://192.168.10.x:8080/
curl.exe -s -o NUL -w "%{http_code}`n" http://192.168.10.x:8080/server-status
คำสั่งแรกควรได้ 200 คำสั่งที่สองควรได้ 403 เพราะ /server-status มี Require local ใน httpd.conf ผลที่ผมวัดจริงจากอีกเครื่องใน LAN หลัง deploy.cmd -Lan -ResetConfig และเปิดกฎ firewall (ใช้พอร์ต 80, 443 และ 3306 ตามที่อธิบายในขั้นที่ 30)
- http ได้ 200 และ https ได้ 200
?info=1ได้หน้าปกติ ไม่ใช่ phpinfo (phpinfo ดูได้เฉพาะจากเครื่อง APM เอง)/server-statusได้ 403- พอร์ต 3306 ของ MariaDB ปิด ต่อไม่ติด
ตามที่ตั้งใจไว้หมด ส่วนการเปิดผ่าน HTTPS จากเครื่องอื่นโดยไม่ขึ้นคำเตือน ต้องนำเข้า apm-ca.crt ในเครื่องนั้นก่อนตามที่เขียนในหัวข้อใบรับรอง และต้องเปิดด้วยชื่อหรือ IP ที่อยู่ใน SAN เท่านั้น
ขั้นที่ 34: เลิกเปิด LAN
หยุด APM (ขั้นที่ 26 ถึง 29) แล้วทำสองอย่าง อย่างแรกลบกฎ firewall ใน pwsh แบบ admin
pwsh -NoProfile -File C:\apm-build\scripts\firewall-lan.ps1 -Remove
ผลที่ควรเห็นคือ [OK] removed firewall rule 'APM Apache (LAN)' อย่างที่สองคือ render config กลับเป็น local ที่ C:\apm-build แบบ user ปกติ
.\deploy.cmd -ResetConfig
เมื่อไม่มี -Lan ค่า $Bind กลับเป็น 127.0.0.1 ตรวจจากบรรทัดสุดท้ายว่า httpd.conf Listen: 127.0.0.1:8080 ขั้นนี้ผมอ่านจากโค้ด ยังไม่ได้ลองย้อนกลับจริง
ทำไม MariaDB ไม่เคยเปิดออก LAN
ผมตั้งใจให้ -Lan แตะเฉพาะ Apache เพราะ root ของ MariaDB ใน APM ไม่มีรหัสผ่าน (เหมือน XAMPP) ซึ่งปลอดภัยได้เพราะมันฟังแค่ 127.0.0.1 ถ้าฐานข้อมูลออก LAN ใครในวงเดียวกันก็ต่อเข้ามาเป็น root ได้ทันที สามชั้นที่กันไว้
my.iniตายตัว บรรทัดbind-address = 127.0.0.1ใน template ไม่ได้ใช้ตัวแปร{{BIND}}เลยไม่ว่าจะสั่ง-Lanหรือไม่ ค่านี้ไม่เปลี่ยน (และdeployพิมพ์db 127.0.0.1:3307ในบรรทัดสรุปทุกครั้ง)- กฎ firewall ผูกกับ
httpd.exeตัวเดียว ต่อให้มีใครเปลี่ยน bind ของ MariaDB ไปเอง Windows Firewall ก็ไม่มีกฎให้mariadbd.exeรับขาเข้า - พอร์ตในกฎมาจากบรรทัด
Listenของ Apache เท่านั้น ไม่มีทางที่ 3307 จะหลุดเข้ากฎโดยบังเอิญ
ถ้าวันหนึ่งคุณต้องการเข้าฐานข้อมูลจากเครื่องอื่นจริงๆ ให้ทำ SSH tunnel หรือตั้ง user เฉพาะพร้อมรหัสผ่านและ bind เอง อย่าเปิดผ่าน -Lan
บั๊กที่เจอหลังซีรีส์ออก และ kit รุ่นใหม่แก้อะไร
หลังจากตอนนี้ออกไปครั้งแรก ผมเอาชุดที่ build เสร็จไปทดสอบต่อ ทั้งแตกไว้ในพาธแปลกๆ และรัน APM สองโฟลเดอร์พร้อมกัน เจอบั๊กเพิ่มห้าตัวที่ทำให้ผมแก้สคริปต์กับ template (บั๊ก PidFile ในหัวข้อสตาร์ตครั้งแรกเป็นอีกตัวที่แก้ในรอบเดียวกัน) แต่ละตัวผมเล่าเป็นอาการ ข้อความที่เห็น สาเหตุ และทางแก้ ตรงที่ผมไม่มีข้อความ error ของจริงเก็บไว้ ผมบอกว่าให้ตรวจอะไรแทนการเดาข้อความ
kit รุ่นที่ปล่อยตอนแรกมีบั๊กทั้งห้าตัวด้านล่าง ให้โหลดใหม่จาก สารบัญ (ไฟล์ใหม่ขนาด 48,142 ไบต์ sha256 ขึ้นต้น 24BA604D) แตกทับ C:\apm-build แล้วรัน .\deploy.cmd -ResetConfig ถ้าคุณยังไม่เคยแก้ config ใน C:\APM ทางนี้จบในคำสั่งเดียว ถ้าแก้ไว้แล้วและไม่อยากให้ทับ ให้รัน .\deploy.cmd เฉยๆ (binary ใหม่ รวมถึง plugin ของ MariaDB จะมาครบ) แล้วคัดลอกบรรทัดที่เปลี่ยนเข้า config ด้วยมือ สามจุดคือ PidFile กับ FcgidWrapper ใน httpd.conf และบล็อก opcache.cache_id ใน php.ini (ตัวอย่างอยู่ในหัวข้อ templates ด้านบน ค่า cache_id ใส่ชื่อที่ไม่ซ้ำกับโฟลเดอร์อื่นก็พอ เช่น apmmain)
บั๊กที่ 1: plugin ของ MariaDB หายหลัง deploy
อาการ: deploy ผ่านทุกบรรทัด แต่ DLL ของ plugin MariaDB หายจาก C:\APM ฟีเจอร์ที่ต้องใช้ plugin ตัวนั้น (เช่น engine ARCHIVE หรือการล็อกอินแบบ ed25519) เลยใช้ไม่ได้ตามไปด้วย ผมไม่ได้เก็บข้อความ error ของ MariaDB ไว้ ให้ตรวจที่ไฟล์แทน
Get-ChildItem C:\APM\mariadb\lib\plugin -Filter *.dll -ErrorAction SilentlyContinue | Select-Object -First 5 Name
ถ้าไม่มีอะไรพิมพ์ออกมา โฟลเดอร์หายหรือว่างเปล่า เทียบกับต้นทางที่ C:\apm-build\stage\mariadb\lib\plugin ซึ่งต้องมี DLL อย่าง ha_archive และ auth_ed25519 อยู่
สาเหตุ: รายการข้ามโฟลเดอร์ของ robocopy สำหรับ MariaDB มีคำว่า lib ใน /XD ผมใส่ไว้เพื่อตัดโฟลเดอร์ lib ระดับบนที่มีแต่ไฟล์ .lib สำหรับนักพัฒนา แต่ /XD lib ตัดโฟลเดอร์ ชื่อ lib ทุกตัวในทุกระดับ รวม lib\plugin ที่เก็บ DLL ของ plugin ซึ่งต้องใช้ตอนรัน
ทางแก้: เอา lib ออกจาก /XD ของ MariaDB ส่วนไฟล์ *.lib ถูกตัดด้วย /XF อยู่แล้ว
# เดิม
mariadb = @('/XD', 'include', 'lib', 'symbols', 'sql-bench', '/XF', '*.pdb', '*.lib', '*.exp', 'my.ini')
# ใหม่ (พร้อมคอมเมนต์ในสคริปต์)
mariadb = @('/XD', 'include', 'symbols', 'sql-bench', '/XF', '*.pdb', '*.lib', '*.exp', 'my.ini')
คนที่ deploy ไปแล้วแค่โหลด kit ใหม่แล้วรัน .\deploy.cmd อีกรอบ ไม่ต้อง -ResetConfig เพราะ robocopy จะคัดลอกโฟลเดอร์ plugin ที่ขาดมาให้ โดยไม่แตะ my.ini และข้อมูล
บั๊กที่ 2: เปลี่ยนพอร์ตฐานข้อมูลด้วย -ResetConfig แล้วไม่มีผล
อาการ: deploy ใหม่ด้วย .\deploy.cmd -DbPort 3306 -ResetConfig แล้วสตาร์ต MariaDB ตามขั้นที่ 15 พอร์ตยังเป็น 3307 แต่หน้าแรกของ APM ไปต่อพอร์ต 3306 (เพราะ php.ini ถูกเรนเดอร์ใหม่ มี mysqli.default_port = 3306) หน้าเลยขึ้น not reachable ตรวจได้ด้วย
Select-String -Path C:\APM\mariadb\my.ini, C:\APM\data\mysql\my.ini -Pattern '^port'
ถ้ารุ่นเก่า สองไฟล์จะได้เลขพอร์ตไม่ตรงกัน (ไฟล์แรก 3306 ไฟล์ที่สอง 3307) ผลนี้ผมอนุมานจากโค้ด ไม่ได้เก็บผลจริงตอนเจอ
สาเหตุ: server ไม่ได้อ่าน C:\APM\mariadb\my.ini แต่อ่านสำเนาที่ mariadb-install-db วางไว้ใน data\mysql\my.ini ตอนสร้าง datadir ครั้งแรก และรุ่นเก่าไม่เคยแตะไฟล์นั้นอีก -ResetConfig เรนเดอร์ไฟล์ในโฟลเดอร์ mariadb ใหม่ซึ่ง server ไม่อ่าน
ทางแก้: เพิ่มกิ่ง elseif ($ResetConfig) ท้ายช่วง datadir ให้คัดลอก mariadb\my.ini ทับ data\mysql\my.ini และพิมพ์ [OK] C:\APM\data\mysql\my.ini (synced) (โค้ดเต็มอยู่ในสคริปต์ที่วางไว้ด้านบน) ข้อความเก่าในตอนนี้ที่เคยบอกให้แก้ data\mysql\my.ini ด้วยมือ เลยเลิกใช้แล้ว
บั๊กที่ 3: รัน APM สองโฟลเดอร์พร้อมกัน ทุกหน้า PHP ขึ้น 500
อาการ: ผมแตกชุดที่แพ็กเสร็จไปทดสอบอีกโฟลเดอร์ ในขณะที่ C:\APM ยังรันอยู่ ปรากฏว่าทุกหน้า PHP ตอบ 500 ใน error.log ของ Apache (logs\apache\error.log) มีข้อความ
End of script output before headers
พอผมสั่ง php-cgi.exe -v ของโฟลเดอร์ใหม่ตรงๆ ก็เจอข้อความสาเหตุจริง
Opcode handlers are unusable due to ASLR
สาเหตุ: บน Windows OPcache แชร์หน่วยความจำกันตามชื่อ เมื่อ PHP build เดียวกัน รันโดย user เดียวกัน สองโฟลเดอร์เลยไปแย่งหน่วยความจำก้อนเดียวกัน ที่อยู่ที่ก้อนนั้นถูกจัดวางไว้ไม่ตรงกับ process ที่สอง (ASLR สุ่มที่อยู่ต่างกัน) ตัวที่มาทีหลังเลยใช้ opcode ที่เก็บไว้ไม่ได้และตายก่อนส่ง header ถ้าผมไม่ลองรันคู่กัน คงไม่เจอบั๊กนี้เลย
ทางแก้: ตั้งชื่อแคชแยกตามโฟลเดอร์ติดตั้ง และเปิดแคชแบบไฟล์เป็นตัวสำรอง สามบรรทัดใน php.ini ที่อยู่ในหัวข้อ templates ด้านบน (opcache.cache_id, opcache.file_cache, opcache.file_cache_fallback = 1) ส่วนค่า {{CACHE_ID}} ผมให้ 90-deploy.ps1 คำนวณจากพาธ เลยไม่ซ้ำกันระหว่างสองโฟลเดอร์โดยไม่ต้องคิดชื่อเอง และ deploy สร้าง tmp\opcache ไว้ให้
ข้อสังเกต: สองโฟลเดอร์ที่รันพร้อมกันยังต้องใช้พอร์ต Apache และ MariaDB คนละเลข (-HttpPort, -HttpsPort, -DbPort) เพราะ cache_id แก้เฉพาะเรื่องแคช
บั๊กที่ 4: พาธติดตั้งมีช่องว่าง Apache ขึ้น Wrapper cannot be accessed
อาการ: ผมแตกชุดไว้ในโฟลเดอร์ที่ชื่อมีช่องว่าง (ตัวอย่างคือ D:\APM test) แล้ว deploy ลงที่นั่น Apache ฟ้องใน error.log ด้วยข้อความที่มีคำว่า
Wrapper D:/APM cannot be accessed
สังเกตว่าพาธในข้อความถูกตัดที่ช่องว่าง เหลือแค่ D:/APM
สาเหตุ: mod_fcgid แยกบรรทัดคำสั่งของ FcgidWrapper ด้วยช่องว่าง (ตามคอมเมนต์ในไฟล์คือ apr_tokenize_to_argv) เครื่องหมายคำพูดครอบนอกของ directive ใน httpd.conf ถูก Apache ถอดออกก่อนส่งให้ module เลยเหลือพาธเปล่าๆ ที่มีช่องว่าง และถูกตัดเป็นสองส่วน
ทางแก้: ใส่เครื่องหมายคำพูดซ้อนข้างใน (เลี่ยงด้วย \") เพื่อให้ module เห็นเครื่องหมายคำพูดครอบพาธอีกชั้น
FcgidWrapper "\"${APM}/php/php-cgi.exe\"" .php
พาธมาตรฐาน C:\APM ไม่มีช่องว่าง ส่วนใหญ่เลยไม่เจอบั๊กนี้ มันโผล่เมื่อใช้ -Target "D:\APM test" หรือแตกไว้ใต้โฟลเดอร์ที่ชื่อมีช่องว่าง
บั๊กที่ 5: พาธติดตั้งมีวงเล็บ ออกใบรับรองไม่ได้ Undefined constant OPENSSL_KEYTYPE_EC
อาการ: deploy ลงโฟลเดอร์ที่ชื่อมีวงเล็บ เช่น D:\APM (copy) แล้วช่วงออกใบรับรองล้ม ข้อความมาจาก PHP ของ mkcert.php ไม่ใช่จากสคริปต์ deploy
Undefined constant OPENSSL_KEYTYPE_EC
ตามด้วย [ERR] certificate generation failed ของ 90-deploy.ps1
สาเหตุ: สคริปต์ส่งพาธ extension ให้ PHP ด้วย php -d extension_dir=<พาธ> แต่ค่าของ -d ถูกอ่านด้วยไวยากรณ์ INI ซึ่งวงเล็บ ( ) มีความหมายพิเศษ ค่าเลยเพี้ยน PHP หาโฟลเดอร์ extension ไม่เจอ ไม่โหลด openssl และเมื่อ mkcert.php ไปอ้างค่าคงที่ OPENSSL_KEYTYPE_EC ซึ่งมีเฉพาะเมื่อ openssl โหลดแล้ว เลยฟ้องว่าไม่มีค่าคงที่นี้ ข้อความนี้ไม่ได้บอกต้นเหตุตรงๆ ว่า openssl โหลดไม่ขึ้น
ทางแก้: สองจุด จุดแรกใน 90-deploy.ps1 ครอบค่าด้วยเครื่องหมายคำพูดอีกชั้น
& "$Target\php\php.exe" -n -d "extension_dir=`"$Target\php\ext`"" -d extension=openssl `
"$Root\tools\mkcert.php" "$Target\ssl" @certHosts
จุดที่สองใน mkcert.php เช็กก่อนเริ่มว่า openssl โหลดจริง ถ้าไม่ได้ให้บอกต้นเหตุตรงๆ
if (!extension_loaded('openssl')) {
fwrite(STDERR, "[ERR] PHP openssl extension not loaded (check -d extension_dir=\"...\" - quote it, folder names may contain ( ) or spaces)\n");
exit(1);
}
บั๊กที่ 4 และ 5 มาจากรากเดียวกัน คือเดิมผมทดสอบแต่ใน C:\APM ที่พาธสะอาดทุกอย่าง ตอนนี้ถ้าคุณ deploy ไปที่อื่น พาธแปลกๆ ใช้ได้แล้ว
อัปเดตเมื่อมี security release
นี่คือเหตุผลที่ผมทำทั้งซีรีส์ ทุกเวอร์ชันและ hash อยู่ใน versions.json ไฟล์เดียว เมื่อมีข่าว patch ผมแก้ไฟล์นั้น ดึงเฉพาะส่วนที่เปลี่ยน build ใหม่เฉพาะส่วนนั้น แล้ว deploy.cmd อีกรอบ
ขั้นตอนข้างล่างเรียบเรียงจากสคริปต์ที่ผมอ่านและจากการรัน build ซ้ำทั้งชุดโดยไม่เปลี่ยนเวอร์ชัน (ครบวงจรใช้เวลา 10 นาที: fetch 0.0, vcpkg 0.1, apache 1.6, mariadb 0.3, imagemagick 3.3, php 5.0) ผมยังไม่ได้ผ่านการเปลี่ยนเวอร์ชันจริงครบวงจร เพราะยังไม่มี release ถัดไปให้เปลี่ยน ส่วนที่เปลี่ยนใน kit รุ่นใหม่ (`-Only` แยกจุลภาคเอง และ PECL ที่คัดลอกใหม่ทุกรอบ) ผมอ่านจากโค้ดเช่นกัน เลขเวอร์ชันในตัวอย่างด้านล่างเลยเป็นตัวเลขสมมติ ทุกครั้งที่ผมบอกว่าสคริปต์ทำอะไร นั่นมาจากการอ่านโค้ด
เลือกให้ถูกว่าต้อง build ส่วนไหน
ข่าว security มักระบุว่าช่องโหว่อยู่ในโปรแกรมไหน ใช้ตารางนี้ตัดสินใจว่าต้องแก้อะไรและรันสคริปต์ไหน (จาก HOWTO ส่วน "อัปเดตเมื่อมี security patch" ที่ผมสรุปไว้ พร้อมจุดที่ผมเจอเพิ่มจากการอ่านสคริปต์)
| ตัวที่ออก patch | แก้ตรงไหน | รันอะไร |
|---|---|---|
| PHP | sources.php (version, url, sha256) |
00-fetch.ps1 -Only php แล้ว 30-php.ps1 -Step all |
| Apache httpd / APR | sources.httpd หรือ sources.apr sources.apr-util |
00-fetch.ps1 -Only httpd แล้ว 20-apache.ps1 |
| MariaDB | sources.mariadb |
00-fetch.ps1 -Only mariadb แล้ว 40-mariadb.ps1 |
| redis, igbinary, imagick (PECL) | sources.pecl-* และลบบรรทัดของรายการนั้นใน versions.lock.json (ไม่ต้องลบ build\php-pecl แล้ว สคริปต์คัดลอกใหม่ทุกรอบ) |
00-fetch.ps1 -Only pecl-redis (ตามชื่อ) แล้ว 30-php.ps1 -Step build |
| OpenSSL ฝั่ง Apache | ports-overlay\openssl\vcpkg.json (version) และ portfile.cmake (SHA512) |
10-deps-vcpkg.ps1 แล้ว 20-apache.ps1 |
| lib อื่นของ Apache | git -C toolchain\vcpkg pull แล้วแก้ builtin-baseline ใน deps\apache\vcpkg.json เป็น commit ใหม่ |
10-deps-vcpkg.ps1 แล้ว 20-apache.ps1 |
| ImageMagick | sources.imagemagick.version (git tag) |
25-imagemagick.ps1 แล้ว 30-php.ps1 -Step build |
| DLL ของ deps ใน PHP | ไม่ต้องแก้ไฟล์ | 30-php.ps1 -Step all (phpsdk_deps -u ดึงรุ่นล่าสุดให้เอง) |
| ย้ายไป PHP 8.5 | sources.php และ php_deps_series เป็น "8.5" |
ทำเมื่อเครื่อง production ย้ายเท่านั้น |
ข้อสังเกตที่สำคัญ OpenSSL ที่ ฝั่ง PHP มาจากชุด deps ของ PHP team (รอบนี้เป็น 3.0.22) ไม่ใช่ตัวเดียวกับของ Apache (3.5.9) ถ้า OpenSSL ออก patch ต้องดูว่าอยากแก้ฝั่งไหน ฝั่ง Apache แก้ที่ overlay เอง ส่วนฝั่ง PHP รอให้ PHP team ปล่อยชุด deps ใหม่ แล้ว 30-php.ps1 -Step all จะดึงให้
ด้านล่างผมใช้ PHP เป็นตัวอย่างหลัก ส่วน Apache และ MariaDB ขั้นตอนเหมือนกันทุกประการ ต่างกันแค่ชื่อรายการและสคริปต์
ขั้นที่ 35: เปิด terminal และหยุด APM
เปิด pwsh ธรรมดา แล้วหยุดทุกอย่างตามขั้นที่ 26 ถึง 29 ของหัวข้อสตาร์ตครั้งแรก ถ้า APM ยังไม่ได้รัน ขั้นนี้ข้ามได้
ขั้นที่ 36: สำรองเวอร์ชันที่ใช้อยู่ (ทางเลือก แต่ผมแนะนำ)
deploy.cmd ทับไบนารีเดิมด้วย /MIR ทำให้ไม่มีเวอร์ชันเก่าเหลือให้ย้อน ผมเลยแนะนำให้คัดลอกส่วนที่จะเปลี่ยนเก็บไว้ก่อน เช่นกรณี PHP
robocopy C:\APM\php C:\APM-prev\php /MIR /NFL /NDL /NJH /NJS /NP
คำสั่งนี้ไม่พิมพ์อะไร และ robocopy คืนรหัสออก 1 เมื่อคัดลอกสำเร็จ (ไม่ใช่ข้อผิดพลาด) ตรวจว่าโฟลเดอร์ปลายทางมีไฟล์
Test-Path C:\APM-prev\php\php-cgi.exe
True
ถ้าส่วนที่จะอัปเดตคือ MariaDB ให้สำรองฐานข้อมูลด้วยการ dump ก่อนด้วย (ต้องสตาร์ต MariaDB ก่อน แล้วปิดตามเดิมหลังจบ ผมไม่ได้ลองคำสั่งนี้ในรอบทดสอบ)
C:\APM\mariadb\bin\mariadb-dump.exe -uroot --port=3307 --protocol=tcp --all-databases --result-file=C:\APM-prev\all.sql
ขั้นที่ 37: ไปที่โฟลเดอร์ build และสำรอง versions.json
cd C:\apm-build
Copy-Item versions.json versions.json.bak
ไม่มีผลพิมพ์ ไฟล์ .bak ไว้เทียบหรือย้อนถ้าแก้ผิด
ขั้นที่ 38: หาเลขเวอร์ชันใหม่และ sha256 ของแต่ละตัว
ที่มาของแต่ละค่า (ต้องได้ครบสามอย่างต่อหนึ่งรายการ: version, url, sha256)
- PHP: เปิด
https://www.php.net/releases/index.php?json&version=8.4&max=1ใน browser ดูรายการไฟล์ของ release ล่าสุด หาphp-<เวอร์ชัน>.tar.xzแล้วคัดลอกค่า sha256 ของไฟล์นั้นurlคือhttps://www.php.net/distributions/php-<เวอร์ชัน>.tar.xz - httpd และ APR: ไฟล์
.sha256ที่อยู่ข้างไฟล์ tarball บน downloads.apache.org ดึงค่าด้วย pwsh ได้ (สมมติ httpd 2.4.70) เอาเฉพาะช่องแรกซึ่งเป็น hash
((curl.exe -fsSL https://downloads.apache.org/httpd/httpd-2.4.70.tar.bz2.sha256) -split '\s+')[0]
ผลที่ควรเห็นคือสตริงฐานสิบหก 64 ตัวบรรทัดเดียว (ถ้า curl ตอบ 404 แปลว่ารุ่นนั้นถูกย้ายไป archive.apache.org หรือพิมพ์เลขรุ่นผิด)
- MariaDB: ดูที่
https://downloads.mariadb.org/rest-api/mariadb/11.8/latest/ซึ่งบอก hash ของไฟล์ source แต่ละอันในรุ่นล่าสุดurlมีรูปแบบhttps://archive.mariadb.org/mariadb-<เวอร์ชัน>/source/mariadb-<เวอร์ชัน>.tar.gz - ถ้าหา hash บนเว็บต้นทางไม่เจอ: ดาวน์โหลดไฟล์ด้วยมือ แล้วเทียบกับหน้าประกาศ release ด้วย
Get-FileHash <ไฟล์> -Algorithm SHA256อย่าใช้ค่าจากเว็บของบุคคลที่สาม
ขั้นที่ 39: แก้ versions.json
เปิดด้วย Notepad (หรือ editor ที่คุณใช้)
notepad C:\apm-build\versions.json
หาบรรทัดของ php ใต้ sources เปลี่ยนสามค่าในบรรทัดเดียวกัน ตัวอย่างสมมติให้ PHP ออกรุ่น 8.4.28 (หน้าตาก่อนและหลัง ผมย่อ hash ให้สั้นลงในบทความ ของจริงต้องเต็ม 64 ตัว)
ก่อนแก้
"php": { "version": "8.4.27", "url": "https://www.php.net/distributions/php-8.4.27.tar.xz", "sha256": "dcf93923b0e3...d3f0" },
หลังแก้
"php": { "version": "8.4.28", "url": "https://www.php.net/distributions/php-8.4.28.tar.xz", "sha256": "<sha256 ของ php-8.4.28.tar.xz จาก php.net>" },
เปลี่ยนให้ครบทั้งสามช่อง อย่าเปลี่ยนแค่ version เพราะ url กับ sha256 ไม่ตามมาเอง และห้ามเว้น sha256 เป็น null สำหรับรายการที่ต้นทางประกาศ hash ไว้ (ผมตั้งใจให้ PHP, httpd, APR, MariaDB มี hash ตรึงไว้เสมอ)
ขั้นที่ 40: ตรวจว่า JSON ยังถูกรูปแบบ
Get-Content C:\apm-build\versions.json -Raw | ConvertFrom-Json | Out-Null
ผลที่ควรเห็นคือ ไม่มีอะไรพิมพ์ออกมา ถ้ามีตัวหนังสือสีแดงขึ้น แปลว่าไฟล์เสียรูป (ส่วนมากลืมเครื่องหมายคอมมา หรือมีอัญประกาศขาด) กลับไปแก้ที่ขั้นที่ 39 ถ้าแก้ไม่หาย คัดลอก versions.json.bak กลับมาแล้วเริ่มใหม่
ขั้นที่ 41: จัดการรายการที่ใช้ lock file (เฉพาะกรณีที่แก้เลขของรายการที่ไม่มี sha256)
รายการอย่าง pecl-redis, pecl-igbinary, pecl-imagick, mod_fcgid และ toolchain ที่ใน versions.json เป็น "sha256": null ใช้ระบบ trust-on-first-use คือ 00-fetch.ps1 บันทึก hash ครั้งแรกใน versions.lock.json และครั้งต่อไปเทียบกับค่านั้น เมื่อคุณเปลี่ยนเวอร์ชันของรายการเหล่านี้ hash ใหม่จะไม่ตรงกับ hash เก่าใน lock และสคริปต์จะหยุดด้วย [ERR] sha256 mismatch ดังนั้นให้ลบบรรทัดของรายการนั้นออกจาก versions.lock.json ก่อน เช่น ลบบรรทัด "pecl-redis": "..." ออก (ระวังเครื่องหมายคอมมาท้ายบรรทัดสุดท้ายให้ JSON ยังถูกต้อง)
ตอนที่ 6 เคยเตือนให้ลบโฟลเดอร์ build\php-pecl\<ชื่อ> ด้วยมือ เพราะสคริปต์รุ่นแรกคัดลอก PECL ไปไว้ครั้งเดียว ชื่อโฟลเดอร์ไม่มีเลขเวอร์ชันติด พอแก้เวอร์ชันแล้ว build ก็ยังใช้ source เก่าเงียบๆ kit รุ่นใหม่ไม่ต้องทำขั้นนั้นแล้ว 30-php.ps1 ลบ build\php-pecl\<ชื่อ> แล้วคัดลอกจาก src\pecl-<ชื่อ>-<เวอร์ชัน> ใหม่ทุกครั้งที่รัน และถ้า source ของเวอร์ชันที่ระบุยังไม่ถูก fetch จะหยุดด้วย [ERR] <พาธ> missing - run 00-fetch.ps1 ให้ทำขั้นที่ 42 ก่อน ส่วนบรรทัดใน versions.lock.json ยังต้องลบเองเหมือนเดิม เพราะ 00-fetch.ps1 ยังเทียบ hash กับ lock อยู่ ข้ามขั้นนี้ได้เมื่อคุณอัปเดตเฉพาะ PHP, httpd, APR หรือ MariaDB เพราะ 4 ตัวนั้นมี hash ใน versions.json อยู่แล้ว ผมสรุปเรื่อง lock และการคัดลอกใหม่จากการอ่าน 00-fetch.ps1 กับ 30-php.ps1 ยังไม่ได้ทดสอบกับการเปลี่ยนเวอร์ชัน PECL จริง
ขั้นที่ 42: ดึง source ใหม่เฉพาะตัวที่เปลี่ยน
pwsh -NoProfile -File scripts\00-fetch.ps1 -Only php
ผลที่ควรเห็น (เลขเวอร์ชันตามที่คุณแก้ ตัวอย่างสมมติ)
[GET] php 8.4.28
[OK] php -> C:\apm-build\src\php-8.4.28
ไฟล์ที่ดาวน์โหลดเก็บใน downloads\ และแตกลง src\php-8.4.28 โดยโฟลเดอร์ source เก่า (src\php-8.4.27) ยังอยู่ ไม่ถูกลบ ถ้า sha256 ไม่ตรงกับที่คุณใส่ใน versions.json สคริปต์ throw ทันที พร้อมแสดงค่าที่ได้และค่าที่คาดไว้ ห้ามบังคับผ่าน ให้เช็กว่าคัดลอก hash ถูกตัวและถูกไฟล์ก่อน ถ้าต้องดาวน์โหลดและแตกใหม่จริงๆ ใส่ -Force ท้ายคำสั่ง
ถ้าอัปเดตหลายตัวพร้อมกัน ใส่รายชื่อคั่นด้วยจุลภาคได้เลย
pwsh -NoProfile -File scripts\00-fetch.ps1 -Only php,httpd
ผลที่ควรเห็นคือบล็อกของแต่ละตัวตามลำดับ (เลขเวอร์ชันสมมติเหมือนเดิม)
[GET] php 8.4.28
[OK] php -> C:\apm-build\src\php-8.4.28
[GET] httpd 2.4.70
[OK] httpd -> C:\apm-build\src\httpd-2.4.70
ใช้ได้เพราะ pwsh -File ส่ง php,httpd เข้าไปเป็นข้อความก้อนเดียว และ 00-fetch.ps1 รุ่นใหม่แยกจุลภาคเองด้วยบรรทัดนี้ ซึ่งอยู่ใต้บรรทัด param (รุ่นแรกไม่มีบรรทัดนี้ เลยโหลดอะไรไม่ได้และไม่ขึ้น error ดูเรื่องเดิมใน ตอนที่ 2 ใครถือ kit เก่าอยู่ให้โหลดรุ่นใหม่) บรรทัดที่เพิ่มคือ
$Only = @($Only | ForEach-Object { $_ -split ',' } | ForEach-Object { $_.Trim() } | Where-Object { $_ })
ขั้นที่ 43: build เฉพาะส่วนที่เปลี่ยน
สำหรับ PHP
pwsh -NoProfile -File scripts\30-php.ps1 -Step all
เวลาที่วัดได้ของการ build ซ้ำเฉพาะส่วน: php 5.0 นาที, apache 1.6 นาที (รอบแรกของแต่ละส่วนนานกว่า: php 12, apache 4, mariadb 30 นาที) ดูรายละเอียดการอ่านผลของ PHP ในตอนที่ 6 ส่วน Apache ใช้ 20-apache.ps1 และ MariaDB ใช้ 40-mariadb.ps1 ตามตารางข้างบน
ถ้าไม่แน่ใจว่าต้อง build ส่วนไหน การรัน .\build.cmd ทั้งชุดก็ถูกต้องเสมอ ถ้า cache ยังอยู่ ใช้เวลาประมาณ 10 นาทีตามที่วัดได้ รุ่นใหม่ของ build.cmd มีตัวเลือก -To ควบคู่กับ -From ด้วย เช่น .\build.cmd -From php -To php เพื่อหยุดหลังขั้นนั้น (ขั้นที่มีคือ fetch vcpkg apache mariadb imagemagick php)
ขั้นที่ 44: ตรวจเวอร์ชันใน stage ก่อน deploy
C:\apm-build\stage\php\php.exe -v
บรรทัดแรกต้องเป็นเวอร์ชันใหม่ในรูปแบบเดียวกับตอนที่ 6 (เช่น PHP 8.4.28 (cli) ... สำหรับตัวอย่างสมมติ) ถ้ายังเห็นเลขเก่า แปลว่าสคริปต์ยังอ่าน versions.json เวอร์ชันเก่าหรือ build ไม่ผ่าน กลับไปดูขั้นที่ 39 และ 43 ส่วน Apache ตรวจด้วย C:\apm-build\stage\apache\bin\httpd.exe -V และ MariaDB ตรวจด้วย C:\apm-build\stage\mariadb\bin\mariadbd.exe --version
ขั้นที่ 45: deploy ทับ
ที่ C:\apm-build ยืนยันว่า APM หยุดอยู่ (ขั้นที่ 35) แล้วรัน
.\deploy.cmd
ผลที่ควรเห็นคือบล็อก [OK] สามบรรทัดของ binary ตามด้วย [KEEP] สี่บรรทัดของ config, Syntax OK, บรรทัดสรุป not started และบรรทัด httpd.conf Listen: ตามที่แสดงไว้ในหัวข้อ deploy ซ้ำ
ขั้นที่ 46: สตาร์ตและยืนยันว่าเป็นรุ่นใหม่
สตาร์ตตามขั้นที่ 15 ถึง 18 แล้วตรวจสองจุด จุดแรกคือเวอร์ชันของไบนารีที่ติดตั้ง
C:\APM\php\php.exe -v
จุดที่สองคือเปิด http://127.0.0.1:8080/ ในเบราว์เซอร์ แถว PHP ต้องแสดงเวอร์ชันใหม่ และ curl.exe ตามขั้นที่ 19 กับ 20 ต้องได้ 200 ทั้งคู่ เมื่อผ่านแล้ว คำสั่ง Remove-Item C:\APM-prev -Recurse -Force ลบชุดสำรองได้ (ลบเมื่อมั่นใจแล้วเท่านั้น)
ขั้นที่ 47: ถ้ารุ่นใหม่มีปัญหา ย้อนกลับ
หยุด APM (ขั้นที่ 26 ถึง 29) แล้วคัดลอกชุดสำรองกลับ โดยกันไม่ให้ทับ php.ini
robocopy C:\APM-prev\php C:\APM\php /MIR /XF php.ini /NFL /NDL /NJH /NJS /NP
ไม่มีผลพิมพ์ ตรวจด้วย C:\APM\php\php.exe -v ต้องกลับเป็นเลขเก่า แล้วสตาร์ตใหม่ วิธีย้อนนี้ผมออกแบบเอง ยังไม่ได้ซ้อมจริง
การ build และ deploy สำเร็จไม่ได้แปลว่าช่องโหว่ถูกปิด ให้เทียบเลขรุ่นกับที่ประกาศในข่าว security ของ upstream เสมอ (รอบนี้คือ PHP 8.4.27 ขึ้นไป) และอย่าให้เว็บที่ต่ออินเทอร์เน็ตตรงๆ รันบน APM ตามการตั้งค่าเริ่มต้น เพราะ APM ถูกออกแบบให้ฟังที่ 127.0.0.1 และเป็นเครื่องพัฒนา ไม่ใช่เซิร์ฟเวอร์ production
ตารางปัญหาที่เจอทั้งซีรีส์
ตารางแรกคือ 17 อาการที่เกี่ยวกับการ build ทั้งชุด (จากบันทึก HOWTO ส่วนปัญหาที่เจอ รวมข้อ Visual Studio ที่เพิ่มหลังปรับ lib.ps1) ผมใส่ไว้ครบแม้บางข้อเป็นเรื่องของตอนอื่น เพื่อให้ค้นที่เดียวได้ คอลัมน์สุดท้ายบอกว่ารายละเอียดอยู่ตอนไหน ทุกข้อที่ระบุว่า "อยู่ในสคริปต์แล้ว" คือผมแก้ในสคริปต์ให้เรียบร้อยแล้ว คุณไม่ต้องทำอะไร
| อาการ | สาเหตุ | วิธีแก้ | ตอน |
|---|---|---|---|
[ERR] Visual Studio 2022 with C++ x64 tools not found |
lib.ps1 ใช้ vswhere หา VS 2022 ที่มี component VC.Tools.x86.x64 ไม่เจอ (ไม่ได้ลง C++ workload หรือมีแต่ VS รุ่นอื่น) |
ติดตั้ง VS 2022 หรือ Build Tools พร้อม component นั้นตามตอนที่ 1 รุ่นไหนก็ได้ ติดตั้งที่ไหนก็ได้ | 1 |
'buildconf.bat' is not recognized |
environment ตั้ง NoDefaultCurrentDirectoryInExePath=1 cmd เลยไม่ค้นในโฟลเดอร์ปัจจุบัน |
เรียกเป็น .\buildconf.bat / .\configure.bat (อยู่ในสคริปต์แล้ว) |
6 |
configure ไม่รู้จัก --enable-gd |
ชื่อ flag บน Windows ต่างจาก Linux | ใช้ --with-gd, --with-dom และตรวจด้วย -Step help |
6 |
| GD ไม่มี WebP/AVIF, PCRE JIT ไม่ทำงาน | --disable-all ปิด option ที่ปกติเปิดอยู่ |
ใส่ --with-libwebp --with-libavif --with-pcre-jit --enable-redis-session |
6 |
| แก้ configure แล้วผลไม่เปลี่ยน | nmake ใช้ .obj เดิมต่อ | ลบ build\php-obj และ stage\php ก่อน build ทุกครั้ง |
6 |
igbinary/igbinary.h: No such file |
redis include header แบบมี path ของ igbinary นำหน้า | --with-extra-includes=build\php-pecl |
6 |
php.exe ไม่ทำงาน (exit 53) |
DLL ของ deps ไม่อยู่ข้าง exe | ไล่หาด้วย dumpbin /dependents แล้วคัดลอก |
6 |
Compatibility with CMake < 3.5 has been removed |
ไฟล์ CMake ของ APR/httpd เก่า | -DCMAKE_POLICY_VERSION_MINIMUM=3.5 |
3 |
Unexpected enablement value "N" for mod_lua |
CMake ของ httpd รับค่าแค่ O/i/I/a/A และค่าเก่าค้างใน cache | ไม่ใส่ ENABLE_LUA และใช้ cmake --fresh |
3 |
| mod_lua ถูก build ด้วย Lua 5.5 | FindLua51 ไปจับ Lua รุ่นใหม่ของ vcpkg |
เอา lua ออกจาก manifest (เครื่อง production ก็ไม่ใช้ mod_lua) | 3 |
mod_md.so ต้องใช้ libcurl-d.dll |
imported target ของ vcpkg ไม่มี config RelWithDebInfo เลยไปใช้ Debug | -DCMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO=Release;RelWithDebInfo; |
3 |
ไม่มี conf\mime.types |
cmake --install ของ httpd ไม่ติดตั้ง conf |
คัดลอกจาก docs\conf ของ source |
3 |
ImageMagick was build with a 64 channel bit mask and that requires a C++ compiler |
Configure ของ ImageMagick บังคับ channel mask 64 bit บน x64 ซึ่งทำให้ header ใช้ได้กับ C++ เท่านั้น แต่ imagick เขียนด้วย C | แก้ Options.cpp ให้ใช้ 32 bit แล้ว build ImageMagick ใหม่ |
5 |
| Configure ของ ImageMagick build ไม่ผ่านเพราะหา v145 ไม่เจอ | โปรเจกต์ตั้งไว้สำหรับ VS2026 | /p:PlatformToolset=v143 และสั่ง Configure ด้วย /VS2022 |
5 |
imagick โหลดไม่ขึ้น (Class "Imagick" not found) |
ไม่มี extension = imagick ใน php.ini |
เพิ่มใน template แล้ว ถ้า deploy ไปก่อนหน้านี้ให้รัน deploy.cmd -ResetConfig |
6 และ 7 |
Error loading extension section v3_ca ตอนออก cert |
openssl.cnf ที่ยืมมาจาก deps ไม่มี section ที่ต้องใช้ | mkcert.php สร้าง config ของตัวเองทั้งไฟล์ |
7 |
| perl ของ Git ทำให้ OpenSSL พัง | perl ของ Git เป็น cygwin สร้าง path แบบ Unix | ใช้ Strawberry Perl ใน toolchain\ เท่านั้น |
2 และ 4 |
ตารางที่สองคืออาการที่เกิดตอน deploy, สตาร์ต, เปิด LAN และอัปเดต ซึ่งบางข้อเป็นข้อความ error จริงที่ 90-deploy.ps1 พิมพ์
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
[ERR] httpd from C:\APM is running - stop APM before deploying (หรือ mariadbd, php-cgi) |
มี process ของ APM รันอยู่ Windows เขียนทับไฟล์ที่ใช้งานอยู่ไม่ได้ | หยุดตามขั้นที่ 26 ถึง 29 แล้ว deploy ใหม่ |
[ERR] service APM-Apache is running - stop APM before deploying (หรือ APM-MariaDB) |
มี Windows service ของ APM แบบพร้อมใช้รันอยู่ process ของ service เห็นไม่ได้จาก Get-Process เลยมีข้อตรวจแยก |
หยุด service ด้วย Stop-Service (ต้อง admin) แล้ว deploy ใหม่ |
[ERR] ...\stage\apache missing - build first |
ยังไม่ได้ build ส่วนนั้น หรือ build ไม่สำเร็จ | กลับไปทำตอนที่ 3, 4 หรือ 6 ให้จบ |
[ERR] copy templates failed (หรือ tools) |
robocopy ของโฟลเดอร์นั้นคืนรหัส 8 ขึ้นไป (ไฟล์ปลายทางถูกล็อก หรือสิทธิ์ไม่พอ) | ปิดโปรแกรมที่เปิดไฟล์ใน C:\APM\templates กับ tools (เช่น editor) แล้ว deploy ใหม่ |
[ERR] cacert.pem sha256 mismatch |
ไฟล์ CA bundle ที่ดาวน์โหลดไม่ตรงกับ hash ที่ curl.se ประกาศ (โหลดไม่ครบ หรือมี proxy แทรก) | รัน deploy.cmd ใหม่ ถ้าเกิดซ้ำให้ตรวจเครือข่ายและ proxy |
[ERR] certificate generation failed |
mkcert.php ออกใบรับรองไม่สำเร็จ ข้อความ OpenSSL หรือ PHP จะพิมพ์ใต้บรรทัดนี้ |
อ่านข้อความที่พิมพ์ต่อท้าย ตรวจว่า C:\APM\php\ext\php_openssl.dll มีอยู่ ถ้าเห็น Undefined constant OPENSSL_KEYTYPE_EC ดูแถวถัดไป |
Undefined constant OPENSSL_KEYTYPE_EC หรือ [ERR] PHP openssl extension not loaded ตอนออก cert |
พาธติดตั้งมีวงเล็บหรือช่องว่าง ค่า -d extension_dir= เพี้ยน openssl เลยไม่โหลด |
ใช้ kit รุ่นใหม่ที่ครอบค่าด้วย quote หรือย้ายไปพาธที่ไม่มีอักขระพิเศษ (บั๊กที่ 5) |
[ERR] mariadb-install-db failed |
สร้าง datadir ไม่ได้ | ตรวจว่า C:\APM\data\mysql ไม่ใช่โฟลเดอร์ที่ค้างครึ่งๆ กลางๆ ถ้าเป็นให้ลบแล้ว deploy ใหม่ (ลบเฉพาะเมื่อยังไม่มีข้อมูลสำคัญ) |
[ERR] httpd -t failed |
config มี syntax ผิด หรือ module โหลดไม่ได้ | อ่านบรรทัดที่ httpd -t พิมพ์ก่อนหน้า แก้ไฟล์ที่ระบุ |
httpd.exe ตายทันทีหลังสตาร์ต AH00099: could not create C:/APM/apache/logs/httpd.pid |
template เก่าไม่มี PidFile และโฟลเดอร์ apache\logs ไม่มีอยู่ |
ทำตามขั้นที่ 23 ถึง 25 |
ทุกหน้า PHP ตอบ 500, error.log มี End of script output before headers, php-cgi.exe -v ขึ้น Opcode handlers are unusable due to ASLR |
มี APM (หรือ PHP build เดียวกัน) รันอยู่อีกโฟลเดอร์ แย่งหน่วยความจำ OPcache ก้อนเดียวกัน | ใช้ kit รุ่นใหม่ที่ตั้ง opcache.cache_id แยกตามโฟลเดอร์ ถ้าแก้มือให้เติมสามบรรทัด opcache ใน php.ini (บั๊กที่ 3) |
Apache ขึ้น Wrapper D:/APM cannot be accessed (หรือพาธอื่นที่ถูกตัดตรงช่องว่าง) |
พาธติดตั้งมีช่องว่าง mod_fcgid ตัดคำสั่งตรงช่องว่าง |
แก้เป็น FcgidWrapper "\"${APM}/php/php-cgi.exe\"" .php หรือ deploy ด้วย kit รุ่นใหม่ (บั๊กที่ 4) |
ไม่มี plugin ของ MariaDB (ha_archive, auth_ed25519) ใน C:\APM\mariadb\lib\plugin |
kit เก่าใส่ lib ใน /XD ของ robocopy ตัดโฟลเดอร์ lib\plugin ไปด้วย |
โหลด kit ใหม่แล้วรัน .\deploy.cmd อีกรอบ (บั๊กที่ 1) |
เปลี่ยน -DbPort ด้วย -ResetConfig แล้วพอร์ตจริงยังเป็นเลขเก่า หน้าแรกขึ้น not reachable |
server อ่าน data\mysql\my.ini ซึ่ง kit เก่าไม่ sync ให้ |
ใช้ kit ใหม่ (-ResetConfig sync ให้และพิมพ์ (synced)) หรือแก้บรรทัด port ใน data\mysql\my.ini ด้วยมือ (บั๊กที่ 2) |
| สตาร์ต Apache แล้วเข้าหน้าเว็บไม่ได้ | พอร์ตถูกโปรแกรมอื่นใช้อยู่ หรือ Apache ไม่ได้สตาร์ต | ตรวจพอร์ตด้วย Get-NetTCPConnection -LocalPort 8080,8443 และตรวจว่ามี process httpd ของ C:\APM จริง เปลี่ยนพอร์ตด้วย -HttpPort / -HttpsPort กับ -ResetConfig |
หน้าแรกขึ้น MariaDB not reachable (...) |
ยังไม่ได้สตาร์ต MariaDB, พอร์ตไม่ตรงกับ mysqli.default_port ใน php.ini หรือ root มีรหัสผ่านแล้ว |
สตาร์ตตามขั้นที่ 15 และตรวจขั้นที่ 17 ถ้าตั้งรหัสผ่านให้ root หน้านี้จะรายงานว่าเข้าไม่ได้ ซึ่งคาดไว้ |
| เครื่องอื่นเปิดเว็บทาง LAN ไม่ได้ (timeout) | httpd.conf ยังฟังที่ 127.0.0.1 (deploy เตือน [WARN] httpd.conf was kept ...) หรือยังไม่ได้เปิดกฎ firewall หรืออยู่คนละซับเน็ต |
ดูบรรทัด httpd.conf Listen: ว่าเป็น 0.0.0.0:... ถ้าไม่ใช่ให้ deploy.cmd -Lan -ResetConfig รัน scripts\firewall-lan.ps1 แบบ admin และตรวจว่าสองเครื่องอยู่ซับเน็ตเดียวกัน (กฎรับเฉพาะ LocalSubnet) |
| เครื่องอื่นเปิด HTTPS แล้ว browser เตือนใบรับรอง | เครื่องนั้นยังไม่เชื่อ apm-ca.crt หรือเปิดด้วยชื่อ/IP ที่ไม่อยู่ใน SAN (IP เปลี่ยนหลัง deploy) |
นำเข้า apm-ca.crt ในเครื่องนั้น และถ้า IP เปลี่ยนให้ deploy.cmd -Lan -ResetConfig ออกใบรับรองใหม่ |
?info=1 ได้หน้าปกติ ไม่เห็น phpinfo และไม่มีลิงก์ phpinfo() |
เปิดจากเครื่องอื่น phpinfo เปิดให้ 127.0.0.1 และ ::1 เท่านั้น |
ตั้งใจให้เป็นแบบนี้ เปิดจากเครื่อง APM เอง |
browser เตือนว่าใบรับรอง https://localhost:8443 ไม่น่าเชื่อถือ |
ยังไม่ได้นำเข้า CA ของเครื่อง (หรือ Firefox ที่ไม่ได้อ่านที่เก็บของ Windows) | รัน trust-ca.ps1 (ขั้นที่ 8) หรือทำตามขั้นที่ 9 ถึง 12 |
00-fetch.ps1 หยุดด้วย sha256 mismatch หลังเปลี่ยนเลขของ PECL หรือ mod_fcgid |
hash ใหม่ไม่ตรงกับ hash เก่าใน versions.lock.json |
ลบบรรทัดของรายการนั้นใน lock file ตามขั้นที่ 41 |
00-fetch.ps1 -Only php,httpd ไม่ดาวน์โหลดอะไรเลย ไม่มี error (kit เก่า) |
pwsh -File ส่งรายชื่อเป็น string ก้อนเดียว และสคริปต์รุ่นเก่าไม่แยกจุลภาค |
โหลด kit ใหม่ สคริปต์แยกจุลภาคเองแล้ว หรือรันผ่าน -Command |
[ERR] ...\src\pecl-redis-<เวอร์ชัน> missing - run 00-fetch.ps1 ตอน build PHP |
แก้เลขเวอร์ชัน PECL ใน versions.json แล้วยังไม่ได้ fetch |
รัน pwsh -NoProfile -File scripts\00-fetch.ps1 -Only pecl-redis (ชื่อตามตัวที่แก้) แล้ว build ใหม่ |
| เปลี่ยนเลข PECL แล้วยังได้ extension รุ่นเก่า (kit เก่า) | สคริปต์รุ่นเก่าคัดลอก PECL ลง build\php-pecl เฉพาะตอนยังไม่มีโฟลเดอร์ |
ใช้ kit ใหม่ที่ลบและคัดลอกใหม่ทุกรอบ หรือลบ build\php-pecl\<ชื่อ> เอง |
แถวเรื่อง lib\plugin, พอร์ตฐานข้อมูล, พาธมีช่องว่างหรือวงเล็บ และ ASLR เป็นอาการที่ผมเจอตอนทดสอบ kit ส่วนข้อความ error ที่ผมไม่ได้ลอกจาก log จริง ผมระบุว่าให้ตรวจอะไรแทน แถวเรื่อง service, copy templates, LAN, sha256 mismatch ของ lock file และ PECL ผมสรุปมาจากการอ่านสคริปต์ ยังไม่เคยเจอกับตัวเอง แถวอื่นเป็นข้อความหรืออาการที่เกิดจริงในรอบทดสอบ หรือเป็นข้อความที่สคริปต์เขียนไว้ตรงตัว
เช็กลิสต์ปิดซีรีส์
ก่อนถือว่า APM ของคุณพร้อมใช้ ตรวจครบทั้งเจ็ดข้อนี้
.\deploy.cmdจบด้วยบรรทัด[OK] APM deployed to C:\APM ... - not startedและมีSyntax OKก่อนหน้า พร้อมบรรทัดhttpd.conf Listen: 127.0.0.1:8080(หรือ0.0.0.0:8080ถ้าคุณตั้งใจเปิด LAN)C:\APM\sslมีapm-ca.crt,apm-ca.key,localhost.crt,localhost.keyและcacert.pemC:\APMมีLICENSE.txt,templates,toolsและtmp\opcacheและC:\APM\mariadb\lib\pluginมีไฟล์.dll- ขั้นที่ 17 ได้
11.8.9-MariaDB-logและ collationutf8mb4_unicode_ci - ขั้นที่ 19 ได้
200 text/html; charset=UTF-8และขั้นที่ 20 ได้200 - หน้าแรกแสดง
8.4.27 cgi-fcgi, OPcache JIT onและเลขเวอร์ชัน MariaDB (เมื่อ MariaDB รันอยู่) - ขั้นที่ 29 ไม่พิมพ์อะไรออกมา หมายถึงหยุดทุกอย่างสะอาดแล้ว และคุณมี
versions.json.bakหรือสำเนาของversions.jsonที่ใช้งานได้ไว้เป็นต้นแบบการอัปเดตครั้งหน้า
ใครอยากได้ APM ไปใช้เลยโดยไม่ต้องคอมไพล์ ผมปล่อยตัวที่ build เสร็จแล้วไว้ที่ github.com/ChonLaWan/APM-Server โหลด APM-1.0.0-win64.zip (ราว 140 MB) จากหน้า Releases เทียบ sha256 กับไฟล์ .sha256 ที่แนบไว้ แตกไว้ที่ไหนก็ได้ แล้วเปิด manager\APMManager.exe ตัว Manager จะเลือกพอร์ต เขียน config ออก cert และสร้างฐานข้อมูลให้เอง มีปุ่ม Check for updates ที่โหลดรุ่นใหม่จาก Releases มาตรวจ sha256 แล้วแทน binary ให้ โดยไม่แตะ config ฐานข้อมูล และเว็บของเรา
สารบัญ · ตอนก่อนหน้า: ตอนที่ 6 build PHP · ตอนถัดไป: ยังไม่มี (ตัวควบคุมแบบ apmctl จะมาในบทความแยก)
ส่วนตัวผมคิดว่าคุณค่าของทั้งซีรีส์อยู่ที่ขั้นตอนอัปเดตมากกว่าขั้นตอน build ครั้งแรก เพราะ build ครั้งแรกทำครั้งเดียว ส่วนครั้งต่อไปที่มี CVE ใหม่ คุณจะแก้ไฟล์เดียวและรอสิบกว่านาที ผมเก็บ versions.json เป็นไฟล์ที่ต้องแตะบ่อยที่สุดไว้ตั้งแต่แรก และจะเขียนเรื่องตัวควบคุม apmctl แยกต่างหากเมื่อมันใช้งานได้จริง เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย