Пробуем вытащить Dockerfile из готового образа
Когда приходится отлаживать или разбирать Docker-образ, понимание того, как он был собран, может очень пригодиться. В заметке рассмотрим небольшую команду (по правде говоря скорее хардкорный однострочник), которая предлагает довольно изобретательный способ приблизительно восстановить исходный Dockerfile, анализируя историю образа:
docker history --no-trunc python | awk '{$1=$2=$3=$4=""; print $0}' | \ grep -v COMMENT | nl | sort -nr | sed 's/^[^A-Z]*//' | \ sed -E 's/[0-9]+(\.[0-9]+)?(B|KB|MB|GB|kB)[[:space:|]].*//g'
Разберёмся, что здесь происходит:
docker history –no-trunc python
: выводит полную (необрезанную) историю образаpython
, показывая все слои и команды/метаданные, которые использовались при его сборке.awk '{$1=$2=$3=$4=«»; print $0}
': удаляет первые четыре колонки (ID слоя, время создания и прочее), оставляя только команду, использованную на каждом шаге.grep -v COMMENT
: убирает строки-комментарии (обычно это заголовки таблицы).nl
: нумерует строки - это нужно, чтобы потом можно было восстановить порядок (Docker показывает историю в обратном порядке - от последнего слоя к первому).sort -nr
: сортирует строки по убыванию номера - так мы получаем слои от базового к финальному, как в обычном Dockerfile.sed 's/^[^A-Z]*
': пытается убрать указания на размеры файлов и всё, что идёт после них.': убирает номера строк и лишние пробелы в начале. *
sed -E 's/[0-9]+(\.[0-9]+)?(B|KB|MB|GB|kB)space.*g
Когда и зачем это может пригодиться
Эта команда бывает полезна, когда:
- У вас нет доступа к исходному Dockerfile, но есть сам образ.
- Вы хотите проверить образ на наличие потенциально опасных или неэффективных слоёв.
- Нужно понять, как происходит сборка - например, для переноса, отладки или оптимизации.
Пример использования
docker history --no-trunc n8nio/n8n | awk '{$1=$2=$3=$4=""; print $0}' | grep -v COMMENT | nl | sort -nr | sed 's/^[^A-Z]*//' | sed -E 's/[0-9]+(\.[0-9]+)?(B|KB|MB|GB|kB)[[:space:|]].*//g' ADD alpine-minirootfs-3.21.2-aarch64.tar.gz / # buildkit CMD ["/bin/sh"] ENV NODE_VERSION=20.18.2 RUN /bin/sh -c addgroup -g 1000 node && adduser -u 1000 -G node -s /bin/sh -D node && apk add --no-cache libstdc++ && apk add --no-cache --virtual .build-deps curl && ARCH= OPENSSL_ARCH='linux*' && alpineArch="$(apk --print-arch)" && case "${alpineArch##*-}" in x86_64) ARCH='x64' CHECKSUM="b7e78c523c18168074cda97790eac4fc9f00dbfc09052ad5ccc91c36df527265" OPENSSL_ARCH=linux-x86_64;; x86) OPENSSL_ARCH=linux-elf;; aarch64) OPENSSL_ARCH=linux-aarch64;; arm*) OPENSSL_ARCH=linux-armv4;; ppc64le) OPENSSL_ARCH=linux-ppc64le;; s390x) OPENSSL_ARCH=linux-s390x;; *) ;; esac && if [ -n "${CHECKSUM}" ]; then set -eu; curl -fsSLO --compressed "https://unofficial-builds.nodejs.org/download/release/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH-musl.tar.xz"; echo "$CHECKSUM node-v$NODE_VERSION-linux-$ARCH-musl.tar.xz" | sha256sum -c - && tar -xJf "node-v$NODE_VERSION-linux-$ARCH-musl.tar.xz" -C /usr/local --strip-components=1 --no-same-owner && ln -s /usr/local/bin/node /usr/local/bin/nodejs; else echo "Building from source" && apk add --no-cache --virtual .build-deps-full binutils-gold g++ gcc gnupg libgcc linux-headers make python3 py-setuptools && export GNUPGHOME="$(mktemp -d)" && for key in C0D6248439F1D5604AAFFB4021D900FFDB233756 DD792F5973C6DE52C432CBDAC77ABFA00DDBF2B7 CC68F5A3106FF448322E48ED27F5E38D5B0A215F 8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600 890C08DB8579162FEE0DF9DB8BEAB4DFCF555EF4 C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C 108F52B48DB57BB0CC439B2997B01419BD92F80A A363A499291CBBC940DD62E41F10027AF002F8B0 ; do gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; done && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION.tar.xz" && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc && gpgconf --kill all && rm -rf "$GNUPGHOME" && grep " node-v$NODE_VERSION.tar.xz\$" SHASUMS256.txt | sha256sum -c - && tar -xf "node-v$NODE_VERSION.tar.xz" && cd "node-v$NODE_VERSION" && ./configure && make -j$(getconf _NPROCESSORS_ONLN) V= && make install && apk del .build-deps-full && cd .. && rm -Rf "node-v$NODE_VERSION" && rm "node-v$NODE_VERSION.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt; fi && rm -f "node-v$NODE_VERSION-linux-$ARCH-musl.tar.xz" && find /usr/local/include/node/openssl/archs -mindepth 1 -maxdepth 1 ! -name "$OPENSSL_ARCH" -exec rm -rf {} \; && apk del .build-deps && node --version && npm --version # buildkit ENV YARN_VERSION=1.22.22 RUN /bin/sh -c apk add --no-cache --virtual .build-deps-yarn curl gnupg tar && export GNUPGHOME="$(mktemp -d)" && for key in 6A010C5166006599AA17F08146C2130DFD2497F5 ; do gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; done && curl -fsSLO --compressed "https://yarnpkg.com/downloads/$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz" && curl -fsSLO --compressed "https://yarnpkg.com/downloads/$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz.asc" && gpg --batch --verify yarn-v$YARN_VERSION.tar.gz.asc yarn-v$YARN_VERSION.tar.gz && gpgconf --kill all && rm -rf "$GNUPGHOME" && mkdir -p /opt && tar -xzf yarn-v$YARN_VERSION.tar.gz -C /opt/ && ln -s /opt/yarn-v$YARN_VERSION/bin/yarn /usr/local/bin/yarn && ln -s /opt/yarn-v$YARN_VERSION/bin/yarnpkg /usr/local/bin/yarnpkg && rm yarn-v$YARN_VERSION.tar.gz.asc yarn-v$YARN_VERSION.tar.gz && apk del .build-deps-yarn && yarn --version && rm -rf /tmp/* # buildkit COPY docker-entrypoint.sh /usr/local/bin/ # buildkit ENTRYPOINT ["docker-entrypoint.sh"] CMD ["node"] COPY / / # buildkit RUN /bin/sh -c rm -rf /tmp/v8-compile-cache* # buildkit WORKDIR /home/node ENV NODE_ICU_DATA=/usr/local/lib/node_modules/full-icu EXPOSE map[5678/tcp:{}] ARG N8N_VERSION=1.76.1 RUN |1 N8N_VERSION=1.76.1 /bin/sh -c if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi # buildkit LABEL org.opencontainers.image.title=n8n LABEL org.opencontainers.image.description=Workflow Automation Tool LABEL org.opencontainers.image.source=https://github.com/n8n-io/n8n LABEL org.opencontainers.image.url=https://n8n.io LABEL org.opencontainers.image.version=1.76.1 ENV N8N_VERSION=1.76.1 ENV NODE_ENV=production ENV N8N_RELEASE_TYPE=stable RUN |1 N8N_VERSION=1.76.1 /bin/sh -c set -eux; npm install -g --omit=dev n8n@${N8N_VERSION} --ignore-scripts && npm rebuild --prefix=/usr/local/lib/node_modules/n8n sqlite3 && rm -rf /usr/local/lib/node_modules/n8n/node_modules/@n8n/chat && rm -rf /usr/local/lib/node_modules/n8n/node_modules/n8n-design-system && rm -rf /usr/local/lib/node_modules/n8n/node_modules/n8n-editor-ui/node_modules && find /usr/local/lib/node_modules/n8n -type f -name "*.ts" -o -name "*.js.map" -o -name "*.vue" | xargs rm -f && rm -rf /root/.npm # buildkit ARG TARGETPLATFORM=linux/arm64 ARG LAUNCHER_VERSION=1.1.0 COPY n8n-task-runners.json /etc/n8n-task-runners.json # buildkit RUN |3 N8N_VERSION=1.76.1 TARGETPLATFORM=linux/arm64 LAUNCHER_VERSION=1.1.0 /bin/sh -c if [[ "$TARGETPLATFORM" = "linux/amd64" |]]; then export ARCH_NAME="amd64"; elif [[ "$TARGETPLATFORM" = "linux/arm64" |]]; then export ARCH_NAME="arm64"; fi; mkdir /launcher-temp && cd /launcher-temp && wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz && wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256 && echo "$(cat task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256) task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz" > checksum.sha256 && sha256sum -c checksum.sha256 && tar xvf task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz --directory=/usr/local/bin && cd - && rm -r /launcher-temp # buildkit COPY docker-entrypoint.sh / # buildkit RUN |3 N8N_VERSION=1.76.1 TARGETPLATFORM=linux/arm64 LAUNCHER_VERSION=1.1.0 /bin/sh -c mkdir .n8n && chown node:node .n8n # buildkit ENV SHELL=/bin/sh USER node ENTRYPOINT ["tini" "--" "/docker-entrypoint.sh"]
Как видно, получаем нечто отдалённо напоминающее настоящий Dockerfile. Конечно, это не полное восстановление: комментарии, порядок ENV
и ARG
, детали RUN
могут отличаться, но всё же это вполне пригодно для анализа.
Есть ли способ лучше?
Да, определённо. Как видно из вывода команды docker history
в связке с sed
и awk
, результат выходит довольно шумным. Но всё же он даёт общее представление о том, что находится внутри образа.
Более продвинутая альтернатива - это инструмент под названием dive.
Его нужно установить на вашу локальную машину.
Вот процесс установки на macOS и пример использования:
brew install dive ==> Auto-updating Homebrew... Adjust how often this is run with HOMEBREW_AUTO_UPDATE_SECS or disable with HOMEBREW_NO_AUTO_UPDATE. Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`). ==> Auto-updated Homebrew! Updated 1 tap (homebrew/cask). ==> New Casks voicenotes You have 8 outdated formulae and 3 outdated casks installed. ==> Downloading https://ghcr.io/v2/homebrew/core/dive/manifests/0.13.1 ############################################################################################################################ 100.0% ==> Fetching dive ==> Downloading https://ghcr.io/v2/homebrew/core/dive/blobs/sha256:f09a27e21a4b76122d74e9a776219ab7377efaf30dff7d8d7e3016aac375d14a ############################################################################################################################ 100.0% ==> Pouring dive--0.13.1.arm64_sequoia.bottle.tar.gz 🍺 /opt/homebrew/Cellar/dive/0.13.1: 6 files, 9MB ==> Running `brew cleanup dive`... Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP. Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`). dive When and Why to Use This This command is useful when: You dont have access to the Dockerfile but have the image. You want to audit an image for potentially dangerous or inefficient layers. You need to understand the build process for migration or optimization. dive n8nio/n8n Image Source: docker://n8nio/n8n Extracting image from docker-engine... (this can take a while for large images) Analyzing image... Building cache...
Что касается Linux, то он пока далеко не в каждом репозитории появился. Из более-менее известных дистрибутивов есть в репозиториях ALT Linux и Arch Linux. Впрочем в Debian и RH всё равно без проблем можно поставить, вместо установки из репо, можно загрузить со страницы релизных сборок на гитхабе: https://github.com/wagoodman/dive/releases
Выводы
Хотя связка docker history
с shell-скриптами - это вполне изобретательный способ заглянуть внутрь Docker-образа и понять, как он был собран, до идеала ей далеко. Оптимизация слоёв и обрезанные метаданные могут скрыть реальные шаги сборки.
Если хочется чего-то более точного, интерактивного и удобного в анализе - dive даёт гораздо более ясное представление о слоях образа и соответствующих командах. Независимо от того, проверяете ли вы сторонние образы, отлаживаете CI-пайплайн или пытаетесь оптимизировать собственный Dockerfile - правильно подобранный инструмент может сэкономить время и избавить от гаданий.