Docker 最佳实战:Docker 部署单节点 ElasticSearch 实战

Docker 最佳实战:Docker 部署单节点 ElasticSearch 实战

2024 年云原生运维实战文档 99 篇原创计划 第 015 篇 |Docker 最佳实战「2024」系列 第 010 篇

你好,欢迎来到运维有术

今天分享的内容是 Docker 最佳实战「2024」 系列文档中的 Docker 部署单节点 ElasticSearch 实战

本文将详细介绍如何用 Docker 容器及 Docker Compose 部署单节点 ElasticSearch,并配置基于 x-pack 的认证和 TLS 加密。

实战服务器配置 (架构 1:1 复刻小规模生产环境,配置略有不同)

主机名 IP CPU(核) 内存(GB) 系统盘(GB) 数据盘(GB) 用途
docker-node-1 192.168.9.81 4 16 40 100 Docker 节点 1
docker-node-2 192.168.9.82 4 16 40 100 Docker 节点 2
docker-node-3 192.168.9.83 4 16 40 100 Docker 节点 3
合计 3 12 48 120 300

实战环境涉及软件版本信息

  • 操作系统:openEuler 22.03 LTS SP3
  • Docker:24.0.7
  • ElasticSearch:7.17.20

1. 前置条件

  • 配置系统内核参数
bash 复制代码
echo "vm.max_map_count=262144" >> /etc/sysctl.conf
sysctl -w vm.max_map_count=262144
  • 准备密码

本文所有涉及密码的配置,均使用通用密码 PleaseChangeMe

生产环境,请用密码生成器生成20位以上 不带特殊符号只包含大小写字母和数字混合组成的密码。

2. 准备前置数据

2.1 创建数据目录

bash 复制代码
mkdir -p /data/containers/elasticsearch/{data,plugins,logs}
chown 1000:0 /data/containers/elasticsearch/{data,logs}
mkdir -p /data/containers/elasticsearch/config/certs

2.2 创建 ElasticSearch 自定义配置文件

实现 ElasticSearch 服务自定义配置有两种方案:

  • Docker-compose 中设置环境变量
  • 编写 elasticsearch.yml 配置文件,挂载到容器配置文件目录

本文选择第二种,编辑 elasticsearch.yml 配置文件,挂载到容器 /usr/share/elasticsearch/config 目录的方案。

创建配置文件,vi /data/containers/elasticsearch/config/elasticsearch.yml

yaml 复制代码
# 基本配置
cluster.name: es-cluster
discovery.type: single-node
network.host: 0.0.0.0
http.port: 9200
​
# 启用 xpack 及 TLS
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
​
# 证书配置
xpack.security.transport.ssl.keystore.type: PKCS12
xpack.security.transport.ssl.truststore.type: PKCS12
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.keystore.path: certs/elastic-certificates.p12
xpack.security.transport.ssl.truststore.path: certs/elastic-certificates.p12
#xpack.security.transport.ssl.keystore.password: PleaseChangeMe
#xpack.security.transport.ssl.truststore.password: PleaseChangeMe
​
# 其他配置
# 禁用 geoip
ingest.geoip.downloader.enabled: false
​
# 启用审计
xpack.security.audit.enabled: true

2.3 创建 CA 文件

  1. 执行下面的命令生成 CA 文件
bash 复制代码
cd /data/containers/elasticsearch
docker run -it --rm \
-v ./config/certs:/usr/share/elasticsearch/config/certs \
elasticsearch:7.17.20 \
bin/elasticsearch-certutil ca --out config/certs/elastic-stack-ca.p12 --pass "PleaseChangeMe"

说明:

  • --pass 生产环境一定要替换成自己的密码

正确执行后,输出结果如下:

