如何在 Dockerfile 本身中设置断点?

How to set breakpoint in Dockerfile itself?

搜索上面显示了很多关于如何在 docker 容器中为应用 运行ning 设置断点的结果,但我对在 Dockerfile 中设置断点很感兴趣 本身,使得docker build在断点处暂停。例如 Dockerfile:

FROM ubuntu:20.04

RUN echo "hello"
RUN echo "bye"

我正在寻找一种在 RUN echo "bye" 上设置断点的方法,这样当我调试这个 Dockerfile 时,图像将以非交互方式构建到 RUN echo "bye"点,独家。之后,我将能够与容器交互 运行 命令。在实际的 Dockerfile 中,断点之前有 RUNs 更改正在构建的图像的文件系统,我想通过能够在断点处分析图像的文件系统在断点处以交互方式 运行 命令,例如 cd / ls / find

我认为这不可能直接实现 - 该功能已被 discussed 拒绝。

我调试 Dockerfile 的一般做法是在“断点”之后注释所有步骤,然后是 运行 docker build,然后是 docker run -it image bashdocker run -it image sh (取决于您是否在容器内安装了 bash)。
然后,我有一个交互式 shell,我可以 运行 命令来调试为什么后面的阶段会失败。

不过,我同意能够设置断点并四处查看将是一个方便的功能。

您无法设置断点 本身 ,但您可以在构建序列中的任意点(步骤之间)获得交互式 shell。

让我们构建您的形象:

Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM ubuntu:20.04
 ---> 1e4467b07108
Step 2/3 : RUN echo "hello"
 ---> Running in 917b34190e35
hello
Removing intermediate container 917b34190e35
 ---> 12ebbdc1e72d
Step 3/3 : RUN echo "bye"
 ---> Running in c2a4a71ae444
bye
Removing intermediate container c2a4a71ae444
 ---> 3c52993b0185
Successfully built 3c52993b0185

带有十六进制 ID 的 ---> 0123456789ab 的每一行都有一个有效的图像 ID。所以从这里你可以

docker run --rm -it 12ebbdc1e72d sh

这将在第一个 RUN 命令生成的部分图像上为您提供交互式 shell。

不要求构建作为一个整体成功。如果 RUN 步骤失败,您可以使用此技术在该步骤之前立即在图像上获得交互式 shell 并手动重新 运行 命令。如果您有一个很长的 RUN 命令,您可能需要将其分成两部分以便能够在命令序列中的特定点进行调试 shell。

您可以使用 Remote shell debugging 技巧在中间容器中 运行 命令。

确保您的容器映像包含基本实用程序,例如 netcat (nc) 和 fuser。这些实用程序支持从任何中间容器映像“呼叫主页”。在家里,您将使用 netcat(或 socat)接听电话。此 netcat 会将您的命令发送到容器,并打印其结果。这种调试方法甚至适用于 Docker 构建在云端某处未知工作节点上的文件。

示例:

FROM debian:testing-slim

# Set environment variables for calling home from breakpoints (BP)
ENV BP_HOME=<IP-ADDRESS-OF-YOUR-HOST>
ENV BP_PORT=33720
ENV BP_CALLHOME='BP_FIFO=/tmp/$BP.$BP_HOME.$BP_PORT; (rm -f $BP_FIFO; mkfifo $BP_FIFO) && (echo "\"c\" continues"; echo -n "($BP) "; tail -f $BP_FIFO) | nc $BP_HOME $BP_PORT | while read cmd; do if test "$cmd" = "c" ; then echo -n "" >$BP_FIFO; sleep 0.1; fuser -k $BP_FIFO >/dev/null 2>&1; break; else eval $cmd >$BP_FIFO 2>&1; echo -n "($BP) "  >$BP_FIFO; fi; done'

# Install needed utils (netcat, fuser)
RUN apt update && apt install -y netcat psmisc

# Now you are ready to run "eval $BP_CALLHOME" wherever you want to call home.

RUN BP=before-hello eval $BP_CALLHOME

RUN echo "hello"

RUN BP=after-hello eval $BP_CALLHOME

RUN echo "bye"

在 启动 Docker 构建之前,开始等待和接听来自 Docker 文件的呼叫 。在主主机上 运行 nc -k -l -p 33720(或者 socat STDIN TCP-LISTEN:33720,reuseaddr,fork)。

上面的例子在家里是这样的:

$ nc -k -l -p 33720
"c" continues
(before-hello) echo *
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
(before-hello) id
uid=0(root) gid=0(root) groups=0(root)
(before-hello) c
"c" continues
(after-hello)
...

