dockerTips
dockerTips
优质教程:Docker — 从入门到实践
docker 的主要使用场景
当我的产品中包含了多个软件,同时包含了相关的数据的时候,我希望实现一个整体包,用户拿到这个整体包之后直接一键启动就可以看到效果,docker 就可以实现这个。
虽然虚拟机也可以实现这个,但是没有 docker 轻量。
配合 docker-compose,部署和维护更加的方便
我们一般将容器的数据文件挂载到宿主机上,假设这个数据有多个版本,那么只要挂载不同的目录,就可以实现容器不同数据的版本的切换,这样就很容易实现了容器数据多版本管理。
镜像(image)相当于虚拟机模板,一个镜像可以以不同的容器名称启动多个容器
docker 相关技术栈
如何通过 x86 环境下载 arm 镜像
有的镜像会专门打一个 arm 版,例如 xxx-arm,有的则是集成在一个名字下面,比如这样
你如果在 x86 上 pull 这个镜像,再导出来,那在 arm 下是用不了的,你要在 pull 的时候指定文件签名,例如这个 tdengine 的 3.0.1.8 的 arm 版是
docker pull tdengine/tdengine:3.0.1.8@sha256:4a4240eabc83908bafa38bb8a9a3155ddaabf193763c877f9c41d1d79bfcf2fc
docker pull tdengine/tdengine-aarch64:3.0.4.0
不过 tdengine 有一个专门的 arm 镜像,所以没这个问题,我只是举了个例子,实际你们直接用下面这个镜像就行了:Docker镜像地址
docker pull tdengine/tdengine-aarch64:3.0.1.8
docker pull tdengine/tdengine-aarch64:3.0.4.0
在一台 x86 电脑上 pull 完了,执行下面的命令就能导出 tar 包,执行命令的目录就是导出的 tar 包所在的目录。
docker save -o tdengine-aarch64-3.0.1.8.tar docker.io/tdengine/tdengine-aarch64:3.0.1.8
docker save -o tdengine-aarch64-3.0.4.0.tar docker.io/tdengine/tdengine-aarch64:3.0.4.0
jib-maven-plugin
https://github.com/GoogleContainerTools/jib 将 Java 应用打包成 docker 镜像
教程:Docker 与 Jib(maven 插件版)实战_ITPUB 博客
docker 镜像下不下来的问题的解决方案,jibDockerBuild 发生 Unauthorized for registry-1.docker.io/library/openjdk_fangd_h 的博客-CSDN 博客
我想在本地搭建一个 docker 仓库,然后把镜像拉下来之后,离线打包,就跟 maven 的本地仓库一样。算了,我不想在本地装虚拟机,太重了,算了。
学会了这个插件,我就可以自己制作自己的 docker 包,然后全平台部署,哦 yes。
示例配置:
<!-- 打包 docker 镜像 -->
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.2.1</version>
<configuration>
<!--from 节点用来设置镜像的基础镜像,相当于 Docerkfile 中的 FROM 关键字-->
<from>
<!-- 使用的公司仓库,可以替换,必须添加 host 192.168.115.208 harbor.dameng.io -->
<image>
openjdk:8u342-jre@sha256:46a298905a037f46e5bc93aae3b061e0e148beab5098e6d0c26d5e7981ac36e3
</image>
<auth>
<username>${docker.auth.username}</username>
<password>${docker.auth.password}</password>
</auth>
</from>
<to>
<!-- 镜像名称和 tag,用英文冒号分割-->
<!-- 使用属性 docker.image.name 表示打出的 image 的名称,不建议使用当前构建的名字 ${project.name} ,因为 image 的名字不能有大写字母-->
<image>${docker.image.name}:1.0</image>
<!-- <auth>-->
<!-- <username>${docker.auth.username}</username>-->
<!-- <password>${docker.auth.password}</password>-->
<!-- </auth>-->
</to>
<outputPaths>
<tar>${project.build.directory}/${docker.image.name}-image.tar</tar>
<digest>${project.build.directory}/${docker.image.name}-image.digest</digest>
<imageId>${project.build.directory}/${docker.image.name}-image.id</imageId>
<imageJson>${project.build.directory}/${docker.image.name}-image.json</imageJson>
</outputPaths>
<!--容器相关的属性-->
<container>
<!-- 主程序类 -->
<mainClass>com.example.tdenginedatainsert.TDengineDataInsertApplication</mainClass>
<!--jvm 内存参数-->
<jvmFlags>
<jvmFlag>-XX:+UseContainerSupport</jvmFlag>
<jvmFlag>-XX:InitialRAMPercentage=50.0</jvmFlag>
<jvmFlag>-XX:MinRAMPercentage=50.0</jvmFlag>
<jvmFlag>-XX:MaxRAMPercentage=75.0</jvmFlag>
<jvmFlag>-Xss512K</jvmFlag>
<jvmFlag>-XX:MetaspaceSize=256m</jvmFlag>
<jvmFlag>-XX:MaxMetaspaceSize=512m</jvmFlag>
<jvmFlag>-Djava.awt.headless=true</jvmFlag>
<jvmFlag>-Dfile.encoding=utf-8</jvmFlag>
<jvmFlag>-Djava.security.egd=file:/dev/./urandom</jvmFlag>
<jvmFlag>-XX:+DisableExplicitGC</jvmFlag>
<jvmFlag>-XX:-UseAdaptiveSizePolicy</jvmFlag>
<jvmFlag>-Duser.timezone=GMT+08</jvmFlag>
</jvmFlags>
<!--要暴露的端口-->
<ports>
<port>8090</port>
</ports>
<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
</container>
<!--开启允许上传镜像到仓库中-->
<!-- <allowInsecureRegistries>true</allowInsecureRegistries>-->
<!--外部文件-->
<!-- 注意两点路径的对应: -->
<!-- 1. 项目运行时的工作路径,就是打出的 docker 包运行起来的容器的 Linux 系统的根路径 -->
<!-- 2. extraDirectories 的 paths 的 path 的 into 标签中指定的相对路径的起点,是打出的 docker 包运行起来的容器的 Linux 系统的根路径 -->
<extraDirectories>
<paths>
<path>
<from>${project.build.directory}/classes/config</from>
<into>/config</into>
</path>
<path>
<from>${project.build.directory}/classes/coordinateData</from>
<into>/coordinateData</into>
</path>
</paths>
</extraDirectories>
</configuration>
</plugin>
<!-- 打包 docker 镜像 end -->
如何使用本地的 tar 包来打镜像
<from>
<image>tar://path/to/saved.tar</image>
</from>
Docker run
runoob@runoob:~$ docker run -it nginx:latest /bin/bash
root@b8573233d675:/#
Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
02.
03. -d, --detach=false 指定容器运行于前台还是后台,默认为 false
04. -i, --interactive=false 打开 STDIN,用于控制台交互
05. -t, --tty=false 分配 tty 设备,该可以支持终端登录,默认为 false
06. -u, --user="" 指定容器的用户
07. -a, --attach=[] 登录容器(必须是以 docker run -d 启动的容器)
08. -w, --workdir="" 指定容器的工作目录
09. -c, --cpu-shares=0 设置容器 CPU 权重,在 CPU 共享场景使用
10. -e, --env=[] 指定环境变量,容器中可以使用该环境变量
11. -m, --memory="" 指定容器的内存上限
12. -P, --publish-all=false 指定容器暴露的端口
13. -p, --publish=[] 指定容器暴露的端口
14. -h, --hostname="" 指定容器的主机名
15. -v, --volume=[] 给容器挂载存储卷,挂载到容器的某个目录
16. --volumes-from=[] 给容器挂载其他容器上的卷,挂载到容器的某个目录
17. --cap-add=[] 添加权限,权限清单详见:http://linux.die.net/man/7/capabilities
18. --cap-drop=[] 删除权限,权限清单详见:http://linux.die.net/man/7/capabilities
19. --cidfile="" 运行容器后,在指定文件中写入容器 PID 值,一种典型的监控系统用法
20. --cpuset="" 设置容器可以使用哪些 CPU,此参数可以用来容器独占 CPU
21. --device=[] 添加主机设备给容器,相当于设备直通
22. --dns=[] 指定容器的 dns 服务器
23. --dns-search=[] 指定容器的 dns 搜索域名,写入到容器的/etc/resolv.conf 文件
24. --entrypoint="" 覆盖 image 的入口点
25. --env-file=[] 指定环境变量文件,文件格式为每行一个环境变量
26. --expose=[] 指定容器暴露的端口,即修改镜像的暴露端口
27. --link=[] 指定容器间的关联,使用其他容器的 IP、env 等信息
28. --lxc-conf=[] 指定容器的配置文件,只有在指定--exec-driver=lxc 时使用
29. --name="" 指定容器名字,后续可以通过名字进行容器管理,links 特性需要使用名字
30. --net="bridge" 容器网络设置:
31. bridge 使用 docker daemon 指定的网桥
32. host //容器使用主机的网络
33. container:NAME_or_ID >//使用其他容器的网路,共享 IP 和 PORT 等网络资源
34. none 容器使用自己的网络(类似--net=bridge),但是不进行配置
35. --privileged=false 指定容器是否为特权容器,特权容器拥有所有的 capabilities
36. --restart="no" 指定容器停止后的重启策略:
37. no:容器退出时不重启
38. on-failure:容器故障退出(返回值非零)时重启
39. always:容器退出时总是重启
40. --rm=false 指定容器停止后自动删除容器(不支持以 docker run -d 启动的容器)
41. --sig-proxy=true 设置由代理接受并处理信号,但是 SIGCHLD、SIGSTOP 和 SIGKILL 不能被代理
这里重点说一下 -v
参数,这个参数除了指定挂载的路径外,还可以指定挂载内容的同步方式
docker run --name my-custom-nginx-container -v /host/path/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx
其中 :ro
指定的就是 readonly 模式,其他模式呢?总共有三种模式 rw、ro 和不指定模式,这个参数关系到宿主机与容器的文件、文件夹变化关系,下面来一一详解
-
不指定
-
文件:宿主机修改该文件后容器里面看不到变化;容器里面修改该文件,宿主机也看不到变化
-
文件夹:不管是宿主机还是容器内修改、新增、删除文件,都会相互同步
-
-
ro
-
文件:容器内不能修改,会提示 read-only
-
文件夹:容器内不能修改、新增、删除文件夹中的文件,会提示 read-only
-
-
rw
-
文件:不管是宿主机还是容器内修改,都会相互同步,但容器内不允许删除,会提示 Device or resource busy;宿主机删除文件,容器内的不会被同步
-
文件夹:不管是宿主机还是容器内修改、新增、删除文件,都会相互同步
-
-
shared
请看《NFSTips.md》
Docker desktop
放弃在 Windows 上装 docker desktop,报错

