☰
宝塔面板Docker镜像固化:内网离线部署与Nginx反代全流程
2026/10/21 13:05:55 网站建设 项目流程

“宝塔面板做成 Docker 镜像,固化后导出到局域网直接使用”——这套流程听着挺小众,但实际做下来非常上头。我最近刚好在部门内网环境里把这个方案完整跑通了一次。场景是这样的:有几台服务器只能访问内网,外网基本不通,但开发、演示都要统一的 Linux 站点环境,手工一台台装宝塔装 Nginx 再配站,折腾一次至少一两个小时,还不算中间踩的依赖坑。于是我就想,能不能在能联网的机器上把宝塔面板连 Nginx 一起装进 Docker,做成镜像导出去,内网机器拿到镜像直接用。答案是可以,而且跑通之后效率高得离谱。这篇就把整个思路、命令、坑和可复用的配置都拆开讲清楚。

1. 为什么要在局域网折腾一个宝塔镜像

1.1 这个项目解决的真实痛点

先说需求本质。开发测试环境、内网演示环境、边缘机房的服务器,最常见的困境就是“没有外网”。你在一台新机器上装宝塔面板,看起来只是跑个安装脚本,实际上安装过程会去拉一堆系统依赖、PHP 扩展、Nginx 编译需要的源码包。外网一断,安装进度直接卡死在下载那一步。就算侥幸装完,也不保证两台机器上的版本、配置、站点是完全一致的。

Docker 镜像恰恰解决的就是“一次构建、处处运行”的问题。把宝塔面板、Nginx、PHP、站点初始配置全部打进一个镜像,导成 tar 包,通过 U 盘、局域网共享、HTTP 下载都行。目标机器只需要有 Docker,一条 docker load 加一条 docker run,几分钟内就能得到一个面板可用、站点可访问的完整环境。

这个方案的本质,相当于把一台已经装好宝塔、装好 Nginx、建好测试站的服务器,整个“拍快照”封起来,然后在另一台机器上“解压还原”。和虚拟机模板一个道理,只不过用的是 Docker 的镜像机制,轻量不少。

1.2 方案对比:为什么选择“手动装 + commit + save”

当时我列了三种思路,可以给你参考。

方案优点缺点适合场景
每台机器原生命令装宝塔无额外学习成本无外网环境下基本装不上;多次安装版本不一致单台机器、外网通畅
直接拉现成的宝塔 Docker 镜像省事,一条 pull 搞定镜像源必须可达;版本、组件不可控;内网环境照样拉不动临时测试、外网可控
自己构建镜像并导出分发一次构建,处处可用;完全离线部署;配置可定制初次构建耗时长;镜像体积偏大;需要理解 Docker 基本机制局域网批量部署、离线环境

我最终选了第三种。理由很现实:我手里的内网机器连 Docker Hub 都连不上,更别提拉什么现成镜像。自己做镜像的过程虽然第一次比较折腾,但构建完成之后,后续复制环境的边际成本几乎为零。

具体到“怎么构建”,又有一个重要的选择:用 Dockerfile 自动化构建,还是进入容器手动装完再用 docker commit 固化成镜像。Dockerfile 当然更“正经”,可复现性更好。但宝塔的安装过程有随机安全入口、有交互式输出、有面板初始账号密码生成,而且安装时间很长,期间往往要手动处理各种依赖问题。这种情况下硬写 Dockerfile 反而是在给自己添堵。所以我选择了手动进容器安装,装完配置好,再 docker commit。

打个比方,Dockerfile 像流水线生产,适合产品需要反复重建;docker commit 像请师傅手工做完一件家具再整体翻模,适合你要的是“这台机器的当前状态”。宝塔这种带大量运行时生成文件的东西,明显更适合后者。

1.3 Nginx 放在哪里:两种拓扑怎么选

标题里明确有“docker+nginx”,这里 Nginx 的角色就必须先说清楚,否则后面配置会乱。我见过两种做法,各有各的道理。

第一种是最直接的:容器内部装宝塔自带的 Nginx,站点监听 80 和 443,docker run 的时候把宿主机的 80、443 端口直接映射到容器。访问宿主机 IP 就等于访问容器里的网站。这种做法简单,适合容器独占宿主机端口、不打算做多站点入口管理的场景。

第二种是宿主机再装一层 Nginx,监听 80 和 443,把请求反向代理到容器内部 Nginx 的 8080 端口。也就是宿主机 Nginx 做入口网关,容器内 Nginx 做实际站点服务。好处很明显:宿主机这层可以统一做 SSL 证书、日志记录、访问限流;后面容器重启、IP 变化,对访问方来说入口 IP 始终不变;如果同一个宿主机还要跑其他业务,也可以按域名分流。

