CentOS Stream 9 服务器初始化

适用范围:全新安装完成的 CentOS Stream 9。本文命令均以 root 执行。磁盘、LVM、/etc/fstab、SSH 和 Docker 配置会改变系统状态,执行前必须确认主机、目标磁盘、卷组名与容量。

1. 实施原则

  • 保留一个可用的 root 控制台或第二个 SSH 会话,修改 SSH 和挂载配置时不要只依赖唯一 SSH 连接。
  • 不要把 LVM 卷组(VG)的空间全部分配给逻辑卷(LV);保留一部分空闲空间以便后续扩容。
  • CentOS Stream 9 默认的 XFS 支持在线扩容但不支持缩容。/home、/data 的初始大小必须审慎规划。
  • 保持 SELinux 为 Enforcing,不要为了 Docker 而关闭 SELinux。

2. 安装后调整现有 LVM

本节针对当前这台服务器的实际布局,不是重新分区:

/dev/sda                 200G
├─/dev/sda1             600M   /boot/efi
├─/dev/sda2               1G   /boot
└─/dev/sda3           198.4G   LVM PV
├─cs-root               70G   /
├─cs-swap             7.9G   swap
└─cs-home           120.5G   /home

当前 /home 使用 XFS。XFS 可以扩容但不能缩小,所以不能直接使用 lvreduce。安全做法是备份 /home,注销所有使用者,删除并重建一个较小的 /home,再恢复数据。下文假设新 /home 为 20G,并把释放的空间留给后续 /data。

先确认真实布局:

lsblk -f
pvs
vgs
lvs -a -o vg_name,lv_name,lv_size,lv_attr,devices
df -hT

2.1 备份 /home

确认备份放在 /root,不要放在 /home 内。根卷当前有足够空间时执行:

cd /root
df -hT / /home
cp -a /etc/fstab /etc/fstab.before-home-rebuild
OLD_HOME_UUID=$(blkid -s UUID -o value /dev/cs/home)
tar --numeric-owner --xattrs --acls --selinux \
 -czpf /root/home-backup.tar.gz -C /home .
gzip -t /root/home-backup.tar.gz
tar -tzf /root/home-backup.tar.gz >/dev/null
sha256sum /root/home-backup.tar.gz

只有在压缩包可读且校验完成后才继续。备份文件位于 /root,删除 /dev/cs/home 不会删除它。

2.2 强制注销桌面用户

重建 /home 会关闭用户程序。先确认当前会话:

loginctl list-sessions
loginctl user-status mars.zhang
who

强制注销 mars.zhang 的全部桌面和终端会话,未保存的数据会丢失:

loginctl terminate-user mars.zhang

验证该用户不再有会话或进程:

loginctl list-sessions
pgrep -a -u mars.zhang || true

如果仍有残留进程,先尝试正常终止;只有确认可以丢弃该用户未保存状态时才使用 KILL:

pkill -TERM -u mars.zhang
sleep 3
pgrep -a -u mars.zhang || true
pkill -KILL -u mars.zhang

2.3 卸载并重建 /home

不要让 root 的当前目录位于 /home,并检查是否仍有进程占用:

cd /root
fuser -vm /home

确认没有重要进程后卸载。不要使用 umount -l 或 umount -f 掩盖占用问题:

umount /home
findmnt /home

findmnt /home 没有输出后,删除原 120.5G 逻辑卷并创建 20G 新卷:

lvremove /dev/cs/home
lvcreate -L 20G -n home cs
mkfs.xfs -f /dev/cs/home
mkdir -p /home
mount /dev/cs/home /home

2.4 恢复数据并修正开机挂载

NEW_HOME_UUID=$(blkid -s UUID -o value /dev/cs/home)
tar --numeric-owner --xattrs --acls --selinux \
-xzpf /root/home-backup.tar.gz -C /home
restorecon -RF /home
grep -nE '[[:space:]]/home[[:space:]]' /etc/fstab

如果 /etc/fstab 使用旧 UUID,将其替换为新 UUID;如果使用 /dev/mapper/cs-home,通常无需修改:

sed -i "s/$OLD_HOME_UUID/$NEW_HOME_UUID/" /etc/fstab
findmnt --verify
mount -a

最后检查数据、文件系统和释放出来的 VG 空间:

findmnt /home
df -hT /home
ls -la /home
du -sh /home
pvs
vgs
lvs -o vg_name,lv_name,lv_size,lv_attr,devices

完成后,cs 卷组应有约 100G 空闲空间,可按第 7 节创建 /data。LVM 的关系是:磁盘分区 /dev/sda3 是物理卷(PV),PV 属于卷组 cs(VG),从 VG 分配逻辑卷(LV),LV 格式化后挂载到 /、/home、/data 等目录。

3. 创建管理员用户

以下以 mars.zhang 为例:

useradd -m -s /bin/bash -G wheel mars.zhang
passwd mars.zhang
id mars.zhang
getent group wheel

CentOS Stream 默认允许 wheel 组 sudo。通过 visudo 检查,不要直接编辑 sudoers:

visudo

确认有如下未注释规则:

%wheel  ALL=(ALL)       ALL

在第二个终端验证,成功前不要退出 root:

su - mars.zhang
sudo -v
sudo whoami
exit

4. 配置仓库与常用工具

dnf makecache
dnf check-update || true
dnf install -y epel-release
dnf makecache
dnf install -y lrzsz sysstat iotop iftop htop telnet bc httpd-tools nethogs expect

如果必须使用阿里云 EPEL,可改用:

dnf install -y https://mirrors.aliyun.com/epel/epel-release-latest-9.noarch.rpm
dnf makecache

可选测试 EPEL:

dnf install -y cowsay sl

5. SSH 优化

备份配置后修改。公司若使用 Kerberos/GSSAPI 单点认证,不应关闭 GSSAPI:

cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F-%H%M%S)
vi /etc/ssh/sshd_config

增加或修改:

UseDNS no
GSSAPIAuthentication no

先检查语法,再重启并从新终端登录验证:

sshd -t
systemctl restart sshd
systemctl status sshd --no-pager

6. Chrony 时间同步

timedatectl status
timedatectl set-timezone Asia/Shanghai
dnf install -y chrony
cp -a /etc/chrony.conf /etc/chrony.conf.bak.$(date +%F-%H%M%S)
vi /etc/chrony.conf

在 chrony.conf 中新增以下行;如有内部 NTP,优先使用内部时间源:

server ntp1.aliyun.com iburst prefer
systemctl enable --now chronyd
chronyc sources -v
chronyc tracking
timedatectl status

7. 创建独立 /data LVM 数据卷

7.1 使用现有 VG 的空闲空间

先确认 VG 名与可用容量。以下示例用卷组 cs 创建 80 GiB 的数据卷;应按实际空间修改 80G:

vgs
lvs
lvcreate -L 80G -n data cs
mkfs.xfs -f /dev/cs/data
mkdir -p /data
mount /dev/cs/data /data

LV 名为 data,对应设备为 /dev/cs/data,device-mapper 中显示为 /dev/mapper/cs-data。不建议把 LV 再命名为 cs-data。

获取 UUID 并设置自动挂载:

blkid /dev/cs/data
cp -a /etc/fstab /etc/fstab.bak.$(date +%F-%H%M%S)
vi /etc/fstab

将实际 UUID 写入一行:

UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx  /data  xfs  defaults  0  0

验证挂载与 fstab:

findmnt --verify
umount /data
mount -a
findmnt /data
df -hT /data
vgs
lvs -o vg_name,lv_name,lv_size,lv_attr

7.2 当前 VG 无空闲空间时增加新磁盘

新增磁盘后,先严格确认设备名,不能凭猜测对系统盘执行 pvcreate:

lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS

以下仅在确认新盘为 /dev/sdb 时执行:

pvcreate /dev/sdb
vgextend cs /dev/sdb
vgs

然后按 7.1 创建 /dev/cs/data。若扩展的是系统盘而非新增磁盘,必须按实际分区布局先扩展分区和 PV,不能直接套用 /dev/sdb 命令。

8. 通过 Docker 官方仓库安装 Docker

