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 大模型等实战技能。未来运维生涯始终有我坐在你的副驾

版权声明

  • 所有内容均属于原创,感谢阅读、收藏,转载请联系授权,未经授权不得转载
相关推荐
ly21st1 小时前
elasticsearch安全认证
安全·elasticsearch
Mitch3111 小时前
【漏洞复现】CVE-2014-3120 & CVE-2015-1427 Expression Injection
运维·web安全·elasticsearch·docker·apache
m0_748251082 小时前
docker安装nginx,docker部署vue前端,以及docker部署java的jar部署
java·前端·docker
m0_748240252 小时前
docker--压缩镜像和加载镜像
java·docker·eureka
开心最重要(*^▽^*)3 小时前
Metricbeat安装教程——Linux——Metricbeat监控ES集群
linux·elasticsearch
叫我DPT3 小时前
Elasticsearch 数据存储底层机制详解
elasticsearch·搜索引擎·全文检索
努力的布布3 小时前
Elasticsearch-索引的批量操作
大数据·elasticsearch·搜索引擎·全文检索
斑驳竹影3 小时前
ElasticSearch存储引擎
大数据·elasticsearch·搜索引擎
lmxnsI4 小时前
docker使用笔记
笔记·docker·容器
木卫二号Coding4 小时前
Docker-构建自己的Web-Linux系统-镜像webtop:ubuntu-kde
linux·ubuntu·docker