我最终采用的是第二种。至于原因,还是回到“内网运维”这个场景——你不可能每次重启容器都去通知开发人员换 IP。入口稳定比什么都重要。下面的实操也会按这个拓扑来讲。

2. 动手前必须想清楚的三件事

2.1 基础镜像和容器启动方式

很多人第一次在 Docker 里装宝塔,直接docker run -it centos:7 bash进去就跑安装脚本,结果各种诡异问题:安装到一半服务起不来、面板打不开、重启容器后宝塔彻底失联。根源绝大多数出在“PID 1 进程”和“systemd”的问题上。

普通容器的 PID 1 就是你的 shell 或者某个前台进程。而宝塔面板需要管理 Nginx、MySQL、PHP 一堆服务,靠的是 systemd 或者 init 脚本。容器里如果只有 bash,就没有一个完整的服务管理机制,服务就算装上也无法自启和维护。

解决办法是用/usr/sbin/init作为容器的启动命令,让 systemd 成为容器内的 PID 1。配合--privileged=true给足权限(宝塔要操作 iptables、挂载、系统参数等),才能基本模拟一台真实服务器的环境。

基础镜像选择上,我用的是centos:7。宝塔对 CentOS 系的兼容性历史最久,yum 源问题也好解决。Ubuntu、Debian 也能跑,但遇到奇怪问题的概率高一些,内网环境下排查成本更高,不如直接用 CentOS 7 省心。

2.2 端口规划

端口规划是整个镜像方案中最容易忽略、后期最头疼的环节。宝塔面板默认端口是 8888,面板内置的 phpMyAdmin 常走 888 端口,Web 站点走 80/443,FTP 走 20/21 以及一大段被动端口。如果这些端口在宿主机上已经被占用,那端口映射就得重新规划。

下面是我这次的端口分配,直接贴出来供参考。

端口用途本次处理方式
8888宝塔面板入口映射到宿主机 8888,可内网访问
888phpMyAdmin映射到宿主机 888,非必需可关闭
80容器内站点监听容器内改为 8080,不直接占宿主机 80
443HTTPS 站点映射到宿主机 443(或由宿主机 Nginx 承担,按需)
8080容器内 Nginx 站点端口映射到宿主机 8080,作为宿主机 Nginx 的反代目标
20/21FTP 服务按需映射,本次省略

仔细看会发现,我没有把宿主机 80 映射给容器 80。这是因为宿主机 80 要留给宿主机 Nginx 做统一入口。容器内站点监听 8080,宿主 Nginx 再反代过去。这样 80 端口只属于宿主机 Nginx,不会和容器内服务打架,也不会出现“宿主机 Nginx 和容器 Nginx 都想占 80”的僵局。

思路一定要在开始动手前定下来。后面如果中途想改拓扑,容器内部 Nginx 配置和宿主机 Nginx 配置都要动,费时费力。

2.3 容器安全与面板加固

内网环境不等于绝对安全。镜像一旦分发到多个节点,面板的账号密码、安全入口就是所有节点的共同入口。所以我在构建镜像时做的第一件加固就是改默认安全入口、换强密码、绑定访问 IP。

宝塔的默认安全入口是一串随机字符串,安装的时候会展示。建议把这串字符串直接改成固定的、好记但不好猜的路径,比如在面板设置里改安全入口为/panel_admin_xxx。同时关闭面板的“离线模式”以外的多余对外服务,比如不需要的 phpMyAdmin 端口就不要映射。

还要注意一点:镜像里保存的密码是构建时的密码。分发出去后,各个节点如果共用同一个密码,万一泄露就是批量泄露。所以不要在镜像里设置过于重要的生产密码。这个镜像定位为“内部开发与演示环境”,生产数据和服务一律不放在这种镜像里。

3. 从零到一:容器内安装宝塔的完整过程

3.1 先用一条 run 命令把容器拉起来

前面理论讲了一堆,现在开始实际操作。首先在能联网的机器上,确认 Docker 已安装并正常启动,然后执行这条命令创建容器:

docker run -itd \ --name bt-node \ --privileged=true \ -p 8888:8888 \ -p 8080:8080 \ -p 443:443 \ -p 888:888 \ centos:7 \ /usr/sbin/init

