有时候你会觉得,离线部署是个"小众需求",但真做过几轮企业内网项目后你会发现,这才是最普遍的场景。
我去年给一个金融客户搭测试环境,机房在内网,别说访问 Docker Hub,连常规软件源都要走审批流程。机器是标准的 x86_64 架构,操作系统 CentOS 7.9,需要在上面部署 MySQL、Redis、Kafka 和 Nginx 这套常见中间件组合,而且没有外网。整个环境搭建从 Docker 引擎到各个中间件镜像,全部要走离线通道。
这篇文章就把我在这类环境下的完整操作思路和踩坑记录梳理一遍,覆盖 Docker 引擎离线安装、镜像离线搬运、中间件容器化部署(MySQL 8.0、Redis 主从、Kafka 集群、Nginx)、以及 docker compose 离线使用这几个核心环节。所有命令和配置都基于 x86 系统架构,适配常见的 CentOS / Ubuntu 等 Linux 发行版。
1. 为什么非要在离线环境里搞Docker:我的内网部署经历
1.1 离线场景不是少数,而是大多数
多数人学 Docker 都在自己笔记本上,联网、拉镜像、docker run 一把梭,觉得离线部署根本没必要。但只要你真正接触过企业内网、政务云、军工项目,就会发现"能联网"才是例外,离线才是常态。
这类环境有几个共性特点:
- 机器处于隔离网段,与外网物理隔离或仅有单向通道
- 软件包必须经过安全扫描和审批才能进入生产网
- 操作系统版本往往老旧,内核版本和 glibc 版本跨度很大
- 没有现成的 yum/apt 软件源,或者软件源同步滞后严重
在这种环境下,使用 Docker 反而更有优势。因为 Docker 镜像本身就是一种"自带运行环境和依赖"的交付物,只要引擎装好了,镜像导进去了,中间件跑起来的概率远大于直接部署二进制包。你不需要去解决 MySQL 依赖的 libaio、numactl,不需要编译 Redis,不需要处理 Kafka 需要的 Java 环境,镜像把这一切都包装好了。
我在那个项目里最大的体会是:离线环境的矛盾不在"Docker 能不能用",而在"怎么把 Docker 引擎和镜像高效地搬进内网"。把这个核心链路打通,后续所有中间件的部署节奏都和在线环境差不多。
1.2 离线安装的真实难点:不是拷贝文件,而是处理依赖链
很多人以为离线安装就是"把安装包复制过去,解压,运行",这是最大的误解。
初次尝试时,我直接在目标机器上拿了一个 docker-ce 的 rpm 包安装,系统立刻报依赖缺失:containerd.io >= 1.6.x、docker-ce-cli、docker-buildx-plugin、docker-compose-plugin。逐个下载之后再装,又报 container-selinux 缺失。这种依赖链问题在纯内网环境里会卡住不少人。
所以离线安装 Docker 的第一个核心原则是:不要试图手动逐个解决依赖,而是用包管理器的缓存机制一次性拉全所有依赖包。
具体做法我放在下一章,这里先强调一个认知:Docker 离线安装的重点不是 Docker 本身,而是"如何获取一套完整且版本匹配的安装包集合"。
1.3 开工前必须完成的物料准备
进入操作之前,先把物料清单准备好。以我常用的方案为例,你需要在联网机器上准备以下几样东西:
| 物料 | 用途 | 说明 |
|---|---|---|
| Docker 引擎安装包 | 安装 docker 服务 | 通过 yum/apt 下载全量依赖包,或使用静态二进制包 |
| 中间件镜像 tar 文件 | 离线导入 Docker 镜像 | docker save 导出,按需准备 MySQL、Redis、Kafka、Nginx 等 |
| docker compose 二进制或插件包 | 多容器编排 | 根据 compose 文件版本准备对应版本 |
| 镜像仓库镜像(可选) | 搭建内网 registry | 镜像多、环境多时强烈推荐 |
还有一件容易被忽略的事:确认目标机器的 CPU 架构。标题里提到 x86 系统架构,对应的镜像和安装包都是 x86_64 或 amd64 版本。如果你在 ARM 机器上执行同样的命令,会得到 exec format error 这类让人困惑的错误。用 uname -m 确认,输出 x86_64 代表这是 x86 架构。
另外,建议在联网机器上先建一个工作目录,比如 ~/docker-offline/,把所有下载内容集中管理,这样拷入内网时不会遗漏。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Docker引擎离线安装:从联网机器到内网机器的全链路
2.1 三步拿到Docker离线安装包
Docker 提供了多种安装方式,离线场景下我推荐两种,按系统类型选择。
第一种:基于包管理器缓存(适合 CentOS/RHEL 系列)
在联网机器上执行:
bash复制# 安装 yum 插件
yum install -y yum-plugin-downloadonly
# 创建目录,下载 Docker 及全部依赖
mkdir -p ~/docker-offline
yum install --downloadonly --downloaddir=~/docker-offline \
docker-ce docker-ce-cli containerd.io docker-compose-plugin
这会把你需要的 rpm 包和它们的所有依赖全部下载到 ~/docker-offline 目录,不会执行安装。注意 docker-compose-plugin 是 Docker Compose v2 插件,建议一并准备,后面编排多中间件时用得上。
第二种:静态二进制包(适合 Ubuntu/Debian 和特殊情况)
如果目标系统用 yum 不好处理,改用静态二进制包。Docker 官方发布页面提供 docker-{version}.tgz 的静态包,解压后直接可用,不依赖具体发行版。
bash复制# 在联网机器上下载
wget https://download.docker.com/linux/static/stable/x86_64/docker-24.0.7.tgz
tar -xzf docker-24.0.7.tgz
ls docker/
# 可以看到 containerd、containerd-shim-runc-v2、ctr、docker、dockerd、docker-init、docker-proxy、runc
这个包里的二进制文件可以直接拷入目标机器的 /usr/local/bin/ 目录。它的好处是不依赖系统包管理器,坏处是你需要手动创建 systemd service 文件来管理 dockerd 的启动。
我建议将两种方式都准备好,到了现场根据目标机器的实际情况选一种用,总比临时抓瞎强。
2.2 目标机器上的安装操作与验证
基于 rpm 包的安装流程:
把 ~/docker-offline 目录整个拷贝到目标机器(用 U 盘、内网传输工具都行),然后执行:
bash复制cd /path/to/docker-offline
# 直接安装当前目录下所有 rpm,自动顺序安装
rpm -Uvh *.rpm
# 启动 Docker 服务
systemctl enable --now docker
# 验证版本
docker --version
docker compose version
rpm -Uvh *.rpm 会自动处理当前目录下 rpm 包之间的依赖关系,只要包全,一般一步就能装上。装完用 docker info 看下基本信息,确认 Storage Driver、Cgroup Driver 都正常。
基于静态二进制的安装流程:
bash复制# 拷贝二进制到系统目录
cp docker/* /usr/local/bin/
# 创建 systemd service 文件
cat > /etc/systemd/system/docker.service <<'EOF'
[Unit]
Description=Docker Daemon
After=network-online.target
[Service]
Type=notify
ExecStart=/usr/local/bin/dockerd
ExecReload=/bin/kill -s HUP $MAINPID
LimitNOFILE=1048576
LimitNPROC=infinity
LimitCORE=infinity
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
# 重新加载 systemd 并启动
systemctl daemon-reload
systemctl enable --now docker
静态包方案还包含 containerd、runc 等组件,但不需要你手动启动它们,dockerd 会自动拉起。
2.3 docker服务启动失败的高频原因排查
离线环境装好 Docker 后,第一次启动不成功的情况我遇到过不止一次。最常见的几类问题:
问题一:iptables 相关报错
bash复制journalctl -u docker | grep -i error
# 常见输出:iptables failed: iptables --wait -t nat -A DOCKER ...
在 CentOS 7 上,firewalld 和 Docker 的 iptables 管理经常冲突。最简单的处理方式是先启动 Docker,再启动 firewalld,或者直接禁用 firewalld:
bash复制systemctl stop firewalld
systemctl disable firewalld
systemctl restart docker
问题二:SELinux 阻止容器运行
bash复制# 临时关闭
setenforce 0
# 永久关闭
sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
如果你的安全基线要求不能关 SELinux,就得给 Docker 配置 SELinux 标签,但这在离线环境下配置成本较高,我个人的建议是先以 permissive 模式跑通流程,后续再按安全要求加固。
问题三:存储驱动不被支持
老系统(CentOS 7 默认内核 3.10)上 Docker 默认存储驱动是 overlay2,如果文件系统不支持,dmesg 会看到 overlay 相关报错。可以切换为 vfs 驱动:
bash复制mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<'EOF'
{
"storage-driver": "vfs"
}
EOF
systemctl restart docker
注意 vfs 驱动性能比 overlay2 差不少,磁盘占用也大,但胜在兼容性最好。优先还是应该升级内核或者换文件系统,vfs 只是兜底方案。如果磁盘空间和性能都紧张,建议提前确认内核和文件系统满足 overlay2 的支持条件。
3. 镜像怎么搬进内网:save/load之外,还有Registry方案
3.1 docker save与docker load:小规模部署的标配
引擎装好之后,下一步就是让内网里的镜像"从无到有"。
在联网机器上准备好需要的镜像,然后导成 tar 包:
bash复制# 拉取镜像(在联网机器上)
docker pull mysql:8.0
docker pull redis:7.0
docker pull zookeeper:3.8
docker pull bitnami/kafka:3.5
docker pull nginx:1.25
# 导出为 tar 文件
docker save -o mysql-8.0.tar mysql:8.0
docker save -o redis-7.0.tar redis:7.0
docker save -o zookeeper-3.8.tar zookeeper:3.8
docker save -o kafka-3.5.tar bitnami/kafka:3.5
docker save -o nginx-1.25.tar nginx:1.25
在目标机器上导入:
bash复制docker load -i mysql-8.0.tar
docker load -i redis-7.0.tar
docker load -i zookeeper-3.8.tar
docker load -i kafka-3.5.tar
docker load -i nginx-1.25.tar
这里有几个细节值得注意:
第一,建议按镜像名+版本号命名 tar 文件。我第一次部署时直接 docker save -o mysql.tar mysql:8.0,到内网 docker load 之后,镜像名还能对上,但如果你同时导入了多个 tag 的 MySQL,tag 信息容易混乱,后续 docker run 时容易拉错版本。
第二,尽量用精简镜像。比如 Kafka 使用 bitnami/kafka 会比 wurstmeister/kafka 体积小一些,Nginx 用 nginx:alpine 比 nginx:latest 小不少。离线传输文件的成本是实打实的,镜像精简能省不少事。
第三,如果有多个 tar 包,导入时记得用脚本管理:
bash复制#!/bin/bash
for tar in *.tar; do
echo "importing $tar ..."
docker load -i $tar
done
3.2 自建Registry:当镜像数量开始失控时
如果只是三五台机器部署几个中间件,docker save / docker load 完全够用。但如果你管理的是几十台机器,或者后续要持续更新镜像,tar 包方案就捉襟见肘了——你需要在每台机器上重复导入,而且版本更新后还要重新传输整个 tar 文件。
这时就该在内网搭一个私有镜像仓库,核心组件就是 Docker 官方提供的 registry:2 镜像。
在联网机器上准备好:
bash复制docker pull registry:2
docker save -o registry-2.tar registry:2
到内网一台有更多磁盘空间的机器上导入并启动:
bash复制docker load -i registry-2.tar
docker run -d --name registry \
-p 5000:5000 \
-v /data/registry:/var/lib/registry \
--restart=always \
registry:2
然后在其他内网机器的 /etc/docker/daemon.json 里配置这个私有仓库地址:
json复制{
"insecure-registries": ["registry.internal:5000"]
}
配置后重启 Docker:
bash复制systemctl restart docker
之后你就可以像在线环境一样使用内网仓库了:
bash复制# 在其他机器上
docker pull registry.internal:5000/mysql:8.0
# 或者通过 tag 推送
docker tag mysql:8.0 registry.internal:5000/mysql:8.0
docker push registry.internal:5000/mysql:8.0
注意配置了 insecure-registries 后,Docker 会跳过对仓库 HTTPS 证书的校验,这是离线内网环境的常用做法。如果公司的安全规范不允许 HTTP 仓库,就得额外搭建 HTTPS 证书体系,这个成本相对高,一般私有内网环境不需要。
从我的实际经验来看,只要机器数量超过 5 台,或者有后续迭代需求,直接搭一个 registry 是更省力的选择。tar 包方案适合一次性部署,registry 适合长期维护。
3.3 离线环境下的镜像源问题与尺寸优化
关于"docker镜像下载慢"这些热词,在线环境多半是镜像源问题,但离线环境下没有镜像源配置可言——你没有外网可连。唯一能优化的是"搬运"环节。
几个实用经验:
- 对镜像做瘦身。用
docker images查看镜像体积,能换 alpine 版本就换 alpine 版本。一个python:3.11-slim和python:3.11之间可能相差 300MB。 - 用 docker history 排查镜像分层。如果 tar 文件异常大,可以用
docker history <image>查看每一层的大小,找到不必要的层。但要注意,历史层不能像 git 那样随便删,除非你重新构建镜像。 - 批量搬运时做压缩。
docker save出的 tar 文件在传输前可以再 gzip 一次,通常能压缩掉 20% 到 40% 的体积:
bash复制docker save mysql:8.0 | gzip > mysql-8.0.tar.gz
# 目标机器上
gunzip -c mysql-8.0.tar.gz | docker load
这条命令我每次离线部署都会用,省下的传输时间非常可观。
4. 中间件实操一:MySQL 8.0的容器化部署与数据持久化
4.1 搭一个能用的MySQL 8.0容器,记住这几个参数
MySQL 是内网项目里最常用的中间件之一。离线环境中,镜像已通过 docker save / docker load 备好,剩下就是 docker run 的参数设计。
一个典型的生产级启动命令:
bash复制docker run -d --name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD='MyStr0ngPass!' \
-e MYSQL_DATABASE=appdb \
-e TZ=Asia/Shanghai \
--restart=always \
-v /data/mysql/conf:/etc/mysql/conf.d \
-v /data/mysql/data:/var/lib/mysql \
-v /data/mysql/logs:/var/log/mysql \
mysql:8.0
逐项解释下参数的作用:
-e MYSQL_ROOT_PASSWORD:设置 root 账户的密码。这是初始化 MySQL 时最关键的环境变量,首次启动时生效。-e MYSQL_DATABASE:自动创建指定名称的数据库。可以省略,但建议设置,省得后面手动执行CREATE DATABASE。-e TZ=Asia/Shanghai:设置容器时区。不设置的话,MySQL 默认 UTC 时间,和业务系统的本地时间会有 8 小时偏差,排查问题时会很迷惑。-v /data/mysql/data:/var/lib/mysql:数据目录持久化。这是最重要的一条,容器删了重建,数据不丢就靠它。-v /data/mysql/conf:/etc/mysql/conf.d:自定义配置文件目录。这个目录下的.cnf文件会被 MySQL 自动加载。--restart=always:宿主机重启后容器自动拉起,内网机器一般没有频繁的运维干预,这个参数很重要。
启动后验证:
bash复制docker ps | grep mysql8
docker exec -it mysql8 mysql -uroot -p'MyStr0ngPass!'
4.2 数据初始化、字符集与配置优化
MySQL 8.0 默认字符集是 utf8mb4,但为了保险起见,我还是建议在配置文件里显式声明:
ini复制# /data/mysql/conf/my-custom.cnf
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init_connect = 'SET NAMES utf8mb4'
[client]
default-character-set = utf8mb4
[mysql]
default-character-set = utf8mb4
修改后重启容器:
bash复制docker restart mysql8
这里要提醒一下,如果你在初始化数据之后才改字符集,MySQL 可能会拒绝修改字符集。最佳实践是在容器首次启动前就把配置文件挂载好。我就吃过这个亏,当时容器跑了一周后才发现字符集不对,改配置重启直接报错,最后还是重新初始化了数据。
另一个常见的优化是调整 lower_case_table_names 参数。如果业务代码里对表名大小写不敏感,建议设置为 1:
ini复制[mysqld]
lower_case_table_names = 1
但注意这个参数在 MySQL 8.0 中必须在初始化数据目录之前设置,否则改完重启会报错。Linux 下 MySQL 8.0 的默认值是 0,也就是区分大小写,Windows 上才是 1。如果你的历史项目在 Windows 上开发,拿到 Linux 上跑的时候经常会出现"表不存在"的问题,就是这个参数在作怪。
4.3 常见坑:权限、时区、内存占用
权限坑: 容器内 MySQL 默认以 mysql 用户运行,宿主机挂载目录的属主必须匹配:
bash复制mkdir -p /data/mysql/{conf,data,logs}
chown -R 999:999 /data/mysql
这里的 999:999 是 MySQL 容器内 mysql 用户的 UID/GID。如果你不设置,MySQL 可能因为无法写入数据目录而启动失败,错误日志却不太直观。
时区坑: 上面提到过,务必设置 TZ 环境变量。排查时可以用 SELECT NOW(); 看当前时间,和宿主机 date 对比,一眼就能发现偏差。
内存坑: MySQL 8.0 默认的 innodb_buffer_pool_size 是 128MB,但对小内存机器来说,多个中间件同时跑起来内存会很紧张。可以在配置文件中调低:
ini复制[mysqld]
innodb_buffer_pool_size = 256M
performance_schema = OFF
performance_schema 占用的内存比较多,对普通业务场景关闭它影响不大。我在 4G 内存的机器上同时跑 MySQL、Redis、Kafka 时就靠这个避免 OOM。
备份恢复: 容器里的 MySQL 照样可以用 mysqldump 做逻辑备份:
bash复制# 备份
docker exec mysql8 sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --all-databases' > backup.sql
# 恢复
cat backup.sql | docker exec -i mysql8 mysql -uroot -p'MyStr0ngPass!'
内网没有云数据库的快照功能,定时备份脚本必须自己写好。我一般配合 crontab 做每日全备,保留最近 7 天的备份文件。
5. 中间件实操二:Redis主从复制与Kafka集群
5.1 用Docker拉起Redis主从架构
Redis 作为缓存中间件,在离线环境里部署最常见的方式就是主从结构,用来保证高可用基础。即便内网环境规模不大,主从也比单机稳妥,至少在重启维护时能减少业务影响。
先启动主节点:
bash复制docker run -d --name redis-master \
-p 6379:6379 \
-v /data/redis-master:/data \
--restart=always \
redis:7.0 redis-server --appendonly yes --requirepass 'RedisPass2024'
再启动从节点:
bash复制docker run -d --name redis-slave \
-p 6380:6379 \
--link redis-master \
-v /data/redis-slave:/data \
--restart=always \
redis:7.0 redis-server --appendonly yes \
--requirepass 'RedisPass2024' \
--replicaof redis-master 6379 \
--masterauth 'RedisPass2024'
注意几个参数:
--appendonly yes:开启 AOF 持久化,让内存数据在重启后不丢失。--requirepass:设置访问密码。主从都要设置,从节点还需要--masterauth来认证主节点。--replicaof redis-master 6379:从节点声明自己的主节点是谁。这里因为用了--link,容器内可以直接通过容器名访问主节点。- 从节点的数据目录挂载
-v /data/redis-slave:/data,保证持久化。
验证主从状态:
bash复制docker exec -it redis-master redis-cli -a 'RedisPass2024' info replication
输出中 connected_slaves:1 表示从节点连接正常。这整个配置在离线环境没有任何额外依赖,只要镜像导入成功,命令就能跑起来。
5.2 Kafka集群离线部署:镜像清单与内存配置
Kafka 是消息中间件里部署起来相对费心的一类,但 Docker 化之后至少解决了一半问题,重点是镜像选择和参数配置。
我建议用 Kafka 3.5 之后的版本,它自带 KRaft 模式,可以逐步替代 ZooKeeper。不过目前生产上 ZooKeeper 模式仍然比较常见,而且离线环境不建议追求最新,稳字当头。下面按 ZooKeeper 模式演示,这也是大多数存量项目的选择。
首先准备 ZooKeeper 和 Kafka 两个镜像:
bash复制# 在联网机器上
docker pull zookeeper:3.8
docker pull bitnami/kafka:3.5
docker save -o zookeeper-3.8.tar zookeeper:3.8
docker save -o kafka-3.5.tar bitnami/kafka:3.5
启动 ZooKeeper:
bash复制docker run -d --name zookeeper \
-p 2181:2181 \
-e ZOO_MY_ID=1 \
-e ZOO_SERVERS='server.1=zookeeper:2888:3888' \
-v /data/zookeeper/data:/data \
--restart=always \
zookeeper:3.8
启动 Kafka:
bash复制docker run -d --name kafka \
-p 9092:9092 \
-e KAFKA_BROKER_ID=0 \
-e KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092 \
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 \
--link zookeeper \
-v /data/kafka:/bitnami/kafka \
--restart=always \
bitnami/kafka:3.5
这里最容易被忽略、也最常出问题的参数是 KAFKA_ADVERTISED_LISTENERS。
这个参数决定了 Kafka 注册到 ZooKeeper 里的地址,客户端连接时会拿到这个地址。如果你的客户端在容器外(宿主机上),需要把它设置为宿主机可访问的地址,比如 PLAINTEXT://192.168.1.100:9092。
我遇到过不止一次:Kafka 容器起来了,ZooKeeper 连接也正常,但客户端连不上,报 Connection refused。排查到最后,就是 ADVERTISED_LISTENERS 设置成了容器内部主机名,容器外的客户端根本解析不了。
在单机单 Kafka 的场景下,最简单的做法:
bash复制-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://<宿主机IP>:9092
如果 Kafka 也要容器间互通(比如消费者也在容器里),可以配置多个监听器。但要注意,多监听器的配置在 Kafka 里规则比较复杂,新手建议先跑通单机场景再看集群扩展。
5.3 集群容器之间的网络通信细节
多容器之间通信,主要有两种方式:
--link:单机多容器场景最简单直接的方式,容器名即主机名。上面 Redis 和 Kafka 的例子都用到了。- 自定义 bridge 网络:更规范的方式,推荐使用。
bash复制docker network create middleware-net
# 创建容器时指定网络
docker run -d --name redis-master --network middleware-net \
...
docker run -d --name redis-slave --network middleware-net \
...
自定义网络的好处是:容器间可以通过服务名互相访问,不需要 --link 的附加机制,且自定义网络自带 DNS 解析,容器重启 IP 变化不影响访问。
离线环境里,这个方式尤其受欢迎。因为内网机器的 IP 往往是运维分配的,不排除后续调整。用网络别名而不是硬编码 IP,迁移时只需在新机器上重新创建容器,不用改配置。
6. 中间件实操三:Nginx网关与Spring Boot应用组合
6.1 Nginx容器化:静态资源与反向代理
Nginx 在中间件里的角色通常是统一入口。用容器部署 Nginx 相对轻松,但关键是配置文件管理。
启动命令:
bash复制docker run -d --name nginx \
-p 80:80 \
-p 443:443 \
-v /data/nginx/conf.d:/etc/nginx/conf.d \
-v /data/nginx/html:/usr/share/nginx/html \
-v /data/nginx/logs:/var/log/nginx \
--network middleware-net \
--restart=always \
nginx:1.25
在 /data/nginx/conf.d/ 下放入站点配置:
nginx复制server {
listen 80;
server_name app.internal;
location / {
proxy_pass http://app-server: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;
}
location /static/ {
alias /usr/share/nginx/html/static/;
expires 7d;
}
}
注意 proxy_pass http://app-server:8080; 这里的 app-server 是后端应用在自定义网络中的容器名。只要你的 Spring Boot 应用容器也在 middleware-net 网络中,Nginx 就能通过容器名访问到它。
6.2 将Spring Boot应用容器化并接入网关
离线环境下,Spring Boot 应用通常也是以镜像方式搬入内网。构建镜像的动作可以在开发机上提前完成(联网环境拉取基础镜像),然后把镜像 tar 包拷入内网。
一个典型的 Dockerfile:
dockerfile复制FROM openjdk:17-jdk-slim
ENV TZ=Asia/Shanghai
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
WORKDIR /app
COPY app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
构建和导出:
bash复制docker build -t app-server:1.0 .
docker save -o app-server-1.0.tar app-server:1.0
内网导入并启动:
bash复制docker load -i app-server-1.0.tar
docker run -d --name app-server \
--network middleware-net \
-e SPRING_PROFILES_ACTIVE=prod \
-e DB_HOST=mysql8 \
-e REDIS_HOST=redis-master \
--restart=always \
app-server:1.0
Spring Boot 应用通过环境变量获取中间件地址,DB_HOST=mysql8、REDIS_HOST=redis-master 对应容器名。这样应用代码里不用写死 IP,容器迁移时方便得多。
6.3 多中间件联合启动:docker compose离线用法
到了这一步,你可能会想:每次启动容器都要敲一长串 docker run 命令,实在繁琐。而且容器多了以后,启动顺序、网络配置、卷挂载都容易出错。
这时候就该用 docker compose 了。离线环境下,只要你提前准备了 docker-compose-plugin 或单独的 docker-compose 二进制,使用方法和在线环境完全一样。
一个综合的 compose 文件示例:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
container_name: mysql8
restart: always
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: 'MyStr0ngPass!'
TZ: 'Asia/Shanghai'
volumes:
- /data/mysql/data:/var/lib/mysql
- /data/mysql/conf:/etc/mysql/conf.d
redis:
image: redis:7.0
container_name: redis
restart: always
ports:
- "6379:6379"
command: redis-server --appendonly yes --requirepass 'RedisPass2024'
volumes:
- /data/redis:/data
nginx:
image: nginx:1.25
container_name: nginx
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- /data/nginx/conf.d:/etc/nginx/conf.d
- /data/nginx/html:/usr/share/nginx/html
depends_on:
- app-server
app-server:
image: app-server:1.0
container_name: app-server
restart: always
environment:
SPRING_PROFILES_ACTIVE: prod
DB_HOST: mysql
REDIS_HOST: redis
depends_on:
- mysql
- redis
使用方式:
bash复制# 启动所有服务
docker compose up -d
# 查看状态
docker compose ps
# 查看日志
docker compose logs -f
# 停止所有服务
docker compose down
离线环境下唯一需要注意的:docker compose 使用前先确认已经装好。如果用的是静态二进制方案装的 Docker,还需要单独下载 docker-compose 插件或独立二进制。我一般直接下载独立的 docker-compose-linux-x86_64 放到 /usr/local/bin/docker-compose,兼容性最好。
7. 复盘与避坑:离线部署路上我踩过的那些坑
7.1 最容易被忽略的glibc与内核版本问题
Docker 本身对系统层有一定要求,比如 64 位架构、Linux 内核版本 3.10 以上。但真正容易翻车的不是内核,而是 glibc 版本。
我在一个 CentOS 7.4 的老机器上尝试运行较新版本的容器时,遇到过容器启动直接退出,docker logs 看到 version GLIBC_2.28 not found 之类报错。这不是 Docker 引擎的问题,而是容器镜像里的二进制对宿主机内核/系统库版本要求更高。
解决办法有几种:
- 升级操作系统到 CentOS 7.9+ 或更高版本(离线环境下也要走 YUM 源,成本较高)
- 使用兼容性更好的镜像版本,比如
mysql:8.0的较老小版本、ubuntu:20.04系镜像 - 如果只是 Kafka 等 Java 类中间件报错,检查 JDK 版本是否匹配
另外,docker run 容器的兼容性主要取决于内核特性,不是 glibc。Docker 容器共享宿主机内核,如果镜像里的程序用了内核新特性(比如较新的系统调用),老内核上就会报错。这类问题定位比较隐蔽,建议离线部署前先确认宿主机内核版本:
bash复制uname -r
如果内核版本低于 3.10,Docker 本身都可能起不来。优先升级内核,别在这个环节死磕。
7.2 镜像导入后tag丢失的教训
这是我亲身经历过的一个坑。当时在联网机器上,我执行了:
bash复制docker pull mysql:8.0
docker pull mysql:latest
然后 docker save -o mysql.tar mysql:8.0 并拷入内网导入。导入之后 docker images 发现只有 mysql:8.0,没有 mysql:latest 的 tag。原因是 docker save 默认只保存指定 tag 对应的镜像和其依赖层,不会保存同一个镜像的其他 tag。
这个坑的变体是:如果你 docker pull 了某个镜像的多个 tag,docker save 时只指定一个 tag,内网导入后只有那个 tag 可用。如果部署脚本里写死了 mysql:latest,就会白白多踩一次"镜像不存在"的坑。
解决办法很简单——按 tag 分别导出,或者导出时一次性写多个 tag:
bash复制docker save -o mysql.tar mysql:8.0 mysql:latest
如果镜像已经在内网导入,发现缺 tag,可以重新导出并再次导入,docker load 会合并镜像层,不会重复占用大空间。
7.3 离线环境下容器重启后的数据安全
离线环境的运维手段有限,如果容器因为宿主机重启而异常停止,数据还在不在,完全取决于你的卷挂载是否做对了。
我的经验清单是:
- MySQL 必须挂载
/var/lib/mysql - Redis 如果开了 AOF,必须挂载
/data - ZooKeeper 必须挂载
/data和/datalog - Kafka 必须挂载数据目录
- Nginx 的配置目录必须挂载,否则容器重建就是一台空配置 Nginx
所有容器都加上 --restart=always,确保系统重启后 Docker 能自动拉起容器。这是离线环境最基础的兜底保障。
另外,容器升级或者替换时,一定要先确认旧容器的数据已经完整写入挂载卷,再执行删除。最稳妥的方式是:先 docker stop,再 docker rename 备份旧容器,再创建新容器,确认新容器正常后再删旧容器。
7.4 一次完整的离线内网清单核对
最后整理一份我在每个离线项目开始前都要核对一遍的清单,方便你直接抄作业:
| 项目 | 是否必备 | 检查要点 |
|---|---|---|
| Docker 引擎离线包 | 必备 | 确认包含 docker-ce、cli、containerd、compose 插件 |
| 中间件镜像 tar | 必备 | 确认 tag 完整,文件名包含版本号 |
| registry:2 镜像 | 强烈建议 | 机器多、后续要更新时必配 |
| 目标机器架构 | 必备 | 统一为 x86_64,避免混用 ARM 镜像 |
| 数据目录挂载规划 | 必备 | 建议所有中间件数据挂到独立目录 |
| 配置文件映射 | 必备 | 提前准备 MySQL 字符集、Nginx 站点配置 |
| 备份恢复方案 | 强烈建议 | mysqldump 脚本 + crontab |
| 时间同步 | 建议 | 内网机器如果不做 NTP,容器日志和宿主日志时间会乱 |
这套流程在 x86 系统架构下是通用的,不管操作系统是 CentOS 还是 Ubuntu,核心思路一致,只是包管理命令略有不同。
离线部署这件事,说难确实有门槛,但它不是靠"背命令"能解决的,核心是把依赖链、镜像传输、数据持久化这几条主线想清楚。只要主线通了,中间件再多也只是重复劳动。后续我还会针对 ARM 架构的离线部署单独写一篇,毕竟国产化替代之后,ARM 机器的需求也越来越多了。
