macOS使用docker安装宝塔实战

Mac使用docker安装宝塔

项目背景

初衷

由于 macOS 上无法直接安装宝塔面板(因为宝塔仅支持 Linux 系统),但可以通过 ‌Docker 容器‌ 技术在 Mac 上运行一个 Linux 环境,并在其中安装宝塔面板。Docker功能的强大给开发人员带来了很大的便利。正巧,最近的开发项目需要搭建宝塔环境,本人通过Docker成功安装了宝塔,现总结下操作经验,避免以后遗忘,也希望能给大家带来帮助。

背景知识介绍

docker安装各类容器的步骤是统一的,首先要拉取镜像;其次是创建容器,常见容器比如nginx、python、mysql、redis、php等,如果我们开发需要不同的版本,创建不同版本的容器即可,是非常方便的;执行docker命令进入容器进行各种操作,比如mysql容器,进入后我们就可以执行mysql命令;最后启动容器,该过程指定与我们的宿主主机对应的端口进行通信。

项目实例

下面以安装宝塔为例,详细介绍下安装步骤及遇到的问题。

1、拉取镜像

拉取Linux镜像,Ubuntu或CentOS镜像都可以。

复制代码
 docker pull ubuntu:22.04

执行命令报错,错误信息如下:

复制代码
# 报错信息
docker安装宝塔报错:Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)

# 原因分析
Docker 默认的官方镜像仓库 registry-1.docker.io 位于国外,在国内网络环境下访问速度慢或不稳定,导致连接超时。

为 Docker 配置‌国内镜像加速器,如图所示:

点击保存并重启docker。

bash 复制代码
# 1、验证配置是否生效
# 打开终端,执行命令:
docker info
# Registry Mirrors部分展示刚添加的镜像地址,配置生效。
# 2、重新拉取镜像
docker pull ubuntu:22.04
# 错误消失,拉取速度显著提升

2、创建并启动容器

复制代码
# 创建并启动一个名为 bt-panel 的 Ubuntu 容器
# --privileged=true: 赋予特权,以便宝塔管理服务
# -p: 端口映射,格式为 宿主机端口:容器端口
# -v: 目录挂载,将本地文件夹映射到容器内,方便管理网站文件
docker run -itd --name bt-panel \
--privileged=true \
-p 8888:8888 \
-p 80:80 \
-p 443:443 \
-p 3306:3306 \
-p 22:22 \
-v ~/www:/www/wwwroot \
ubuntu:22.04 \
/bin/bash

注:~/www 是你 Mac 本地的文件夹,你可以改为其他路径。
如果 8888 端口被占用,可改为 -p 8889:8888,访问时也需对应修改端口。

3、进入容器并安装宝塔面板

复制代码
# 1、执行docker命令进入容器
docker exec -it bt-panel /bin/bash

# 2、容器内操作以下命令,更新软件源并安装必要工具
apt update
apt install -y wget curl sudo

#3、下载并运行宝塔安装脚本
# 使用宝塔官方最新安装脚本
wget -O install.sh https://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh ed8484bec

注:安装过程中会提示选择地区(输入 y 或对应数字)和时区(选择 Asia/Shanghai)。
等待安装完成,屏幕最后会显示 ‌外网面板地址‌、‌username‌ 和 ‌password‌,请务必截图或复制保存。

4、访问宝塔面板

复制代码
# 在浏览器中输入:
http://127.0.0.1:8888/XXXX
对应本人安装的宝塔面板地址:http://127.0.0.1:8888/eedebdfd

5、兼容问题修复

5.1问题排查过程

此时遇到了大问题,一直访问不成功,于是进行排查。

复制代码
# 1、通过宝塔命令查看状态
bt status

# 2、重新启动宝塔面板
bt restart

# 3、查看面板错误日志
tail -n 50 /www/server/panel/logs/error.log

# 4、查看系统日志
dmesg | tail -n 20

一顿操作猛如虎之后,根据系统错误日志,发现了真相:

5.2 修复步骤
✅重建 pyenv
bash 复制代码
# 在 bt-panel 容器内执行
# 1. 先备份
tar zcf /root/panel_pyenv_backup_$(date +%F).tar.gz /www/server/panel/pyenv

