Docker打包基础2

目录

  • 1 调试 Docker 构建
    • 1.1 调试构建过程
  • 2 ImportError 和 ModuleNotFoundError
    • 2.1 是否使用了正确的 Python 解释器
      • 2.1.1 解决方案
    • 2.2 是否成功激活了虚拟环境
      • 2.2.1 解决方案
    • 2.3 如果不是通过 `pip` 安装的,确保代码位置正确
      • 2.3.1 解决方案
    • 2.4 `python -m` 要求代码位于当前工作目录中
      • 2.4.1 解决方案
  • 3 `docker history` 命令
    • 3.1 镜像构建
    • 3.2 Docker 镜像运行什么
    • 3.3 基础镜像包含什么
    • 3.4 哪些命令使镜像变大
    • 3.5 提取构建参数
    • 3.6 找出镜像过大的原因

1 调试 Docker 构建

你有了一个全新的 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

那么,现在该怎么办呢?那个日志文件并不在你主机的文件系统中,而是在构建过程中创建的临时镜像里。

1.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 的最后一行(或多行),并继续进行构建。

2 ImportError 和 ModuleNotFoundError

你的代码在本地计算机上运行正常,但当你尝试使用 Docker 打包时,却不断遇到 ImportErrorModuleNotFoundError:Python 找不到你的代码。

出现这种情况的原因有很多,有些与 Python 本身有关,有些则与 Docker 有关。

2.1 是否使用了正确的 Python 解释器

在 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 将使用这个解释器。

有时,这种情况不太容易察觉,比如当你安装一个依赖于 pythonpython3 系统包的系统包,或者尝试通过 apt-get install python3-numpy 安装库时。

2.1.1 解决方案

如果你(错误地)使用了官方的 python 基础镜像,那么:

  • 不要手动安装 Python。
  • 如果你正在安装系统包,请检查 Python 是否是其中一个依赖的系统包。如果是,请确保使用的是 /usr/local/bin/ 版本。
  • 使用 pip 安装 Python 库,而不是 apt-get

2.2 是否成功激活了虚拟环境

如果你使用的是像虚拟环境(virtualenv)或 Conda 环境这样的隔离环境,那么情况与上述类似:你有两个不同版本的 Python,即系统 Python 和隔离环境中的 Python。你需要确保代码在同一个 Python 环境中安装和运行。

2.2.1 解决方案

确保你正确激活了虚拟环境,或者正确激活了 Conda 环境。

2.3 如果不是通过 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.pylibrary.pymain.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'

注意,当前工作目录并不重要。重要的是你要导入的代码与你运行的主脚本在同一个目录中。

2.3.1 解决方案

要么使用 pip install 安装所有代码,要么确保所有代码都在同一个目录中。

2.4 python -m 要求代码位于当前工作目录中

如果你使用 python -m yourpackagepython -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

2.4.1 解决方案

确保你的当前工作目录与代码所在的目录相同:

FROM python:3.8-slim-buster
WORKDIR /code
COPY myapp .

为了避免 ImportError,建议采取以下措施:

  • 确保只安装了一个版本的 Python。
  • 如果你使用了虚拟环境(virtualenv)或 Conda 环境,确保它已正确激活。
  • 要么使用 pip 安装所有代码,要么:
    • 确保所有代码都在同一个目录中。
    • 如果你还使用了 python -m,确保你的工作目录与代码所在的目录相同。

3 docker history 命令

想了解一个 Docker 镜像,没有比 docker history 命令更有用的工具了。无论是告诉你为什么镜像如此之大,还是帮助你理解基础镜像的构建方式,history 命令都能让你窥探任何镜像的内部结构,让你看到好的、坏的和丑陋的方面。

让我们来看看这个命令的作用,它能让我们对 Docker 镜像的构建有哪些了解,以及一些它为何如此有用的示例。

3.1 镜像构建

考虑以下 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。我们可以用它来回答一些问题。

3.2 Docker 镜像运行什么

要弄清楚镜像将运行什么,我们只需要找到最顶层的 ENTRYPOINTCMD。我们可以使用 --no-trunc 参数来显示完整的、未截断的命令:

$ docker image history mysteryimage --no-trunc | grep ENTRYPOINT
sha256:24e6dd67bf8a   4 minutes ago       /bin/sh -c #(nop)  ENTRYPOINT ["python" "example.py"]

3.3 基础镜像包含什么

我们可以看到构建基础镜像的内容:可以通过每层的创建时间来区分基础镜像和当前镜像。

你还可以看到基础镜像的 ID,如果你想使用 docker run 运行它的话。

3.4 哪些命令使镜像变大

注意上面的输出中有一个 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。

3.5 提取构建参数

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  ....

3.6 找出镜像过大的原因

虽然 docker history 在理解镜像的构建方式方面很有用,偶尔也能让你了解到不安全的设置,但它最有用的地方是找出镜像过大的原因。

当你遇到过大的镜像时,首先应该做的是使用 docker image history 来查看哪些层对镜像大小的贡献最大。通常,这足以让你确切地知道发生了什么。

你可能感兴趣的:(docker,eureka,容器)