不知道你有没有被问过类似的问题想搭一套日志检索环境想给业务数据做个全文搜索或者只是想在本地把 Elasticsearch 和 Kibana 这对组合先跑起来——结果搜了一圈资料发现要么是老的 6.x 教程要么是讲了半截的 docker-compose 片段照着弄完不是启动失败就是 Kibana 死活连不上 ES。我前段时间又完整搭了一遍 docker 安装 elasticsearch 和 kibana 的流程这次把从环境准备到生产加固的所有关键点重新捋了一遍整理成下面这篇东西。不管是第一次接触容器的朋友还是已经用过 ES 但没试过容器化部署的兄弟照着一步步做都能跑通跑通之后也敢放手去用。这套组合用容器部署的最大价值是把 ES 和 Kibana 的安装、升级、回滚变成几个命令的事数据通过数据卷持久化在宿主机上容器怎么删怎么换都不会丢。但容器化也带来了一些非常典型的坑内存参数没调好启动崩、Kibana 连不上 ES、容器重启后 IP 漂移导致集群失联、数据卷权限不对直接被拒绝访问。这些坑我在下文里都会用实际处理的思路讲清楚。1. 为什么要把 ES 和 Kibana 搬进容器我踩过的裸机部署坑1.1 版本升级和回滚曾经是我最头疼的事先说个现象ES 和 Kibana 的版本耦合非常敏感Kibana 的大版本必须和 ES 完全一致小版本也只能在允许范围内浮动。裸机部署的时候我第一次升级 ES直接下载新版本压缩包覆盖旧目录结果启动起来发现数据目录格式不兼容只能回滚。回滚更折腾幸好还有旧包备份但整个服务已经中断了四十分钟。用容器之后这种事情就没再发生过。镜像本身就是版本的一个固化快照想升级就拉一个新 tag 的镜像启一个新容器把同一个数据卷挂上去数据不动。如果升级后发现版本不兼容或者其他问题把新容器停掉用旧镜像再跑一个容器一切回到升级前的状态时间只需要几秒钟。这里有个实操提醒每次升级前容器名称不用换但强烈建议先执行一次索引快照备份。ES 在容器里跑数据虽然落在数据卷里但一旦出现索引损坏之类的问题单靠文件拷贝恢复远不如快照恢复来得干净。关于快照的配置后面第 7 节会说。1.2 多环境隔离与数据迁移容器带来的直接红利裸机部署还有一个让我很头疼的地方一台服务器上想同时跑 ES 的测试环境和灰度环境就得处理端口冲突、JDK 版本冲突、配置目录隔离这些乱七八糟的问题。我曾在同一台机器上维护过两套 ES最后不得不用两个不同的系统用户分别指定不同的数据目录和端口维护成本特别高。换成容器之后这个问题的解法就干净很多了。每套环境用一个独立的容器或容器组端口映射分开数据卷分开网络也分开。想复现一个和生产一致的环境直接把镜像 tag 和数据卷的挂载配置搬过去就行不再需要跑一堆初始化脚本。正是因为容器天然把“运行环境”和“宿主机”隔离开ES 对系统 JDK 的依赖、对内核参数的要求都被收拢到一个可控的范围内这才让我敢放心地把测试、预发、生产都跑在同一批机器上。2. 动手前必须先确认的三件事内核参数、内存账本和镜像版本真正开始执行 docker run 之前我建议你先停下来做三件事。这三件事看似不起眼但恰恰是网上教程最容易跳过、又最能决定成败的环节。2.1 vm.max_map_count 不调整ES 几乎必然启动失败第一件事是调整宿主机内核参数vm.max_map_count。ES 底层的 Lucene 索引存储对内存映射区域的数量要求非常高默认值 65530 在现代 Linux 系统上基本不够用。我第一次在干净服务器上直接跑 ES 容器启动没几秒就退出docker logs 里明确写着max virtual memory areas vm.max_map_count [65530] is too low一句废话都没有。解决办法是在宿主机上执行sudo sysctl -w vm.max_map_count262144这只是临时生效重启机器后又会回到默认值。要持久化把配置写进系统文件echo vm.max_map_count262144 | sudo tee -a /etc/sysctl.conf sudo sysctl -p这里有个容易忽略的细节如果宿主机上还跑着其他也依赖内存映射的服务比如某些数据库、消息队列调高这个参数一般不会对它们产生副作用。262144 这个值是 ES 官方文档长期使用的推荐值不是越大越好但在这个规模下是安全的。2.2 JVM 堆内存与机器内存的分配逻辑第二件事是算清楚内存账本。ES 是 Java 写的JVM 堆内存的设置直接决定它能处理多少数据。这里最常见的错误是机器明明有 32G 内存就直接把ES_JAVA_OPTS里的Xms和Xmx都设成 30G然后启动没多久整个宿主机被卡死。ES 进程实际占用的内存远不止 JVM 堆那部分。Lucene 大量使用堆外内存和文件系统缓存堆设得太大反而挤占了操作系统文件缓存的空间GC 压力也跟着上去。官方长期给出的经验值是堆内存不超过物理内存的 50%并且上限建议不要超过 32G。以一台 16G 内存的虚拟机举例我给 ES 分配 4G 堆留下足够余量给操作系统缓存和 Kibana这是比较合理的起始配置-e ES_JAVA_OPTS-Xms4g -Xmx4g注意Xms和Xmx必须设置成一样大否则 JVM 运行过程中堆大小频繁伸缩会带来不必要的停顿。如果你的机器内存确实有限1G 或 512M 也能跑起来做功能验证只是数据量一大就会明显变慢。内存账本这关过了后面才有折腾的空间。2.3 镜像版本统一性ES 和 Kibana 的 tag 必须拉同一套第三件事是镜像版本的统一。ES 和 Kibana 不像普通 Web 应用那样前后端解耦Kibana 对 ES 版本的要求是硬性的。同一套 tag 大版本不一致Kibana 启动时会在日志里直接报版本不匹配页面怎么刷新都是服务端错误。我这次用的版本是 8.13.4拉取命令如下docker pull docker.elastic.co/elasticsearch/elasticsearch:8.13.4 docker pull docker.elastic.co/kibana/kibana:8.13.4这里顺便提醒一句latesttag 在 ES 的世界里尽量少用。官方镜像虽然会更新 latest但 ES 版本升级往往伴随着数据格式和 API 的变化latest 漂移到新版本后你很难确定下次启动容器时跑的是什么版本。锁死具体 tag 是容器化部署 ES 的基本素养。镜像tag 建议说明elasticsearch8.13.4具体小版本锁死版本避免 driftkibana8.13.4与 ES 完全一致大版本不一致直接连不上latest不推荐数据兼容性风险高3. 网络与数据卷设计让容器重启不再丢数据ES 不是无状态应用它的数据目录和配置目录都必须持久化在宿主机上。这一节讲的是怎么设计网络和数据卷让容器无论怎么重启、删除、重建服务都能自动恢复。3.1 自定义 bridge 网络为什么不用默认网络一开始我图省事直接让 ES 和 Kibana 都跑在 docker 默认的 bridge 网络上结果 Kibana 配置里写 ES 的地址时只能用 IP因为默认网络默认不支持容器名解析。容器 IP 一旦因为重启发生变化Kibana 里的地址就得跟着改非常被动。正确的做法是创建一个自定义 bridge 网络理由很简单自定义网络内置 DNS 解析同一个网络里的容器可以互相用容器名访问。这样 Kibana 连接 ES 的地址直接写http://es-node1:9200就行IP 怎么变都无所谓docker network create es-net这个网络是一次性创建、长期复用的。后面每启一个容器都指定--network es-net它们就自动处于同一个内网环境里对外只暴露必要的端口ES 的 9300 节点通信端口完全不需要暴露到宿主机外面。3.2 命名数据卷的目录规划与挂载映射数据持久化我用的是命名卷而不是 bind mount。这里有必要解释一下为什么命名卷由 docker 管理首次挂载时如果卷是空的docker 会把容器镜像里对应目录的内容和权限自动初始化进去而 bind mount 直接映射宿主机上的一个普通目录目录所有者和权限可能和容器内 ES 进程的期望不一致一启动就报数据目录不可写。命名卷的创建和使用docker volume create es-data docker volume create es-config docker volume create es-logs接下来在启动 ES 容器时把这三个卷分别挂到容器内的对应路径-v es-data:/usr/share/elasticsearch/data -v es-config:/usr/share/elasticsearch/config -v es-logs:/usr/share/elasticsearch/logs配置目录单独用一个卷主要是因为我后面想改elasticsearch.yml的时候可以直接在卷里改容器重启后配置就能生效不需要再重做镜像。如果你不需要频繁改配置只挂数据卷也完全够用。3.3 容器删除却保留数据的验证方法每次我把容器删掉重建之后都会做一次“空跑验证”来自我检查先停容器再删除容器但保留数据卷然后重新用同样的命令启动一个新容器。启动完成后原来写入的测试索引还在这说明持久化是通的。这个问题的本质是ES 容器本身是“一次性”的所有状态都存放在数据卷里。只要数据卷在容器外部的工具如docker exec、docker logs都只是辅助手段。我见过有人把数据写在容器可写层里然后整天担心容器崩溃丢失数据那其实是用容器的方式糟蹋了 ES 这种有状态服务。4. 启动 ES 容器我把完整命令拆给你看网络和数据卷都准备好之后就可以正式启动 ES 容器了。下面这个命令是经过多次验证的完整版我逐段拆解参数的含义。4.1 环境变量的作用与堆内存设置ES 8.x 镜像默认开启了安全认证所以启动命令里不需要显式加xpack.security.enabled参数但必须处理好认证信息的获取。先看完整命令docker run -d \ --name es-node1 \ --network es-net \ -p 9200:9200 \ -p 9300:9300 \ -e discovery.typesingle-node \ -e ES_JAVA_OPTS-Xms4g -Xmx4g \ -e xpack.security.enabledtrue \ -v es-data:/usr/share/elasticsearch/data \ -v es-config:/usr/share/elasticsearch/config \ -v es-logs:/usr/share/elasticsearch/logs \ docker.elastic.co/elasticsearch/elasticsearch:8.13.4逐项拆解--name es-node1容器名同时也是自定义网络里的主机名Kibana 就用这个名字访问它。-p 9200:9200暴露 HTTP API 端口所有 REST 请求都走这个端口。-p 9300:9300暴露节点间通信端口单节点部署时不强制暴露但保留下来方便以后扩展集群。discovery.typesingle-node告诉 ES 当前是单节点模式。如果你不加这个参数ES 会误以为自己在搭建多节点集群然后因为找不到其他节点而一直处于等待状态。ES_JAVA_OPTS-Xms4g -Xmx4g堆内存设置原理在第 2 节已经讲过这里直接配置到位。xpack.security.enabledtrue8.x 默认就是开启的写上是为了显式表达意图这个 ES 不是玩具Kibana 登录需要账号密码。4.2 初始化完成后如何验证节点健康容器启动后不能急着用ES 初始化过程一般需要几十秒。查看初始化状态最直接的方式是看日志docker logs -f es-node1当看到日志里出现message: started之类的字样就说明节点已经成功加入并对外服务了。此时可以用 curl 验证 HTTP 接口是否正常curl -k -u elastic:密码 https://localhost:92008.x 默认开启了 HTTPS所以要加-k跳过证书校验。账号密码怎么拿第一次启动容器时日志里会打印自动生成的 elastic 用户初始密码和一个 enrolment token。如果你没来得及保存可以用下面这个命令重置密码docker exec -it es-node1 bin/elasticsearch-reset-password -u elastic执行后会交互式询问是否重置输入 y 即可拿到新密码。这个密码强烈建议存到密码管理器里后面 Kibana 登录和 API 调用全都要用到它。4.3 单节点部署与未来扩展为多节点的接口单节点模式的配置虽然能立刻跑通但它有两个天然限制一是数据只有一份副本一旦数据卷损坏恢复起来非常困难二是索引分片无法分配到第二个节点上集群总承载能力被限制在单机范围内。所以即便现在只需要一台机器我也建议在数据卷里预留好cluster.initial_master_nodes和discovery.seed_hosts的配置格式。等以后有第二台机器时只需要把discovery.type改成多节点模式再让各个节点通过es-net网络互相发现。具体的扩展方式不是本文重点但启动命令里保留-p 9300:9300这个动作就是为将来留的口子。5. 让 Kibana 和 ES 握手成功那些连接报错的根因Kibana 和 ES 的连接问题是整套部署里出现频率最高的故障类型。我踩过三次每次原因都不一样。这一节把最常见的握手失败链路完整拆开。5.1 容器 IP 漂移导致的经典故障第一次搭的时候我先启动 ES 容器然后用docker inspect es-node1查到它的 IP 地址写进 Kibana 配置。当时能连上但过了几天重启宿主机ES 容器的 IP 变了Kibana 里配置的地址还是旧 IP直接失联。这个故障的根因就是我在第 3 节强调的容器 IP 不是稳定资源容器名在自定义网络里才是。正确做法是让 Kibana 通过--network es-net和 ES 处于同一网络然后用容器名es-node1作为连接地址。5.2 网络策略与账号认证的联动配置第二次搭的时候Kibana 一直在报connect ECONNREFUSED我排查网络策略查了半天最后发现是 Kibana 容器在默认网络里而 ES 容器在自定义网络 es-net 里。两个网络不互通连接当然被拒。直到这时我才真正理解了网络隔离的含义Docker 的默认 bridge 网络和自定义 bridge 网络彼此隔离SO 上一个容器在哪个网络里决定了它能访问哪些服务。所以 Kibana 启动命令必须显式指定--network es-net只用-p 5601:5601把 Kibana 的页面端口暴露给宿主机即可。账号认证层面的联动也发生过一次问题。ES 里有一个专门给 Kibana 使用的kibana_system用户我用它来对接时密码是从 ES 初始化日志里拿到的 enrollment token 自动生成的。如果你手动改了密码或者在 ES 里重建了这个用户Kibana 的ELASTICSEARCH_PASSWORD环境变量也要同步更新。5.3 从 Kibana 控制台验证数据通路Kibana 启动命令完整版如下docker run -d \ --name kib01 \ --network es-net \ -p 5601:5601 \ -e ELASTICSEARCH_HOSTShttp://es-node1:9200 \ -e ELASTICSEARCH_USERNAMEkibana_system \ -e ELASTICSEARCH_PASSWORD你的kibana系统用户密码 \ docker.elastic.co/kibana/kibana:8.13.4启动后浏览器访问http://localhost:5601。如果 ES 那边已经开启安全认证Kibana 第一次打开页面会让你粘贴 enrollment token然后在登录页用 elastic 用户登录。验证数据通路的快速方法是在 Kibana 左侧菜单进入 Dev Tools执行一条最简单的查询GET _cluster/health返回status : green或者yellow都说明通路正常。这里解释一下两个状态的区别green 表示所有主分片和副本分片都就绪yellow 表示主分片可用但副本分片未分配在单节点模式下因为副本没有第二个节点可放显示 yellow 是正常现象不用紧张。6. 跑了三个月遇到的故障排查笔记ES 容器化部署不是“跑起来”就结束了真正的工作量在持续维护阶段。下面是我在实际运行中遇到过、并且完整复现过处理链路的三类典型故障。6.1 容器正常但 9200 端口拒绝连接现象docker ps显示 es-node1 容器处于 Up 状态日志里也没有报错但宿主机上curl localhost:9200直接 connection refused。排查链路先看端口映射是否生效docker port es-node1应输出9200/tcp - 0.0.0.0:9200。如果映射正常再用docker exec -it es-node1 curl -k https://localhost:9200在容器内部尝试访问。如果容器内能通而宿主机不能通问题出在端口绑定或防火墙如果容器内也不能通说明 ES 进程本身没有监听成功。最终根因一般是 JVM 堆内存设置过大导致启动过程中 OOMOS 层面的端口监听虽然在容器里被初始化但进程已经无力处理新连接。处理办法是回到第 2 节调整ES_JAVA_OPTS把堆内存降到一个合理值后重建容器。6.2 数据卷权限错误导致的启动崩溃现象容器启动后立即退出docker logs 里提示AccessDeniedException: /usr/share/elasticsearch/data。这个问题的触发条件通常是你用了 bind mount 挂载宿主机目录而不是命名卷而宿主机目录的所有者是 root容器内 ES 进程是 uid 1000 的用户没有写权限。排查和处理# 查看目录使用者 ls -ld /path/to/es-data # 修改所有者为 uid 1000 sudo chown -R 1000:1000 /path/to/es-data这是 bind mount 最容易踩的坑。命名卷一般不会出现这个问题因为第一次创建时 docker 自动做了目录初始化。如果你必须用 bind mount 来把数据目录放到特定磁盘上记得把目录权限这条写进部署文档防止以后换个同事来维护时又踩一遍。6.3 磁盘写满导致索引变成只读现象某天 Kibana 的日志索引突然无法写入新数据Kibana 里查历史索引正常但时间筛选器里看不到新日志。排查步骤在宿主机执行df -h确认磁盘占有率大概率是接近 100%。用curl -k -u elastic:密码 https://localhost:9200/_all/_settings查看索引设置你会发现index.blocks.read_only_allow_delete被置为 true。ES 会在磁盘水位线达到 95% 时自动对索引进块这是保护机制防止在磁盘写满时索引文件损坏。处理方法是清出磁盘空间后执行curl -k -u elastic:密码 -X PUT \ https://localhost:9200/_all/_settings \ -H Content-Type: application/json \ -d {index.blocks.read_only_allow_delete: null}执行后索引自动恢复可写。这类问题最好在监控层面提前预防ES 的_cluster/health接口里有两个磁盘相关的字段建议接入告警。故障现象根因解决路径9200 拒绝连接JVM 堆过大导致 OOM调小 ES_JAVA_OPTS 后重建容器数据目录不可写bind mount 目录所有者是 rootchown -R 1000:1000 数据目录索引只读磁盘水位线超限清理磁盘后解除 blocks 设置7. 进入生产前的三个补强密码、中文分词和快照如果你搭这套环境只是为了本地写 demo 玩前 6 节的内容已经足够。但如果你跟我一样跑的是给业务用的日志检索或者搜索服务下面这三个补强点建议认真看完。7.1 xpack 安全认证配置要点ES 8.x 默认开启的安全认证分为传输层和 HTTP 层两层。传输层的证书在首次启动时自动生成不需要干预HTTP 层的访问需要用户名密码。前面已经介绍了怎么获取和重置 elastic 用户的密码这里补充两个容易被忽略的点一是kibana_system用户的密码不要直接用 elastic 的密码因为 Kibana 内部会把系统用户的凭证存储到自身的加密 keystore 里如果混用Kibana 页面请求和系统调用容易产生权限混淆。二是在生产环境里不要通过环境变量把密码写进 docker run 命令因为环境变量会被docker inspect看到。更好地做法是用 Docker Secrets 或者在容器启动后通过 keystore 工具写入。7.2 中文 IK 分词器的容器化安装方式除了日志检索很多业务场景需要中文全文检索。ES 默认的标准分词器对中文的处理是按字切分搜“中华人民共和国”会被切成“中”“华”“人”“民”“共”“和”“国”基本没法用。IK 分词器是目前最常用的中文分词方案。容器化安装 IK 插件的常见做法有两种。第一种是在容器内直接执行docker exec -it es-node1 bin/elasticsearch-plugin install IK插件zip包的下载地址 docker restart es-node1第二种是先把插件 zip 包下载到宿主机解压后放到命名卷对应目录再重启容器。相比第一种第二种更可控因为你可以提前在本地校验插件的版本和 ES 版本是否匹配。IK 插件的版本号必须和 ES 大版本严格一致比如 ES 8.13.4 就需要 IK 对应 8.13.4 的 release 包装错版本 ES 会拒绝启动。安装完成后在索引 mapping 里把analyzer指定为ik_max_word即可对中文文本做细粒度分词。7.3 快照备份即使容器化也不能裸奔容器化让部署变得容易但数据安全不会因为容器化就自动变好。ES 的数据卷确实把数据留在了宿主机上但服务器硬盘损坏、误删索引、升级事故这些场景仍然需要真正的备份手段。ES 官方推荐的备份方式就是快照。要启用快照首先在elasticsearch.yml里配置快照仓库路径path.repo: [/usr/share/elasticsearch/snapshot]然后启动容器时额外挂载一个快照目录卷-v /var/backups/es-snapshot:/usr/share/elasticsearch/snapshot重启容器后用 API 创建一个文件系统快照仓库curl -k -u elastic:密码 -X PUT \ https://localhost:9200/_snapshot/my_backup \ -H Content-Type: application/json \ -d {type: fs, settings: {location: /usr/share/elasticsearch/snapshot}}之后定期调用一次快照接口就行curl -k -u elastic:密码 -X PUT \ https://localhost:9200/_snapshot/my_backup/snapshot_$(date %Y%m%d)最开始我嫌麻烦想着容器都持久化了应该不用做备份。直到有一次误删了一个核心索引虽然有数据卷但删除操作已经同步写进了目录数据卷里也是删完的状态。最后只能靠快照恢复。从那天起我再也没有省过这一步。关于版本升级我再分享一点个人习惯ES 和 Kibana 的版本我从来不会盲目追新会先在测试环境里把新版本镜像跑起来挂上一个临时数据卷写入一批测试数据查询结果确认没有问题再考虑生产环境滚动升级。容器化给了我们快速尝试的底气但该有的谨慎一步也不该省。希望这篇东西能帮你少走几趟弯路把 docker 安装 elasticsearch 和 kibana 这条路走得比我当初顺畅一些。