确认 /data 已正常挂载后执行:

findmnt /data
df -hT /data
dnf install -y dnf-plugins-core
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

如访问官方源较慢,可选用清华镜像。镜像源需由维护团队验证同步与可信性:

sed -i 's+https://download.docker.com+https://mirrors.tuna.tsinghua.edu.cn/docker-ce+' /etc/yum.repos.d/docker-ce.repo

也可改用中科大源,二选一,不与清华源混用:

dnf config-manager --add-repo http://mirrors.ustc.edu.cn/docker-ce/linux/centos/docker-ce.repo

9. 设置 Docker 数据目录 /data/docker

创建目录并写入配置。第三方镜像加速器是供应链依赖,生产环境只保留已审核、稳定可用的地址;敏感镜像建议使用自建可信仓库。

mkdir -p /etc/docker /data/docker
cat > /etc/docker/daemon.json <<'EOF'
{
 "registry-mirrors": [
   "https://docker.1ms.run",
   "https://docker.1panel.live",
   "https://docker.m.ixdev.cn",
   "https://hub.rat.dev",
   "https://docker.xuanyuan.me",
   "https://dockerproxy.net",
   "https://docker-registry.nmqu.com",
   "https://hub.amingg.com",
   "https://docker.amingg.com",
   "https://docker.hlmirror.com",
   "https://hub1.nat.tf",
   "https://hub2.nat.tf",
   "https://hub3.nat.tf",
   "https://docker.m.daocloud.io",
   "https://docker.kejilion.pro",
   "https://docker.367231.xyz",
   "https://hub.1panel.dev",
   "https://dockerproxy.cool",
   "https://docker.apiba.cn",
   "https://proxy.vvvv.ee"
],
 "data-root": "/data/docker"
}
EOF
python3 -m json.tool /etc/docker/daemon.json >/dev/null
systemctl enable --now docker
systemctl status docker --no-pager

验证 Docker、Compose 和实际数据目录:

docker version
docker info | grep -E 'Docker Root Dir|Registry Mirrors'
docker run --rm hello-world
docker compose version
du -sh /data/docker

默认只由 root 使用 Docker。将普通用户加入 docker 组近似授予 root 级主机控制能力,除非有明确需求,不执行 usermod -aG docker <user>。

10. 配置 Bash 提示符

对当前用户生效;若需要管理员用户也使用,请以该用户身份执行:

cat >> ~/.bashrc <<'EOF'
export PS1='[\[\e[34;1m\]\u@\[\e[0m\]\[\e[32;1m\]\H\[\e[0m\]\[\e[31;1m\] \w\[\e[0m\]]\$ '
EOF
source ~/.bashrc

11. 最终验收

hostnamectl
timedatectl status
getenforce
systemctl is-active sshd chronyd docker
findmnt /data
df -hT / /home /data
pvs
vgs
lvs
docker info | grep 'Docker Root Dir'
docker compose version

交付记录建议包含主机名、IP、系统版本、PV/VG/LV 名称与大小、/data UUID、Docker 版本、镜像源配置和验证日期;不得记录明文密码。

默认逐步询问是否执行,输入 y 执行该步骤;使用 -y 自动执行全部步骤,包括 LVM 重建和强制注销用户:

bash initialize-centos9.sh
bash initialize-centos9.sh -y

可调整参数:

bash initialize-centos9.sh \
  --home-size 20G \
  --data-size 80G \
  --admin-user mars.zhang \
  --docker-mirror official

流程包括管理员创建、/home 备份和重建、/data 创建、EPEL 与工具、SSH、Chrony、Docker /data/docker 配置、提示符与最终验证。管理员密码始终通过 passwd 交互输入,不会出现在脚本参数中。

已通过 Bash 语法检查和 --help 验证,未在任何服务器执行。复制到目标 Linux 服务器后执行 chmod 750 initialize-centos9.sh。

#!/usr/bin/env bash
# CentOS Stream 9 initialization for the documented cs/root/home layout.
# Default mode asks before each step. Use -y only after reviewing this script.
set -Eeuo pipefail

