☰
x86架构下Docker离线安装与中间件容器化部署实操指南
2026/10/13 4:04:12 网站建设 项目流程

1. 离线部署的整体思路与方案选型

1.1 为什么需要离线安装docker

先聊一个我经常被问到的问题:好好的服务器,为什么非要离线装docker?

答案其实很现实——很多项目环境是物理隔离的内网,比如政务云、金融机房、电力专网,甚至是某些企业内部研发网。在这些网络环境里,服务器能上电、能跑业务,但访问不了外网,更别说直接连docker hub拉镜像了。有的环境甚至yum源都是内网定制的,外网源根本不通。

另一个场景是交付。我给客户做过一套模拟项目X的交付,现场环境要求把整套中间件(MySQL、Redis、消息队列)在两小时内跑起来。客户机房能给的就是几台裸机,没有外网,没有现成的镜像源,甚至没有一台能临时当镜像仓库的机器。这个时候,你如果在现场才去装docker、才去想办法拉镜像,那交付时间就彻底失控了。

所以,离线安装不是说“图省事”,而是很多企业级环境的硬性要求。把这套流程吃透,你在任何内网环境都能快速把容器运行时搞定,再配合离线镜像包,把中间件整套拉起来,这是运维和交付人员必须具备的基本功。

还有一个容易被忽略的点:x86系统架构。标题特别强调了这一点,因为不同CPU架构拉取的镜像是不通用的。你在一台ARM服务器上导出的镜像,扔到x86服务器上直接跑,大概率会报“exec format error”。我这篇文章讲的所有内容,默认都是x86_64体系,也就是Intel或者AMD的CPU。你如果现场用的是ARM架构(比如鲲鹏、飞腾),那拉镜像的时候要选对应架构的tag,导出导入的流程一样,但镜像本身不能混用。

1.2 三种主流方案对比

离线装docker,我实际操作下来主要有三条路:

  • 方案A:rpm包离线安装。在有网的机器上,用yumdownloader或者直接手动下载docker相关的rpm包,拷贝到目标机器上,用rpm -ivh或者yum localinstall离线安装。这个方案最直接,适合那种目标机器上什么都没有、连yum源都不通的场景。

  • 方案B:内网yum源。如果现场已经有一套定制的内网yum仓库,可以把docker的rpm包扔进去,然后目标机器配置指向内网源,正常yum install。这个方案对机器多、需要批量装的情况更友好,但前提是你得有权限往yum源里放东西,而且得先有一套能用的内网源基础设施。

  • 方案C:静态二进制解压。官方其实提供docker二进制压缩包,下载后解压,把可执行文件扔到/usr/bin,配置systemd服务托管。这个方案很干净,不依赖rpm包管理器,适合那种系统版本特别老、或者你不想污染系统rpm库的场景。

我个人的建议是:如果机器数量少(1到10台),直接rpm包离线安装,维护成本最低,卸载也干净。如果机器数量多(几十台往上),优先内网yum源,批量操作省心。静态二进制方案适合特殊系统,或者你想把docker目录整个定制化的场景。

方案没有绝对的好坏,核心是看现场条件限制。你到了现场再临时纠结方案,那是最要命的。

1.3 选择离线版本号的注意点

离线安装有一个特别容易踩的坑:版本选择。

很多人在有网环境装docker,直接curl -fsSL https://get.docker.com | sh,装完是啥版本就是啥版本,也无所谓。但离线环境不一样,你带进去什么版本,现场就是什么版本,想升级还得重新拷包进网。

我的经验是:选择centos 7或者RHEL 7.9兼容的docker 20.10.x版本,或者直接选docker 24.x系列。20.10这个系列在国内很多企业的存量环境里验证过,稳定性和兼容性都很好,不管是老内核(3.10)还是新内核(5.x),表现都可控。docker 24.x对containerd的版本要求更高,如果你现场系统特别老(比如centos 7.2),有些依赖可能需要额外处理。

还有一个更稳妥的做法:连docker-compose(或者现在的docker compose插件)一起带进去。部署中间件不可能全靠裸docker run,编排才是常态,我后面会详细讲。

我本人在现场通常带的是一整套rpm目录,包括:

  • docker-ce(主程序)
  • docker-ce-cli(命令行工具)
  • containerd.io(容器运行时)
  • docker-compose-plugin(compose v2插件)