# 2. 卸载所有冲突包 + 重装正确版本
/www/server/panel/pyenv/bin/pip3 uninstall -y gevent gevent-websocket greenlet
# 删干净所有重复 dist-info 和孤立 .so
rm -rf /www/server/panel/pyenv/lib/python3.7/site-packages/gevent-20.12.1.dist-info \
       /www/server/panel/pyenv/lib/python3.7/site-packages/greenlet-0.4.17.dist-info \
       /www/server/panel/pyenv/lib/python3.7/site-packages/greenlet.cpython-37m-x86_64-linux-gnu.so

# 3. 安装匹配 gevent 22 + greenlet 2.x 组合 (greenlet <3.0)
/www/server/panel/pyenv/bin/pip3 install --no-cache-dir \
    "gevent==22.10.2" \
    "gevent-websocket==0.10.1" \
    "greenlet>=2.0,<3.0"

# 4. 清理所有 __pycache__
find /www/server/panel -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null; true

# 5. 重启面板(关键!)
bt restart

宝塔面板可以正常访问,如下图所示:


6、根本原因分析(了解)

6.1. 崩溃现场(已确认)
复制代码
python3[43824]: segfault at a9 ip 000055abe47ee078 sp 00007ffc95be8338 error 4 in python3.7
  • 故障地址 0xa9 :典型的 NULL + 0xa9 偏移空指针解引用

  • 错误码 4:用户态读失败(page not present, user read)

  • 崩溃点机器码解码

    asm 复制代码
    mov   rax, [rdi+0x8]    ; 从第一个参数(某个结构体)偏移+8取出指针
    test  BYTE [rax+0xa9], 0x40   ; <-- 崩溃! rax == NULL

    即:结构体 rdi+0x8 位置本该有一个子指针,但它是 NULL,随后访问 NULL+0xa9 触发段错误。这是C 扩展(greenlet/gevent)上下文切换时破坏 PyThreadState 链表指针的典型特征。


6.2. 根本原因:gevent / greenlet 多版本混乱 + C ABI 严重不兼容

site-packages 目录下同时存在两个不同版本的 gevent 和两个不同版本的 greenlet(4 份 dist-info!):

版本 A(旧) 版本 B(新,运行时生效) 版本要求/兼容性
gevent 20.12.1 dist-info 残留 22.10.2 实际运行 gevent 20.12.1 要求:greenlet >= 0.4.17, < 2.0
greenlet 0.4.17 dist-info + 孤立 .so(109KB) 3.0.3 实际运行(1.4MB .so) greenlet 3.x 的 C 结构体/API 相对 0.4.x 完全重写
Python --- 3.7.9 (2021年旧版) greenlet 3.x 虽宣称兼容 3.7,但与旧版 gevent 20.x 编译出来的 C 扩展 .so_gevent_cgreenlet.cpython-37m.so 等)ABI 完全不匹配

此外还有一个孤立残留文件

复制代码
/www/server/panel/pyenv/lib/python3.7/site-packages/greenlet.cpython-37m-x86_64-linux-gnu.so
   (109KB, 2021年1月, 对应 greenlet 0.4.17 旧版)
6.3. 崩溃机制推断
  1. 某次宝塔面板的升级/补丁过程未彻底清理旧版 ,导致 gevent-20.12.1.dist-info + greenlet-0.4.17 旧 C 扩展残留与新版 gevent 22 + greenlet 3 共存。
  2. 运行时 gevent 的旧版 C 扩展(_gevent_cgreenlet.so_gevent_cevent.so 等)调用 greenlet 3.x 的上下文切换函数。
  3. 由于两版本 PyGreenlet / PyThreadState 相关结构体字段大小、偏移、链表指针布局完全不同,切换过程中把 PyThreadState->next(结构体偏移 +0x8,对应崩溃代码 mov rax,[rdi+0x8])写成了 NULL
  4. 下次 Python 解释器进入主循环检查 tracing flag 时([rax+0xa9], 0x40),因 rax=NULL 触发 segfault at a9
  5. 这解释了:为何我们单独跑 greenlet/gevent 测试能通过(压力低、没走到触发旧 C 扩展的代码路径);但在 BT-Panel 高并发实际请求(软件列表更新、任务执行等)下就有几率命中。

