一台帶有一個或多個 /24 IP 段的站群伺服器,可以承載數百個網站,每個網站使用獨立 IP。在控製麵板裡逐個點選幾百次既慢又容易出錯;用幾份模板加一個指令碼,幾分鐘就能完成同樣的工作,而且每個站點的配置完全一致。本文介紹如何規劃域名與 IP 的對應關係、防止站點串到錯誤的 IP 上(串站)、用模板生成 Nginx 虛擬主機、用 acme.sh 批次簽發 SSL 證書,以及檢查每個域名是否都在自己的 IP 上正常響應。
用一個純文本檔案作為唯一的資料來源:每行一個站點,寫明域名和對應的 IP。其他所有配置都由它生成。
mkdir -p /root/cluster
cat > /root/cluster/sites.txt <<'EOF2'
example-a.com 203.0.113.10
example-b.com 203.0.113.11
example-c.net 203.0.113.12
EOF2
# Check that every IP in the list is actually bound on the server
awk '{print $2}' /root/cluster/sites.txt | while read ip; do ip -br addr | grep -qw "$ip" || echo "NOT BOUND: $ip"; done
Nginx 會先根據監聽地址(IP 和埠)選出 server,再匹配主機名;主機名不匹配的請求,會由該地址的預設站點(default server)響應,如果沒有專門指定,就是該地址上配置的第一個站點。在站群上,這意味著有人直接訪問裸 IP,或者把另一個站點的主機名發到這個 IP 時,會看到你的某個站點,從而把這些站點關聯起來。需要在兩處攔截這類請求:一是為尚未部署站點的 IP 設定兜底配置(見下文),二是在模板中為每個 IP 設定預設站點(步驟 3)。
cat > /etc/nginx/conf.d/00-default.conf <<'EOF2'
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
EOF2
# Hide the Nginx version in headers and error pages
sed -i 's/^\s*#\?\s*server_tokens.*/ server_tokens off;/' /etc/nginx/nginx.conf
grep -q 'server_tokens off' /etc/nginx/nginx.conf || sed -i '/http {/a\ server_tokens off;' /etc/nginx/nginx.conf
模板中的佔位符由指令碼按站點逐一替換。每個站點只監聽自己的 IP,該 IP 上任何其他主機名的請求都會被直接斷開。
mkdir -p /etc/nginx/sites-cluster /www/sites
cat > /root/cluster/vhost-http.tpl <<'EOF2'
# Unknown hostnames on this IP: close the connection
server {
listen __IP__:80 default_server;
server_name _;
return 444;
}
server {
listen __IP__:80;
server_name __DOMAIN__ www.__DOMAIN__;
root /www/sites/__DOMAIN__/public;
index index.html index.php;
access_log /www/sites/__DOMAIN__/logs/access.log;
error_log /www/sites/__DOMAIN__/logs/error.log;
location /.well-known/acme-challenge/ { allow all; }
location / { try_files $uri $uri/ =404; }
}
EOF2
# Load the generated files from the http block
grep -q 'sites-cluster' /etc/nginx/nginx.conf || sed -i '/http {/a\ include /etc/nginx/sites-cluster/*.conf;' /etc/nginx/nginx.conf
cat > /root/cluster/deploy.sh <<'EOF2'
#!/bin/bash
set -e
while read -r DOMAIN IP; do
[ -z "$DOMAIN" ] && continue
mkdir -p /www/sites/$DOMAIN/public /www/sites/$DOMAIN/logs
[ -f /www/sites/$DOMAIN/public/index.html ] || echo "<h1>$DOMAIN</h1>" > /www/sites/$DOMAIN/public/index.html
sed -e "s/__DOMAIN__/$DOMAIN/g" -e "s/__IP__/$IP/g" /root/cluster/vhost-http.tpl > /etc/nginx/sites-cluster/$DOMAIN.conf
done < /root/cluster/sites.txt
chown -R www-data:www-data /www/sites 2>/dev/null || chown -R nginx:nginx /www/sites
nginx -t && systemctl reload nginx
EOF2
chmod +x /root/cluster/deploy.sh && /root/cluster/deploy.sh
把各站點的內容上傳到 /www/sites/DOMAIN/public。以後新增站點時,只需在 sites.txt 中追加行,再執行一次指令碼;已有內容不會被覆蓋。
安裝 acme.sh 並將 CA 設定為 Let's Encrypt(參見使用 Certbot 或 acme.sh 申請免費 SSL 證書)。下面的迴圈通過網站根目錄為每個域名簽發證書、安裝證書,並在該站點自己的 IP 上新增 HTTPS server 塊。
cat > /root/cluster/ssl.sh <<'EOF2'
#!/bin/bash
mkdir -p /etc/nginx/ssl
while read -r DOMAIN IP; do
[ -z "$DOMAIN" ] && continue
[ -f /etc/nginx/ssl/$DOMAIN.crt ] && continue # already done
~/.acme.sh/acme.sh --issue -d $DOMAIN -d www.$DOMAIN -w /www/sites/$DOMAIN/public || { echo "FAILED: $DOMAIN"; continue; }
~/.acme.sh/acme.sh --install-cert -d $DOMAIN \
--key-file /etc/nginx/ssl/$DOMAIN.key --fullchain-file /etc/nginx/ssl/$DOMAIN.crt \
--reloadcmd "systemctl reload nginx"
cat >> /etc/nginx/sites-cluster/$DOMAIN.conf <<EOF3
server {
listen $IP:443 ssl default_server;
ssl_reject_handshake on; # unknown hostnames on this IP (Nginx 1.19.4+)
}
server {
listen $IP:443 ssl;
http2 on;
server_name $DOMAIN www.$DOMAIN;
ssl_certificate /etc/nginx/ssl/$DOMAIN.crt;
ssl_certificate_key /etc/nginx/ssl/$DOMAIN.key;
root /www/sites/$DOMAIN/public;
index index.html index.php;
access_log /www/sites/$DOMAIN/logs/access.log;
}
EOF3
sleep 5
done < /root/cluster/sites.txt
nginx -t && systemctl reload nginx
EOF2
chmod +x /root/cluster/ssl.sh && /root/cluster/ssl.sh
建議每批只跑幾十個域名;Let's Encrypt 限制同一帳戶在短時間內申請新證書的數量。DNS 尚未指向正確 IP 的域名會簽發失敗;修正 DNS 後重新執行指令碼即可,已完成的域名會自動跳過。證書續期由 acme.sh 的定時任務(cron)自動完成。
# Each domain must answer on its own IP with HTTP 200 (or 301 if you redirect to HTTPS)
while read -r DOMAIN IP; do
code=$(curl -s -o /dev/null -w '%{http_code}' --resolve $DOMAIN:80:$IP http://$DOMAIN/)
dns=$(dig +short $DOMAIN A | tail -1)
echo "$DOMAIN $IP http=$code dns=$dns $( [ "$dns" = "$IP" ] && echo OK || echo DNS-MISMATCH)"
done < /root/cluster/sites.txt
# The bare IP must not show any site (connection closed = correct)
curl -sI http://203.0.113.10/ || echo "closed as expected"
curl -skI https://203.0.113.10/ || echo "handshake rejected as expected"
可以:寶塔面板可以在站點設定中為每個站點繫結 IP,幾十個站點時完全夠用(參見為每個網站分配獨立 IP)。站點達到幾百個時,模板加指令碼更快、更不容易出錯,資料庫和監控仍然可以交給寶塔管理。
如果站點需要抓取內容或呼叫 API,出站請求預設從伺服器主 IP 發出。如需讓它們使用各自站點的 IP,請參見為每個網站分配獨立 IP 中的出站 IP 部分。
對於 SEO 站群,把站點分散到多個 /24 段(不同 C 段),並讓模板、統計帳戶和託管資訊彼此區分開,比 IP 數量本身更重要。參見多 C 段 IP 的 SEO 站群伺服器。
如果按以上步驟操作後問題仍未解決,請提交工單聯絡 IMIDC 7×24 技術支援,並附上伺服器 IP、系統版本、出問題的域名和 IP,以及 nginx -t 的輸出,方便工程師快速定位。