这样四件套齐了,后续部署中间件的时候就不用再纠结缺组件。

2. 基础环境准备与检查清单

2.1 操作系统与内核版本确认

到了现场,第一件事不是急着拷安装包,而是先把目标机器的底细摸清楚。

我在一次现场交付中遇到过这种事:客户说机器系统是centos 7,结果上去一看是centos 7.2,内核3.10.0-327。这个内核版本配合docker高版本会出现overlay2存储驱动不支持的情况,虽然docker会自动回退到vfs,但性能惨不忍睹。最后是让客户帮忙升级了内核补丁,折腾了大半天。

所以,进机房第一件事,执行这几条命令:

cat /etc/os-release uname -r arch

重点确认三点:系统是什么发行版、内核是多少、CPU架构是不是x86_64。

docker官方对centos系统的建议是内核版本不低于3.10,但如果用overlay2驱动,3.10.0-514(对应centos 7.3)以下的版本会有兼容性问题。怎么判断当前内核是否支持overlay2?最简单的方法:cat /proc/filesystems | grep overlay,有输出就说明内核支持,没输出就只能用vfs,性能打折但能用。

系统版本太老的情况下,比如centos 6.x,那docker 20.10装起来会很别扭,我建议直接和客户沟通先升级系统,否则你后续排查问题会被系统底子拖死。

2.2 磁盘分区与存储规划

磁盘问题是我遇到的最多“现场翻车”原因之一。

docker装完后默认的数据目录是/var/lib/docker,容器日志、镜像层、容器可写层全在这里。如果你现场服务器的根分区只有50G,而你要部署MySQL数据、ES索引、Kafka日志,那这个分区一天就能被打满。

先看磁盘情况:

df -h lsblk

如果/var/lib/docker所在分区空间不够,有两个处理思路:

一是挂数据盘。在安装之前先把新磁盘格式化、挂载到某个路径(比如/data),然后配置docker使用/data/docker作为数据目录。这个操作在docker的配置文件中直接指定,不用重新安装。

二是如果docker已经装了,数据目录也可以迁移。我的建议是最好在装完docker之后、拉镜像之前就把目录改好,否则镜像和容器数据迁移会额外增加一步,而且容易漏网。

磁盘规划还有一个容易被忽略的点:inode耗尽。这个我不会在这里展开,后面常见问题环节细说。

2.3 基础依赖与软件包准备

docker的rpm包依赖还是比较多的。离线安装最烦的就是缺依赖,现场没网,缺一个就得想办法传一次。

20.10版本的常见依赖包括:

  • container-selinux
  • containerd.io
  • docker-ce-cli
  • iptables、libtool、libseccomp等系统基础库

我的习惯是,在有网的机器上,用一个干净环境执行:

yumdownloader --resolve --destdir=/tmp/docker_offline docker-ce docker-ce-cli containerd.io

把docker本体和所有依赖全部下载到本地目录。--resolve参数会自动解析依赖,连带依赖包一起下载,这个比手动yum install --downloadonly更直观,而且不会把无关的包卷进来。

不过要注意,不同系统版本和不同yum源环境,依赖包列表可能不一样。严格来说,最靠谱的获取方式是在一台和现场同版本系统的机器上执行下载,这样依赖的版本才能完全对上。

软件包准备好了之后,不要马上拷进去就装。现场确认一下目标机器的selinux状态:

getenforce

docker和selinux的关系非常微妙。CentOS 7默认selinux是enforcing,这时候装docker20.10以上版本一般没问题,但容器里某些操作会被selinux拦,特别是挂载目录、访问设备节点这些场景。我个人见到过太多“容器起不来查半天是selinux拦了”的情况了。

如果现场业务对selinux没硬性要求,我一般建议临时改为permissive:setenforce 0,然后修改/etc/selinux/config改成disabled。但如果客户安全策略要求selinux必须开启,那就保持enforcing,尽量不要动,遇到权限问题再针对性处理。

3. docker离线安装实操

3.1 离线包的拷贝与校验

把rpm包从本机拷到目标机器的方式,最常见的是U盘拷贝或者scp。如果现场只有Windows跳板机,那一般是先传到跳板机,再通过跳板机传到Linux服务器。

我自己的习惯是,拷贝完成之后做一次校验,防止传输过程中文件损坏。方法很简单:

在有网机器下载完成后,记录所有rpm包的sha256值:

sha256sum /tmp/docker_offline/*.rpm > /tmp/docker_offline/checksum.txt

把校验文件一起拷到现场,目标机器上执行:

sha256sum -c checksum.txt

看到每个文件都显示OK,再进行安装。这一步看起来很“多余”,但实际救过我一命——有一次U盘拷贝的文件在传输过程中损坏,直接rpm安装报错,还很难排查。有了校验,30秒就能定位是文件问题还是系统问题。

3.2 rpm包安装与启动验证

安装命令很简单,但有讲究。

最稳妥的安装方式是:

cd /tmp/docker_offline rpm -ivh *.rpm

rpm -ivh会逐个安装,遇到依赖缺漏会明确报错。如果报错提示缺了某个依赖,说明你下载依赖的时候漏包了。这时候去有网的机器补下载,再传进来,重新执行安装。

还有一种更省事的方式:

yum localinstall *.rpm

这个命令会利用本地的rpm缓存自动解决依赖顺序,不用手动排安装顺序,体验好很多。但前提是你现场机器能正常使用yum命令(不需要外网,本地localinstall本来就不走网络)。

安装完成之后,先不要急着启动,把配置改好了再启动,省得反复重启服务。

配置docker服务自启动,然后设置数据目录。先创建配置目录:

mkdir -p /etc/docker

然后编辑daemon.json,我下面单独讲。

配置完成后再执行:

systemctl enable docker systemctl start docker systemctl status docker

查看docker版本验证:

docker version docker info

注意,docker version会同时显示Client和Server两部分信息。如果只有Client信息而没有Server信息,说明docker服务没起来,或者daemon启动失败了。这时候journalctl -u docker查日志,看具体报错。

3.3 docker核心配置:daemon.json详解

daemon.json是docker守护进程的配置文件,很多问题排查到最后都回到这里。

我总结了一份在离线/内网环境下比较实用的配置:

{ "data-root": "/data/docker", "exec-opts": ["native.cgroupdriver=systemd"], "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" }, "storage-driver": "overlay2", "registry-mirrors": [], "insecure-registries": ["10.0.0.10:5000"], "iptables": true, "ip-forward": true, "bridge": "none" }

每项背后的逻辑我说下:

>docker pull mysql:8.0.32 docker pull redis:7.0.11 docker pull nginx:1.24.0 docker pull rabbitmq:3.11-management docker pull eclipse-mosquitto:2.0 docker pull elasticsearch:7.17.12 docker pull kibana:7.17.12 docker pull bitnami/kafka:3.5.0

这里我不推荐拉latest标签。离线环境没有网络,latest标签的镜像内容会随时间漂移,而且如果现场需要复现,latest的不确定性太大。指定精确版本号,保证可复现性,这是我交付时的基本原则。

拉完之后,导出为tar包:

docker save -o images_middleware.tar mysql:8.0.32 redis:7.0.11 nginx:1.24.0

注意,docker save和docker export是两个完全不同的命令。save是把镜像打包(包含所有层,可以重新导入成为镜像),export是把容器打包(把容器的文件系统导出来,导入之后没有历史层信息)。离线分发镜像,必须用save,不要用export。

打包完之后,检查一下tar包大小。一个MySQL镜像大概500MB左右,ES接近1GB,全套中间件打包下来可能有几个GB。拷贝的时候要考虑传输方式和时间,用U盘没问题,但走慢速跳板机传输的话,时间成本要提前估算。

4.2 镜像导入:现场离线导入

到了现场之后,把tar包拷贝进去,执行导入:

docker load -i images_middleware.tar

加载过程中会看到每一层逐步加载的日志。加载完成后,验证镜像是否正确:

docker images

这里会显示所有镜像的仓库名、标签和大小。如果导入之后发现镜像名带<none>,说明在save的时候没有指定正确的仓库名,或者镜像本身是旧的构建方式。问题不大,可以通过docker tag重新打标签。

还有一种情况:tar包损坏导致load失败。load失败会直接报错,错误信息通常会提示archive/tar: invalid tar header之类的。这种情况下只能重新拷贝一遍,基本就是传输导致的问题。

4.3 搭建私有镜像仓库(Harbor)

如果现场机器比较多,或者后续还有版本更新需求,我不建议每台机器都手动load镜像包,而是建议搭一套私有镜像仓库。Harbor是业界最常用的方案,功能完整、有Web界面、支持项目权限控制。

离线环境下搭Harbor的思路是:

在有网的机器上拉取Harbor的离线安装包(Harbor官方提供离线安装包,压缩包约1GB左右),传到现场。

然后现场准备一台机器作为镜像仓库服务器,安装docker和docker-compose(Harbor的部署依赖docker compose)。

Harbor的安装过程几行命令:

tar -xzf harbor-offline-installer-v2.10.0.tgz cd harbor cp harbor.yml.tmpl harbor.yml

编辑harbor.yml,重点改几个配置:

  • hostname:改成这台机器在局域网内的访问地址,比如10.0.0.10。
  • port:默认是80端口,如果想换成其他端口比如5000,就改对应配置。
  • harbor_admin_password:管理员初始密码,必须改,这是安全底线。

然后启动安装:

./install.sh

看到✔----Harbor has been installed and started successfully.----的日志就说明装好了。

访问http://10.0.0.10,能打开Harbor的Web管理界面。

接下来把中间件镜像推到Harbor:

docker tag mysql:8.0.32 10.0.0.10/library/mysql:8.0.32 docker push 10.0.0.10/library/mysql:8.0.32

这里描述一下关键点:Harbor默认创建library项目,这也是docker官方默认项目名。推镜像的时候,镜像名的前缀必须包含仓库地址,docker才能识别推送到哪个仓库。

所有需要跑中间件的服务器,配置daemon.json里的insecure-registries加入Harbor的地址,然后就可以直接:

docker pull 10.0.0.10/library/mysql:8.0.32

这样比每台机器load镜像包方便太多。

但说实话,如果就三五台机器,手动load完全够用。Harbor适合规模稍大、有版本迭代需求的场景。

4.4 内网节点配置拉取私有仓库镜像

这里有一个细节,很多人第一次配置的时候会卡住。

在内网节点的daemon.json里加insecure-registries之后,一定要重启docker才能生效:

systemctl daemon-reload systemctl restart docker

然后才能正常pull私有仓库的镜像。

如果重启之后仍然报错,提示certificate signed by unknown authority或者http: server gave HTTP response to HTTPS client,基本就是两个原因:

  • 忘了加insecure-registries或者在改配置之前已经启动过docker,没重启。
  • 节点访问不到仓库地址,网络不通。用curl http://10.0.0.10/v2/测试一下,如果能返回一个JSON响应,说明网络通。

为什么http: server gave HTTP response to HTTPS client这个错误这么经典?因为docker客户端默认用HTTPS访问仓库,而私有仓库只提供HTTP服务的时候,握手协议对不上,就会报这个错。加了insecure-registries之后,docker客户端会放行对该地址的非HTTPS连接。

5. 中间件容器化部署实操

5.1 快速部署MySQL 8实战

中间件部署是标题里最实在的部分。我拿MySQL 8举例,完整跑一遍。

单机小规模场景下,直接docker run比compose更轻快。但如果要持久化目录、密码、字符集、备份恢复都做好管理,建议还是用compose。

先看目录结构规划:

/opt/mysql/ ├── conf/ │ └── my.cnf ├── data/ └── docker-compose.yml

my.cnf内容按常见的生产配置给出:

[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_general_ci default-time-zone='+08:00' max_connections=500 innodb_buffer_pool_size=2G slow_query_log=1 slow_query_log_file=/var/log/mysql/slow.log long_query_time=2

character-set-server指定utf8mb4,这是为了支持完整的Unicode字符(包括emoji),也是当前主流推荐。max_connections和innodb_buffer_pool_size需要根据机器内存调整,如果机器是16G内存,2G的buffer pool是基准值。

docker-compose.yml:

version: '3.8' services: mysql: image: 10.0.0.10/library/mysql:8.0.32 container_name: mysql8 restart: always ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: "YourStrongPassword" TZ: Asia/Shanghai volumes: - /opt/mysql/data:/var/lib/mysql - /opt/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf

注意几个细节:

容器内MySQL默认数据目录是/var/lib/mysql,把宿主机/opt/mysql/data挂载进去,数据会持久化在宿主机。即使容器删除重建,数据还在。

TZ: Asia/Shanghai必须配置。MySQL容器基础镜像默认时区是UTC,如果你不设置,数据库记录的now()时间会比北京时间晚8小时。虽然Java应用可以通过连接串指定时区,但数据库直接返回的时间不对,经常导致各种排查困扰。这个我踩过太多次。

启动:

cd /opt/mysql docker compose up -d

查看启动状态:

docker logs mysql8 --tail 50

看到ready for connections的日志就说明启动成功了。

验证连接:

docker exec -it mysql8 mysql -uroot -p

然后执行:

select version(); select now();

确认版本和时区都正常。

MySQL部署里最容易被坑的一个点是:容器初始化完成后会创建一个匿名卷,如果你第一次启动时没挂载数据目录,之后想补挂载,数据不会自动迁移。所以数据目录的挂载必须第一次启动之前就配好。

5.2 Redis部署与密码认证

Redis的容器化部署相对简单,但有几个细节会影响后续使用的稳定性。

我还是用compose方式:

version: '3.8' services: redis: image: 10.0.0.10/library/redis:7.0.11 container_name: redis7 restart: always ports: - "6379:6379" command: ["redis-server", "--appendonly", "yes", "--requirepass", "YourRedisPassword"] volumes: - /opt/redis/data:/data

关键点:

--appendonly yes开启AOF持久化,Redis默认是RDB快照持久化,AOF记录每次写操作,数据安全性更高。生产环境建议AOF和RDB都开启,但容器化部署一般配置AOF即可。

--requirepass设置访问密码。虽然Redis通常只在内网访问,但内网不等于安全,之前有过多次Redis未授权访问提权的安全事件,密码必须设。

启动:

cd /opt/redis docker compose up -d

测试连接:

docker exec -it redis7 redis-cli -a YourRedisPassword ping

返回PONG说明正常。

Redis容器有一个体验不太好的点:默认没有配置文件,所有配置都通过command参数传。如果配置项特别多(比如设置maxmemory、设置持久化策略),command会变得很长。这种情况下建议把redis.conf写好挂载进容器,command里只写redis-server /etc/redis/redis.conf。两种方式效果一样,看个人习惯。

5.3 用Nginx做内网反向代理

Nginx容器部署和前面的思路有些不同。Nginx本身是一个反向代理服务,它的主要价值在于路由各种中间件暴露的HTTP接口,比如让团队统一通过一台Nginx访问不同的中间件服务。

配置Nginx容器:

version: '3.8' services: nginx: image: 10.0.0.10/library/nginx:1.24.0 container_name: nginx restart: always ports: - "80:80" - "443:443" volumes: - /opt/nginx/conf.d:/etc/nginx/conf.d:ro - /opt/nginx/logs:/var/log/nginx

conf.d目录挂载自定义配置文件,实现反向代理。比如把某台机器上的ES服务代理到统一入口:

server { listen 80; server_name es.internal; location / { proxy_pass http://192.168.1.20:9200; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

配置好之后执行:

nginx -s reload

容器内执行reload还是直接在宿主机执行?其实你如果配置了volume挂载,直接在宿主机编辑配置文件,然后在宿主机上执行:

docker exec nginx nginx -s reload

Nginx容器重启成本低,但生产环境频繁停止重启会影响在途连接。用reload方式是平滑重载,不中断服务,这是Nginx的特性,也是我推荐这种方式的原因。

5.4 消息队列和搜索组件部署要点

RabbitMQ、Kafka、Elasticsearch这类组件,容器化部署的思路类似,但细节差异很大。

RabbitMQ直接跑带management插件的镜像,可以省去后续装插件的步骤:

services: rabbitmq: image: 10.0.0.10/library/rabbitmq:3.11-management container_name: rabbitmq ports: - "5672:5672" - "15672:15672" environment: RABBITMQ_DEFAULT_USER: admin RABBITMQ_DEFAULT_PASS: "StrongPassword"

15672是Web管理界面,5672是AMQP协议端口。基础镜像不带management的,也可以进去rabbitmq-plugins enable rabbitmq_management手动开启,但直接选management镜像更省事。

Kafka的容器化部署要注意一点:Kafka天然依赖zookeeper,除非你用的是KRaft模式(新版Kafka可以不用zookeeper)。如果中间件场景里已经有zookeeper容器,Kafka可以直接配置连接。用bitnami的kafka镜像时,它通常会带一个内置的zookeeper服务或者让你额外起一个zookeeper容器。

services: zookeeper: image: 10.0.0.10/library/bitnami/zookeeper:3.9 ports: - "2181:2181" environment: ALLOW_ANONYMOUS_LOGIN: "yes" kafka: image: 10.0.0.10/library/bitnami/kafka:3.5.0 ports: - "9092:9092" environment: KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper:2181 ALLOW_PLAINTEXT_LISTENER: "yes" KAFKA_CFG_LISTENERS: PLAINTEXT://:9092 KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092

Kafka有好几个监听器配置,看起来完全一样但含义不同:listeners是Kafka进程实际监听的地址,advertised.listeners是告诉客户端应该连接哪个地址。如果两者不匹配,客户端会连接失败。这是Kafka容器化部署最常见的坑,没有之一。

ES的话,核心注意点是:ES容器默认以elasticsearch用户运行,挂载数据目录的权限必须匹配。宿主机创建/opt/es/data之后,要执行chown -R 1000:1000 /opt/es/data,把属主改成uid 1000,否则ES容器启动时会报目录权限错误。

另外ES容器启动时如果报vm.max_map_count太低,需要在宿主机执行:

sysctl -w vm.max_map_count=262144

这个参数是Linux内核控制单个进程能拥有的内存映射区域数上限,ES这个软件对它的要求比较高。这个配置修改之后,重启系统就丢了,要写进/etc/sysctl.conf持久化。

6. 常见问题与排查技巧

6.1 容器启动后无法访问排查思路

这个问题出现频率最高。

容器起来了,docker ps能看到状态是Up,但外部访问不通。我通常按这个顺序排查:

第一步,确认端口映射是否正确:

docker port <容器名>

看到类似3306/tcp -> 0.0.0.0:3306的输出,说明映射没问题。

第二步,确认容器内服务是否真的在监听端口:

docker exec <容器名> netstat -tlnp

如果没有netstat命令,可以docker exec <容器名> ss -tlnp。

第三步,宿主机上直接访问端口:

curl http://localhost:3306

curl能通,但外部机器不通,那就是防火墙问题了。在宿主机上执行:

systemctl status firewalld

如果开着,可能需要开放端口:

firewall-cmd --zone=public --add-port=3306/tcp --permanent firewall-cmd --reload

或者干脆停掉防火墙(如果是内网环境且安全策略允许的话):

systemctl stop firewalld

第四步,如果宿主机上curl都不通,基本是容器内服务没起来。看日志:

docker logs <容器名> --tail 100

从这里基本能定位问题。

6.2 容器时间与宿主机时间不一致

容器默认时区通常和宿主机一致(因为共享内核时钟),但时区设置不一定一致。很多官方镜像默认时区是UTC,导致日志时间、定时任务时间都与北京时间差8小时。

解决办法我在部署MySQL的时候已经提过:有TZ环境变量的就设置TZ: Asia/Shanghai。

但有些中间件镜像不识别TZ环境变量,那就得挂载宿主机的时区文件:

volumes: - /etc/localtime:/etc/localtime:ro

这个挂载把宿主机的/etc/localtime映射进容器,容器内所有读时间的程序都会用宿主机的时区设置。

还有一个更底层的方式:启动命令里加-e TZ=Asia/Shanghai之外,如果容器内的/etc/timezone文件没有内容,有的程序会读取这个文件。可以挂载:

volumes: - /etc/timezone:/etc/timezone:ro

但/etc/timezone文件不是所有Linux版本都有,centos和ubuntu的位置不一样,建议先实测一下。

6.3 磁盘空间被日志占满

容器日志是磁盘杀手。

如果你没限制日志大小,一个长期运行的容器,container日志文件可以到几十个GB。这个问题我们之前配过daemon.json的log-opts限制,那只对之后创建的容器生效,已经存在的容器仍然用旧配置。

现场遇到日志占满的处理办法:

先查哪个容器占的空间大:

du -sh /var/lib/docker/containers/* | sort -rh | head -10

找到对应容器后,清空日志文件。这里注意,不要直接删日志文件,正确的做法是:

truncate -s 0 /var/lib/docker/containers/<容器ID>/*-json.log

用truncate清空而不是rm删除,是因为容器持有这个日志文件的文件句柄,直接删掉文件不会释放磁盘空间(句柄还在),只有清空内容才能立即释放。这个技巧在线上用过很多次,非常管用。

更彻底的做法是,所有新容器都建议加一个:

logging: options: max-size: "100m" max-file: "3"

在compose文件的每个服务下都配这个,双保险。

6.4 镜像导入慢或导入失败

docker load导入几个GB的镜像包时,有时候会显得非常慢,甚至看起来像是卡死了。

这种情况多数不是docker的问题,而是磁盘IO瓶颈。如果导入到网络存储或者机械硬盘,几个GB的文件解压会花很长时间。判断是否卡死的办法是:

iostat

看磁盘IO是否持续在忙,如果busy率很高,说明还在干活,耐心等。

另一个问题是导入时空间不足。docker load会把镜像所有层解压到数据目录,临时需要的空间大致是镜像包大小的2倍。所以确保/var/lib/docker所在分区有足够空间。

导入失败还有一种情况是版本兼容问题。用旧版本docker导入新版本docker save的镜像,有可能失败。比如docker 19.03导入docker 24.x保存的镜像,个别格式可能不兼容。如果你现场docker版本偏老,建议在有网机器上save的时候,加上--platform linux/amd64参数明确架构,并且尽量找和现场docker版本接近的机器做save。

6.5 inode耗尽:一个容易被忽略的坑

前面提过inode问题,这里单独说下。

docker容器在运行过程中会创建大量文件:临时文件、层文件、日志、容器内部文件改动。如果数据目录所在文件系统的inode耗尽,即使磁盘空间还有剩余,也没法创建新文件。

检查inode使用率:

df -i

如果/var/lib/docker所在分区的IUse%接近100%,说明inode已经紧张。

inode耗尽可能导致各种诡异现象:容器文件系统操作报错、docker pull镜像没反应、容器删除也删不掉。现场遇到这种问题,除了清理无用容器和悬空镜像外,最根本的办法是保证数据目录所在分区格式化的时候指定了足够的inode:

mkfs.ext4 -i 1024 /dev/sdb1

-i 1024表示每1024字节分配一个inode,比默认的4096/8192要高很多,能容纳更多小文件。这种方式适合数据目录这种会产生大量小文件的场景。

7. 实用运维经验与交接建议

7.1 镜像清单与版本台账

离线环境最大的风险是“不知道现场到底装了啥”。我在交付的时候,一定会做一份镜像与版本台账,包含以下信息:

  • 镜像名和tag
  • 每个镜像对应的中间件版本
  • 部署在哪台机器
  • 数据目录挂载位置
  • 端口映射情况
  • 启动命令或compose文件位置

这份台账不光是给客户看的,也是给自己留的。隔了几个月再去现场排查问题,如果没有台账,光靠docker ps反推容器配置,效率太低。

台账用简单的Markdown表格维护即可,不需要上什么专业工具。

7.2 备份与恢复

虽然容器化部署简化了安装过程,但数据备份不能省。

MySQL数据备份用mysqldump或者物理备份都行,看数据量。我这里更推荐在容器外执行:

docker exec mysql8 sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --all-databases' > /backup/mysql_backup.sql

Redis持久化文件直接备份/opt/redis/data目录下的appendonly.aof文件。

Kafka的话,如果需要备份消息数据,直接备份日志目录。但Kafka本身一般用于流式处理,消息过一段时间会被消费掉或过期清理,备份策略需要业务方参与确定。

7.3 与客户/团队的交接说明

最后说一句我在多次交付中总结出来的经验:离线部署docker和中间件,技术难度其实没有特别高,真正的价值在于流程和规范。

交付的时候,给客户一份环境说明文档,包含:

  • docker和compose版本
  • 私有仓库地址和访问方法
  • 所有中间件的连接方式(IP、端口、账号、密码由客户方保管)
  • 数据目录位置和备份策略
  • 常见问题的应急处理办法

这份文档比技术本身更重要。我见过太多人只顾着把docker跑起来,结果三个月后客户换了个运维,啥都看不懂,又得重新排查。项目交接的最终交付物,一定是一份能让人独立接手的文档,而不只是一堆运行中的容器。

至于后续扩展,比如把编排方案切换到更完整的容器管理平台、增加监控告警体系、实现中间件配置的自动化巡检,这些都是在打好离线容器基础之后的自然演进方向,等基础稳定了再逐步推进也不迟。

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

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

立即咨询