最近有不少朋友在折腾 Elasticsearch,尤其是本地开发环境或者内网环境部署,问我最多的问题就是:怎么用 docker-compose 一把梭把 ES 跑起来,还要把 IK 分词器一起搞定。毕竟裸装 ES 要配 JDK、调内核参数、处理系统服务,稍微碰到生产环境还得考虑集群配置,折腾一圈下来半天就没了。而 IK 分词器又是做中文搜索绕不开的插件,很多内网环境没外网权限,在线安装插件根本走不通,所以离线安装的需求特别多。
这篇文章我就从零开始,把 docker-compose 安装 Elasticsearch 的完整过程拆开讲,附带离线 IK 分词器的安装和校验方法,最后把常见报错和排查思路也一并整理出来。内容偏向实操,每一步都给了可以直接复制的配置和命令,适合刚接触 ES 的开发者,也适合要在内网环境快速交付的运维朋友。
1. 整体设计思路:为什么选择 docker-compose 而不是直接裸装
1.1 裸装 ES 的痛点,真实碰过才知道
早期我在 CentOS 上手动装 ES,第一步装 JDK 就开始头疼。ES 对 JDK 版本有要求,系统自带 OpenJDK 版本不对,还得去下载特定版本。装完 JDK 解压 ES 包,又要创建专用用户,因为 ES 不允许 root 直接跑。接着调 /etc/security/limits.conf,改文件描述符上限和线程数,再改 /etc/sysctl.conf 里的 vm.max_map_count。一套流程下来,光环境准备就能写满一篇博客。更不用说出问题的时候,到底是 JDK 问题还是系统参数问题还是 ES 配置问题,排查链路又长又乱。
还有 JVM 堆内存配置,jvm.options 里的 -Xms 和 -Xmx 必须显式设置,不然 ES 会按机器内存比例自动分配,生产机器要是内存大,很容易把机器拖死。这些坑单独看都不难,但串在一起对新手很不友好。
1.2 docker-compose 解决了什么
docker-compose 解决的核心问题,是把 ES 的运行环境封装成镜像,宿主机只需要有 Docker 引擎,JDK、目录权限、环境变量全都由镜像维护。你写一个 docker-compose.yml,定义镜像、端口、数据卷、环境变量,一条 docker-compose up -d 命令就能把 ES 启动起来,升级、回滚、迁移都变成文件操作。
另外,ELK 技术栈通常不止 ES 一个组件,后面大概率还要加 Kibana、Logstash,或者用 Filebeat 采集日志。docker-compose 的优势就在这里体现出来了:多个服务写在同一个文件里,一条命令统一启动,容器间通过服务名直接通信,不用记 IP。我见过不少团队后来还用它把 Prometheus 和 Grafana 编排在一起做监控,思路都是一样的。哪怕你暂时只需要 ES,用 compose 管理也等于给后续扩展留好了位置。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 部署前准备:基础环境、版本选型与资源配置
2.1 Docker 和 docker-compose 安装速览
既然方案定了 docker-compose,第一步就是把基础工具准备好。Docker 安装比较主流,Linux 上可以用官方脚本,也可以直接用发行版自带的软件源安装。Windows 和 macOS 就直接装 Docker Desktop,装好之后自带 docker-compose 插件,省去单独安装的步骤。
这里单独说一下 docker-compose 的版本问题。旧版需要单独安装 docker-compose 二进制文件,新版 Docker Engine 集成了 docker compose(中间有空格)子命令。两种写法在某些环境里不太一样,建议先执行 docker-compose version 或 docker compose version 确认可用性。本文写作时,两种命令我试过都能跑通,但配置文件是完全一样的。
2.2 Elasticsearch 版本怎么选
版本选择这个事,很多教程一笔带过,但其实是最容易踩坑的地方。ES 版本迭代很快,大版本之间配置项和 API 差异不小。如果你是新项目,优先选择 7.x 的最新稳定版或者 8.x 的最新稳定版。我这里以 7.17.0 为例,因为 IK 分词器对 7.17.0 的适配版本最齐全,网上搜到的资料也最多,坑相对少。
还有一个关键点:IK 分词器版本必须和 ES 版本严格对应。比如 ES 7.17.0 就对应 IK 7.17.0,ES 7.10.2 就对应 IK 7.10.2。版本对不上,插件加载会直接报错。所以确定 ES 版本之后,IK 版本也就锁定了,后面下载的时候千万核对清楚。
2.3 内存与系统参数调整,提前做省得后面报错
ES 是 Java 应用,JVM 堆内存默认是机器物理内存的一半,这个值在容器里很危险。如果你宿主机是 16G 内存,容器默认按宿主机内存的一半去申请,可能直接导致 docker-compose up 的时候容器起不来,或者宿主机资源被吃满。
所以在 compose 文件里,必须显式指定 ES_JAVA_OPTS 环境变量,控制 JVM 堆大小。我的建议是开发环境 -Xms1g -Xmx1g 就够了,生产环境再按数据量评估,通常不超过 32G,超过这个值 JVM 的压缩指针会失效,反而浪费内存。
另外还有一个隐藏的系统参数:vm.max_map_count。ES 在 Linux 上运行需要这个值至少为 262144,否则启动日志里会出现 max virtual memory areas vm.max_map_count [65530] likely too low 的错误,容器会反复重启。这个参数在宿主机上执行:
bash复制sysctl -w vm.max_map_count=262144
但这样只是临时生效,服务器重启后就没了。要永久生效,需要写入 /etc/sysctl.conf:
bash复制echo 'vm.max_map_count=262144' >> /etc/sysctl.conf
sysctl -p
如果要限制容器的 CPU 和内存,也可以在 compose 文件里用 mem_limit 和 cpus 指定。开发环境一般不用,但多服务共存的机器上建议加上,防止 ES 把资源吃光,其他容器集体变卡。
3. 编写 docker-compose.yml:核心配置逐段拆解
3.1 目录结构规划
动手写 compose 文件之前,先把宿主机的目录规划好。ES 容器是无状态的,服务重启数据不能丢,所以必须挂载数据目录。IK 分词器插件我也建议挂载出来,方便后续离线替换和升级。
我的习惯是建一个 es 项目目录,里面划分 data、plugins、logs 三个子目录,对应挂载到容器内。这样的好处是升级版本的时候,数据还在原目录,插件也能复用。
bash复制mkdir -p /opt/es/{data,plugins,logs}
chmod 777 /opt/es/data /opt/es/plugins /opt/es/logs
这里 chmod 777 是很多教程不会特意解释的细节。ES 容器内部默认用 elasticsearch 用户运行,UID 是 1000,如果宿主机目录权限不对,容器写数据时就会报权限不足。图省事就 777,严谨一点可以 chown -R 1000:1000 /opt/es。
3.2 compose 文件详解
以下是我实际在用的 docker-compose.yml,单节点模式,生产环境只需要在这个基础上加节点配置:
yaml复制version: '3.8'
services:
elasticsearch:
image: elasticsearch:7.17.0
container_name: es01
restart: always
environment:
- node.name=es01
- cluster.name=es-docker-cluster
- discovery.type=single-node
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
ports:
- "9200:9200"
- "9300:9300"
volumes:
- /opt/es/data:/usr/share/elasticsearch/data
- /opt/es/plugins:/usr/share/elasticsearch/plugins
- /opt/es/logs:/usr/share/elasticsearch/logs
networks:
- es-net
networks:
es-net:
driver: bridge
逐项说明一下关键点。
image 指定镜像版本,我用的 elasticsearch:7.17.0,官方镜像国内拉取可能慢,可以先配置 Docker 镜像加速源。restart: always 保证容器意外退出后能自动拉起,这点对服务稳定性很重要。
environment 里最关键的是 discovery.type=single-node。ES 默认是集群模式,单节点启动时会因为找不到其他节点而反复尝试,加上这个参数就告诉它:我就是单机,别等别人了。如果你只是本地测试,这行不能漏。
bootstrap.memory_lock=true 配合 ulimits 里的 memlock 设置,作用是锁定 ES 进程的内存,防止 JVM 堆被 swap 到磁盘。ES 官方文档明确建议生产环境开启,否则 GC 性能会明显下降。开发环境如果内存比较紧,也可以把这行注释掉,不影响启动。
3.3 端口和网络设计
9200 是 HTTP 端口,REST API 和 Kibana 连接都走它。9300 是节点间通信端口,单节点模式下其实用不到,但保留下来方便后面扩集群。注意端口别跟宿主机已有的服务冲突,我见过有人 9200 被其他服务占了,ES 的端口映射失败,容器一直处于异常状态。
网络使用了自定义 bridge 网络 es-net。自定义网络的好处是容器之间能用服务名互相访问,比如后面加 Kibana 服务,直接连 elasticsearch:9200 就行,不用再去查 IP。如果以后要加集群节点,节点之间也能通过服务名通信,配置会方便很多。
3.4 用环境变量限制 JVM 堆内存
ES 官方镜像有一个特殊的 JVM 配置方式:在环境变量里设置 ES_JAVA_OPTS。我之前看过有些教程让用户直接改镜像里的 jvm.options 文件,这种方案不推荐,因为容器重建后修改就丢了,而且改的是镜像内部文件,容易出各种诡异问题。
正确做法是:
yaml复制- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
-Xms 是初始堆大小,-Xmx 是最大堆大小,这两个值最好一致,避免 JVM 运行时动态扩容导致性能抖动。1g 的配置适合数据量不大的场景,如果数据量到了几十 GB,再去调整也不迟。注意这里设置的是 JVM 堆,不是容器内存,容器内存限制要额外用 mem_limit 控制,建议 mem_limit 至少是 JVM 堆的 2 倍,给堆外内存留空间。
4. 离线 IK 分词器:下载、安装与校验
4.1 为什么坚持用离线方式装 IK 分词器
IK 分词器是 ES 生态里最常用的中文分词插件。ES 的插件管理命令 elasticsearch-plugin install 是支持在线安装的,命令会从官方仓库下载插件包。但这里有一个尴尬的现实:IK 分词器并不在 Elastic 官方插件仓库里,它是社区项目,在线安装命令实际上要手动指定 URL 下载 zip 包。
内网环境就更麻烦,没有外网权限,elasticsearch-plugin 命令根本下载不了任何东西。所以离线安装成了最稳妥、最通用的方案:在一台有外网的机器上下载好 zip 包,拷贝到内网,然后放到插件目录里。这也是我在内网环境反复验证过的方式,稳定可靠。
4.2 下载 IK 分词器,注意版本严格对应
IK 分词器的 GitHub 仓库是 medcl/elasticsearch-analysis-ik,每个 ES 版本都有一个对应的 release。下载时先找到和你 ES 版本一致的 tag,比如 7.17.0,然后下载 elasticsearch-analysis-ik-7.17.0.zip。
下载链接格式通常是:
code复制https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.0/elasticsearch-analysis-ik-7.17.0.zip
注意仓库里的版本号有的带 v 前缀,有的不带,下载前多看一眼 release 页面。如果你无法访问外部站点,可以找一台能联网的机器下载后用 U 盘或内网文件服务器拷过去。
下载完先校验一下文件是否完整,zip 包一般有几十 MB,如果下载中断可能损坏。再用 unzip -l 看一眼包内容,正常应该包含核心 jar 包、配置文件和一些词典文件,比如 main.dic、stopword.dic 等。
4.3 挂载本机制作插件目录
离线安装 IK 有两条路线。一条是先启动 ES 容器,再用 docker exec 进入容器执行 elasticsearch-plugin install 命令,这种方案需要容器内有外网或者能访问本地文件,步骤繁琐,容器一删又没了。
另一条就是本文推荐的:不装进容器,而是把宿主机的插件目录挂载进容器。做法很简单,在宿主机 /opt/es/plugins 下新建 ik 目录,把解压后的文件放进去:
bash复制mkdir -p /opt/es/plugins/ik
unzip elasticsearch-analysis-ik-7.17.0.zip -d /opt/es/plugins/ik
目录结构如下:
code复制/opt/es/plugins/ik
├── commons-codec-1.9.jar
├── commons-logging-1.2.jar
├── elasticsearch-analysis-ik-7.17.0.jar
├── httpclient-4.5.2.jar
├── httpcore-4.4.4.jar
├── IKAnalyzer.cfg.xml
├── main.dic
├── quantifier.dic
├── stopword.dic
└── plugin-descriptor.properties
因为 compose 文件里已经挂载了 /opt/es/plugins:/usr/share/elasticsearch/plugins,所以这个 ik 目录会自动出现在容器的插件目录中。
这里有一个必须注意的细节:插件目录和插件内部文件的属主和权限要正确。ES 容器启动时会检查插件目录权限,如果属主不是 elasticsearch 用户,可能会拒绝加载。最简单的处理方式:宿主机上对 /opt/es 执行 chown -R 1000:1000 /opt/es,1000 是容器内 elasticsearch 用户的 UID。如果之前已经手动 chmod 777 了,一般也没问题,但严谨点还是把属主改对。
4.4 启动容器并确认 IK 插件加载
配置好目录之后,在 /opt/es 目录下执行:
bash复制docker-compose up -d
第一次启动会拉取镜像,等待时间取决于网络。启动完成后执行:
bash复制docker-compose ps
看到 es01 状态为 Up 基本就说明容器起来了。然后调用 ES 的 _cat/plugins 接口确认 IK 插件是否被正确加载:
bash复制curl http://localhost:9200/_cat/plugins
如果输出里包含 analysis-ik,就说明插件注册成功。这一步能确认插件是否生效,不要跳过。
如果 _cat/plugins 里看不到 IK,但容器又正常启动了,大概率是目录挂载没生效或者插件目录结构不对。检查一下宿主机 /opt/es/plugins/ik 里的 plugin-descriptor.properties 是否存在,缺失这个文件 ES 会忽略整个目录。
5. 启动集群与分词效果验证
5.1 验证 ES 服务健康状态
插件加载没问题后,先看 ES 节点本身是否健康。执行:
bash复制curl http://localhost:9200/
正常情况下会返回一段 JSON,里面有 cluster_name、cluster_uuid、version 等信息。注意看 version 里的 number 字段,确认版本确实是 7.17.0。
再查集群健康状态:
bash复制curl http://localhost:9200/_cluster/health?pretty
单节点环境下,status 通常是 yellow,这是因为单节点没有副本分片,主分片都分配了,但副本分片无法分配。这是正常现象,不是故障。只有出现了 red 才需要关注,说明有主分片未分配。
5.2 IK 分词效果实测
分词器装没装好,最终要看分词效果。ES 的 _analyze API 可以直接测试。IK 分词器主要提供两种分词模式:
ik_smart:最粗粒度切分,适合搜索关键词ik_max_word:最细粒度切分,会把词语拆到最细,适合建立索引
先测 ik_smart:
bash复制curl -X POST http://localhost:9200/_analyze?pretty \
-H 'Content-Type: application/json' \
-d '{
"analyzer": "ik_smart",
"text": "南京市长江大桥"
}'
如果 IK 插件生效,返回的分词结果不是单字拆散,而是组合成有意义的词语,比如"南京市""长江大桥"这样的粒度。如果返回结果里一个字一个字拆,说明 IK 没生效,请求可能落到了标准分词器上。
再测 ik_max_word:
bash复制curl -X POST http://localhost:9200/_analyze?pretty \
-H 'Content-Type: application/json' \
-d '{
"analyzer": "ik_max_word",
"text": "南京市长江大桥"
}'
ik_max_word 会比 ik_smart 拆出更多可能的词,包括"南京""南京市""长江大桥""大桥"等。这种细粒度适合索引阶段,可以让搜索召回更全。
5.3 配置自定义词典,处理专有名词
IK 分词器内置词典对通用中文效果不错,但碰到人名、地名、产品名或者网络热词,默认词典往往分得不准。IK 支持自定义词典,这也是很多老手实际使用时绕不开的一步。
IK 的配置文件是 IKAnalyzer.cfg.xml,在插件目录下:
xml复制<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<entry key="ext_dict">custom/mydict.dic</entry>
<entry key="ext_stopwords">custom/stopword.dic</entry>
</properties>
在插件目录下创建 custom 子目录,放入 mydict.dic 和 stopword.dic,每行一个词。比如我在 mydict.dic 里加过"阿里云""腾讯云""容器化"这类词,重启 ES 之后,这些词就能被 ik_smart 直接识别成整体,而不是拆开。
修改词典后需要重启容器:
bash复制docker-compose restart
注意:restart 不会重新创建容器,挂载的配置文件改动会直接生效。如果你改了 compose 文件里的挂载路径,才需要 docker-compose up -d 重新创建容器。
6. 常见问题排查与避坑实录
6.1 容器反复重启,多半是系统参数问题
ES 容器最常见的问题就是启动后马上退出,docker-compose ps 里看到状态一会儿 Up 一会儿 Restarting。先用日志定位:
bash复制docker-compose logs elasticsearch
日志里出现 max virtual memory areas vm.max_map_count [65530] likely too low,说明宿主机 vm.max_map_count 没调。按前面 2.3 节的方法设成 262144 再重启容器。
如果日志里出现 memory locking requested for elasticsearch process but memory is not locked,说明 bootstrap.memory_lock=true 生效了,但 ulimits 里的 memlock 没有设置成功。检查 compose 文件里 ulimits 部分是否写对,或者宿主机是否对容器限制了内存锁定。开发环境嫌麻烦的话,可以把 bootstrap.memory_lock=true 改成 false,不影响 IK 分词器的使用。
6.2 Spring Boot 健康检查一直报 health check failed
网上相关搜索里经常出现 e.elasticsearchrestclienthealthindicator : elasticsearch health check failed。这个报错其实是 Spring Boot 应用在连接 ES 时的健康检查失败,不是 ES 本身挂了。
遇到这个报错,先确认应用连接的地址和端口对不对。如果 ES 在 docker 里映射了 9200:9200,应用访问 localhost:9200 通常没问题。如果应用也在容器里,注意别用 localhost,要用 ES 容器的服务名或宿主机 IP。
还有一个原因是 ES 开启了安全认证但应用没配账号密码。7.x 版本如果设置了 xpack.security.enabled=true,所有请求都要带用户名密码,Spring Boot 需要配置 spring.elasticsearch.rest.username 和 spring.elasticsearch.rest.password。日常开发如果不需要安全认证,就在 compose 配置里显式设置 xpack.security.enabled=false,避免默认配置带来的麻烦。
6.3 IK 分词器不生效,先检查版本和目录结构
IK 分词器最常见的坑,就是我前面反复强调的版本对应关系。ES 7.17.0 必须搭配 IK 7.17.0,差一个小版本都可能启动报错或者插件加载失败。
插件目录解压后,确认 plugin-descriptor.properties 文件里写的 version 是不是和 ES 版本一致。这个文件是插件的元信息,ES 加载插件时第一时间校验它。
还有一点:IK 插件的 jar 包和依赖包必须在同一目录下,不能把 zip 直接扔到 plugins 目录就开始启动。ES 只认目录结构,不会自动解压。我第一次装的时候就是把 zip 原封不动扔进去了,结果 ES 日志直接报找不到插件描述文件。
6.4 Elasticsearch license 过期和集群安全配置
搜索热词里有人提到 Elasticsearch license,这通常指 X-Pack 的授权。7.1 之后的版本,基础安全功能(比如 TLS)是免费提供的,但一些高级功能需要白金版 license。如果你用 docker-compose 启动 ES 后看到 license 相关警告,先确认是不是用了默认的基础 license。
开发环境一般不需要关心 license,但如果要连 Kibana,ES 又没有配置安全认证,Kibana 7.x 启动时可能会提示安全配置缺失。最省事的方案是在 compose 配置里显式开启或关闭 X-Pack:
yaml复制- xpack.security.enabled=false
我个人建议开发环境直接关掉安全认证,减少踩坑面。生产环境再按安全规范开启 TLS 和账号体系,那时候再处理 license 也不迟。
6.5 端口映射失败和 docker-compose 限制容器配置
有时候容器起不来,日志没输出,先看是不是端口冲突。执行:
bash复制netstat -tlnp | grep 9200
如果有其他进程占用,把 compose 文件里的宿主机端口映射改掉,比如改成 9201:9200。容器里监听 9200 不变,只是宿主机访问入口变成 9201。
另外,docker-compose 本身可以对容器做资源限制,在服务下加:
yaml复制 mem_limit: 2g
cpus: 2.0
这能防止多个容器在同一台机器上互相抢资源。如果出现容器运行时卡顿、查询超时,可以检查一下是不是被 mem_limit 限制了。docker stats 可以实时查看容器资源占用,排查问题很管用。
6.6 常见问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 容器反复重启 | vm.max_map_count 过小 |
sysctl -w vm.max_map_count=262144 并写入 /etc/sysctl.conf |
| 启动报 memory locking 错误 | bootstrap.memory_lock=true 但 memlock 未正确设置 |
检查 compose 的 ulimits.memlock 配置,或改成 false |
| IK 分词器未加载 | 版本不匹配、目录结构不对 | 核对 IK 版本与 ES 完全一致,确认 plugin-descriptor.properties 存在 |
| 分词结果是单字 | 索引或查询用了 standard 分词器 |
在 mapping 里显式指定 ik_smart 或 ik_max_word |
| Spring Boot 健康检查失败 | 地址错误或安全认证未配置 | 检查 spring.elasticsearch.rest.uris,确认是否需要用户名密码 |
| 自定义词典不生效 | 词典路径不对或未重启 | 确认 IKAnalyzer.cfg.xml 路径,docker-compose restart 后重试 |
| 端口无法访问 | 宿主端口被占用 | netstat -tlnp 排查,修改端口映射 |
| 磁盘空间不足 | 数据目录或日志目录膨胀 | docker system df 查看,清理旧容器和镜像,设置日志轮转 |
写在最后的一点经验
这套 docker-compose + 离线 IK 分词器的组合,我在本地开发机器和内网服务器上都验证过很多遍,可以说踩过的坑都写在上面了。如果只让我说一条最值得记住的经验,那就是 ES 和 IK 的版本号必须逐步核对,哪怕只是小版本不一致,也会白白浪费很多排查时间。
还有一件事:数据目录一定要用宿主机挂载,别把数据留在容器内部。容器这玩意说删就删,重建一次很轻松,但如果数据没挂载出来,哭都来不及。插件目录同理,用挂载的方式管理,升级版本的时候只需要替换宿主机的文件,重新创建容器就完事。
最后再分享一个小技巧:重启容器之前,先执行 docker-compose config 校验一下 compose 文件格式,很多低级错误这一步就能看出来。等稳定跑起来之后,再考虑要不要加 Kibana、加集群节点,那都是后话了。希望这篇能帮你少踩几个坑。
