449 lines
14 KiB
Bash
Executable File
449 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
|
||
# ============================================
|
||
# 服务器基础环境安装脚本(公共依赖)
|
||
# 安装: 系统工具 + Docker + Nginx + Certbot + 防火墙
|
||
# 可独立运行,也可被其他部署脚本 source 调用
|
||
# ============================================
|
||
|
||
# 避免重复 source
|
||
if [ -n "${_BASE_SETUP_LOADED:-}" ]; then
|
||
return 0 2>/dev/null || true
|
||
fi
|
||
_BASE_SETUP_LOADED=1
|
||
|
||
# ===== 终端颜色 =====
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m'
|
||
|
||
log() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||
step() { echo -e "\n${CYAN}========== $* ==========${NC}"; }
|
||
|
||
# ===== 修复 Windows CRLF 行尾 =====
|
||
# 从 Windows scp 上传的文件可能包含 \r,source 时会导致错误
|
||
# 用法: fix_crlf file1 [file2 ...]
|
||
fix_crlf() {
|
||
for f in "$@"; do
|
||
[ -f "$f" ] && sed -i 's/\r$//' "$f"
|
||
done
|
||
}
|
||
|
||
# ===== 检查 root =====
|
||
check_root() {
|
||
if [ "$(id -u)" -ne 0 ]; then
|
||
error "请使用 root 用户运行此脚本: sudo bash $0"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# ===== 检测包管理器 =====
|
||
detect_pkg_mgr() {
|
||
if [ -n "${PKG_MGR:-}" ]; then
|
||
return
|
||
fi
|
||
if command -v apt-get &> /dev/null; then
|
||
PKG_MGR="apt"
|
||
elif command -v dnf &> /dev/null; then
|
||
PKG_MGR="dnf"
|
||
elif command -v yum &> /dev/null; then
|
||
PKG_MGR="yum"
|
||
else
|
||
error "不支持的系统,需要 apt/dnf/yum 包管理器"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# ===== 系统初始化 =====
|
||
init_system() {
|
||
step "系统初始化"
|
||
detect_pkg_mgr
|
||
log "检测到包管理器: $PKG_MGR"
|
||
log "正在更新系统软件包..."
|
||
|
||
case "$PKG_MGR" in
|
||
apt)
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
apt-get update -qq
|
||
apt-get upgrade -y -qq
|
||
apt-get install -y -qq curl wget git gnupg2 ca-certificates \
|
||
lsb-release software-properties-common openssl cron
|
||
;;
|
||
dnf|yum)
|
||
$PKG_MGR update -y -q
|
||
$PKG_MGR install -y -q curl wget git gnupg2 ca-certificates openssl cronie
|
||
;;
|
||
esac
|
||
|
||
if [ ! -f /etc/timezone ] || [ "$(cat /etc/timezone 2>/dev/null)" != "Asia/Shanghai" ]; then
|
||
timedatectl set-timezone Asia/Shanghai 2>/dev/null || true
|
||
log "时区已设置为 Asia/Shanghai"
|
||
fi
|
||
|
||
log "系统初始化完成"
|
||
}
|
||
|
||
# ===== 安装 Docker =====
|
||
install_docker() {
|
||
step "安装 Docker"
|
||
|
||
if command -v docker &> /dev/null; then
|
||
log "Docker 已安装: $(docker --version)"
|
||
else
|
||
detect_pkg_mgr
|
||
log "正在安装 Docker (使用阿里云镜像)..."
|
||
|
||
case "$PKG_MGR" in
|
||
apt)
|
||
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg \
|
||
| gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \
|
||
https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable" \
|
||
> /etc/apt/sources.list.d/docker.list
|
||
apt-get update -qq
|
||
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||
;;
|
||
dnf)
|
||
dnf config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
|
||
dnf install -y -q docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||
;;
|
||
yum)
|
||
yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
|
||
yum install -y -q docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||
;;
|
||
esac
|
||
log "Docker 安装完成: $(docker --version)"
|
||
fi
|
||
|
||
systemctl enable --now docker
|
||
log "Docker 服务已启动"
|
||
|
||
if ! docker compose version &> /dev/null; then
|
||
error "Docker Compose V2 不可用"
|
||
error "请手动安装: apt install docker-compose-plugin"
|
||
exit 1
|
||
fi
|
||
log "Docker Compose 已就绪: $(docker compose version --short)"
|
||
}
|
||
|
||
# ===== 配置 Docker 镜像加速 =====
|
||
configure_docker_mirrors() {
|
||
local mirrors="${DOCKER_REGISTRY_MIRRORS:-}"
|
||
if [ -z "$mirrors" ]; then
|
||
log "未配置 DOCKER_REGISTRY_MIRRORS,跳过镜像加速"
|
||
return
|
||
fi
|
||
|
||
mkdir -p /etc/docker
|
||
|
||
local json_array
|
||
json_array=$(echo "$mirrors" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
|
||
| grep -v '^$' | awk '{printf "\"%s\",", $0}' | sed 's/,$//')
|
||
|
||
local need_restart=0
|
||
if [ -f /etc/docker/daemon.json ]; then
|
||
if ! grep -q "registry-mirrors" /etc/docker/daemon.json; then
|
||
need_restart=1
|
||
fi
|
||
else
|
||
need_restart=1
|
||
fi
|
||
|
||
if [ "$need_restart" -eq 1 ]; then
|
||
cat > /etc/docker/daemon.json <<EOF
|
||
{
|
||
"registry-mirrors": [${json_array}],
|
||
"log-driver": "json-file",
|
||
"log-opts": {
|
||
"max-size": "10m",
|
||
"max-file": "3"
|
||
}
|
||
}
|
||
EOF
|
||
log "Docker 镜像加速已配置:"
|
||
echo "$mirrors" | tr ',' '\n' | sed 's/^/ → /'
|
||
systemctl restart docker
|
||
log "Docker 已重启以应用镜像加速"
|
||
else
|
||
log "Docker 镜像加速已存在,跳过"
|
||
fi
|
||
}
|
||
|
||
# ===== 安装 Nginx(官方稳定版仓库)=====
|
||
install_nginx() {
|
||
step "安装 Nginx"
|
||
detect_pkg_mgr
|
||
|
||
if command -v nginx &> /dev/null; then
|
||
log "Nginx 已安装: $(nginx -v 2>&1)"
|
||
else
|
||
log "正在安装 Nginx(官方 stable 仓库)..."
|
||
case "$PKG_MGR" in
|
||
apt)
|
||
# 添加 Nginx 官方 GPG 密钥
|
||
curl -fsSL https://nginx.org/keys/nginx_signing.key \
|
||
| gpg --dearmor -o /usr/share/keyrings/nginx-archive-keyring.gpg
|
||
# 添加官方 stable 仓库
|
||
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
|
||
http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" \
|
||
> /etc/apt/sources.list.d/nginx.list
|
||
# 优先使用官方仓库
|
||
printf "Package: *\nPin: origin nginx.org\nPin-Priority: 900\n" \
|
||
> /etc/apt/preferences.d/99nginx
|
||
apt-get update -qq
|
||
apt-get install -y -qq nginx
|
||
;;
|
||
dnf)
|
||
cat > /etc/yum.repos.d/nginx.repo <<'REPO'
|
||
[nginx-stable]
|
||
name=nginx stable repo
|
||
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
|
||
gpgcheck=1
|
||
enabled=1
|
||
gpgkey=https://nginx.org/keys/nginx_signing.key
|
||
module_hotfixes=true
|
||
REPO
|
||
dnf install -y -q nginx
|
||
;;
|
||
yum)
|
||
cat > /etc/yum.repos.d/nginx.repo <<'REPO'
|
||
[nginx-stable]
|
||
name=nginx stable repo
|
||
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
|
||
gpgcheck=1
|
||
enabled=1
|
||
gpgkey=https://nginx.org/keys/nginx_signing.key
|
||
module_hotfixes=true
|
||
REPO
|
||
yum install -y -q nginx
|
||
;;
|
||
esac
|
||
log "Nginx 安装完成"
|
||
fi
|
||
|
||
systemctl enable --now nginx
|
||
|
||
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled /var/www/certbot
|
||
|
||
# Nginx 官方包使用 conf.d/ 而非 sites-enabled/,添加 include
|
||
if ! grep -q "sites-enabled" /etc/nginx/nginx.conf; then
|
||
if grep -q "^http {" /etc/nginx/nginx.conf; then
|
||
sed -i '/^http {/a \ include /etc/nginx/sites-enabled/*;' /etc/nginx/nginx.conf
|
||
else
|
||
sed -i '/include.*conf\.d/a \ include /etc/nginx/sites-enabled/*;' /etc/nginx/nginx.conf
|
||
fi
|
||
fi
|
||
|
||
log "Nginx 配置就绪"
|
||
}
|
||
|
||
# ===== 安装 Certbot =====
|
||
install_certbot() {
|
||
detect_pkg_mgr
|
||
|
||
if command -v certbot &> /dev/null; then
|
||
log "Certbot 已安装: $(certbot --version 2>&1)"
|
||
return
|
||
fi
|
||
|
||
log "正在安装 Certbot..."
|
||
case "$PKG_MGR" in
|
||
apt) apt-get install -y -qq certbot python3-certbot-nginx ;;
|
||
dnf) dnf install -y -q certbot python3-certbot-nginx ;;
|
||
yum) yum install -y -q certbot python3-certbot-nginx ;;
|
||
esac
|
||
log "Certbot 安装完成"
|
||
}
|
||
|
||
# ===== 配置防火墙(基础端口)=====
|
||
setup_firewall_base() {
|
||
step "配置防火墙(基础端口)"
|
||
|
||
if command -v ufw &> /dev/null; then
|
||
ufw --force enable 2>/dev/null || true
|
||
ufw allow ssh comment "SSH" 2>/dev/null || true
|
||
ufw allow 80/tcp comment "HTTP" 2>/dev/null || true
|
||
ufw allow 443/tcp comment "HTTPS" 2>/dev/null || true
|
||
ufw reload 2>/dev/null || true
|
||
log "UFW 防火墙规则已添加 (22, 80, 443)"
|
||
elif command -v firewall-cmd &> /dev/null; then
|
||
systemctl enable --now firewalld 2>/dev/null || true
|
||
firewall-cmd --permanent --add-service=ssh 2>/dev/null || true
|
||
firewall-cmd --permanent --add-service=http 2>/dev/null || true
|
||
firewall-cmd --permanent --add-service=https 2>/dev/null || true
|
||
firewall-cmd --reload 2>/dev/null || true
|
||
log "Firewalld 规则已添加 (22, 80, 443)"
|
||
else
|
||
warn "未检测到防火墙工具,请手动开放端口: 22, 80, 443"
|
||
fi
|
||
}
|
||
|
||
# ===== 开放额外端口 =====
|
||
# 用法: firewall_allow_port <端口> [描述]
|
||
firewall_allow_port() {
|
||
local port="$1"
|
||
local comment="${2:-Custom}"
|
||
|
||
if command -v ufw &> /dev/null; then
|
||
ufw allow "$port"/tcp comment "$comment" 2>/dev/null || true
|
||
ufw reload 2>/dev/null || true
|
||
elif command -v firewall-cmd &> /dev/null; then
|
||
firewall-cmd --permanent --add-port="$port"/tcp 2>/dev/null || true
|
||
firewall-cmd --reload 2>/dev/null || true
|
||
fi
|
||
log "已开放端口: $port ($comment)"
|
||
}
|
||
|
||
# ===== 申请 SSL 证书 =====
|
||
# 用法: setup_ssl_cert <域名> <邮箱> [站点名称]
|
||
setup_ssl_cert() {
|
||
local domain="$1"
|
||
local email="$2"
|
||
local site_name="${3:-$domain}"
|
||
|
||
step "配置 SSL 证书: ${domain}"
|
||
|
||
install_certbot
|
||
|
||
# 部署临时 Nginx 配置(仅 HTTP,用于 ACME 验证)
|
||
cat > "/etc/nginx/sites-available/${site_name}" <<NGINX_TEMP
|
||
server {
|
||
listen 80;
|
||
listen [::]:80;
|
||
server_name ${domain};
|
||
|
||
location /.well-known/acme-challenge/ {
|
||
root /var/www/certbot;
|
||
}
|
||
|
||
location / {
|
||
return 200 'Service is being configured...';
|
||
add_header Content-Type text/plain;
|
||
}
|
||
}
|
||
NGINX_TEMP
|
||
|
||
ln -sf "/etc/nginx/sites-available/${site_name}" "/etc/nginx/sites-enabled/${site_name}"
|
||
# 移除默认站点避免冲突(Ubuntu 包和官方包路径不同)
|
||
rm -f /etc/nginx/sites-enabled/default
|
||
rm -f /etc/nginx/conf.d/default.conf
|
||
nginx -t && systemctl reload nginx
|
||
|
||
# 申请证书
|
||
if [ ! -d "/etc/letsencrypt/live/${domain}" ]; then
|
||
log "正在申请 SSL 证书: ${domain} ..."
|
||
if ! certbot certonly --webroot \
|
||
-w /var/www/certbot \
|
||
-d "${domain}" \
|
||
--email "${email}" \
|
||
--agree-tos \
|
||
--non-interactive \
|
||
--no-eff-email; then
|
||
error "SSL 证书申请失败!"
|
||
error "请确认:"
|
||
error " 1. 域名 ${domain} 已解析到本服务器 IP"
|
||
error " 2. 服务器 80 端口可从外网访问"
|
||
exit 1
|
||
fi
|
||
log "SSL 证书申请成功"
|
||
else
|
||
log "SSL 证书已存在,跳过申请"
|
||
fi
|
||
|
||
# 配置自动续期
|
||
if ! crontab -l 2>/dev/null | grep -q "certbot renew"; then
|
||
(crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet --post-hook 'systemctl reload nginx'") | crontab -
|
||
log "已配置 SSL 证书自动续期 (每天 03:00 检查)"
|
||
fi
|
||
}
|
||
|
||
# ===== 部署 Nginx 配置 =====
|
||
# 用法: deploy_nginx_conf <模板路径> <域名> <站点名称>
|
||
# 模板中使用 __DOMAIN__ 作为域名占位符
|
||
deploy_nginx_conf() {
|
||
local template="$1"
|
||
local domain="$2"
|
||
local site_name="$3"
|
||
|
||
if [ ! -f "$template" ]; then
|
||
error "Nginx 配置模板不存在: $template"
|
||
exit 1
|
||
fi
|
||
|
||
cp "$template" "/etc/nginx/sites-available/${site_name}"
|
||
sed -i "s/__DOMAIN__/${domain}/g" "/etc/nginx/sites-available/${site_name}"
|
||
|
||
ln -sf "/etc/nginx/sites-available/${site_name}" "/etc/nginx/sites-enabled/${site_name}"
|
||
|
||
nginx -t
|
||
systemctl reload nginx
|
||
log "Nginx 反向代理配置已部署: ${site_name} → ${domain}"
|
||
}
|
||
|
||
# ===== 加载 base .env =====
|
||
load_base_env() {
|
||
local base_dir="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||
if [ -f "$base_dir/.env" ]; then
|
||
fix_crlf "$base_dir/.env"
|
||
set -a
|
||
source "$base_dir/.env"
|
||
set +a
|
||
fi
|
||
}
|
||
|
||
# =============================================================
|
||
# 独立运行模式:直接安装全部基础环境
|
||
# =============================================================
|
||
_base_main() {
|
||
echo -e "${CYAN}"
|
||
echo " ____"
|
||
echo " | __ ) __ _ ___ ___"
|
||
echo " | _ \\ / _\` / __|/ _ \\"
|
||
echo " | |_) | (_| \\__ \\ __/"
|
||
echo " |____/ \\__,_|___/\\___| Server Base Setup"
|
||
echo -e "${NC}"
|
||
echo ""
|
||
|
||
check_root
|
||
|
||
local base_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
|
||
# 加载 .env
|
||
if [ -f "$base_dir/.env" ]; then
|
||
set -a; source "$base_dir/.env"; set +a
|
||
elif [ -f "$base_dir/.env.example" ]; then
|
||
cp "$base_dir/.env.example" "$base_dir/.env"
|
||
set -a; source "$base_dir/.env"; set +a
|
||
log "已从 .env.example 生成 .env"
|
||
fi
|
||
|
||
init_system
|
||
install_docker
|
||
configure_docker_mirrors
|
||
install_nginx
|
||
install_certbot
|
||
setup_firewall_base
|
||
|
||
echo ""
|
||
log "===== 基础环境安装完成 ====="
|
||
log "已安装: Docker $(docker --version 2>/dev/null | grep -o '[0-9.]*' | head -1)"
|
||
log "已安装: Docker Compose $(docker compose version --short 2>/dev/null)"
|
||
log "已安装: Nginx $(nginx -v 2>&1 | grep -o '[0-9.]*')"
|
||
log "已安装: Certbot $(certbot --version 2>&1 | grep -o '[0-9.]*')"
|
||
echo ""
|
||
log "接下来可以部署各服务:"
|
||
log " Gitea: cd /opt/gitea && bash deploy.sh"
|
||
log " Certd: cd /opt/certd && bash deploy.sh"
|
||
log " Vaultwarden: cd /opt/vaultwarden && bash deploy.sh"
|
||
}
|
||
|
||
# 仅直接运行时执行 main,被 source 时只加载函数
|
||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||
set -euo pipefail
|
||
_base_main "$@"
|
||
fi
|