vbnet 复制代码
[root@docker-node-1 elasticsearch]# docker run -it --rm \
> -v ./config/certs:/usr/share/elasticsearch/config/certs \
> elasticsearch:7.17.20 \
> bin/elasticsearch-certutil ca --out config/certs/elastic-stack-ca.p12 --pass "PleaseChangeMe"
This tool assists you in the generation of X.509 certificates and certificate
signing requests for use with SSL/TLS in the Elastic stack.
​
The 'ca' mode generates a new 'certificate authority'
This will create a new X.509 certificate and private key that can be used
to sign certificate when running in 'cert' mode.
​
Use the 'ca-dn' option if you wish to configure the 'distinguished name'
of the certificate authority
​
By default the 'ca' mode produces a single PKCS#12 output file which holds:
    * The CA certificate
    * The CA's private key
​
If you elect to generate PEM format certificates (the -pem option), then the output will
be a zip file containing individual files for the CA certificate and private key
  1. 查看是否生成证书
arduino 复制代码
[root@docker-node-1 elasticsearch]# ls config/certs/
elastic-stack-ca.p12

2.4 创建 elastic-certificates.p12 证书

  1. 执行下面的命令创建 elastic-certificates.p12 证书
arduino 复制代码
docker run -it --rm \
-v ./config/certs:/usr/share/elasticsearch/config/certs \
elasticsearch:7.17.20 \
bin/elasticsearch-certutil cert --silent --ca config/certs/elastic-stack-ca.p12 --out config/certs/elastic-certificates.p12 --ca-pass "PleaseChangeMe" --pass "PleaseChangeMe"

说明:

  • --ca-pass CA 证书的密码
  • --pass p12 证书的密码

正确执行后,输出结果如下:

bash 复制代码
[root@docker-node-1 elasticsearch]# docker run -it --rm \
> -v ./config/certs:/usr/share/elasticsearch/config/certs \
> elasticsearch:7.17.20 \
> bin/elasticsearch-certutil cert --silent --ca config/certs/elastic-stack-ca.p12 --out config/certs/elastic-certificates.p12 --ca-pass "PleaseChangeMe" --pass "PleaseChangeMe"
[root@docker-node-1 elasticsearch]# ls config/certs/
elastic-certificates.p12  elastic-stack-ca.p12
  1. 配置证书文件权限
bash 复制代码
chown -R 1000.0 config/certs/

2.5 生成加密的 keystore 文件

默认情况下,Elasticsearch 自动生成用于安全设置的密钥存储库文件elasticsearch.keystore

该文件的用途是存储需要加密的 key/value 配置数据。但是该文件默认只是被简单的模糊(obfuscated)处理,并没有加密。用命令 elasticsearch-keystore list 可以轻松读取到文件内容。生产环境建议做加密处理

  1. 执行下面的命令创建 elasticsearch.keystore 文件
bash 复制代码
docker run -it --rm \
-v ./config:/usr/share/elasticsearch/config \
elasticsearch:7.17.20 \
bin/elasticsearch-keystore create -p

正确执行后,输出结果如下:

bash 复制代码
[root@docker-node-1 elasticsearch]# docker run -it --rm \
> -v ./config:/usr/share/elasticsearch/config \
> elasticsearch:7.17.20 \
> bin/elasticsearch-keystore create -p
Enter new password for the elasticsearch keystore (empty for no password):
Enter same password again:
Created elasticsearch keystore in /usr/share/elasticsearch/config/elasticsearch.keystore
[root@docker-node-1 elasticsearch]# ls config/
certs  elasticsearch.keystore  elasticsearch.yml

注意: 命令执行过程中,需按提示输入两次密码

  1. 添加 p12 证书的密码配置添加到 keystore 文件
bash 复制代码
# keystore.secure_password
docker run -it --rm \
-v ./config:/usr/share/elasticsearch/config \
elasticsearch:7.17.20 \
bin/elasticsearch-keystore add xpack.security.transport.ssl.keystore.secure_password
​
# truststore.secure_password
docker run -it --rm \
-v ./config:/usr/share/elasticsearch/config \
elasticsearch:7.17.20 \
bin/elasticsearch-keystore add xpack.security.transport.ssl.truststore.secure_password

正确执行后,输出结果如下:

bash 复制代码
# 正确执行没有任何输出
[root@docker-node-1 elasticsearch]# docker run -it --rm \
> -v ./config:/usr/share/elasticsearch/config \
> elasticsearch:7.17.20 \
> bin/elasticsearch-keystore add xpack.security.transport.ssl.keystore.secure_password
Enter password for the elasticsearch keystore :
Enter value for xpack.security.transport.ssl.keystore.secure_password:
​
[root@docker-node-1 elasticsearch]# docker run -it --rm \
> -v ./config:/usr/share/elasticsearch/config \
> elasticsearch:7.17.20 \
> bin/elasticsearch-keystore add xpack.security.transport.ssl.truststore.secure_password
Enter password for the elasticsearch keystore :
Enter value for xpack.security.transport.ssl.truststore.secure_password:

注意:

  • 命令执行过程中,请按提示输入两次密码
  • 第一次密码是 elasticsearch.keystore 文件的密码,第二次密码是 secure_password 的密码
  1. 验证 elasticsearch.keystore 是否加密
ruby 复制代码
docker run -it --rm \
-v ./config/:/usr/share/elasticsearch/config \
elasticsearch:7.17.20 \
bin/elasticsearch-keystore list

正确执行后,输出结果如下:

ruby 复制代码
[root@docker-node-1 elasticsearch]# docker run -it --rm \
> -v ./config/:/usr/share/elasticsearch/config \
> elasticsearch:7.17.20 \
> bin/elasticsearch-keystore list
Enter password for the elasticsearch keystore :
keystore.seed
xpack.security.transport.ssl.keystore.secure_password
xpack.security.transport.ssl.truststore.secure_password

注意: 提示 Enter password for the elasticsearch keystore : 输入正确的密码后显示文件内容,说明文件已经加密。

3. 安装部署 ElasticSearch

3.1 创建 docker-compose.yml 文件

创建配置文件,vi /data/containers/elasticsearch/docker-compose.yml

yaml 复制代码
name: 'elasticsearch'
services:
  elasticsearch:
    restart: always
    image: elasticsearch:7.17.20
    container_name: es-single
    ulimits:
      nproc: 65535
      memlock:
        soft: -1
        hard: -1
    environment:
      - TZ=Asia/Shanghai
      - ES_JAVA_OPTS=-Xms2048m -Xmx2048m
      - KEYSTORE_PASSWORD=PleaseChangeMe
    volumes:
      - ./data:/usr/share/elasticsearch/data
      - ./plugins:/usr/share/elasticsearch/plugins
      - ./logs:/usr/share/elasticsearch/logs
      - ./config/certs/:/usr/share/elasticsearch/config/certs
      - ./config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
      - ./config/elasticsearch.keystore:/usr/share/elasticsearch/config/elasticsearch.keystore
    networks:
      - app-tier
    ports:
      - 9200:9200
      - 9300:9300
networks:
  app-tier:
    name: app-tier
    driver: bridge
    #external: true
    #ipam:
    #  config:
    #    - subnet: 172.22.1.0/24

说明:

  • ES_JAVA_OPTS 需根据服务器实际配置调整 JAVA_OPTS 配置
  • KEYSTORE_PASSWORD 必须跟生成加密的 elasticsearch.keystore 文件时使用的密码一致,否则 ES 启动会失败
  • ipam 配置了 app-tier 的网络地址,本文注释了,生产环境建议合理规划配置。
  • external: true , 同一台服务器其他服务已经创建网络 app-tier 时,创建 elasticsearch 服务时会报错,可以启用这个参数。

3.2 创建并启动 ElasticSearch 服务

  • 启动服务
bash 复制代码
cd /data/containers/elasticsearch
docker compose up -d
  • 正确执行后,输出结果如下
ini 复制代码
[root@docker-node-1 elasticsearch]# docker compose up -d
[+] Running 1/2
 ⠸ Network app-tier     Created                                                                                                                                 0.4s
 ✔ Container es-single  Started 

3.3 验证容器状态

  • 查看 ElasticSearch 容器状态