参数逐一解释:

  • --name bt-node给容器起名,后面 commit、exec 都用这个名字。
  • --privileged=true给容器 root 权限,宝塔在管理防火墙和系统服务时需要。
  • -p 8888:8888映射面板端口。
  • -p 8080:8080给容器内 Nginx 站点监听端口预留。
  • -p 443:443留着以后上 HTTPS。
  • -p 888:888映射宝塔的 phpMyAdmin 入口,不需要的可以去掉。
  • centos:7基础镜像。
  • /usr/sbin/init最关键,让 systemd 作为容器主进程。

创建完用docker ps看容器是否在运行。如果状态是 Exited,多半是/usr/sbin/init在当前内核环境有问题,先用docker logs bt-node看日志,再继续排查。

3.2 容器内的基础环境准备

容器起来后,进入容器内部:

docker exec -it bt-node bash

进去后第一件事是换 yum 源。因为 CentOS 7 的默认源已经有不少失效或极慢的情况,直接装东西极其痛苦。我习惯换成国内常见的镜像源,具体源地址根据你自己网络的实际情况配置,也可以直接用官方源碰碰运气。换完源后执行:

yum install -y wget curl vim net-tools tar

注意这里的每一项都有用。wget 和 curl 是装机必备,等下要下载安装脚本;vim 用来改配置;net-tools 提供 ifconfig 和 netstat,排查网络问题时离不了;tar 是后面压缩备份的依赖。

顺手把时区也改了,不然后面板日志时间对不上:

ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

还有一个容易被忽略的点:容器默认没有 swap,而宝塔安装过程中很可能要编译 MySQL 或 PHP,内存不够时直接 OOM。如果宿主机内存不大,建议在docker run时加上内存限制或者提前给宿主机分配 swap 空间。我这次构建的机器有 8G 内存,只装 Nginx 不装 MySQL 的情况下没问题,但如果要装 MySQL,至少保证 4G 以上可用内存,否则编译到一半被 kill 是家常便饭。

3.3 安装宝塔面板

基础环境就绪后,开始安装宝塔面板。宝塔官网提供在线一键安装脚本,核心就是两条命令:

wget -O /tmp/install.sh <宝塔官方下载地址> bash /tmp/install.sh

官方脚本会检测系统环境,过程中会安装一大堆依赖,输出几百行日志属正常现象。耐心等,不要中途关终端。装完后,终端底部会打印面板访问地址、用户名、密码、安全入口。把这段信息复制保存下来。

如果你所在的环境连宝塔官方下载地址都不通,那只能在有外网的机器上先把脚本下载好,再通过内网传入容器。实际操作中我更推荐这种方式:在宿主机上下载好脚本,通过docker cp拷进容器再执行,更加可控。

安装完成后立刻验证面板服务状态:

ps aux | grep bt

能看到宝塔面板相关进程在跑,就说明基本成功。

3.4 配置 Nginx 并创建一个测试站点

面板装好之后,打开浏览器访问http://宿主机IP:8888/安全入口,首次进入会要求同意用户协议,然后进入面板首页。在面板的软件商店里找到 Nginx,点击安装。这里有个选择:可以装 Nginx 1.x 稳定版,也可以装 OpenResty,我用的是 Nginx 稳定版,兼容性最稳。

Nginx 装好后,打开“网站”页面,添加站点。站点域名这里有个小技巧:内网环境没有真实域名,可以直接填宿主机的内网 IP,或者填一个形如demo.local的假域名并在浏览器 hosts 里做解析。我建议填假域名配合 hosts 解析,这样以后迁移到别的 IP 也不用改站点配置。

关键一步:创建站点时,把端口从默认的 80 改到 8080。在站点设置里找到“域名”或“端口”配置,改成8080。这一步对应前面说的拓扑——容器内 Nginx 监听 8080,宿主机 80 由宿主机 Nginx 负责。

创建完成后,在容器内测试:

curl -I http://127.0.0.1:8080

如果能收到 HTTP 200 响应,说明容器内 Nginx 和站点已经正常工作。这个测试站点会在 commit 之后被带进镜像,分发到其他机器后,访问者的第一眼就是一个活站点,而不是光秃秃的面板登录页。

4. 宿主 Nginx 反代接入 + 镜像固化导出

4.1 配置宿主机 Nginx 反向代理

容器内部已经跑起来了,现在回到宿主机层面。宿主机上安装 Nginx(注意是宿主机上的 Nginx,不是容器里的),然后写一个 server 块,把宿主机 80 端口的请求反代到容器内 Nginx 的 8080。

配置示例:

server { listen 80; server_name demo.local 192.168.10.10; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }

这里192.168.10.10要换成你宿主机的实际内网 IP。proxy_pass指向127.0.0.1:8080,是因为我们已经把容器 8080 映射到了宿主机 8080,宿主机访问自己的 8080 就等于访问容器内 Nginx。