VG=cs
HOME_LV=/dev/cs/home
HOME_MOUNT=/home
HOME_SIZE=20G
DATA_LV_NAME=data
DATA_MOUNT=/data
DATA_SIZE=80G
ADMIN_USER=mars.zhang
TARGET_USER=mars.zhang
DOCKER_MIRROR=tuna
AUTO_YES=0
BACKUP_FILE=
FSTAB_BACKUP=

usage() {
  cat <<'EOF'
Usage: initialize-centos9.sh [options]

Options:
  -y, --yes             Run every selected step without step prompts
  --admin-user USER     Administrator account, default: mars.zhang
  --home-size SIZE      Recreated /home size, default: 20G
  --data-size SIZE      New /data size, default: 80G
  --docker-mirror NAME  official, tuna, ustc, or none; default: tuna
  -h, --help            Show help

The LVM step is destructive: it backs up /home, deletes /dev/cs/home,
recreates it, restores data, and creates /dev/cs/data.
The administrator password is always entered interactively.
EOF
}

fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }

valid_size() { [[ $1 =~ ^[1-9][0-9]*[GgMm]$ ]] || fail "Invalid size: $1"; }

confirm_step() {
  local title=$1
  if (( AUTO_YES )); then
    printf '\n>>> %s [auto-confirmed]\n' "$title"
    return 0
  fi
  printf '\n>>> %s\n' "$title"
  read -r -p "Run this step? [y/N] " answer
  [[ $answer =~ ^([yY]|[yY][eE][sS])$ ]]
}

run_step() {
  local title=$1 fn=$2
  if confirm_step "$title"; then
    "$fn"
  else
    printf 'Skipped: %s\n' "$title"
  fi
}

while [[ $# -gt 0 ]]; do
  case $1 in
    -y|--yes) AUTO_YES=1; shift ;;
    --admin-user) ADMIN_USER=${2:?Missing value}; shift 2 ;;
    --home-size) HOME_SIZE=${2:?Missing value}; shift 2 ;;
    --data-size) DATA_SIZE=${2:?Missing value}; shift 2 ;;
    --docker-mirror) DOCKER_MIRROR=${2:?Missing value}; shift 2 ;;
    -h|--help) usage; exit 0 ;;
    *) fail "Unknown option: $1" ;;
  esac
done

valid_size "$HOME_SIZE"
valid_size "$DATA_SIZE"
[[ $DOCKER_MIRROR =~ ^(official|tuna|ustc|none)$ ]] || fail "docker-mirror must be official, tuna, ustc, or none"
[[ $EUID -eq 0 ]] || fail "Run as root."
for command_name in awk blkid df dnf findmnt fuser loginctl lvcreate lvremove lvs mkfs.xfs mount pgrep pvs tar umount vgs; do
  command -v "$command_name" >/dev/null || fail "Required command not found: $command_name"
done

step_user() {
  if id "$ADMIN_USER" >/dev/null 2>&1; then
    printf 'User %s already exists; keeping its password.\n' "$ADMIN_USER"
  else
    useradd -m -s /bin/bash "$ADMIN_USER"
    printf 'Created user %s.\n' "$ADMIN_USER"
    passwd "$ADMIN_USER"
  fi
  usermod -aG wheel "$ADMIN_USER"
  id "$ADMIN_USER"
  printf 'Verify sudo from a second terminal before closing root.\n'
}