bash 复制代码
[root@docker-node-1 elasticsearch]# docker compose ps
NAME        IMAGE                   COMMAND                  SERVICE         CREATED          STATUS                  PORTS
es-single   elasticsearch:7.17.20   "/bin/tini -- /usr/l..."   elasticsearch   16 seconds ago   Up Less than a second   0.0.0.0:9200->9200/tcp, :::9200->9200/tcp, 0.0.0.0:9300->9300/tcp, :::9300->9300/tcp
  • 查看 ElasticSearch 服务日志
bash 复制代码
# 通过日志查看 elasticsearch 是否有异常,结果略
docker compose logs -f

4. 密码配置

4.1 为保留用户自动生成初始密码

执行下面的命令:

arduino 复制代码
docker exec -it es-single bin/elasticsearch-setup-passwords auto

正确执行后,输出结果如下:

ini 复制代码
[root@docker-node-1 elasticsearch]# docker exec -it es-single bin/elasticsearch-setup-passwords auto
Enter password for the elasticsearch keystore :
Initiating the setup of passwords for reserved users elastic,apm_system,kibana,kibana_system,logstash_system,beats_system,remote_monitoring_user.
The passwords will be randomly generated and printed to the console.
Please confirm that you would like to continue [y/N]y


Changed password for user apm_system
PASSWORD apm_system = dFeUZ5kSgq3Gh4GNVZSJ

Changed password for user kibana_system
PASSWORD kibana_system = YUuHRRQ9NX7ZbdGj40hY

Changed password for user kibana
PASSWORD kibana = YUuHRRQ9NX7ZbdGj40hY

Changed password for user logstash_system
PASSWORD logstash_system = oCqLt1l1ZWCB9eWkKoMS

Changed password for user beats_system
PASSWORD beats_system = iMGY5hLUJBCBHPUrBm2k

Changed password for user remote_monitoring_user
PASSWORD remote_monitoring_user = 7YJ8pTA1fIiTJEGKcHIT

Changed password for user elastic
PASSWORD elastic = Uhfiv3zGRvGsNN58shT0

说明:

  • 命令执行时需要输入 elasticsearch keystore 文件的密码
  • 请记录并妥善保存自动生成的密码

4.2 创建自定义管理员用户

创建一个自定义的管理员用户用于日常管理。

执行下面的命令:

bash 复制代码
docker exec -it es-single bin/elasticsearch-users useradd elasticadmin -p PleaseChangeMe -r superuser

5. 验证测试 ElasticSearch

5.1 命令行查看集群节点

执行下面的命令:

ini 复制代码
curl -X GET -u elasticadmin "localhost:9200/_cat/nodes?v=true&pretty"

正确执行后,输出结果如下:

sql 复制代码
[root@docker-node-1 elasticsearch]# curl -X GET -u elasticadmin "localhost:9200/_cat/nodes?v=true&pretty"
Enter host password for user 'elasticadmin':
ip         heap.percent ram.percent cpu load_1m load_5m load_15m node.role   master name
172.20.0.2           16          45   0    0.04    0.14     0.34 cdfhilmrstw *      5e53c312d114

说明: 按提示输入用户 elasticadmin 的密码。

6. 常见问题

6.1 问题1