配置好后重载 Nginx:

nginx -t && nginx -s reload

然后从局域网的另一台机器访问http://宿主机IP/,如果能看到刚才创建的测试站点,说明宿主机 Nginx 反代链路通了。

这一步是整个方案里最体现“docker+nginx”价值的地方。宿主机 Nginx 管入口,容器 Nginx 管站点,分离两层职责。以后不管容器怎么换 IP、怎么重启,对外入口不变。

4.2 提交镜像前先做瘦身

容器功能验证完毕,接下来要固化镜像。但直接 commit 会把容器里一大堆临时文件也一起带走:yum 缓存、安装包、日志、临时下载文件,镜像做出来动辄十几个 GB,内网传输想哭。

我的瘦身动作分三步。第一步在容器内清 yum 缓存和临时文件:

docker exec -it bt-node bash -c "yum clean all && rm -rf /var/cache/yum/*"

第二步清理宝塔自身的日志和备份缓存。站点日志在/www/wwwlogs目录下,测试产生的访问日志全部清掉。安装包缓存一般在/root或者/tmp下面,也一并删除。

第三步是停止容器再提交。虽然 Docker 支持运行中 commit,但停止状态能保证文件系统一致,避免提交出来的镜像里出现写到一半的配置。执行:

docker stop bt-node docker commit bt-node bt-panel:bt1.0

commit 的过程会快照整个容器的文件系统,包括系统配置、宝塔面板、Nginx、站点文件。耐心等几分钟,完成后用docker images查看镜像大小。如果体积还在 5G 以内,属于正常范围。

4.3 导出镜像并通过局域网分发

镜像提交好后,导出为 tar 包。常见的命令:

docker save -o bt-panel-bt1.0.tar bt-panel:bt1.0

这个 tar 会非常大,建议压一下再分发:

gzip bt-panel-bt1.0.tar

压缩完就得到一个bt-panel-bt1.0.tar.gz。这里说一句docker save和docker export的区别。save 保存的是完整的镜像分层结构,load 之后还原出来的还是一个标准镜像,可以直接用来docker run,而且 commit 的历史都在。export 导出的是容器文件系统的扁平 tar,import 之后得到的镜像没有分层历史,也不太适合后续二次分发。我们的目标是让内网机器拿到镜像直接运行,所以要用 save 加 load。

分发方式上,我这次用了很土但很稳的办法:在存放 tar 包的目录起一个临时 HTTP 服务。

cd /data/docker-images && python3 -m http.server 8000

内网目标机器上直接下载:

curl -O http://192.168.10.10:8000/bt-panel-bt1.0.tar.gz

如果没有 python3,也可以用scp直接传到对端,或者通过局域网共享目录拷贝。小范围分发用 scp,批量分发用 HTTP 更省事。我实际测试下来,一个 4G 左右的 tar.gz 在千兆内网里传输,大概一两分钟就完成,体验完全可以接受。

4.4 目标机器导入与运行

镜像包传到目标机器后,导入:

docker load -i bt-panel-bt1.0.tar.gz

load 支持 gzip 压缩包,会自动解压。加载完确认镜像存在:

docker images | grep bt-panel

然后启动容器,这里的端口映射必须和构建时保持一致,否则面板能开但站点端口对不上:

docker run -itd \ --name bt-node \ --privileged=true \ -p 8888:8888 \ -p 8080:8080 \ -p 443:443 \ -p 888:888 \ bt-panel:bt1.0 \ /usr/sbin/init

容器启动后,执行docker exec -it bt-node bash进入容器,确认一下宝塔的关键服务都活着:

ps aux | grep bt

接着在浏览器访问http://目标机器IP:8888/安全入口,输入构建时保存的账号密码。如果能看到面板,说明镜像分发成功。再访问http://目标机器IP/,如果能看到之前创建的测试站点,说明宿主机 Nginx 这一层还需要在目标机器上补配置。对,镜像里固化的只有容器内 Nginx,宿主机 Nginx 并不在镜像里,所以每台目标机器要么自己装宿主机 Nginx 做反代,要么直接访问http://目标机器IP:8080看站点。

这就是为什么我在前面强调“先定拓扑”。如果目标机器只想要最简环境,不搞宿主机 Nginx,访问 8080 端口完全够用。但如果要做统一入口,每台目标机器都需要复制一份宿主机 Nginx 配置,这个配置本身也可以做成一个脚本或者一个小的 Docker 容器分发下去。

5. 我踩过的坑和排查速查表

5.1 面板装不上的几种原因

