最近在工作中遇到了一个 Docker 镜像无法运行的问题,事后总结时发现其中有几个点挺有意思,值得记录下来以备后用,也可以避免其他人踩坑。

一、运行镜像报错

我的开发环境是 Windows,使用 Docker Desktop 作为本地 Docker 环境。最近在一个项目中需要把 war 包打成 Docker 镜像并推送到仓库里去,却发现打好的这个镜像运行不起来,运行时报下面这样的错:

# docker run --rm -it manager
standard_init_linux.go:207: exec user process caused "no such file or directory"

检查 Dockerfile 文件,是非常简单的:

FROM openjdk:8-jre-alpine

ADD entrypoint.sh entrypoint.sh
ADD *.war app.war

EXPOSE 8088

RUN chmod 755 entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]

镜像是基于 openjdk 的基础镜像,将 entrypoint.sh*.war 拷贝到镜像中,并将 entrypoint.sh 作为默认的启动脚本文件。初看应该是没问题的,war 包在本地使用 java -jar 也可以跑起来。

二、检查镜像

那么这个镜像是怎么回事?看报错信息,应该是运行镜像时,找不到某个文件或目录,但是我的这个镜像一共就两个文件,一个 entrypoint.sh 一个 war 包,难道这两个文件没有 ADD 到镜像里?

于是试着运行命令 docker run --rm -it manager sh,想着进 shell 查看下文件,发现依然报错,难道镜像里没有 sh 脚本?试着连换了几个命令:docker run --rm -it manager /bin/bashdocker run --rm -it manager ls 发现都是这个错,这怎么可能呢,连 ls 都没有?

无奈 Google 之,才发现原来如果镜像配置了 entrypoint,要使用下面这样的方式来检查镜像:

# docker run --rm -it --entrypoint sh manager

进到容器里来了之后,使用 ls 可以看到文件都在,没毛病:

/ # ls
app.war        entrypoint.sh  lib            opt            run            sys            var
bin            etc            media          proc           sbin           tmp
dev            home           mnt            root           srv            usr

使用 java -jar app.war 可以正常运行,但是奇怪的是,./entrypoint.sh 脚本却运行不了:

/ # ./entrypoint.sh
sh: ./entrypoint.sh: not found

这个脚本文件明明就在这,却报错 not found,这让我一度怀疑人生,遇到灵异事件了。

三、换行惹的祸

不得已只能继续 Google 之,发现网络上跟我一样的人还有很多,脚本无法运行最可能的原因是 Sha-Bang 写的有问题,所谓 Sha-Bang 就是 #!,通常会写在 shell 文件第一行,用于指定命令行解释器。类似于下面这样:

/ # cat entrypoint.sh
#!/bin/sh
blabla

使用 ls /bin/sh 发现 /bin/sh 文件也在,应该没问题啊。苦思冥想之际,突然脑海中闪过一个想法,难道这里有隐藏字符?一般遇到这种灵异事件的时候,都很可能和隐藏字符有关。使用 cat -v 查看文件,果然发现这里的 /bin/sh 后面多了个 ^M

/ # cat -v entrypoint.sh
#!/bin/sh^M
blabla

顿时豁然开朗,这不就是 Windows 下的换行符吗?一切都是换行符惹的祸。

使用 dos2unix 将 entrypoint.sh 文件中的 Windows 格式的换行符转为 UNIX 格式,再一次使用 docker build,这次终于运行成功了。

四、最坑的 Git 配置

到这里本来已经结束了,不过后来又发生了一件小事,让我又发现另一个坑,才找到了这个问题最根本的原因。当我解决了这个问题之后,就去给同事分享,可是听了我的分享之后,同事却一脸懵逼的表示自己从来都没遇到过这个问题,并且在他电脑上给我演示了一遍 docker builddocker run,一切正常,并没有报错,同样的一份代码,为什么在我的电脑上 build 就有问题?看着同事对我露出的迷之微笑,我又一次陷入了困惑。

我让他把 entrypoint.sh 发给我,看了下,他的换行符格式竟然是 UNIX 的,可是我本地代码换行符明明是 Windows 的,使用 git status 也可以看到 nothing to commit, working tree clean,表明本地代码和仓库代码是一样的,为什么换行符却不一样呢?

查看 Git 的配置文件,和他的对比了一下,发现一个很可能的疑点:

[core]
    autocrlf = true

查询官网文档,终于找到了原因,原来 Git 在 pull 代码的时候可能会偷偷的对你的代码做手脚。如果你配置了 autocrlf = true,那么当你签出代码时,Git 会自动的把 LF 转换成 CRLF,然后当你 push 的时候,又自动的将 CRLF 转换成 LF。这看上去很贴心的功能,实际上却有着很大的漏洞,譬如像我这样,直接在本地打镜像,或者需要直接将 shell 文件上传到 Linux 服务器上运行的,都可能会出问题。关键这个配置在 Windows 下默认是打开的,所以建议把这个配置关掉:

$ git config --global core.autocrlf false

坑之总结

  • 坑一:镜像如果配置了 entrypointdocker run 的时候应该加上 --entrypoint 参数来检查镜像
  • 坑二:脚本运行时报 not found,最可能的原因是 Sha-Bang 写的有问题,检查脚本文件中是否存在隐藏字符,譬如 Windows 的换行符,这里不得不吐槽下,这个 not found 提示真的让人迷惑,提示里能不能把隐藏字符也带上?
  • 坑三:建议关闭 Git 的 autocrlf 配置

参考

  1. How to see docker image contents
  2. Standard_init_linux.go:175 exec user process caused no such file
  3. GitHub 第一坑:换行符自动转换
  4. 自定义 Git - 配置 Git
扫描二维码,在手机上阅读!