step_lvm() {
  [[ -b "$HOME_LV" ]] || fail "$HOME_LV does not exist; this script expects the original 120G /home LV."
  [[ $(findmnt -n -o FSTYPE -M "$HOME_MOUNT") == xfs ]] || fail "$HOME_MOUNT must be an XFS mount."
  [[ -z $(findmnt -n -M "$DATA_MOUNT" 2>/dev/null || true) ]] || fail "$DATA_MOUNT is already mounted."
  [[ ! -e "/dev/$VG/$DATA_LV_NAME" ]] || fail "/dev/$VG/$DATA_LV_NAME already exists."
  awk '$1 !~ /^#/ && $2 == "/home" { found=1 } END { exit !found }' /etc/fstab || fail "No active /home entry in /etc/fstab."
  awk '$1 !~ /^#/ && $2 == "/data" { found=1 } END { exit found ? 0 : 1 }' /etc/fstab && fail "An active /data entry already exists in /etc/fstab."
  [[ -d "$DATA_MOUNT" ]] || mkdir -p "$DATA_MOUNT"
  if find "$DATA_MOUNT" -mindepth 1 -maxdepth 1 -print -quit | grep -q .; then
    fail "$DATA_MOUNT is not empty."
  fi

  cd /root
  local timestamp old_uuid new_uuid data_uuid home_bytes root_free required
  timestamp=$(date +%F-%H%M%S)
  BACKUP_FILE="/root/home-backup-${timestamp}.tar.gz"
  FSTAB_BACKUP="/etc/fstab.before-home-rebuild.${timestamp}"
  old_uuid=$(blkid -s UUID -o value "$HOME_LV")
  [[ -n $old_uuid ]] || fail "Could not read old /home UUID."
  home_bytes=$(du -sxB1 "$HOME_MOUNT" | awk '{print $1}')
  root_free=$(df -B1 --output=avail /root | awk 'NR==2 {print $1}')
  required=$((home_bytes + 1024*1024*1024))
  (( root_free >= required )) || fail "Insufficient /root free space for backup."

  cp -a /etc/fstab "$FSTAB_BACKUP"
  printf 'Creating and verifying %s\n' "$BACKUP_FILE"
  tar --numeric-owner --xattrs --acls --selinux -czpf "$BACKUP_FILE" -C "$HOME_MOUNT" .
  gzip -t "$BACKUP_FILE"
  tar -tzf "$BACKUP_FILE" >/dev/null
  sha256sum "$BACKUP_FILE" | tee "${BACKUP_FILE}.sha256"

  if id "$TARGET_USER" >/dev/null 2>&1; then
    if loginctl list-sessions --no-legend | awk -v user="$TARGET_USER" '$3 == user { found=1 } END { exit !found }'; then
      loginctl terminate-user "$TARGET_USER" || true
    else
      printf 'No active login session for %s; no logout was needed.\n' "$TARGET_USER"
    fi
    if pgrep -u "$TARGET_USER" >/dev/null; then pkill -TERM -u "$TARGET_USER" || true; sleep 3; fi
    if pgrep -u "$TARGET_USER" >/dev/null; then pkill -KILL -u "$TARGET_USER" || true; fi
  fi
  pgrep -u "$TARGET_USER" >/dev/null && fail "Processes for $TARGET_USER remain."

  printf 'Processes accessing %s (kernel mount is expected):\n' "$HOME_MOUNT"
  fuser -vm "$HOME_MOUNT" || true
  if (( ! AUTO_YES )); then
    read -r -p "Type REBUILD_HOME and press Enter to delete the old LV: " answer
    [[ $answer == REBUILD_HOME ]] || fail "Destructive confirmation not received."
  else
    printf 'Destructive LVM action auto-confirmed by -y.\n'
  fi

  umount "$HOME_MOUNT"
  findmnt -n -M "$HOME_MOUNT" >/dev/null 2>&1 && fail "$HOME_MOUNT is still mounted."
  lvremove -y "$HOME_LV"
  lvcreate -L "$HOME_SIZE" -n home "$VG"
  mkfs.xfs -f "$HOME_LV"
  mount "$HOME_LV" "$HOME_MOUNT"

  new_uuid=$(blkid -s UUID -o value "$HOME_LV")
  sed -i "s|$old_uuid|$new_uuid|g" /etc/fstab
  tar --numeric-owner --xattrs --acls --selinux -xzpf "$BACKUP_FILE" -C "$HOME_MOUNT"
  command -v restorecon >/dev/null && restorecon -RF "$HOME_MOUNT" || true
  umount "$HOME_MOUNT"
  mount "$HOME_MOUNT"

  lvcreate -L "$DATA_SIZE" -n "$DATA_LV_NAME" "$VG"
  mkfs.xfs -f "/dev/$VG/$DATA_LV_NAME"
  data_uuid=$(blkid -s UUID -o value "/dev/$VG/$DATA_LV_NAME")
  printf 'UUID=%s  %s  xfs  defaults  0  0\n' "$data_uuid" "$DATA_MOUNT" >> /etc/fstab
  mount "$DATA_MOUNT"
  findmnt --verify
  findmnt -M "$HOME_MOUNT"
  findmnt -M "$DATA_MOUNT"
  df -hT "$HOME_MOUNT" "$DATA_MOUNT"
  vgs "$VG"
  lvs -o vg_name,lv_name,lv_size,lv_attr,devices "$VG"
  printf 'Backup retained at %s\n' "$BACKUP_FILE"
}

