原创内容,转载请注明出处:https://www.myzhenai.com.cn/post/4932.html
这里的lnmp不是军哥的那个lnmp.org的一键脚本,而是linux下自动安装nginx、mysql、php的一键脚本,w则是自动安装wordpress
install_lnmp_wp.sh
作用:在 Debian/Ubuntu 上以 root 交互安装一套 LNMP + WordPress 一键环境。
流程概要:
特点:set -euo pipefail、步骤失败时 trap 报当前步骤;适合新机器从零搭 WP;不自动 HTTPS(脚本末尾提示自行配 SSL)。
脚本中容易出错的是Mysql的安装,因为Debian12的官方源中已经删除了Mysql8.0的一些版本,还有一些说明是Debian12还没有完全支持Mysql8.0,所以我们只能是通过从Mysql官网下载指定文件包来进行操作。
注:此脚本只能安装amd64架构的,arm我没试过,但好像Mysql8.0的安装包不适用。如果脚本在线不能进行安装,请下载离线包装包放到与脚本同目录下进行安装。
通过网盘分享的文件:lnmpw
链接: https://pan.baidu.com/s/1Fx2wOAHv6i1m_oM373BJvw?pwd=uqrs 提取码: uqrs
–来自百度网盘超级会员v10的分享
安装前,请在脚本collect_inputs()里填入相关信息,否则会安装出错。
#!/usr/bin/env bash
set -euo pipefail
# ===================== 交互输入参数(运行时赋值) =====================
WEB_DIR=""
WEB_USER=""
WEB_GROUP=""
DOMAIN=""
MYSQL_ROOT_PASS=""
MYSQL_DB=""
MYSQL_USER=""
MYSQL_PASS=""
PHP_VERSION=""
WP_MAIN_URL=""
WP_BACKUP_URL=""
AUTO_BACKUP_OLD_MYSQL_DATA=""
# ==================================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
STEP=""
trap 'echo -e "\n${RED}❌ 步骤 ${STEP} 执行失败${NC}"; exit 1' ERR
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}❌ 请用 root 执行${NC}"
exit 1
fi
export DEBIAN_FRONTEND=noninteractive
step() {
STEP="$1"
echo -e "\n${GREEN}===== ${STEP} =====${NC}"
}
prompt_required() {
local var_name="$1"
local prompt_text="$2"
local input
while true; do
read -r -p "${prompt_text}: " input
if [ -n "${input}" ]; then
eval "${var_name}=\"\${input}\""
return
fi
echo -e "${YELLOW}⚠️ 不能为空,请重新输入。${NC}"
done
}
prompt_default() {
local var_name="$1"
local prompt_text="$2"
local default_value="$3"
local input
read -r -p "${prompt_text} [默认: ${default_value}]: " input
input="${input:-$default_value}"
eval "${var_name}=\"\${input}\""
}
prompt_yesno_default() {
local var_name="$1"
local prompt_text="$2"
local default_value="$3"
local input
while true; do
read -r -p "${prompt_text} [y/n, 默认: ${default_value}]: " input
input="${input:-$default_value}"
case "${input}" in
y|Y) eval "${var_name}=1"; return ;;
n|N) eval "${var_name}=0"; return ;;
*) echo -e "${YELLOW}⚠️ 请输入 y 或 n。${NC}" ;;
esac
done
}
collect_inputs() {
step "参数输入"
echo -e "${GREEN}请按提示输入安装参数(回车可使用默认值)${NC}"
prompt_required WEB_DIR "请输入网站路径(例如 /home/wwwroot/example.com)"
prompt_default WEB_USER "请输入网站用户" "www"
prompt_default WEB_GROUP "请输入网站用户组" "www"
prompt_required MYSQL_ROOT_PASS "请输入 MySQL root 密码"
prompt_default MYSQL_USER "请输入 WordPress 数据库用户" "wordpress"
prompt_required MYSQL_PASS "请输入 WordPress 数据库密码"
prompt_default MYSQL_DB "请输入 WordPress 数据库名" "wordpress"
prompt_default PHP_VERSION "请输入 PHP 版本(8.3 或 8.0)" "8.3"
prompt_default WP_MAIN_URL "请输入 WordPress 主下载地址" "https://cn.wordpress.org/latest-zh_CN.tar.gz"
prompt_default WP_BACKUP_URL "请输入 WordPress 备用下载地址" "https://wordpress.org/latest-zh_CN.tar.gz"
prompt_yesno_default AUTO_BACKUP_OLD_MYSQL_DATA "检测到旧 MySQL 数据目录时自动备份并清理" "y"
prompt_default DOMAIN "请输入站点域名(server_name)" "localhost"
if [ "${WEB_DIR#/}" = "${WEB_DIR}" ]; then
echo -e "${RED}❌ 网站路径必须是绝对路径(以 / 开头)${NC}"
exit 1
fi
echo -e "\n${GREEN}参数确认:${NC}"
echo "WEB_DIR=${WEB_DIR}"
echo "WEB_USER=${WEB_USER}"
echo "WEB_GROUP=${WEB_GROUP}"
echo "MYSQL_ROOT_PASS=******"
echo "MYSQL_USER=${MYSQL_USER}"
echo "MYSQL_PASS=******"
echo "MYSQL_DB=${MYSQL_DB}"
echo "PHP_VERSION=${PHP_VERSION}"
echo "WP_MAIN_URL=${WP_MAIN_URL}"
echo "WP_BACKUP_URL=${WP_BACKUP_URL}"
echo "AUTO_BACKUP_OLD_MYSQL_DATA=${AUTO_BACKUP_OLD_MYSQL_DATA}"
echo "DOMAIN=${DOMAIN}"
read -r -p "确认开始安装吗?[y/N]: " confirm
if [[ ! "${confirm}" =~ ^[Yy]$ ]]; then
echo "已取消。"
exit 0
fi
}
# 0) 基础工具
collect_inputs
step "0/9 安装基础依赖"
apt update -y
apt install -y --no-install-recommends \
wget curl ca-certificates lsb-release gnupg2 apt-transport-https \
tar gzip nginx libaio1 libncurses6 libtinfo6 \
gcc g++ make cmake libssl-dev debconf-utils
dpkg --configure -a || true
# 1) 创建 www:www 和站点目录
step "1/9 创建网站用户和目录"
getent group "${WEB_GROUP}" >/dev/null || groupadd -r "${WEB_GROUP}"
id -u "${WEB_USER}" >/dev/null 2>&1 || useradd -r -s /usr/sbin/nologin -M -g "${WEB_GROUP}" "${WEB_USER}"
mkdir -p "${WEB_DIR}"
chown -R "${WEB_USER}:${WEB_GROUP}" "${WEB_DIR}"
chmod 755 "${WEB_DIR}"
# 2) 安装 PHP 8.3(sury)
step "2/9 安装 PHP ${PHP_VERSION}"
curl -fsSL -o /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb
dpkg -i /tmp/debsuryorg-archive-keyring.deb
cat >/etc/apt/sources.list.d/php.sources <<EOF
Types: deb
URIs: https://packages.sury.org/php/
Suites: $(lsb_release -sc)
Components: main
Signed-By: /usr/share/keyrings/debsuryorg-archive-keyring.gpg
EOF
apt update -y
apt install -y --no-install-recommends \
php${PHP_VERSION} php${PHP_VERSION}-fpm php${PHP_VERSION}-mysql \
php${PHP_VERSION}-curl php${PHP_VERSION}-gd php${PHP_VERSION}-mbstring \
php${PHP_VERSION}-xml php${PHP_VERSION}-zip php${PHP_VERSION}-opcache
# PHP-FPM 用 www:www(LNMP 风格)
PHP_POOL="/etc/php/${PHP_VERSION}/fpm/pool.d/www.conf"
sed -i "s/^user = .*/user = ${WEB_USER}/" "${PHP_POOL}"
sed -i "s/^group = .*/group = ${WEB_GROUP}/" "${PHP_POOL}"
sed -i "s/^listen.owner = .*/listen.owner = ${WEB_USER}/" "${PHP_POOL}"
sed -i "s/^listen.group = .*/listen.group = ${WEB_GROUP}/" "${PHP_POOL}"
grep -q "^listen.mode" "${PHP_POOL}" \
&& sed -i "s/^listen.mode = .*/listen.mode = 0660/" "${PHP_POOL}" \
|| echo "listen.mode = 0660" >> "${PHP_POOL}"
systemctl restart "php${PHP_VERSION}-fpm"
systemctl enable "php${PHP_VERSION}-fpm"
# 3) 安装 MySQL 8.0.45(官方 deb bundle)
step "3/9 安装 MySQL 8.0.45"
# 旧数据目录处理(避免交互弹窗)
if [[ -d /var/lib/mysql && -n "$(ls -A /var/lib/mysql 2>/dev/null || true)" ]]; then
if [[ "${AUTO_BACKUP_OLD_MYSQL_DATA}" == "1" ]]; then
BAK="/root/mysql-data-backup-$(date +%Y%m%d-%H%M%S).tar.gz"
echo -e "${YELLOW}⚠️ 检测到旧 /var/lib/mysql,备份到 ${BAK}${NC}"
tar -zcf "${BAK}" -C / var/lib/mysql
fi
systemctl stop mysql >/dev/null 2>&1 || true
rm -rf /var/lib/mysql
fi
cd /tmp
MYSQL_BUNDLE_URL="https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-server_8.0.45-1debian12_amd64.deb-bundle.tar"
wget -O mysql-bundle.tar "${MYSQL_BUNDLE_URL}" --no-check-certificate
tar -xf mysql-bundle.tar
apt install -y --no-install-recommends libmecab2
# 非交互预置
echo "mysql-community-server mysql-community-server/root-pass password ${MYSQL_ROOT_PASS}" | debconf-set-selections
echo "mysql-community-server mysql-community-server/re-root-pass password ${MYSQL_ROOT_PASS}" | debconf-set-selections
echo "mysql-community-server mysql-community-server/default-auth-override select Use Legacy Authentication Method (Retain MySQL 5.x Compatibility)" | debconf-set-selections
install_deb_if_exists() {
local p="$1"
local d
d=$(ls -1 ${p} 2>/dev/null | head -n1 || true)
[[ -n "${d}" ]] && dpkg -i "${d}" || true
}
# 兼容 mysql-* / mysql-community-* 两种命名
install_deb_if_exists "mysql-common_*.deb"
install_deb_if_exists "mysql-community-client-plugins_*.deb"
install_deb_if_exists "mysql-community-client-core_*.deb"
install_deb_if_exists "mysql-community-client_*.deb"
install_deb_if_exists "mysql-client-core_*.deb"
install_deb_if_exists "mysql-client_*.deb"
install_deb_if_exists "mysql-community-server-core_*.deb"
install_deb_if_exists "mysql-community-server_*.deb"
install_deb_if_exists "mysql-server-core_*.deb"
install_deb_if_exists "mysql-server_*.deb"
apt -f install -y --no-install-recommends
getent group mysql >/dev/null || groupadd -r mysql
id -u mysql >/dev/null 2>&1 || useradd -r -s /usr/sbin/nologin -M -g mysql mysql
mkdir -p /var/lib/mysql /run/mysqld /var/log/mysql
chown -R mysql:mysql /var/lib/mysql /run/mysqld /var/log/mysql
chmod 750 /var/lib/mysql
chmod 755 /run/mysqld
# systemd 兜底,避免“ready了却被超时杀掉”
mkdir -p /etc/systemd/system/mysql.service.d
cat >/etc/systemd/system/mysql.service.d/override.conf <<EOF
[Service]
Type=simple
TimeoutStartSec=0
PIDFile=
EOF
systemctl daemon-reload
systemctl stop mysql >/dev/null 2>&1 || true
pkill -9 mysqld_safe >/dev/null 2>&1 || true
pkill -9 mysqld >/dev/null 2>&1 || true
systemctl start mysql
systemctl enable mysql
# root/业务库/业务用户
mysql -uroot -p"${MYSQL_ROOT_PASS}" <<EOF
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '${MYSQL_ROOT_PASS}';
CREATE DATABASE IF NOT EXISTS ${MYSQL_DB} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '${MYSQL_USER}'@'localhost' IDENTIFIED WITH mysql_native_password BY '${MYSQL_PASS}';
ALTER USER '${MYSQL_USER}'@'localhost' IDENTIFIED WITH mysql_native_password BY '${MYSQL_PASS}';
CREATE USER IF NOT EXISTS '${MYSQL_USER}'@'127.0.0.1' IDENTIFIED WITH mysql_native_password BY '${MYSQL_PASS}';
ALTER USER '${MYSQL_USER}'@'127.0.0.1' IDENTIFIED WITH mysql_native_password BY '${MYSQL_PASS}';
GRANT ALL PRIVILEGES ON ${MYSQL_DB}.* TO '${MYSQL_USER}'@'localhost';
GRANT ALL PRIVILEGES ON ${MYSQL_DB}.* TO '${MYSQL_USER}'@'127.0.0.1';
FLUSH PRIVILEGES;
EOF
# 4) 配置 Nginx(worker 也改为 www)
step "4/9 配置 Nginx"
if grep -qE '^user\s+' /etc/nginx/nginx.conf; then
sed -i "s/^user .*/user ${WEB_USER};/" /etc/nginx/nginx.conf
else
sed -i "1i user ${WEB_USER};" /etc/nginx/nginx.conf
fi
rm -f /etc/nginx/sites-enabled/default
SITE_NAME="${DOMAIN}"
cat >"/etc/nginx/sites-available/${SITE_NAME}" <<EOF
server {
listen 80;
listen [::]:80;
server_name ${DOMAIN};
root ${WEB_DIR};
index index.php index.html index.htm;
access_log /var/log/nginx/${SITE_NAME}.access.log;
error_log /var/log/nginx/${SITE_NAME}.error.log;
location / {
try_files \$uri \$uri/ /index.php?\$args;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php${PHP_VERSION}-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}
EOF
ln -sf "/etc/nginx/sites-available/${SITE_NAME}" "/etc/nginx/sites-enabled/${SITE_NAME}"
nginx -t
systemctl restart nginx
systemctl enable nginx
# 5) 安装 WordPress
step "5/9 安装 WordPress"
cd /tmp
if ! wget -O wp.tar.gz "${WP_MAIN_URL}" --no-check-certificate; then
wget -O wp.tar.gz "${WP_BACKUP_URL}" --no-check-certificate
fi
rm -rf /tmp/wordpress
tar -zxf wp.tar.gz -C /tmp
cp -a /tmp/wordpress/. "${WEB_DIR}/"
# 用官方 sample 生成配置
cp -f "${WEB_DIR}/wp-config-sample.php" "${WEB_DIR}/wp-config.php"
sed -i "s/database_name_here/${MYSQL_DB}/" "${WEB_DIR}/wp-config.php"
sed -i "s/username_here/${MYSQL_USER}/" "${WEB_DIR}/wp-config.php"
sed -i "s/password_here/${MYSQL_PASS}/" "${WEB_DIR}/wp-config.php"
sed -i "s/localhost/127.0.0.1/" "${WEB_DIR}/wp-config.php"
chown -R "${WEB_USER}:${WEB_GROUP}" "${WEB_DIR}"
find "${WEB_DIR}" -type d -exec chmod 755 {} \;
find "${WEB_DIR}" -type f -exec chmod 644 {} \;
chmod 600 "${WEB_DIR}/wp-config.php"
# 6) 验证
step "6/9 服务验证"
systemctl is-active --quiet nginx || (echo "nginx 未运行" && exit 1)
systemctl is-active --quiet "php${PHP_VERSION}-fpm" || (echo "php-fpm 未运行" && exit 1)
systemctl is-active --quiet mysql || (echo "mysql 未运行" && exit 1)
mysql -u"${MYSQL_USER}" -p"${MYSQL_PASS}" -h127.0.0.1 -e "SELECT 1;" >/dev/null
# 7) 完成信息
step "7/9 安装完成"
echo -e "${GREEN}✅ LNMP + WordPress 安装完成${NC}"
echo "网站目录: ${WEB_DIR}"
echo "运行用户: ${WEB_USER}:${WEB_GROUP}"
echo "MySQL root 密码: ${MYSQL_ROOT_PASS}"
echo "WordPress DB: ${MYSQL_DB}"
echo "WordPress User/Pass: ${MYSQL_USER} / ${MYSQL_PASS}"
echo "访问地址: http://${DOMAIN}"
# 8) 可选测试页
step "8/9 生成 info.php(可选)"
echo "<?php phpinfo();" > "${WEB_DIR}/info.php"
chown "${WEB_USER}:${WEB_GROUP}" "${WEB_DIR}/info.php"
chmod 644 "${WEB_DIR}/info.php"
echo "测试页: http://${DOMAIN}/info.php (确认后建议删除)"
# 9) 结束
step "9/9 结束"
echo -e "${YELLOW}提示:上线前请删除 info.php,配置 HTTPS。${NC}"
setup_ssl_lnmp.sh
这个脚本是使用acme.sh安装ssl证书的一键脚本,在安装脚本前,需要输入域名、网站目录等信息,可以重复输入,如果不需要重复输入,请在输入完一个域名和网站目录后,输入n跳出这一步
#!/usr/bin/env bash
set -euo pipefail
# 交互式 SSL 安装脚本(Debian + Nginx + acme.sh)
# 规则:
# 1) 不新建任何站点配置文件
# 2) 80/443 必须在同一个 server 段
# 3) 只在现有配置中补 listen 443 与 SSL 项
if [[ $EUID -ne 0 ]]; then
echo "请使用 root 执行。"
exit 1
fi
if ! command -v nginx >/dev/null 2>&1; then
echo "未检测到 nginx 命令,请先安装 Nginx。"
exit 1
fi
LE_EMAIL_DEFAULT="admin@example.com"
read -r -p "请输入用于申请证书的邮箱 [${LE_EMAIL_DEFAULT}]: " LE_EMAIL
LE_EMAIL="${LE_EMAIL:-$LE_EMAIL_DEFAULT}"
declare -a DOMAINS=()
declare -A WEBROOTS=()
declare -A EXTRA_NAMES=()
echo
echo "========== 域名录入 =========="
while true; do
read -r -p "请输入主域名(如 www.example.com): " domain
domain="$(echo "$domain" | xargs)"
[[ -z "$domain" ]] && { echo "域名不能为空。"; continue; }
read -r -p "请输入 ${domain} 的站点路径(如 /home/wwwroot/www.example.com): " webroot
webroot="$(echo "$webroot" | xargs)"
[[ -z "$webroot" ]] && { echo "路径不能为空。"; continue; }
read -r -p "可选:请输入该主域名的附加域名(逗号分隔,如 www.example.com;留空跳过): " extra
extra="$(echo "$extra" | xargs)"
DOMAINS+=("$domain")
WEBROOTS["$domain"]="$webroot"
EXTRA_NAMES["$domain"]="$extra"
echo "已添加:$domain -> $webroot ; 附加域名: ${extra:-无}"
read -r -p "继续输入?(y 继续 / n 结束并开始安装): " more
case "${more,,}" in
y|yes) ;;
n|no) break ;;
*) echo "输入不识别,默认继续。" ;;
esac
done
[[ ${#DOMAINS[@]} -eq 0 ]] && { echo "未录入任何域名,脚本结束。"; exit 1; }
echo
echo "========== 录入确认 =========="
for d in "${DOMAINS[@]}"; do
echo "- ${d} -> ${WEBROOTS[$d]} ; 附加: ${EXTRA_NAMES[$d]:-无}"
done
read -r -p "确认开始安装?(y/n): " go
if [[ "${go,,}" != "y" && "${go,,}" != "yes" ]]; then
echo "已取消。"
exit 0
fi
echo
echo ">>> 自动检测 Nginx 配置目录"
if [[ -d /usr/local/nginx/conf/vhost ]]; then
VHOST_DIR="/usr/local/nginx/conf/vhost"
elif [[ -d /etc/nginx/sites-enabled ]]; then
VHOST_DIR="/etc/nginx/sites-enabled"
elif [[ -d /etc/nginx/conf.d ]]; then
VHOST_DIR="/etc/nginx/conf.d"
else
echo "未找到常见 Nginx 站点目录。"
exit 1
fi
echo "检测结果:${VHOST_DIR}"
echo
echo ">>> 安装依赖"
apt-get update -y
apt-get install -y curl socat cron ca-certificates openssl
echo
echo ">>> 安装 acme.sh(gitee raw)"
mkdir -p /root/.acme.sh
if [[ ! -x /root/.acme.sh/acme.sh ]]; then
curl --connect-timeout 10 --max-time 120 -fsSL https://gitee.com/neilpang/acme.sh/raw/master/acme.sh -o /root/.acme.sh/acme.sh
chmod +x /root/.acme.sh/acme.sh
fi
ACME="/root/.acme.sh/acme.sh"
"${ACME}" --home /root/.acme.sh --config-home /root/.acme.sh --register-account -m "${LE_EMAIL}" || true
"${ACME}" --home /root/.acme.sh --config-home /root/.acme.sh --set-default-ca --server letsencrypt
mkdir -p /etc/nginx/ssl
for d in "${DOMAINS[@]}"; do
webroot="${WEBROOTS[$d]}"
extra="${EXTRA_NAMES[$d]}"
if [[ ! -d "${webroot}" ]]; then
echo "跳过 ${d}:站点路径不存在 -> ${webroot}"
continue
fi
conf_file="$(grep -Ril -E "server_name[[:space:]].*\\b${d}\\b" "${VHOST_DIR}" 2>/dev/null | head -n1 || true)"
if [[ -z "${conf_file}" ]]; then
echo "未找到 ${d} 的现有配置文件,按规则不新建,已跳过。"
continue
fi
echo
echo ">>> 处理域名:${d}"
echo "配置文件:${conf_file}"
# 1) 签发/续签(证书未到期时会提示 skipping,继续流程)
issue_cmd=( "${ACME}" --home /root/.acme.sh --config-home /root/.acme.sh --issue -d "${d}" -w "${webroot}" --keylength ec-256 )
if [[ -n "${extra}" ]]; then
IFS=',' read -ra arr <<< "${extra}"
for n in "${arr[@]}"; do
n="$(echo "$n" | xargs)"
[[ -n "$n" ]] && issue_cmd+=( -d "$n" )
done
fi
set +e
issue_out="$("${issue_cmd[@]}" 2>&1)"
issue_rc=$?
set -e
echo "${issue_out}"
if [[ ${issue_rc} -ne 0 ]] && ! echo "${issue_out}" | grep -q "Skipping. Next renewal time is"; then
echo "签发失败:${d}"
exit 1
fi
# 2) 安装证书到固定路径
mkdir -p "/etc/nginx/ssl/${d}"
"${ACME}" --home /root/.acme.sh --config-home /root/.acme.sh --install-cert -d "${d}" --ecc \
--fullchain-file "/etc/nginx/ssl/${d}/fullchain.cer" \
--key-file "/etc/nginx/ssl/${d}/${d}.key"
# 3) 防跨站文件
user_ini="${webroot}/.user.ini"
if [[ ! -f "${user_ini}" ]]; then
cat > "${user_ini}" <<EOF
open_basedir=${webroot}/:/tmp/:/proc/
EOF
chown www:www "${user_ini}" || true
chmod 644 "${user_ini}" || true
echo "已创建 ${user_ini}"
fi
# 4) 同一个 server 段补 443 与 SSL 项(不新建 server 块)
cp -a "${conf_file}" "${conf_file}.bak.$(date +%F-%H%M%S)"
if grep -Eq "listen[[:space:]]+443([[:space:]]|;)" "${conf_file}"; then
echo "已存在 listen 443,跳过监听项。"
else
perl -0777 -i -pe 's/(listen\s+80[^\n;]*;)/$1 . "\n listen 443 ssl http2;"/se' "${conf_file}"
echo "已补 listen 443(同一 server 段)。"
fi
if grep -q "ssl_certificate /etc/nginx/ssl/${d}/fullchain.cer;" "${conf_file}"; then
echo "已存在该域名证书配置,跳过。"
else
NB_DOMAIN="${d}" perl -0777 -i -pe '
my $domain = $ENV{"NB_DOMAIN"};
my $q = quotemeta($domain);
s/(server_name\s+[^;]*\b$q\b[^;]*;)/
$1
. "\n ssl_certificate /etc/nginx/ssl/$domain/fullchain.cer;"
. "\n ssl_certificate_key /etc/nginx/ssl/$domain/$domain.key;"
. "\n ssl_protocols TLSv1.2 TLSv1.3;"
. "\n ssl_ciphers HIGH:!aNULL:!MD5;"
/se;
' "${conf_file}"
echo "已补 SSL 证书配置(同一 server 段)。"
fi
# 5) 证书域名匹配检查(主域 + 附加域)
cert_file="/etc/nginx/ssl/${d}/fullchain.cer"
if [[ -f "${cert_file}" ]]; then
san="$(openssl x509 -in "${cert_file}" -noout -ext subjectAltName 2>/dev/null || true)"
if ! echo "${san}" | grep -q "DNS:${d}"; then
echo "警告:证书 SAN 不含主域 ${d},请检查签发参数。"
fi
if [[ -n "${extra}" ]]; then
IFS=',' read -ra arr2 <<< "${extra}"
for n in "${arr2[@]}"; do
n="$(echo "$n" | xargs)"
[[ -z "$n" ]] && continue
if ! echo "${san}" | grep -q "DNS:${n}"; then
echo "警告:证书 SAN 不含附加域 ${n}。"
fi
done
fi
fi
# 6) PHP 规则检查(仅提示,不擅改业务)
if ! grep -Eq "fastcgi_pass|enable-php|location[[:space:]]+~[[:space:]]+\\\.php" "${conf_file}"; then
echo "警告:${conf_file} 未检测到 PHP 处理规则,HTTPS 可能出现下载 PHP。"
fi
done
echo
echo ">>> 生成续期子脚本:/root/renew_ssl_lnmp.sh"
{
echo '#!/usr/bin/env bash'
echo 'set -euo pipefail'
echo
echo 'ACME="/root/.acme.sh/acme.sh"'
echo 'HOME_DIR="/root/.acme.sh"'
echo 'NOW_MONTH="$(date +%Y-%m)"'
echo 'NEED_RELOAD=0'
echo
echo 'renew_if_expire_this_month() {'
echo ' local domain="$1"'
echo ' local cert_file="$2"'
echo ' local fullchain="$3"'
echo ' local keyfile="$4"'
echo
echo ' if [[ ! -f "$cert_file" ]]; then'
echo ' echo "[$domain] 未找到本地证书文件,尝试续签并安装。"'
echo ' else'
echo ' local end_raw end_month'
echo ' end_raw="$(openssl x509 -enddate -noout -in "$cert_file" | cut -d= -f2)"'
echo ' end_month="$(date -d "$end_raw" +%Y-%m)"'
echo ' echo "[$domain] 当前到期时间: $end_raw"'
echo ' if [[ "$end_month" != "$NOW_MONTH" ]]; then'
echo ' echo "[$domain] 本月不到期,跳过续签。"'
echo ' return 0'
echo ' fi'
echo ' echo "[$domain] 本月到期,开始续签。"'
echo ' fi'
echo
echo ' "$ACME" --home "$HOME_DIR" --config-home "$HOME_DIR" --renew -d "$domain" --ecc || true'
echo ' "$ACME" --home "$HOME_DIR" --config-home "$HOME_DIR" --install-cert -d "$domain" --ecc \'
echo ' --fullchain-file "$fullchain" \'
echo ' --key-file "$keyfile"'
echo ' NEED_RELOAD=1'
echo '}'
echo
for d in "${DOMAINS[@]}"; do
echo "renew_if_expire_this_month \"${d}\" \"/etc/nginx/ssl/${d}/fullchain.cer\" \"/etc/nginx/ssl/${d}/fullchain.cer\" \"/etc/nginx/ssl/${d}/${d}.key\""
done
echo
echo 'if [[ "$NEED_RELOAD" -eq 1 ]]; then'
echo ' nginx -t'
echo ' systemctl reload nginx'
echo ' echo "检测到本月到期证书,已续签并重载 Nginx。"'
echo 'else'
echo ' echo "本月无到期证书,无需续签。"'
echo 'fi'
} > /root/renew_ssl_lnmp.sh
chmod +x /root/renew_ssl_lnmp.sh
echo "已生成 /root/renew_ssl_lnmp.sh"
echo "可选添加定时任务:"
echo '(crontab -l 2>/dev/null; echo "0 3 1 * * /root/renew_ssl_lnmp.sh >> /var/log/renew_ssl_lnmp.log 2>&1") | crontab -'
echo
echo ">>> 校验 Nginx 配置"
nginx_test_out="$(nginx -t 2>&1 || true)"
echo "${nginx_test_out}"
if echo "${nginx_test_out}" | grep -q "conflicting server name"; then
echo "检测到 server_name 冲突,已停止,不执行 reload。请先清理冲突。"
exit 1
fi
if ! echo "${nginx_test_out}" | grep -q "test is successful"; then
echo "nginx -t 未通过,已停止。"
exit 1
fi
systemctl reload nginx
echo
echo "全部完成。"
renew_ssl_lnmp.sh
这是自动续签ssl证书的脚本,会自动匹配证书安装目录、acme.sh安装目录,自动检测证书到期时间。
sudo crontab -e
在cron里新增一项计划任务,假设你的脚本安装目录在 /usr/local/bin/renew_ssl_lnmp.sh
0 0 1 * * /usr/local/bin/renew_ssl_lnmp.sh >> /var/log/renew_ssl_lnmp.log 2>&1
每个月的1日的零点零分开始执行
#!/usr/bin/env bash
# =============================================================================
# renew_ssl_lnmp.sh — 通用 acme.sh 证书续期并写回 Nginx 使用的路径
#
# 自动适配常见环境:
# • acme.sh 安装位置:/root/.acme.sh、当前用户 ~/.acme.sh、PATH 中的 acme.sh 等
# • 证书部署目录(可多选合并扫描):
# - setup_ssl_lnmp.sh / 常见 Debian:/etc/nginx/ssl/<域名>/
# - LNMP 一键包常见:/usr/local/nginx/conf/ssl/<域名>/
# 单域名下识别 fullchain.cer | fullchain.pem | fullchain.crt,
# 私钥 <域名>.key | privkey.pem | key.pem
#
# 需 root 执行(默认 reload nginx)。
#
# 用法:
# sudo ./renew_ssl_lnmp.sh
# sudo SSL_ROOTS="/etc/nginx/ssl /usr/local/nginx/conf/ssl" ./renew_ssl_lnmp.sh
#
# 环境变量(可选):
# SSL_ROOT 仅使用此证书根(与旧版兼容;设置后不再自动追加其它默认路径)
# SSL_ROOTS 空格分隔多个证书根,覆盖自动列表
# ACME_HOME acme.sh 数据目录(默认自动探测)
# ACME_BIN acme.sh 可执行文件(默认自动探测)
# RENEW_ECC auto(默认)| ecc | rsa — 续签时是否带 --ecc(auto 按目录判断)
# RENEW_POLICY acme(默认)| monthly
# NGINX_RELOAD 1(默认)| 0
# STRICT 1| 0(默认)
#
# Cron 示例:
# 0 3 * * * /usr/local/bin/renew_ssl_lnmp.sh >> /var/log/renew_ssl_lnmp.log 2>&1
# =============================================================================
set -euo pipefail
RENEW_POLICY="${RENEW_POLICY:-acme}"
RENEW_ECC="${RENEW_ECC:-auto}"
NGINX_RELOAD="${NGINX_RELOAD:-1}"
STRICT="${STRICT:-0}"
DRY_RUN=0
# 未设置 SSL_ROOT / SSL_ROOTS 时扫描这些目录(存在的才会参与)
DEFAULT_SSL_ROOTS="/etc/nginx/ssl /usr/local/nginx/conf/ssl"
usage() {
cat <<'EOF'
renew_ssl_lnmp.sh — acme.sh 续期并写回 Nginx 证书路径(多证书根、多 acme 路径自动探测)
用法:
sudo ./renew_ssl_lnmp.sh [选项]
选项:
-h, --help 显示本说明
-n, --dry-run 列出域名与解析到的证书路径,不执行续签
--ssl-root 只使用这一证书根(等同 SSL_ROOT)
--ssl-roots 空格分隔多个根目录(等同 SSL_ROOTS)
--acme-home 指定 acme.sh 数据目录
--acme-bin 指定 acme.sh 可执行文件
--policy acme | monthly
环境变量: SSL_ROOT, SSL_ROOTS, ACME_HOME, ACME_BIN, RENEW_ECC, RENEW_POLICY,
NGINX_RELOAD, STRICT(详见脚本头部注释)。
EOF
}
log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*"; }
warn() { printf '[%s] WARN: %s\n' "$(date '+%F %T')" "$*" >&2; }
die() { printf '[%s] ERROR: %s\n' "$(date '+%F %T')" "$*" >&2; exit 1; }
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
die "请使用 root 执行(或 sudo)。"
fi
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-n|--dry-run) DRY_RUN=1; shift ;;
--ssl-root) SSL_ROOT="${2:?}"; shift 2 ;;
--ssl-roots) SSL_ROOTS="${2:?}"; shift 2 ;;
--acme-home) ACME_HOME="${2:?}"; shift 2 ;;
--acme-bin) ACME_BIN="${2:?}"; shift 2 ;;
--policy)
RENEW_POLICY="${2:?}"
[[ "${RENEW_POLICY}" == acme || "${RENEW_POLICY}" == monthly ]] || die "--policy 只能是 acme 或 monthly"
shift 2
;;
*) die "未知参数: $1 (使用 --help)" ;;
esac
done
command -v openssl >/dev/null 2>&1 || die "未找到 openssl 命令。"
# ---------- acme.sh 探测 ----------
# 官方布局:数据目录与可执行脚本在同一目录,如 /root/.acme.sh/acme.sh
auto_detect_acme() {
if [[ -n "${ACME_HOME:-}" && -z "${ACME_BIN:-}" && -x "${ACME_HOME}/acme.sh" ]]; then
ACME_BIN="${ACME_HOME}/acme.sh"
return 0
fi
if [[ -n "${ACME_BIN:-}" && -x "${ACME_BIN}" ]]; then
if [[ -z "${ACME_HOME:-}" ]]; then
ACME_HOME="$(cd "$(dirname "${ACME_BIN}")" && pwd)"
fi
return 0
fi
local c
for c in "/root/.acme.sh/acme.sh" "${HOME}/.acme.sh/acme.sh"; do
if [[ -x "$c" ]]; then
ACME_BIN="$c"
ACME_HOME="$(cd "$(dirname "$c")" && pwd)"
return 0
fi
done
if c="$(command -v acme.sh 2>/dev/null || true)"; [[ -n "$c" && -x "$c" ]]; then
ACME_BIN="$c"
if [[ -z "${ACME_HOME:-}" ]]; then
if [[ -d "/root/.acme.sh" ]]; then
ACME_HOME="/root/.acme.sh"
else
ACME_HOME="$(cd "$(dirname "$c")" && pwd)"
fi
fi
return 0
fi
return 1
}
ACME_BIN="${ACME_BIN:-}"
ACME_HOME="${ACME_HOME:-}"
if ! auto_detect_acme; then
die "未找到 acme.sh。请安装或设置 ACME_BIN / ACME_HOME。"
fi
[[ -d "${ACME_HOME}" ]] || die "ACME_HOME 不是目录: ${ACME_HOME}"
# ---------- 证书根列表 ----------
collect_ssl_roots() {
local r roots=()
if [[ -n "${SSL_ROOT:-}" ]]; then
printf '%s\n' "${SSL_ROOT}"
return
fi
if [[ -n "${SSL_ROOTS:-}" ]]; then
for r in ${SSL_ROOTS}; do
[[ -n "$r" && -d "$r" ]] && printf '%s\n' "$r"
done
return
fi
for r in ${DEFAULT_SSL_ROOTS}; do
[[ -d "$r" ]] && printf '%s\n' "$r"
done
}
mapfile -t SSL_ROOT_LIST < <(collect_ssl_roots | sort -u)
if [[ ${#SSL_ROOT_LIST[@]} -eq 0 ]]; then
die "未发现证书根目录。请创建 /etc/nginx/ssl 或 /usr/local/nginx/conf/ssl,或设置 SSL_ROOT / SSL_ROOTS。"
fi
# 在某一 ssl_root/<domain>/ 下解析 fullchain + key
resolve_cert_paths_in_root() {
local domain="$1"
local base="$2"
local fullchain="" keyfile=""
if [[ -f "${base}/fullchain.cer" ]]; then fullchain="${base}/fullchain.cer"
elif [[ -f "${base}/fullchain.pem" ]]; then fullchain="${base}/fullchain.pem"
elif [[ -f "${base}/fullchain.crt" ]]; then fullchain="${base}/fullchain.crt"
fi
if [[ -f "${base}/${domain}.key" ]]; then keyfile="${base}/${domain}.key"
elif [[ -f "${base}/privkey.pem" ]]; then keyfile="${base}/privkey.pem"
elif [[ -f "${base}/key.pem" ]]; then keyfile="${base}/key.pem"
fi
printf '%s\t%s\n' "$fullchain" "$keyfile"
}
# 返回:fullchain<TAB>keyfile<TAB>ssl_root(选用第一个同时存在链与钥的根)
resolve_cert_paths() {
local domain="$1"
local root base p fc k
for root in "${SSL_ROOT_LIST[@]}"; do
base="${root}/${domain}"
[[ -d "$base" ]] || continue
p="$(resolve_cert_paths_in_root "$domain" "$base")"
fc="$(printf '%s' "$p" | cut -f1)"
k="$(printf '%s' "$p" | cut -f2)"
if [[ -n "$fc" && -n "$k" ]]; then
printf '%s\t%s\t%s\n' "$fc" "$k" "$root"
return 0
fi
done
printf '\t\t\n'
return 1
}
domain_has_partial_ssl_dir() {
local domain="$1"
local root base p fc k
for root in "${SSL_ROOT_LIST[@]}"; do
base="${root}/${domain}"
[[ -d "$base" ]] || continue
p="$(resolve_cert_paths_in_root "$domain" "$base")"
fc="$(printf '%s' "$p" | cut -f1)"
k="$(printf '%s' "$p" | cut -f2)"
if [[ -n "$fc" || -n "$k" ]]; then
return 0
fi
done
return 1
}
discover_domains_from_ssl_roots() {
local root d domain
for root in "${SSL_ROOT_LIST[@]}"; do
for d in "${root}"/*; do
[[ -e "$d" ]] || continue
[[ -d "$d" ]] || continue
domain="$(basename "$d")"
[[ -n "$domain" && "$domain" != "*" ]] || continue
domain_has_partial_ssl_dir "$domain" && printf '%s\n' "$domain"
done
done
}
discover_domains_from_acme_list() {
"${ACME_BIN}" --home "${ACME_HOME}" --config-home "${ACME_HOME}" --list 2>/dev/null \
| awk 'NR>1 && $1!="" && $1!="No" { print $1 }' | sort -u
}
merge_domain_lists() {
local tmp d
tmp="$(mktemp)"
discover_domains_from_ssl_roots >>"$tmp"
while IFS= read -r d; do
[[ -z "$d" ]] && continue
domain_has_partial_ssl_dir "$d" && printf '%s\n' "$d"
done < <(discover_domains_from_acme_list) >>"$tmp" 2>/dev/null || true
sort -u "$tmp"
rm -f "$tmp"
}
# --ecc:与签发时一致;LNMP / setup_ssl 多为 ECC
ecc_args_for_domain() {
local domain="$1"
case "${RENEW_ECC}" in
ecc) printf '%s\n' --ecc; return ;;
rsa) return ;;
auto)
if [[ -d "${ACME_HOME}/${domain}_ecc" ]]; then
printf '%s\n' --ecc
return
fi
if [[ -d "${ACME_HOME}/${domain}" ]]; then
return
fi
printf '%s\n' --ecc
;;
*) die "RENEW_ECC 只能是 auto/ecc/rsa" ;;
esac
}
should_try_renew_monthly() {
local cert_file="$1"
local domain="$2"
local now_month
now_month="$(date +%Y-%m)"
if [[ ! -f "$cert_file" ]]; then
log "[$domain] 未找到本地证书文件,将尝试续签。"
return 0
fi
local end_raw end_month
end_raw="$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2)"
if [[ -z "$end_raw" ]]; then
warn "[$domain] 无法读取证书到期时间,将尝试续签。"
return 0
fi
end_month="$(date -d "$end_raw" +%Y-%m 2>/dev/null || true)"
log "[$domain] 当前到期时间: $end_raw"
if [[ "$end_month" != "$now_month" ]]; then
log "[$domain](monthly 策略)非本月到期,跳过。"
return 1
fi
log "[$domain](monthly 策略)本月到期,将续签。"
return 0
}
reload_nginx_if_needed() {
if [[ "${NGINX_RELOAD}" != "1" ]]; then
log "已跳过 Nginx reload(NGINX_RELOAD=${NGINX_RELOAD})。"
return 0
fi
if ! command -v nginx >/dev/null 2>&1; then
warn "未找到 nginx 命令,跳过 reload。"
return 0
fi
nginx -t
systemctl reload nginx 2>/dev/null || service nginx reload 2>/dev/null || nginx -s reload
log "Nginx 已重载。"
}
NEED_RELOAD=0
FAIL=0
mapfile -t DOMAINS < <(merge_domain_lists)
if [[ ${#DOMAINS[@]} -eq 0 ]]; then
warn "未发现可处理域名。"
warn "请在证书根下创建 <域名> 目录,并放置 fullchain(.cer/.pem)与私钥(域名.key 或 privkey.pem)。"
warn "当前扫描的证书根: ${SSL_ROOT_LIST[*]}"
exit 1
fi
log "SSL_ROOTS=${SSL_ROOT_LIST[*]} ACME_HOME=${ACME_HOME} ACME_BIN=${ACME_BIN} RENEW_POLICY=${RENEW_POLICY} RENEW_ECC=${RENEW_ECC}"
log "自动识别 ${#DOMAINS[@]} 个域名: ${DOMAINS[*]}"
for domain in "${DOMAINS[@]}"; do
line="$(resolve_cert_paths "$domain" || true)"
fullchain="$(printf '%s' "$line" | cut -f1)"
keyfile="$(printf '%s' "$line" | cut -f2)"
ssl_root_used="$(printf '%s' "$line" | cut -f3)"
if [[ -z "$fullchain" || -z "$keyfile" ]]; then
warn "[$domain] 在证书根下未同时找到 fullchain 与私钥,跳过。(已扫描: ${SSL_ROOT_LIST[*]})"
FAIL=1
continue
fi
mapfile -t ECC_ARG < <(ecc_args_for_domain "$domain")
if [[ "${RENEW_POLICY}" == monthly ]]; then
should_try_renew_monthly "$fullchain" "$domain" || continue
fi
if [[ "$DRY_RUN" -eq 1 ]]; then
log "[$domain] dry-run: ssl_root=${ssl_root_used} ecc_arg=[${ECC_ARG[*]}] fullchain=$fullchain key=$keyfile"
continue
fi
set +e
renew_out="$("${ACME_BIN}" --home "${ACME_HOME}" --config-home "${ACME_HOME}" --renew -d "${domain}" "${ECC_ARG[@]:-}" 2>&1)"
renew_rc=$?
set -e
printf '%s\n' "$renew_out"
if echo "$renew_out" | grep -qiE 'Skipping\.|Cert not yet due|not due for renewal'; then
log "[$domain] acme.sh:尚未到续期窗口,跳过部署。"
continue
fi
if [[ $renew_rc -ne 0 ]] && ! echo "$renew_out" | grep -qiE 'success|renewed|Renew'; then
warn "[$domain] acme.sh --renew 失败(退出码 $renew_rc)。若证书为 RSA,可尝试 RENEW_ECC=rsa。"
FAIL=1
[[ "${STRICT}" == "1" ]] && die "STRICT=1,终止。"
continue
fi
if ! "${ACME_BIN}" --home "${ACME_HOME}" --config-home "${ACME_HOME}" --install-cert -d "${domain}" "${ECC_ARG[@]:-}" \
--fullchain-file "$fullchain" \
--key-file "$keyfile"; then
warn "[$domain] install-cert 失败。"
FAIL=1
[[ "${STRICT}" == "1" ]] && die "STRICT=1,终止。"
continue
fi
NEED_RELOAD=1
log "[$domain] 证书已更新: $fullchain"
done
if [[ "$DRY_RUN" -eq 1 ]]; then
exit 0
fi
if [[ "$NEED_RELOAD" -eq 1 ]]; then
reload_nginx_if_needed
else
log "无需重载 Nginx(无证书更新或均在续期窗口外)。"
fi
if [[ "$FAIL" -eq 1 ]]; then
exit 1
fi
exit 0
sicnature ---------------------------------------------------------------------
I P 地 址: 216.73.216.187
区 域 位 置: 美国加利福尼亚洛杉矶
系 统 信 息:
Original content, please indicate the source:
同福客栈论坛 | 蟒蛇科普 | 海南乡情论坛 | JiaYu Blog
sicnature ---------------------------------------------------------------------

没有评论