伙计,Docker 使事情变得 困难 。这是我制定的解决方法:

  1. 在你想要断点的地方插入FROM scratch
  2. 运行 docker build . --stage=<n-1> 其中 <n> 是“断点”之前的 FROM 命令数。例如,如果它是单阶段构建,请使用 --stage=0
    • 或者,如果您已经用 FROM <image> AS <stage> 命名了您想要断点的阶段,那么您可以改用 --stage=<stage>

Docker 已经缓存了你所有成功的层(即使你看不到它们),并且因为 FROM “断点”出现在(可能不成功的)兴趣点之前,构建应该全部来自缓存并且非常快。

例如,如果我的 Dockerfile 看起来像这样:

FROM debian:bullseye AS build

RUN apt-get update && apt-get install -y \
    build-essential cmake ninja-build \
    libfontconfig1-dev libdbus-1-dev libfreetype6-dev libicu-dev libinput-dev libxkbcommon-dev libsqlite3-dev libssl-dev libpng-dev libjpeg-dev libglib2.0-dev

<SNIP lots of other setup commands>

ADD my_source.tar.xz /
WORKDIR /my_source

RUN ./configure -option1 -option2
RUN cmake --build . --parallel
RUN cmake --install .


FROM alpine
COPY --from=build /my_build /my_build

...

然后我可以像这样添加一个“断点”:

FROM debian:bullseye AS build

RUN apt-get update && apt-get install -y \
    build-essential cmake ninja-build \
    libfontconfig1-dev libdbus-1-dev libfreetype6-dev libicu-dev libinput-dev libxkbcommon-dev libsqlite3-dev libssl-dev libpng-dev libjpeg-dev libglib2.0-dev

<SNIP lots of other setup commands>

ADD my_source.tar.xz /
WORKDIR /my_source

#### BREAKPOINT ###
FROM scratch
#### BREAKPOINT ###

RUN ./configure -option1 -option2
RUN cmake --build . --parallel
RUN cmake --install .


FROM alpine
COPY --from=build /my_build /my_build

...

并使用 docker build . --stage=build

触发它

最近(2022 年 5 月)的项目 ktock/buildg offers breakpoints

参见“Interactive debugger for Dockerfile" from Kohei Tokunaga

buildg is a tool to interactively debug Dockerfile based on BuildKit.

  • Source-level inspection
  • Breakpoints and step execution
  • Interactive shell on a step with your own debugigng tools
  • Based on BuildKit (needs unmerged patches)
  • Supports rootless

命令breakb LINE_NUMBER设置断点。

示例:

$ buildg.sh debug --image=ubuntu:22.04 /tmp/ctx
WARN[2022-05-09T01:40:21Z] using host network as the default            
#1 [internal] load .dockerignore
#1 transferring context: 2B done
#1 DONE 0.1s

#2 [internal] load build definition from Dockerfile
#2 transferring dockerfile: 195B done
#2 DONE 0.1s

#3 [internal] load metadata for docker.io/library/busybox:latest
#3 DONE 3.0s

#4 [build1 1/2] FROM docker.io/library/busybox@sha256:d2b53584f580310186df7a2055ce3ff83cc0df6caacf1e3489bff8cf5d0af5d8
#4 resolve docker.io/library/busybox@sha256:d2b53584f580310186df7a2055ce3ff83cc0df6caacf1e3489bff8cf5d0af5d8 0.0s done
#4 sha256:50e8d59317eb665383b2ef4d9434aeaa394dcd6f54b96bb7810fdde583e9c2d1 772.81kB / 772.81kB 0.2s done
Filename: "Dockerfile"
      2| RUN echo hello > /hello
      3| 
      4| FROM busybox AS build2
 =>   5| RUN echo hi > /hi
      6| 
      7| FROM scratch
      8| COPY --from=build1 /hello /
>>> break 2
>>> breakpoints
[0]: line 2
>>> continue
#4 extracting sha256:50e8d59317eb665383b2ef4d9434aeaa394dcd6f54b96bb7810fdde583e9c2d1 0.0s done
#4 DONE 0.3s
...

来自 PR 24:

Add --cache-reuse option which allows sharing the build cache among invocation of buildg debug to make the 2nd-time debugging faster.
This is useful to speed up running buildg multiple times for debugging an errored step.

Note that breakpoints on cached steps are ignored as of now.
Because of this limitation, this feature is optional as of now. We should fix this limitation and make it the default behaviour in the future.