vbnet 复制代码
es-single  | {"type": "server", "timestamp": "2024-05-07T10:00:22,991+08:00", "level": "ERROR", "component": "o.e.i.g.GeoIpDownloader", "cluster.name": "es-cluster", "node.name": "163ac8cb28d7", "message": "exception during geoip databases update", "cluster.uuid": "BenkNlbKQ3a7IiqU5shtOw", "node.id": "iu7VycUqTXyXbsjSGpotVw" ,
es-single  | "stacktrace": ["java.net.UnknownHostException: geoip.elastic.co",
es-single  | "at sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:567) ~[?:?]",
es-single  | "at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327) ~[?:?]",
es-single  | "at java.net.Socket.connect(Socket.java:751) ~[?:?]",
es-single  | "at sun.security.ssl.SSLSocketImpl.connect(SSLSocketImpl.java:304) ~[?:?]",
es-single  | "at sun.net.NetworkClient.doConnect(NetworkClient.java:178) ~[?:?]",
es-single  | "at sun.net.www.http.HttpClient.openServer(HttpClient.java:531) ~[?:?]",
es-single  | "at sun.net.www.http.HttpClient.openServer(HttpClient.java:636) ~[?:?]",
es-single  | "at sun.net.www.protocol.https.HttpsClient.<init>(HttpsClient.java:264) ~[?:?]",
es-single  | "at sun.net.www.protocol.https.HttpsClient.New(HttpsClient.java:377) ~[?:?]",
es-single  | "at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.getNewHttpClient(AbstractDelegateHttpsURLConnection.java:193) ~[?:?]",
es-single  | "at sun.net.www.protocol.http.HttpURLConnection.plainConnect0(HttpURLConnection.java:1237) ~[?:?]",
es-single  | "at sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConnection.java:1123) ~[?:?]",
es-single  | "at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:179) ~[?:?]",
es-single  | "at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1675) ~[?:?]",
es-single  | "at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1599) ~[?:?]",
es-single  | "at java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:531) ~[?:?]",
es-single  | "at sun.net.www.protocol.https.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:307) ~[?:?]",
es-single  | "at org.elasticsearch.ingest.geoip.HttpClient.lambda$get$0(HttpClient.java:55) ~[ingest-geoip-7.17.20.jar:7.17.20]",
es-single  | "at java.security.AccessController.doPrivileged(AccessController.java:571) ~[?:?]",
es-single  | "at org.elasticsearch.ingest.geoip.HttpClient.doPrivileged(HttpClient.java:97) ~[ingest-geoip-7.17.20.jar:7.17.20]",
es-single  | "at org.elasticsearch.ingest.geoip.HttpClient.get(HttpClient.java:49) ~[ingest-geoip-7.17.20.jar:7.17.20]",
es-single  | "at org.elasticsearch.ingest.geoip.HttpClient.getBytes(HttpClient.java:40) ~[ingest-geoip-7.17.20.jar:7.17.20]",
es-single  | "at org.elasticsearch.ingest.geoip.GeoIpDownloader.fetchDatabasesOverview(GeoIpDownloader.java:159) ~[ingest-geoip-7.17.20.jar:7.17.20]",
es-single  | "at org.elasticsearch.ingest.geoip.GeoIpDownloader.updateDatabases(GeoIpDownloader.java:147) ~[ingest-geoip-7.17.20.jar:7.17.20]",
es-single  | "at org.elasticsearch.ingest.geoip.GeoIpDownloader.runDownloader(GeoIpDownloader.java:284) [ingest-geoip-7.17.20.jar:7.17.20]",
es-single  | "at org.elasticsearch.ingest.geoip.GeoIpDownloaderTaskExecutor.nodeOperation(GeoIpDownloaderTaskExecutor.java:100) [ingest-geoip-7.17.20.jar:7.17.20]",
es-single  | "at org.elasticsearch.ingest.geoip.GeoIpDownloaderTaskExecutor.nodeOperation(GeoIpDownloaderTaskExecutor.java:46) [ingest-geoip-7.17.20.jar:7.17.20]",
es-single  | "at org.elasticsearch.persistent.NodePersistentTasksExecutor$1.doRun(NodePersistentTasksExecutor.java:42) [elasticsearch-7.17.20.jar:7.17.20]",
es-single  | "at org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingAbstractRunnable.doRun(ThreadContext.java:777) [elasticsearch-7.17.20.jar:7.17.20]",
es-single  | "at org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:26) [elasticsearch-7.17.20.jar:7.17.20]",
es-single  | "at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) [?:?]",
es-single  | "at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) [?:?]",
es-single  | "at java.lang.Thread.run(Thread.java:1583) [?:?]"] }
  • 解决方案

离线环境未使用 geoip 功能,在 elasticsearch.yml中添加如下配置禁用 geoip downloader