第一类是curl: command not found或者wget: command not found。基础容器里通常没有这些工具,装面板脚本之前必须把 wget、curl 装上。

第二类是安装脚本跑到一半报“没有足够的磁盘空间”。commit 出来的镜像如果已经导入目标机,运行容器时默认使用 Docker 的存储驱动空间,一旦宿主机的磁盘空间紧张,安装过程可能在编译或者下载时被中断。构建前用df -h确认一下空闲空间,至少留 20G 以上。

第三类是脚本压根没跑起来,提示安装环境校验失败。这种情况多半是容器缺少必要的内核模块或权限,确认docker run时带了--privileged=true,并且启动命令是/usr/sbin/init而不是/bin/bash。

5.2 容器端口与防火墙

面板打不开,90% 是端口问题。排查顺序是这样:先curl http://127.0.0.1:8888/安全入口在容器内测,通了说明服务正常;再在宿主机上 curl 宿主机 IP 的 8888,通了说明端口映射正常;最后在局域网其他机器上测,如果只有这一步不通,那就是宿主机防火墙挡了 8888 端口。

内网环境常见的情况是宿主机开了 firewalld,默认只放行 22、80、443 等端口。8888、8080 这些建议在构建镜像前就统一加到防火墙白名单:

firewall-cmd --permanent --add-port=8888/tcp firewall-cmd --permanent --add-port=8080/tcp firewall-cmd --reload

别漏了 8080,站点反代链路断了的一大原因就是宿主机防火墙把 8080 挡了,但 80 是通的,访问网站出现 502。

5.3 commit 后开机服务起不来

这是最容易踩的深坑。你 commit 的时候容器是停止状态,但镜像里保存的系统服务是否能在新容器启动时自动跑起来,完全取决于启动命令是不是/usr/sbin/init。如果你在目标机器上用了docker run ... bt-panel:bt1.0 bash这种方式进入容器,PID 1 不是 systemd,宝塔的服务不会有任何自动拉起逻辑,结果就是面板打不开、Nginx 不在线。

解决方式很简单:目标机器启动容器时必须用和构建时一致的/usr/sbin/init。如果忘了,已经用错了命令启动容器,可以先把容器停了删掉,再重新用正确的命令跑一次。

另外还有一个隐患:构建镜像时如果修改了面板的端口或者安全入口,这些信息不会自动同步到其他节点。我建议在镜像里的/root/README写清楚默认端口、安全入口、账号密码、启动命令说明,这样后续接手的人不用翻聊天记录找信息。

5.4 数据持久化的提醒

如果你打算把这个镜像用于长期运行,而不是只做测试演示,数据持久化必须提前规划。宝塔面板的所有数据都在/www目录,包括网站源码、数据库文件、面板配置。

最直接的办法是把/www挂载到宿主机目录:

docker run -itd \ --name bt-node \ --privileged=true \ -v /data/www:/www \ -p 8888:8888 \ -p 8080:8080 \ bt-panel:bt1.0 \ /usr/sbin/init

这里有个大坑:如果你的宿主机/data/www是一个全新的空目录,把这个空目录挂载到容器的/www,会直接覆盖镜像里已经配置好的全部数据,相当于镜像白做了。正确做法是第一次先不用-v启动容器,跑起来后把容器里的/www内容完整拷贝到宿主机目录:

docker cp bt-node:/www /data/www

然后把容器停掉删掉,再用带-v的命令重新创建容器。这样/www内容在宿主机上有一份,之后再重启容器、升级镜像,数据都还在。

5.5 镜像太大怎么办

如果做完的镜像超过 10G,传输起来确实难受。瘦身的思路除了前面说的清理 yum 缓存、日志、临时文件,还可以考虑只装必要的组件。这次构建我只装了 Nginx 和 PHP,没有装 MySQL,体积直接小了一截。如果一定要带数据库,建议装 MySQL 而不是 MariaDB,虽然体积差不多,但有些依赖包更精简。另外,不要急着把各种扩展全装上,用到哪个装哪个,构建镜像时的“洁癖”会在分发时变成实打实的速度优势。

我个人在实际操作中的体会是,这套“镜像一次建成、内网重复分发”的做法特别适合那种需要快速搭建演示环境或者多节点统一环境的团队。启动容器时用-v挂载数据目录,升级时只需要替换镜像并保留挂载数据,业务迁移的体验几乎可以比拟云平台的模板功能。最后再分享一个小习惯:每次构建完镜像,都在镜像内的/root下留一个文本说明,写清楚构建日期、端口规划、安全入口、账号密码以及启动命令,这样不管是自己三个月后再用,还是同事接手,都不会因为记忆模糊而卡壳。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询