7、番外

本人用CentOS镜像源重新安装了宝塔,登录宝塔面板后,多次安装nginx失败。

bash 复制代码
# 错误日志
宝塔安装nginx失败:Reinstall 1 Package Total download size: 547 k Installed size: 2.0 M Downloading packages: Running transaction check Running transaction test Transaction test succeeded Running transaction Installing : wget-1.14-18.el7_6.1.x86_64 1/1 install-info: No such file or directory for /usr/share/info/wget.info.gz Verifying : wget-1.14-18.el7_6.1.x86_64 1/1 Installed: wget.x86_64 0:1.14-18.el7_6.1 Complete! 检测到系统组件wget不存在,无法继续安装

7.1🎯 根因定位

wget 其实一直都装好了 ,yum 日志里 Installed: wget.x86_64 Complete! 也是千真万确的。真正导致报错 检测到系统组件wget不存在 的原因是:

根因链 证据
Docker 精简 CentOS 7 镜像里 which 命令这个 rpm 包根本没装 rpm -q whichpackage which is not installedtype whichnot found(诊断脚本 T1 T6)
宝塔 /www/server/panel/install/nginx.sh:235-244 使用 which ${RUN_BIN} 判断组件是否存在 SYS_BIN=(wget tar xz unzip) → 依次走 which wgetnginx.sh 第 232~244 行)
③ 因为 which 自身不存在,which wget returncode 永远是 127(不是 wget 不存在,是 which 这个命令自己不存在 RED 重放检测代码:which wget => exit=127,然后重新 yum install -y wget 后再次 which wget 仍然 127 → 走到 echo 检测到系统组件wget不存在(RED 完整复现)
④ 次要:rpm -ql wget 声明应有 /usr/share/info/wget.info.gz,实际该文件缺失(安装时警告 install-info: No such file or directory for /usr/share/info/wget.info.gz)→ 导致 rpm -V wget exit 1,部分脚本会把它当作"安装损坏" 诊断 T2 T7:/usr/share/info/ 目录里只有 dir,wget.info.gz 确实丢了

💡 附带影响:tree / tar / xz / unzip 也同样被 which 误判为"不存在" ------只要容器里没装 which 包,宝塔任何软件安装(PHP/MySQL/Redis......)都会走同样的失败流程。修复 which 包是一次性解决全部这类误报。


7.2🔧 修复动作
bash 复制代码
# 1) 核心修复:安装缺失的 which 包(和宝塔同步安装 tree)
yum install -y which tree
#    → which-2.20-7.el7 / tree-1.6.0-10.el7 成功安装
#    → 此刻 which wget exit=0 ✅, which tar/xz/unzip/tree 全部 exit=0 ✅

# 2) 补 wget rpm 声明应有但实际缺失的 wget.info.gz(消除 rpm -V 非零退出)
gzip -9c /dev/null > /usr/share/info/wget.info.gz; chmod 644 $_
/sbin/install-info /usr/share/info/wget.info.gz /usr/share/info/dir || :
#    → rpm -V wget 只剩 S.5....T. c /etc/wgetrc(仅配置改动,正常)✅

# 3) 重新调用 nginx 安装脚本,后台编译安装
nohup bash -x /www/server/panel/install/nginx.sh install 1.26 > install.log 2>&1 &
#    → 这次不再在 wget 检测处退出,顺利走完:
#      install wget/tree ✔ → jemalloc 编译 ✔ → nginx 1.26.1 源码编译 ✔
#      → init.d 落盘 + chkconfig on ✔ → /etc/init.d/nginx start  Starting nginx... done ✔
#      → 基础网站流量统计程序安装完成 ✔

小结

Docker容器解决了多个不同项目同时开发要求的差异性开发环境问题,愿大家都能被温柔以待,少走一些弯路。共勉~