yaml 复制代码
ingest.geoip.downloader.enabled: false

6.2 问题 2

  • 问题现象

添加新的管理员用户时报错如下:

bash 复制代码
[root@1--2--3--1--2--3--0006 elasticsearch]# docker run -it --rm \
> -v ./config:/usr/share/elasticsearch/config \
> elasticsearch:7.17.20 \
> bin/elasticsearch-users useradd elastic -p elasticPWD -r superuser

ERROR: Invalid username [elastic]... Username [elastic] is reserved and may not be used.
  • 解决方案

elastic 属于 elasticsearch 内置账户的名字不允许使用。换个用户名即可。

6.3 问题 3

  • 问题现象

ElasticSearch 容器启动时报错。

php 复制代码
es-single  | Exception in thread "main" java.lang.IllegalStateException: Keystore passphrase required but none provided.
es-single  |    at org.elasticsearch.bootstrap.Bootstrap.readPassphrase(Bootstrap.java:305)
es-single  |    at org.elasticsearch.bootstrap.Bootstrap.loadSecureSettings(Bootstrap.java:261)
es-single  |    at org.elasticsearch.bootstrap.Bootstrap.loadSecureSettings(Bootstrap.java:247)
es-single  |    at org.elasticsearch.bootstrap.Bootstrap.init(Bootstrap.java:364)
es-single  |    at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:169)
es-single  |    at org.elasticsearch.bootstrap.Elasticsearch.execute(Elasticsearch.java:160)
es-single  |    at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:77)
es-single  |    at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:112)
es-single  |    at org.elasticsearch.cli.Command.main(Command.java:77)
es-single  |    at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:125)
es-single  |    at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:80)
  • 解决方案

编辑 docker-compose.yml,在 environment 配置项中加入 KEYSTORE_PASSWORD=PleaseChangeMe

以上,就是今天分享的内容,下一期我们会分享如何用 Docker 部署 Kibana 并对接 ElasticSearch 集群。敬请持续关注!!!

免责声明:

  • 笔者水平有限,尽管经过多次验证和检查,尽力确保内容的准确性,但仍可能存在疏漏之处。敬请业界专家大佬不吝指教。
  • 本文所述内容仅通过实战环境验证测试,读者可学习、借鉴,但严禁直接用于生产环境由此引发的任何问题,作者概不负责

Get 本文实战视频(请注意,文档视频异步发行,请先关注)

如果你喜欢本文,请分享、收藏、点赞、评论! 请持续关注 @运维有术,及时收看更多好文!

欢迎加入 「知识星球|运维有术」 ,获取更多的 KubeSphere、Kubernetes、云原生运维、自动化运维、AI 大模型等实战技能。未来运维生涯始终有我坐在你的副驾

版权声明

  • 所有内容均属于原创,感谢阅读、收藏,转载请联系授权,未经授权不得转载
相关推荐
小汤猿人类1 小时前
ES关系映射(数据库中的表结构)
大数据·数据库·elasticsearch
Elastic 中国社区官方博客2 小时前
Elasticsearch 8.18 中提供了原生连接 (Native Joins)
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
laimaxgg3 小时前
Docker华为云创建私人镜像仓库
运维·服务器·docker·容器·华为云
Elastic 中国社区官方博客6 小时前
如何在不同版本的 Elasticsearch 之间以及集群之间迁移数据
大数据·数据库·elasticsearch·搜索引擎·全文检索·logstash
坤小满学Java6 小时前
【从0到1学Elasticsearch】Elasticsearch从入门到精通(上)
elasticsearch
CAE虚拟与现实10 小时前
Dockerfile 文件常见命令及其作用
docker·容器·k8s·镜像·dockerhub
帽儿山的枪手12 小时前
程序员必掌握的docker命令
docker·容器·kubernetes
重生之我是cxk12 小时前
docker多架构镜像构建
docker·容器·架构
半吊子的程序狗13 小时前
docker测试镜像源
运维·docker·容器