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状态:
getenforcedocker和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 *.rpmrpm -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.ymlmy.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=2character-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/nginxconf.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 reloadNginx容器重启成本低,但生产环境频繁停止重启会影响在途连接。用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:9092Kafka有好几个监听器配置,看起来完全一样但含义不同: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:3306curl能通,但外部机器不通,那就是防火墙问题了。在宿主机上执行:
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.sqlRedis持久化文件直接备份/opt/redis/data目录下的appendonly.aof文件。
Kafka的话,如果需要备份消息数据,直接备份日志目录。但Kafka本身一般用于流式处理,消息过一段时间会被消费掉或过期清理,备份策略需要业务方参与确定。
7.3 与客户/团队的交接说明
最后说一句我在多次交付中总结出来的经验:离线部署docker和中间件,技术难度其实没有特别高,真正的价值在于流程和规范。
交付的时候,给客户一份环境说明文档,包含:
- docker和compose版本
- 私有仓库地址和访问方法
- 所有中间件的连接方式(IP、端口、账号、密码由客户方保管)
- 数据目录位置和备份策略
- 常见问题的应急处理办法
这份文档比技术本身更重要。我见过太多人只顾着把docker跑起来,结果三个月后客户换了个运维,啥都看不懂,又得重新排查。项目交接的最终交付物,一定是一份能让人独立接手的文档,而不只是一堆运行中的容器。
至于后续扩展,比如把编排方案切换到更完整的容器管理平台、增加监控告警体系、实现中间件配置的自动化巡检,这些都是在打好离线容器基础之后的自然演进方向,等基础稳定了再逐步推进也不迟。