step_tools() {
  dnf makecache
  dnf install -y epel-release
  dnf makecache
  dnf install -y lrzsz sysstat iotop iftop htop telnet bc httpd-tools nethogs expect
}

step_ssh() {
  cp -a /etc/ssh/sshd_config "/etc/ssh/sshd_config.bak.$(date +%F-%H%M%S)"
  grep -qE '^[[:space:]]*UseDNS[[:space:]]+no' /etc/ssh/sshd_config || printf '\nUseDNS no\n' >> /etc/ssh/sshd_config
  grep -qE '^[[:space:]]*GSSAPIAuthentication[[:space:]]+no' /etc/ssh/sshd_config || printf 'GSSAPIAuthentication no\n' >> /etc/ssh/sshd_config
  sshd -t
  systemctl restart sshd
  systemctl is-active sshd
}

step_time() {
  timedatectl set-timezone Asia/Shanghai
  dnf install -y chrony
  cp -a /etc/chrony.conf "/etc/chrony.conf.bak.$(date +%F-%H%M%S)"
  grep -qE '^[[:space:]]*server[[:space:]]+ntp1\.aliyun\.com([[:space:]]|$)' /etc/chrony.conf || printf '\nserver ntp1.aliyun.com iburst prefer\n' >> /etc/chrony.conf
  systemctl enable --now chronyd
  chronyc tracking
}