其中提到了电脑需要准备的东西 Install on Windows | Docker Documentation
如果是 Win11 家庭版,需要开启虚拟化,还要安装 WSL2,挺麻烦的,
Docker + Wasm = Awesome 挺诱人的
Linux 安装 docker
教程:centos7 安装 Docker 详细步骤(无坑版教程) - 腾讯云开发者社区-腾讯云
我们可以在容器启动之后通过 docker exec
进入容器
首先通过 docker ps
获取容器名称名称
方便起见,在
docker run
命令中,就通过--name
参数指定容器名称
然后再执行语句
docker exec -it tdengine /bin/bash
然后就可以像进入了一个虚拟机中一样进行查看和修改,不过重启容器之后,这些修改就会丢失,所以保险的方式还是通过 -v
实现与主机的同步
- docker exec: Enter the container by the
docker exec
command, if exited, the container will not stop. - -i: use interactive mode.
- -t: specify a terminal.
- tdengine: container name, needs to be changed according to the value returned by the docker ps command.
- /bin/bash: load the container and run bash to interact with it.
也可以通过 docker attach tdengine
进入容器
将 E:\DaMeng\数据上传下达、项目部署、docker 包、安装步骤。txt
中的命令复制过来
docker exec 命令能够在运行着的容器中执行命令。docker exec 命令的使用格式:
docker exec [OPTIONS] container_name COMMAND [ARG...]
OPTIONS 说明:
-d,以后台方式执行命令;
-e,设置环境变量
-i,交互模式
-t,设置 TTY
-u,用户名或 UID,例如 myuser:myusergroup
# 直接交互式地进入容器内部
docker exec -it master /bin/bash
# ./disql 是一个交互式的操作,我们可以通过这种方式,进行交互 ,查询或者执行脚本
docker exec -it dmdb /bin/bash -c 'cd /opt/dmdbms/bin ; ./disql SYSDBA/Dameng8888 ; '
# 直接执行某一种脚本
docker exec dmdb /bin/bash -c 'cd /opt/dmdbms/bin ; ./start.sh '
docker 容器的网络类型
Docker 容器运行的时候有 host
、bridge
、none
三种网络可供配置。默认是 bridge
,即桥接网络,以桥接模式连接到宿主机;host
是宿主网络,即与宿主机共用网络;none
则表示无网络,容器将无法联网。
当容器使用 host
网络时,容器与宿主共用网络,这样就能在容器中访问宿主机网络,那么容器的 localhost
就是宿主机的 localhost
。
在 docker 中使用 --network host
来为容器配置 host
网络:注意,一旦采用 host 网络,将不需要 -p
端口隐射
自动重启
添加容器在 docker 运行时自动启有两种方法:
- 在使用 docker run 时,添加下面参数
--restart=always
- 在运行 docker 的时候添加
docker update --restart=always 07fb7442f813
其中 07fb7442f813 是容器 ID,当然你也可以将 07fb7442f813 替换为容器的名字
真鸡儿方便
指定了内存参数之后,-m 4096M
,如果宿主机上剩余的内存不够,启动会失败。
通过 docker logs container_name 可以查看容器的启动日志 ,带上 -f
参数还能滚动更新,带上 --tail 200
可直接看最后 200 行,--tail
等同于 -n
代替
docker logs -f -n container_name
关于 docker 的所有命令:一张脑图整理 Docker 常用命令 - 三分恶的专栏 - SegmentFault 思否
我们也可以控制 docker 容器产生的日志的大小,避免容器中包含几百 G 的日志,这是可以配置的
官方文档:Configure logging drivers | Docker Docs
个人博客:【docker】docker 限制日志文件大小的方法+查看日志文件的方法 - Angel 挤一挤 - 博客园
- 新建
/etc/docker/daemon.json
,若有就不用新建了。添加log-dirver
和log-opts
参数,样例如下:
$ vim /etc/docker/daemon.json
{
"log-driver":"json-file",
"log-opts": {"max-size":"500m", "max-file":"3"}
}
max-size=500m,意味着一个容器日志大小上限是 500M,
max-file=3,意味着一个容器有三个日志,分别是 id+.json、id+1.json、id+2.json。
- 然后重启 docker 的守护线程
命令如下:
systemctl daemon-reload
systemctl restart docker
【需要注意的是:设置的日志大小规则,只对新建的容器有效】
我们在删除容器,删除镜像的时候,
docker stop dmdb
docker rm dmdb
docker rmi -f $(docker images hub.dameng.com/arm64/cdb/dm8 -qa)
如果 容器不存在,就会报错,此时我们可以通过将错误输出禁掉 (> /dev/null 2>&1
) 来保证命令不报错
docker stop $(docker ps -q) > /dev/null 2>&1
docker rm $(docker ps -aq) > /dev/null 2>&1
docker rmi -f $(docker images -qa) > /dev/null 2>&1
这在使用 ansible 来远程部署的时候,非常有用。就算容器不存在,也不会被打断
docker rm -f container_name 如果容器正在运行,会强制停止
修改镜像的名称和版本:
docker tag 【镜像 ID】【镜像名称】:【tag 版本信息】
docker tag 8ef375298394 mysql:v5.7
docker info
:输出的内容包含了存储驱动和 docker 根目录的信息。
docker system prune -a
:清理容器、网络文件、镜像和构建缓存(建议使用 Docker 命令来清理不再使用的容器)
docker inspect container_name
: 查看容器的常规信息,包括保存路径等,有的时候,我们忘记了 docker run 的配置,可以在这里查找,比如 -v
的配置,可以在 HostConfig下的Binds
中看到。
docker image inspect nginx
:查看镜像的常规信息
查看每个容器所占用的大小
docker system df -v
注意,这个容器大小,可能不准,最终,确定的容器的大小,还得看 /var/lib/docker/containers
下文件夹的大小。
Docker 镜像和容器的存储路径
Docker 容器由网络文件、卷和镜像组成。Docker 文件的存储路径取决于你的操作系统。常用操作系统中的路径如下:
- Ubuntu:
/var/lib/docker/
- Fedora:
/var/lib/docker/
- Debian:
/var/lib/docker/
- Windows:
C:\ProgramData\DockerDesktop
- MacOS:
~/Library/Containers/com.docker.docker/Data/vms/0/~
例如在 Ubuntu 系统中,通过 docker info
查看 Docker Root Dir
Client: Docker Engine - Community
Version: 25.0.3
Context: default
Debug Mode: false
Plugins:
buildx: Docker Buildx (Docker Inc.)
Version: v0.12.1
Path: /usr/libexec/docker/cli-plugins/docker-buildx
compose: Docker Compose (Docker Inc.)
Version: v2.24.5
Path: /usr/libexec/docker/cli-plugins/docker-compose
Server:
Containers: 1
Running: 0
Paused: 0
Stopped: 1
Images: 2
Server Version: 25.0.3
Storage Driver: overlay2
Backing Filesystem: extfs
Supports d_type: true
Using metacopy: false
Native Overlay Diff: true
userxattr: false
Logging Driver: json-file
Cgroup Driver: systemd
Cgroup Version: 2
Plugins:
Volume: local
Network: bridge host ipvlan macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
Swarm: inactive
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
Init Binary: docker-init
containerd version: ae07eda36dd25f8a1b98dfbf587313b99c0190bb
runc version: v1.1.12-0-g51d5e94
init version: de40ad0
Security Options:
seccomp
Profile: builtin
cgroupns
Kernel Version: 5.15.150.1-microsoft-standard-WSL2
Operating System: Ubuntu 22.04.3 LTS
OSType: linux
Architecture: x86_64
CPUs: 20
Total Memory: 15.49GiB
Name: LAPTOP-LK
ID: 254e62ee-db00-419d-8a41-21d85456a8c9
Docker Root Dir: /var/lib/docker
Debug Mode: false
Experimental: false
Insecure Registries:
127.0.0.0/8
Live Restore Enabled: false
修改 Docker 的默认存储路径
还有一个中文版本的翻译:
https://zhuanlan.zhihu.com/p/95533274?utm_id=0
方法如下啊
官方文档的修改办法是编辑 /etc/docker/daemon.json
文件:
vi /etc/docker/daemon.json
默认情况下这个配置文件是没有的,这里实际也就是新建一个,然后写入以下内容:
{
"data-root": "/www/docker"
}
此文件还涉及默认源的设定,如果设定了国内源,那么实际就是在源地址下方加一行,写成:
{
"registry-mirrors": ["http://hub-mirror.c.163.com"],
"data-root": "/www/docker"
}
保存退出,然后重启 docker 服务:
systemctl restart docker
再次查看 docker 信息,可以看到目录已经变成了设定的 /www/docker
:
注意,这样迁移,在老目录下的时候 docker 创建的那些容器是不会自动迁移到新的目录下的,新的目录下的 docker 就是一个全新的 docker,你需要从头开始导入镜像,启动容器。
因此,对于容器中的需要持久化的数据,我们最好挂载到宿主机上,这样当我们迁移 docker 的根目录的时候,容器中的数据不会丢失。
docker 目前无法合并镜像,只能做到将多个将多个镜像导出到一个 tar 中。
watchtower,自动更新容器的工具,
GitHub 地址
https://github.com/containrrr/watchtower?spm=a2c6h.12873639.article-detail.9.119f6d67l4VJ9q
使用博客
https://p3terx.com/archives/docker-watchtower.html
podman 也支持自动更新镜像
https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html
docker 镜像的导出和导入
docker 镜像导出保存为 tar 和 tar 包导入成 docker 镜像_docker tar_万山寒的博客-CSDN 博客
# AAA:8.2,8.2 表示镜像版本号
docker save -o /opt/tar/名称.tar AAA:8.2 BBB:5.6
docker stats
命令可以跟 top 命令一样,监控每个容器的 CPU 占用,内存,磁盘,网络 IO,磁盘 IO 等性能指标,非常好用
输出 | 描述 |
---|---|
CONTAINER | 以短格式显示容器的 ID。 |
CPU % | CPU 的使用情况。 |
MEM USAGE / LIMIT | 当前使用的内存和最大可以使用的内存。 |
MEM % | 以百分比的形式显示内存使用情况。 |
NET I/O | 网络 I/O 数据。 |
BLOCK I/O | 磁盘 I/O 数据。 |
PIDS | PID 号。 |
为什么不建议把数据库装 Docker 里?
在 Docker 中水平伸缩只能用于无状态计算服务,而不是数据库
Docker 快速扩展的一个重要特征就是无状态,具有数据状态的都不适合直接放在 Docker 里面,如果 Docker 中安装数据库,存储服务需要单独提供。
目前,TX 云的 TDSQL(金融分布式数据库)和阿里云的 Oceanbase(分布式数据库系统)都直接运行中在物理机器上,并非使用便于管理的 Docker 上。
docker swarm 管理 docker 集群,确实简单又好用
Linux 下有没有工具,可以整个 docker 防火墙,只允许想要指定的端口放行的? - V2EX
我本地没有这个问题,如果本地开启了防火墙(firewalld),没有开启容器需要的端口,然后直接在 docker run -p
中指定这个端口,启动的时候会直接报错。
而且在防火墙关闭的情况下,启动了容器,再开启防火墙,防火墙中没有设置此端口放行,那么容器将不能正常使用。
这都证明了 docker 是没有绕过防火墙或者设置了防火墙规则的
服务器的防护墙程序,可能会在服务器重新启动之后自启,这可能会导致服务无法访问。
在容器内部执行交互式命令
当我们直接在主机上安装数据库的时候,通过数据库自带的交互式命令来执行 sql,可以通过 echo
+ |
来退出交互式命令,例如
echo exit | echo start /ansible/distData/appdata/dmdb/database_dm.sql | ./disql SYSDBA/SYSDBA
但是在容器中安装数据库之后,需要在容器中来执行上述命令,但是无法直接实现,比如下面这样的语句无法执行成功
docker exec -it dmdb echo exit | echo start /ansible/distData/appdata/dmdb/database_dm.sql | ./disql SYSDBA/SYSDBA
一种简单的方式是,将 echo exit | echo start /ansible/distData/appdata/dmdb/database_dm.sql | ./disql SYSDBA/SYSDBA
,保存为一个脚本,映射到容器内部,然后再在宿主机中通过 docker exec -it dmdb xxx.sh
来执行这个脚本,这样就不会有问题了。
我们甚至可以将 echo exit | echo start /ansible/distData/appdata/dmdb/database_dm.sql | ./disql SYSDBA/SYSDBA
写成一个方法,将 /ansible/distData/appdata/dmdb/database_dm.sql
当成一个参数传进来,这样就更方便了。
关于交互式命令,我们在《Linux 小技巧》中了解过。
启动容器的时候报错:Docker Networking Disabled: WARNING: IPv4 forwarding is disabled. Networking will not work
参考博客:Docker Networking Disabled: WARNING: IPv4 forwarding is disabled. Networking will not work
到 /etc/sysctl.conf
中添加
net.ipv4.ip_forward=1
然后重启网络
systemctl restart network
然后再启动容器即正常
通过 docker run 启动容器之后忘记配置怎么办?
可以通过 rekcod 找回,过程很简单
拉取镜像,然后给这个镜像的启动命令给一个别名,方便调用
$ docker pull nexdrew/rekcod
$ alias rekcod="docker run --rm -v /var/run/docker.sock:/var/run/docker.sock nexdrew/rekcod"
然后使用
$ rekcod <container>
或者你可以直接使用,如果你本地没有下载这个镜像,系统会在后台自己下载。
$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock nexdrew/rekcod <container>
其实没有必要这样大费周章,推荐直接使用 docker-compose 编排容器。
docker stop xxx 的时候如何优雅地关闭 SpringBoot 打包出来的 container
一般情况下,以下配置是可行的
ENTRYPOINT ["java", "-jar", "/app.jar"]
如果不行,改成
ENTRYPOINT [ "sh", "-c", "exec java -jar /app.jar"]
注意,exec 和后面的语句要在一起,不能分为两个数组元素