你有了一个全新的 Dockerfile
,现在是时候测试它了:
$ docker build -t mynewimage .
Sending build context to Docker daemon 3.072kB
Step 1/3 : FROM python:3.8-slim-buster
---> 3d8f801fc3db
Step 2/3 : COPY build.sh .
---> 541b65a7b417
Step 3/3 : RUN ./build.sh
---> Running in 9917e3865f96
Building...
Building some more...
Build failed, see /tmp/builderr024321.log for details
The command '/bin/sh -c ./build.sh' returned a non-zero code: 1
那么,现在该怎么办呢?那个日志文件并不在你主机的文件系统中,而是在构建过程中创建的临时镜像里。
那个日志文件并不在你主机的文件系统中,而是在构建过程中创建的临时镜像里。在某些版本的 Docker 中,你可以访问这个临时镜像,但在新的且大多有所改进的 BuildKit 构建系统中却不行。
所以我们将采取不同的方法。
具体说,我们要做的是重新构建镜像,禁用失败的那一行以及其后的所有行。我们当前的 Dockerfile
如下所示:
FROM python:3.8-slim-buster
COPY build.sh .
RUN chmod +x build.sh
RUN ./build.sh
你要确保对 Dockerfile
的更改不会影响层缓存;你可以通过在 .dockerignore
中包含 Dockerfile
来实现这一点。在我们的示例中,我们只复制了 build.sh
,因此对 Dockerfile
的更改不会使缓存失效。
注释掉失败的那一行,现在 Dockerfile
如下所示:
FROM python:3.8-slim-buster
COPY build.sh .
RUN chmod +x build.sh
#RUN ./build.sh
现在我们可以重新构建镜像:
$ docker build -t mynewimage .
...
Successfully built 7759cef14db8
Successfully tagged mynewimage:latest
现在我们可以运行这个镜像,然后手动运行失败的步骤,并且由于我们在容器内部,我们还可以访问生成的日志文件:
$ docker run -it mynewimage /bin/bash
root@c6060c04282d:/# ./build.sh
Build failed, see /tmp/builderr024321.log for details
root@c6060c04282d:/# cat /tmp/builderr024321.log
ONO MISSING FLUX CAPACITOR
root@c6060c04282d:/#
现在我们知道问题所在了:显然缺少 FLUX CAPACITOR
。
一旦我们解决了这个问题,我们可以取消注释 Dockerfile
的最后一行(或多行),并继续进行构建。
你的代码在本地计算机上运行正常,但当你尝试使用 Docker 打包时,却不断遇到 ImportError
或 ModuleNotFoundError
:Python 找不到你的代码。
出现这种情况的原因有很多,有些与 Python 本身有关,有些则与 Docker 有关。
在 Docker 镜像中,很容易误装多个 Python 解释器。当出现这种情况时,你可能使用解释器 A 安装了代码,但却尝试使用解释器 B 来运行它,而解释器 B 自然找不到这些代码。
以下是一个有点牵强的示例,展示了这种情况是如何发生的:
FROM python:3.8-slim-buster
RUN apt-get update && apt-get install -y python3 python3-pip
RUN /usr/bin/pip3 install flask
ENTRYPOINT ["python", "-c", "import flask"]
现在你有两个 Python 解释器:
/usr/local/bin/python
是 Docker 镜像提供的 Python 解释器,这也是 ENTRYPOINT
正在运行的解释器。/usr/bin/python3
是通过 apt-get
安装的 Python 解释器,/usr/bin/pip3
将使用这个解释器。有时,这种情况不太容易察觉,比如当你安装一个依赖于 python
或 python3
系统包的系统包,或者尝试通过 apt-get install python3-numpy
安装库时。
如果你(错误地)使用了官方的 python
基础镜像,那么:
/usr/local/bin/
版本。pip
安装 Python 库,而不是 apt-get
。如果你使用的是像虚拟环境(virtualenv)或 Conda 环境这样的隔离环境,那么情况与上述类似:你有两个不同版本的 Python,即系统 Python 和隔离环境中的 Python。你需要确保代码在同一个 Python 环境中安装和运行。
确保你正确激活了虚拟环境,或者正确激活了 Conda 环境。
pip
安装的,确保代码位置正确这就需要你开始了解 Python 是如何决定从哪里导入代码的。
首先,Python 有一系列标准目录,它会在这些目录中检查导入的模块。你可以通过查看 sys.path
来查看这些目录:
$ docker run -it python:3.8-slim-buster
Python 3.8.3 (default, Jun 9 2020, 17:49:41)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path
['', '/usr/local/lib/python38.zip', '/usr/local/lib/python3.8', '/usr/local/lib/python3.8/lib-dynload', '/usr/local/lib/python3.8/site-packages']
我们暂时忽略第一个条目 ''
。接下来的几个条目基本上是为了让你能够访问 Python 标准库。最后一个以 site-packages
结尾的条目,是 pip
安装包的位置。
只要你使用 pip
安装代码,就不会有问题。无论你当前在哪个目录,都可以成功导入代码。
$ docker run -it python:3.8-slim-buster bash
root@3c19e2314f01:/# pip install --quiet flask
root@3c19e2314f01:/# python -c "import flask; print('success')"
success
root@3c19e2314f01:/# cd /tmp
root@3c19e2314f01:/tmp# python -c "import flask; print('success')"
success
问题通常出现在你不是通过 pip
安装代码,而是将其复制到某个任意目录时。
这时,sys.path
中的第一个条目,即空字符串 ''
,就会起作用。''
表示“将你运行的初始脚本所在的目录添加到 sys.path
中”。由于这可能有点令人困惑,让我们看两个例子。
假设我们有两个文件,main.py
和 library.py
;main.py
会执行 import library
。如果它们在同一个目录中,一切都会正常工作:
$ tree
.
└── code
├── library.py
└── main.py
1 directory, 2 files
$ python code/main.py
Successfully imported library.py
$ cd code/
$ python main.py
Successfully imported library.py
如果它们在不同的目录中,导入将会失败:
$ tree
.
├── code
│ └── library.py
└── main.py
1 directory, 2 files
$ python main.py
Traceback (most recent call last):
File "main.py", line 1, in
import library
ModuleNotFoundError: No module named 'library'
$ cd code/
$ python ../main.py
Traceback (most recent call last):
File "../main.py", line 1, in
import library
ModuleNotFoundError: No module named 'library'
注意,当前工作目录并不重要。重要的是你要导入的代码与你运行的主脚本在同一个目录中。
要么使用 pip install
安装所有代码,要么确保所有代码都在同一个目录中。
python -m
要求代码位于当前工作目录中如果你使用 python -m yourpackage
或 python -m yourmodule
,并且没有使用 pip install
安装所有内容,那么你需要遵循与 如果不是通过 pip
安装的,确保代码位置正确 相同的要求:导入的代码需要与主脚本在同一个目录中。
此外,你的当前目录(可以在 Dockerfile
中使用 WORKDIR
设置)必须与代码所在的目录相同。
$ tree
.
└── code
├── library.py
└── main.py
1 directory, 2 files
$ python -m main
/usr/bin/python: No module named main
$ cd code/
$ python -m main
Successfully imported library.py
如果你的代码是一个包,你需要位于包含该包的目录中:
$ tree
.
├── library.py
└── myapp
├── __init__.py
└── __main__.py
1 directory, 3 files
$ python -m myapp
Successfully imported library.py
$ cd myapp/
$ python -m myapp
/usr/bin/python: No module named myapp
确保你的当前工作目录与代码所在的目录相同:
FROM python:3.8-slim-buster
WORKDIR /code
COPY myapp .
为了避免 ImportError
,建议采取以下措施:
pip
安装所有代码,要么:
python -m
,确保你的工作目录与代码所在的目录相同。docker history
命令想了解一个 Docker 镜像,没有比 docker history
命令更有用的工具了。无论是告诉你为什么镜像如此之大,还是帮助你理解基础镜像的构建方式,history
命令都能让你窥探任何镜像的内部结构,让你看到好的、坏的和丑陋的方面。
让我们来看看这个命令的作用,它能让我们对 Docker 镜像的构建有哪些了解,以及一些它为何如此有用的示例。
考虑以下 Docker 镜像:
$ docker image ls mysteryimage
REPOSITORY TAG IMAGE ID SIZE
mysteryimage latest 24e6dd67bf8a 165MB
给定一个镜像,我们可能会有一些问题:
docker image history
命令,或者 docker history
,可以帮助回答所有这些问题。
$ docker image history mysteryimage
IMAGE CREATED CREATED BY SIZE
24e6dd67 2 mins #(nop) ENTRYPOINT ["python" "exa… 0B
59102aef 2 mins #(nop) COPY file:cc6452cd5813b9d2… 0B
9d84edf3 7 weeks #(nop) CMD ["python3"] 0B
7 weeks set -ex; savedAptMark="$(apt-ma… 8MB
7 weeks #(nop) ENV PYTHON_GET_PIP_SHA256… 0B
7 weeks #(nop) ENV PYTHON_GET_PIP_URL=ht… 0B
7 weeks #(nop) ENV PYTHON_PIP_VERSION=20… 0B
7 weeks cd /usr/local/bin && ln -s idle3… 32B
7 weeks set -ex && savedAptMark="$(apt-… 80MB
7 weeks #(nop) ENV PYTHON_VERSION=3.8.3 0B
7 weeks #(nop) ENV GPG_KEY=E3FF2839C048B… 0B
7 weeks apt-get update && apt-get install… 7MB
7 weeks #(nop) ENV LANG=C.UTF-8 0B
7 weeks #(nop) ENV PATH=/usr/local/bin:/… 0B
7 weeks #(nop) CMD ["bash"] 0B
7 weeks #(nop) ADD file:4d35f6c8bbbe6801c… 69MB
Docker 镜像是分层构建的,每层大致对应 Dockerfile
中的一行。history
命令会显示这些层以及用于创建它们的命令。
所以,我们这里得到的内容或多或少等同于构建该镜像的 Dockerfile
。我们可以用它来回答一些问题。
要弄清楚镜像将运行什么,我们只需要找到最顶层的 ENTRYPOINT
或 CMD
。我们可以使用 --no-trunc
参数来显示完整的、未截断的命令:
$ docker image history mysteryimage --no-trunc | grep ENTRYPOINT
sha256:24e6dd67bf8a 4 minutes ago /bin/sh -c #(nop) ENTRYPOINT ["python" "example.py"]
我们可以看到构建基础镜像的内容:可以通过每层的创建时间来区分基础镜像和当前镜像。
你还可以看到基础镜像的 ID,如果你想使用 docker run
运行它的话。
注意上面的输出中有一个 SIZE
列,它显示了每层的大小。
这意味着你可以知道 Dockerfile
中的哪些特定步骤对镜像大小的贡献最大。在这个例子中,有一个特定步骤贡献了 80MB:
$ docker image history mysteryimage --no-trunc | grep 80MB
7 weeks ago /bin/sh -c set -ex && savedAptMark="$(apt-mark showmanual)" && apt-get update && apt-get install -y --no-install-recommends dpkg-dev gcc libbluetooth-dev libbz2-dev libc6-dev libexpat1-dev libffi-dev libgdbm-dev liblzma-dev libncursesw5-dev libreadline-dev libsqlite3-dev libssl-dev make tk-dev uuid-dev wget xz-utils zlib1g-dev $(command -v gpg > /dev/null || echo 'gnupg dirmngr') && wget -O python.tar.xz "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz" && wget -O python.tar.xz.asc "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc" && export GNUPGHOME="$(mktemp -d)" && gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys "$GPG_KEY" && gpg --batch --verify python.tar.xz.asc python.tar.xz && { command -v gpgconf > /dev/null && gpgconf --kill all || :; } && rm -rf "$GNUPGHOME" python.tar.xz.asc && mkdir -p /usr/src/python && tar -xJC /usr/src/python --strip-components=1 -f python.tar.xz && rm python.tar.xz && cd /usr/src/python && gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" && ./configure --build="$gnuArch" --enable-loadable-sqlite-extensions --enable-optimizations --enable-option-checking=fatal --enable-shared --with-system-expat --with-system-ffi --without-ensurepip && make -j "$(nproc)" LDFLAGS="-Wl,--strip-all" && make install && ldconfig && apt-mark auto '.*' > /dev/null && apt-mark manual $savedAptMark && find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec ldd '{}' ';' | awk '/=>/ { print $(NF-1) }' | sort -u | xargs -r dpkg-query --search | cut -d: -f1 | sort -u | xargs -r apt-mark manual && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false && rm -rf /var/lib/apt/lists/* && find /usr/local -depth \( \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) -o \( -type f -a \( -name '*.pyc' -o -name '*.pyo' \) \) \) -exec rm -rf '{}' + && rm -rf /usr/src/python && python3 --version 80MB
显然,这一步是从源代码编译 Python。
docker image history
命令所报告的命令比原始的 Dockerfile
更有用,因为它们还包含了后续 RUN
命令中构建参数的值。
这对于安全审计很有用。例如,你可能会发现镜像错误地使用了 ARG
命令来处理构建秘密信息,从而无意中泄露了凭证:
$ docker pull itamarst/verysecure
...
$ docker image history itamarst/verysecure
IMAGE CREATED BY
0b51ddadfcd |1 ANOTHER_SECRET=oscillation-overthruster /…
/bin/sh -c #(nop) WORKDIR /tmp
/bin/sh -c #(nop) ARG ANOTHER_SECRET
...
当然,在 Docker 中还有其他更安全的方法来使用构建秘密信息。
或者你可能会在商业构建的 Docker 镜像中发现内部服务器的名称,以及他们仍然在使用 FTP 的事实:
$ docker history --no-trunc image_name_elided | grep ftp
4 weeks ago |2 FTP_PATH=ftp://kits-ftp/kits/unreleased_ftp/PRODUCTS//PRODUCT-dockerubuntux64.tar.gz ....
虽然 docker history
在理解镜像的构建方式方面很有用,偶尔也能让你了解到不安全的设置,但它最有用的地方是找出镜像过大的原因。
当你遇到过大的镜像时,首先应该做的是使用 docker image history
来查看哪些层对镜像大小的贡献最大。通常,这足以让你确切地知道发生了什么。