step_docker() {
  findmnt -M "$DATA_MOUNT" >/dev/null || fail "$DATA_MOUNT is not mounted."
  dnf install -y dnf-plugins-core
  local repo_host
  case $DOCKER_MIRROR in
    official) repo_host=https://download.docker.com ;;
    tuna) repo_host=https://mirrors.tuna.tsinghua.edu.cn/docker-ce ;;
    ustc) repo_host=http://mirrors.ustc.edu.cn/docker-ce ;;
    none) repo_host=https://download.docker.com ;;
  esac
  shopt -s nullglob
  local repo_files=(/etc/yum.repos.d/*docker*.repo)
  local had_docker_repo=0
  if (( ${#repo_files[@]} > 0 )); then
    had_docker_repo=1
  fi
  if (( ${#repo_files[@]} == 0 )); then
    dnf config-manager --add-repo "$repo_host/linux/centos/docker-ce.repo"
    repo_files=(/etc/yum.repos.d/*docker*.repo)
  fi
  # Rewrite every existing Docker repo file so stale official entries cannot win.
  for repo_file in "${repo_files[@]}"; do
    sed -i \
      -e "s#https://download.docker.com#$repo_host#g" \
      -e "s#https://mirrors.tuna.tsinghua.edu.cn/docker-ce#$repo_host#g" \
      -e "s#http://mirrors.ustc.edu.cn/docker-ce#$repo_host#g" \
      "$repo_file"
  done
  local docker_state_dir=/var/lib/initialize-centos9
  local docker_repo_marker=$docker_state_dir/docker-repo-configured
  mkdir -p "$docker_state_dir"
  if [[ -e $docker_repo_marker || $had_docker_repo -eq 1 ]]; then
    printf '检测到 Docker 仓库已配置过,本次刷新 DNF 元数据。\n'
    dnf clean metadata
    dnf makecache
  else
    printf '首次配置 Docker 仓库,跳过额外的 clean metadata/makecache。\n'
  fi
  touch "$docker_repo_marker"
  dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  mkdir -p /etc/docker /data/docker
  cat > /etc/docker/daemon.json <<'EOF'
{
  "registry-mirrors": [
    "https://docker.1ms.run",
    "https://docker.1panel.live",
    "https://docker.m.ixdev.cn",
    "https://hub.rat.dev",
    "https://docker.xuanyuan.me",
    "https://dockerproxy.net",
    "https://docker-registry.nmqu.com",
    "https://hub.amingg.com",
    "https://docker.amingg.com",
    "https://docker.hlmirror.com",
    "https://hub1.nat.tf",
    "https://hub2.nat.tf",
    "https://hub3.nat.tf",
    "https://docker.m.daocloud.io",
    "https://docker.kejilion.pro",
    "https://docker.367231.xyz",
    "https://hub.1panel.dev",
    "https://dockerproxy.cool",
    "https://docker.apiba.cn",
    "https://proxy.vvvv.ee"
  ],
  "data-root": "/data/docker"
}
EOF
  python3 -m json.tool /etc/docker/daemon.json >/dev/null
  systemctl enable --now docker
  systemctl is-active docker
  docker version
  docker compose version
}

step_prompt() {
  local prompt='export PS1='\''[\[\e[34;1m\]\u@\[\e[0m\]\[\e[32;1m\]\H\[\e[0m\]\[\e[31;1m\] \w\[\e[0m\]]\$ '\'''
  for home_dir in /root "/home/$ADMIN_USER"; do
    [[ -d $home_dir ]] || continue
    grep -Fqx "$prompt" "$home_dir/.bashrc" 2>/dev/null || printf '\n%s\n' "$prompt" >> "$home_dir/.bashrc"
  done
}

step_verify() {
  printf '\n===== 最终验收 =====\n'
  printf '\n[主机信息]\n'
  hostnamectl | sed -n 's/^\(Static hostname\|Transient hostname\|Operating System\|Kernel\|Architecture\|Virtualization\):[[:space:]]*/\1: /p'
  printf '\n[时间同步]\n'
  timedatectl status | sed -n 's/^\(Local time\|Time zone\|System clock synchronized\|NTP service\):[[:space:]]*/\1: /p'
  printf '\n[SELinux]\n'
  printf 'SELinux 状态: %s\n' "$(getenforce)"
  printf '\n[系统服务]\n'
  local service state
  for service in sshd chronyd docker; do
    state=$(systemctl is-active "$service" 2>/dev/null || true)
    if [[ $state == active ]]; then
      printf '%-10s 正常(active)\n' "$service"
    elif systemctl list-unit-files --type=service --no-legend "$service.service" 2>/dev/null | grep -q .; then
      printf '%-10s 异常(%s)\n' "$service" "${state:-未运行}"
    else
      printf '%-10s 未安装\n' "$service"
    fi
  done
  printf '\n[LVM 与文件系统]\n'
  findmnt -M /home || printf '/home: 未挂载\n'
  findmnt -M /data || printf '/data: 未挂载\n'
  df -hT / /home /data
  pvs
  vgs
  lvs
  printf '\n[Docker]\n'
  if command -v docker >/dev/null 2>&1; then
    docker info 2>/dev/null | grep 'Docker Root Dir' || printf 'Docker 已安装,但 daemon 未正常响应。\n'
    docker compose version 2>/dev/null || printf 'Docker Compose 插件未正常响应。\n'
  else
    printf 'Docker 命令未找到:Docker 安装步骤尚未完成。\n'
  fi
  printf '\n===== 验收结束 =====\n'
}

cat <<'BANNER'
This script changes system state. The LVM step deletes and recreates /dev/cs/home.
Review the script, verify the target host, and keep a root console or second session.
Use -y only when you accept automatic execution of every selected step.
BANNER

run_step "Create or verify the wheel administrator" step_user
run_step "Back up and rebuild /home, then create /data" step_lvm
run_step "Install EPEL and common tools" step_tools
run_step "Optimize SSH configuration" step_ssh
run_step "Configure Asia/Shanghai time and Chrony" step_time
run_step "Install Docker and configure /data/docker" step_docker
run_step "Configure Bash prompt" step_prompt
run_step "Run final validation" step_verify

printf '\nInitialization script finished.\n'
滚动至顶部