Съдържание на курса
Урок 4 — Слоеве, кеш и контекст
От Dorian Chávez · основател и главен изпълнителен директор на Hábil ·
11:20 ч. е сутринта, а в 12 има демонстрация пред отдела, който ще използва revisor. Остава една подробност: таблицата в отчета отпечатва една колона, изместена с един интервал. Промяната е на един ред. Правиш я, изграждаш образа и чакаш. Тринайсет секунди. Пробваш — не се е получило, поправяш отново. Още тринайсет. И още тринайсет. На петия кръг осъзнаваш, че си прекарал повече време в гледане как терминалът компилира, отколкото в мислене за проблема, и че всяко изграждане компилира цялата програма от нулата, макар че си пипнал само един файл. Междувременно колежката ти, която работи в собствено копие на проекта, пита защо нейното изграждане казва, че е „качило“ 62 MB, щом кодът на revisor тежи няколко килобайта. В нейното копие някой е изтрил .dockerignore, а в папката има резервно копие и историята на Git. Двата въпроса имат един и същ отговор и той не е в кода на revisor: той е в подредбата на четири реда от Dockerfile и във файл, който се казва .dockerignore. В края на този урок същата промяна от един ред се изгражда наново за по-малко от две секунди.
Време: 2 × 45 мин
Какво изграждаш: същия образ от Урок 3, вече като curso-docker-revisor:1.1.0, с Dockerfile, пренареден така, че да използва кеша, с монтиране на кеш (cache mount) за компилацията на Go и с по-пълен .dockerignore. Прави точно същото като 1.0.0; изгражда се наново за секунди, а слоят на компилацията му тежи със 100 MB по-малко.
Какво научаваш: че образът е купчина от слоеве и как да ги видиш с docker history; как builder-ът решава дали даден слой идва от кеша, или се прави наново; защо редът на инструкциите определя колко се изгражда наново; как да разчиташ изграждане с --progress=plain; какво е монтиране на кеш (RUN --mount=type=cache) и защо съдържанието му не остава в образа; и какво пътува в контекста на изграждане и какво не.
Откъде идваш. От Урок 3 носиш първия си Dockerfile: един-единствен етап върху golang:1.27.3-trixie, COPY . ., който вкарва целия проект, RUN go build, USER 10001:10001 и CMD ["revisor"] във форма exec. Носиш и неговия .dockerignore от пет реда и образа curso-docker-revisor:1.0.0 от 1.45 GB. Там беше казано мимоходом, че копирането на всичко наведнъж прави повторното изграждане бавно; тук ще го измериш и поправиш. От Урок 0 носиш идеята, че образът е съставен от слоеве само за четене. Досега това беше едно изречение; в този урок ще ги преброиш един по един.
Какво не прави този урок. Не смалява образа истински: 1.32 GB продължава да е огромно, защото образът все още съдържа компилатора на Go и цял Debian. Отделянето на компилацията от крайния образ е темата на Урок 5. Не засяга и кода на revisor, нито конфигурацията му, нито тайните му. И не учи как да „изтеглиш зависимостите в отделен слой“ — много разпространен трик в ръководствата за Go, — защото revisor няма външни зависимости: използва само стандартната библиотека. Онова, което има, и то в изобилие, е компилация, и това е кешът, който се преподава тук. Раздел 4.8 ти казва как да разпознаеш другия трик, когато го видиш.
За изпълненията. Всички изходи са реални и са получени на 21 октомври 2026 г. на Mac с Colima, която стартира виртуална машина с Ubuntu 24.04.4, същата основа като Linux Mint 22, референтната система на курса. Скриптовете са изпълнени вътре в тази виртуална машина, с Docker 29.2.1 (клиент и демон) и Buildx v0.31.1. Курсът беше планиран с Docker Engine 29.8.2, а най-новата версия, публикувана в този ден, беше 29.9.2, от 20 октомври 2026 г. (бележки към версиите); нищо от онова, на което учи този урок, не се променя между тези версии. Три предупреждения за тези изпълнения. Първо: машината беше заета едновременно с други изграждания, така че времената имат шум; онова, което се запазва във всички изпълнения, е разликата между единия и другия Dockerfile, а не точните милисекунди. Второ: на същата машина се пишеха и други уроци от курса, които създават образи със същите имена (curso-docker-revisor:1.0.0, :1.1.0). За да не им се изтрие нищо и да не им се презапише таг, скриптовете от този урок бяха пуснати с друг префикс на имената благодарение на променливата PREFIJO, която имат, а префиксът беше върнат към нормалния за курса в самата команда, пред очите ти: първият ред на всеки запазен изход казва точно как е изпълнен, например PREFIJO=curso-docker-l04- ./fig04_03.sh 2>&1 | sed 's/curso-docker-l04-/curso-docker-/g'. На твоя компютър просто ./fig04_03.sh отпечатва същото без този sed. Това е единственото изключение от „изпълнява се точно както е“ и съществува само заради споделената машина. Трето: на Mac-а на изпълненията няма инсталиран Go 1.27.3; затова грешката на Go от Решение 3 е измерена в образа golang:1.27.3-trixie. Какво се променя на твоя Mint: времената, идентификаторите на кеша и, ако компютърът ти е с Intel или AMD, вариантът на образа на Go, който се изтегля (amd64 вместо arm64), макар че digest-ът, който ще видиш в реда на FROM, е същият, защото е този на индекса на платформите. Програмите са в programas/04-capas-cache-contexto/, всяка с изхода си до нея, а estado/ пази Dockerfile, .dockerignore и Dockerfile.leccion3 (копието на този от Урок 3, само за сравнение) такива, каквито остават в края.
След урока ще можеш да
- Изброиш с
docker historyслоевете на даден образ и кажеш кои добавят файлове и кои само данни. - Предвидиш, преди да изградиш, кои стъпки от даден Dockerfile ще дойдат от кеша след промяна, и го провериш с
--progress=plain. - Пренаредиш Dockerfile така, че промяна в кода да не повтаря стъпките, които не зависят от него.
- Добавиш монтиране на кеш към
RUNза компилация и покажеш, че съдържанието му живее в builder-а, а не в образа. - Измериш с файл от 50 MB какво пътува в контекста на изграждане, със и без
.dockerignore. - Изтриеш само своя кеш на компилацията, без да засегнеш този на други проекти на същата машина.
Защо, преди как
Изграждането на образ е повтаряне на рецепта. Първия път всяка стъпка се прави наистина: изтегля се основата, копират се файловете, компилира се. Втория път почти всичко е същото като първия: същата основа, същият go.mod, почти същият код. Би било абсурдно всичко да се повтаря. Затова builder-ът пази резултата от всяка стъпка и следващия път, ако нищо от онова, което стъпката използва, не се е променило, използва повторно резултата, вместо да я повтаря. Това се нарича кеш на изграждането (build cache).
Проблемът е, че кешът има правила и те не са онези, които човек предполага. Не е „използвай повторно онова, което не се е променило“. А е „използвай повторно до първата стъпка, която се е променила, и оттам надолу повтори всичко“. Тази разлика е сцената от началото. В Dockerfile от Урок 3 кодът влиза с един-единствен COPY . ., а на следващия ред се компилира. Всяка промяна, дори да е коментар, променя този COPY, и тъй като RUN go build идва след него, цялата компилация се повтаря. С Dockerfile от този урок същата промяна се изгражда наново така (ще го измериш в раздел 4.5):
## 3. Cambio un comentario en internal/reporte/tabla.go
curso-docker-revisor:1.0.0 (Dockerfile.leccion3): 13011 ms
...
curso-docker-revisor:1.1.0 (Dockerfile): 1847 ms
От тринайсет секунди на по-малко от две, със същия код, същата промяна и същия получен образ. На по-малко натоварена машина или в по-голям проект разликата расте. А вторият въпрос от сцената, този за 62-та MB, е другата страна на същата монета: builder-ът също трябва да знае кои файлове може да използва, и ако му ги дадеш всичките, всичките пътуват.
Урокът има осем части:
- Образът е купчина от слоеве (4.1): как да ги видиш с
docker history. - Как решава кешът (4.2): правилата, с Dockerfile от Урок 3 като лош пример.
- Редът има значение (4.3): новият Dockerfile, ред по ред.
- Кешът, който не е слой (4.4):
RUN --mount=type=cache. - Измерване (4.5): хронометърът и
--progress=plain. - Какво пътува в контекста (4.6), с грешката от урока.
- Проверка преди изграждане (4.7).
- Какво ще видиш в други проекти (4.8).
Понятията
4.1 Образът е купчина от слоеве
4.1.1 По един слой на стъпка
Документацията на Docker определя слоя така: „всеки слой на даден образ съдържа набор от промени във файловата система: добавяния, изтривания или модификации“, и обяснява, че това е полезно, защото „позволява слоевете да се използват повторно между образите“ (Docker, „Understanding the image layers“). А страницата за кеша на изграждането свързва слоевете с твоята рецепта: „всяка инструкция в този Dockerfile се превежда в слой на крайния ти образ“ (Docker, „Docker build cache“).
В Урок 0 видя последствието от слоевете за контейнерите: всеки контейнер добавя свой собствен слой отгоре, в който пише. Тук е важна другата половина: как се подреждат долните слоеве, когато изграждаш. FROM не добавя собствен слой: слага отдолу всички слоеве на базовия образ. След това всеки COPY и всеки RUN оставя слой с файловете, които е добавил или променил, а WORKDIR също оставя един, малък, когато трябва да създаде папката. Инструкциите, които само отбелязват нещо, като USER, EXPOSE и CMD, оставят ред в историята, но с нула байта: добавят данни към конфигурацията, а не файлове.
4.1.2 Как да ги видиш с docker history
docker history показва „историята на даден образ“, по един ред на стъпка, от най-новата към най-старата (docker image history). С --format избираш кои колони да видиш: .Size е размерът, който е добавила тази стъпка, а .CreatedBy — командата, която я е създала. Ето историята на образа от Урок 3, взета от fig04_03.sh (целият скрипт е показан в 4.5; тук се гледа само неговият раздел 5):
$ docker history curso-docker-revisor:1.0.0 --format '{{.Size}}\t{{.CreatedBy}}' | head -7 | cut -c1-70
0B CMD ["revisor"]
0B EXPOSE [8080/tcp]
0B USER 10001:10001
110MB RUN /bin/sh -c go build -o /usr/local/bin/re…
115kB COPY . . # buildkit
8.19kB WORKDIR /src
4.1kB WORKDIR /go
Този изход е стабилен на машина arm64; на amd64 размерите може леко да се различават. head -7 запазва седемте горни реда, а cut -c1-70 скъсява всеки ред до 70 знака, за да се побере; … е на Docker, който също скъсява дългите команди (с --no-trunc не го прави).
Чети го отдолу нагоре, което е редът, в който е изграден:
WORKDIR /goне е твой. Това е последната стъпка на образа на Go. Ако махнешhead -7, отдолу се появяват още десетина реда, всичките отgolang:1.27.3-trixie: в референтното изпълнение на курса най-големият беше слой от 290 MB с инсталацията на Go, а по-надолу — няколко от 281, 208 и 156 MB с базовия Debian. Твоят образ започва там, където свършва този на Go.WORKDIR /src, 8.19 kB: първата ти стъпка. Създава папката.COPY . . # buildkit, 115 kB: целият проект. Коментарът# buildkitказва, че слоят е създаден от BuildKit, builder-а, с който се запозна в Урок 3.RUN /bin/sh -c go build ..., 110 MB. Обърни внимание:RUNсе изпълнява с/bin/sh -c, защото в Dockerfile е във форма shell. ЗаRUNтова е наред; правилото за формата exec беше заCMDиENTRYPOINT.USER,EXPOSE,CMD: нула байта. Само данни.
Сега въпросът, с който започва този урок: компилираният revisor тежи около 10 MB. Какво са другите 100 MB в този слой? Отговорът е в раздел 4.4 и е причината новият образ да тежи по-малко.
4.2 Как решава кешът
4.2.1 Трите правила
Документацията за кеша ги обяснява на две страници и те се побират в три правила:
- За
COPYиADDсе броят файловете, но не и датата им. Документацията казва, че builder-ът „изчислява контролна сума за кеша от метаданните на файловете“ и че кешът се обезсилва, „ако метаданните на някой от участващите файлове са се променили“. На практика, което ще измериш в Упражнение 1: промяната на съдържанието на файл го обезсилва, а промяната само на датата му — не. Документацията го казва така: датата на модификация не се брои; „ако се е променил самоmtimeна копираните файлове, кешът не се обезсилва“ (Docker, „Build cache invalidation“). - За
RUNсе брои само текстът на командата. Освен приADDиCOPY, проверката на кеша не гледа файловете; „за намиране на съвпадение се използва само низът на командата“ (същата страница).RUN go build, който вече е бил изпълнен със същия слой отдолу, идва от кеша, дори навън да е излязла друга версия на нещо. - Щом нещо бъде обезсилено, всичко отдолу се повтаря. „Ако даден слой се промени, всички слоеве, които идват след него, също са засегнати. Дори да не биха изградили нищо различно, те все пак трябва да се изпълнят отново“ (Docker, „Docker build cache“). Страницата за обезсилването го казва по същия начин: щом кешът бъде обезсилен, следващите команди създават нови образи и кешът не се използва.
Третото правило е важното за реда. Кешът не е карта на „какво се е променило“; той е верига. Всяко звено зависи от горното. Ако се скъса едно, се късат всички долни.
4.2.2 Dockerfile от Урок 3 в светлината на правилата
Прегледай Dockerfile от Урок 3 с трите правила в ръка:
FROM golang:1.27.3-trixie
WORKDIR /src
COPY . .
RUN go build -o /usr/local/bin/revisor ./cmd/revisor
(Това е фрагмент: целият файл е estado/Dockerfile.leccion3, идентичен с този от Урок 3.) COPY . . зависи от всички файлове в контекста. Променяш коментар в internal/reporte/tabla.go: правило 1, съдържанието се е променило, COPY се обезсилва. Правило 3: RUN go build идва след него, така че се повтаря. И тъй като RUN започва в нов слой, без нищо от предишната компилация, Go компилира всичко от нулата: твоята програма, всеки пакет от internal/ и всеки пакет от стандартната библиотека, който тя използва.
Ето как го видя fig04_03.sh (раздел 3), във формата, който обяснява 4.5:
curso-docker-revisor:1.0.0 (Dockerfile.leccion3): 13011 ms
paso 1 DONE 0.0s FROM docker.io/library/golang:1.27.3-trixie@sha256:604106102
paso 2 CACHED WORKDIR /src
paso 3 DONE 0.0s COPY . .
paso 4 DONE 10.1s RUN go build -o /usr/local/bin/revisor ./cmd/revisor
CACHED означава „дойде от кеша“; DONE с време — „направено наистина“. WORKDIR дойде от кеша; COPY и компилацията бяха направени наново. В това изпълнение стъпка 1, FROM, казва DONE 0.0s: builder-ът потвърждава, че вече има базовия образ, и не прави нищо повече. Ако образът не беше изтеглен (на току-що инсталирана машина или след като си го изтрил), тази стъпка щеше да отнеме толкова, колкото трае изтеглянето, и щеше да го видиш във времето ѝ.
4.3 Редът има значение
4.3.1 Dockerfile от урока
Ръководството на Docker за оптимизация го казва в един ред: „стъпките, които се променят често, трябва да са близо до края на Dockerfile“ (Docker, „Optimize cache usage“). За revisor това дава следния Dockerfile. Той заменя този от Урок 3, в корена на проекта, и се казва Dockerfile:
# syntax=docker/dockerfile:1
# fig04_01.Dockerfile — Lección 4: la misma imagen que la 3, ordenada para la caché.
FROM golang:1.27.3-trixie
WORKDIR /src
# Primero lo que casi nunca cambia (la descripción del módulo)...
COPY go.mod ./
# ...después el código, que cambia en cada edición.
COPY cmd/ cmd/
COPY internal/ internal/
# La caché de compilación de Go vive en un montaje de caché: sobrevive entre
# construcciones, acelera la siguiente y NO queda dentro de la imagen.
RUN --mount=type=cache,id=curso-docker-go-build,target=/root/.cache/go-build \
go build -o /usr/local/bin/revisor ./cmd/revisor
USER 10001:10001
EXPOSE 8080
CMD ["revisor"]
В сравнение с този от Урок 3 се променят две неща: COPY . . е разделен на три, а RUN получи опция --mount. Останалото е същото, ред по ред, и полученият образ прави същото.
4.3.2 Трите COPY
COPY go.mod ./. Файлът go.mod описва модула: името му и версията на Go, която изисква. В revisor той има два реда и почти никога не се променя. Копирането му първо, самостоятелно, създава слой, който оцелява при всяка промяна в кода. В проект със зависимости тук щеше да е и go.sum, както и стъпката за изтегляне (4.8); в revisor ползата е малка, но навикът е същият: каквото се променя най-рядко — най-горе.
COPY cmd/ cmd/ и COPY internal/ internal/. Кодът, в два слоя. cmd/revisor/ е HTTP сървърът; internal/ са пакетите, които идват от курса по Go (конфигурация, заявки, отчет). Разделянето им не променя много скоростта, но показва по-ясно в docker history коя част се е променила, а редът следва правилото: internal/, който се променя повече, докато разработваш отчета, остава отдолу.
Обърни внимание на един страничен ефект, който е безценен и се измерва в 4.6: със селективни COPY само тези три пътя могат да влязат в образа. Забравен .env в папката не влиза, дори .dockerignore да го е пропуснал, защото никой COPY не го иска. .dockerignore продължава да е задължителен, но вече не е единствената бариера.
4.3.3 Същата промяна с новия ред
Сега същият коментар в tabla.go, с този Dockerfile. Това е раздел 3 от fig04_03.sh, втората половина:
curso-docker-revisor:1.1.0 (Dockerfile): 1847 ms
paso 1 DONE 0.0s FROM docker.io/library/golang:1.27.3-trixie@sha256:604106102
paso 2 CACHED WORKDIR /src
paso 3 CACHED COPY go.mod ./
paso 4 CACHED COPY cmd/ cmd/
paso 5 DONE 0.0s COPY internal/ internal/
paso 6 DONE 0.8s RUN go build -o /usr/local/bin/revisor ./cmd/revisor
Правилата, още веднъж. go.mod не се е променил: кеш. cmd/ не се е променил: кеш. internal/ се е променил: прави се наново, а по правило 3 — и RUN. Дотук редът ти спести малко, защото компилацията продължава да се повтаря. Но погледни времето на RUN: 0.8 секунди срещу 10.1 при стария Dockerfile. Компилацията се повтори и въпреки това почти нищо не беше компилирано. Това не го направи редът. Направи го --mount и за него е следващият раздел.
Преди да продължим, доказателството, че новият образ прави същото като стария. Раздел 7 от fig04_03.sh пуска 1.1.0 както в Урок 3, с revisor, който проверява сам себе си, и го пита за състоянието му отвътре на контейнера (образът на Go съдържа curl; така не е нужно да се отваря порт навън):
## 7. La imagen nueva hace lo mismo que la vieja
$ docker exec curso-docker-revisor curl -s http://127.0.0.1:8080/salud
{"estado":"vivo","version":"desarrollo"}
Същото като 1.0.0. "desarrollo" все още е там, защото версията още не се записва в програмата; това идва в Урок 6.
4.4 Кешът, който не е слой: RUN --mount=type=cache
4.4.1 Go вече има собствен кеш
Компилаторът на Go не компилира два пъти едно и също, ако може да го избегне. Документацията му го казва така: „командата go кешира резултатите от компилацията, за да ги използва повторно в бъдещи компилации“, и по подразбиране ги пази в папка с име go-build в кеш папката на потребителя (документация на go, „Build and test caching“). В Linux, за потребителя root, тази папка е /root/.cache/go-build. Ако промениш файл от internal/reporte, Go компилира наново само този пакет и зависимите от него, а останалото взема от кеша.
На твоя компютър този кеш работи сам. Вътре в изграждане на Docker — не: всеки RUN започва в слоя, който му е оставила предишната стъпка, а в Dockerfile от Урок 3 този слой няма кеш на Go, защото COPY току-що е направен наново. Още по-лошо: онова, което Go записва в /root/.cache/go-build по време на RUN, остава в слоя на RUN. Това са излишните 100 MB от 4.1.2.
4.4.2 Монтирането на кеш
RUN --mount позволява да се „създават монтирания на файловата система, до които изграждането има достъп“, а типът cache служи, за да може „контейнерът на изграждането да кешира папките на компилатори и пакетни мениджъри“ (справочник на Dockerfile, RUN --mount=type=cache). Ръководството за оптимизация допълва картината: монтирането на кеш е „постоянно място за кеш“ за изгражданията; кешът се натрупва между изгражданията и се споделя между няколко от тях (Docker, „Optimize cache usage“).
Преведено: по време на този RUN папката /root/.cache/go-build не е част от слоя. Тя е папка, която builder-ът пази отделно и монтира отново при всяко изграждане. Go намира там онова, което е компилирал предишния път, а онова, което записва там, не попада в образа.
Опциите, които използва revisor:
type=cache: типът на монтирането.target=/root/.cache/go-build: къде се появява папката вътре вRUN. Това е папката по подразбиране на кеша на Go, така че Go я използва, без да се настройва нищо. Ръководството на Docker използва същия път в своя пример за Go.id=curso-docker-go-build: името на кеша. Справочникът казва, че е незадължително и служи за „идентифициране на отделни кешове“; ако не го зададеш, стойността му е същата като наtarget. По-надолу ще видиш защо е добре да го зададеш.
Има и други опции (sharing, mode, uid…) и справочникът ги изброява; revisor не се нуждае от тях. Една бележка: монтирането се използва при изграждането, когато потребителят все още е root; USER 10001:10001 идва след това и не му влияе.
4.4.3 Кешът живее в builder-а, а не в образа
Твърденията се проверяват. Раздел 6 от fig04_03.sh търси кеша на двете места, където би могъл да бъде:
## 6. La caché de compilación vive en el constructor, no en la imagen
$ docker buildx du --filter 'description~=curso-docker-fig04-go-build'
ID RECLAIMABLE SIZE LAST ACCESSED
xq0we4j0a5g4a98ftg4xaezzc* true 99.59MB 1 second ago
$ docker buildx du --verbose --filter 'description~=curso-docker-fig04-go-build' | grep -E '^(Description|Type):'
Description: cached mount /root/.cache/go-build from exec /bin/sh -c go build -o /usr/local/bin/revisor ./cmd/revisor with id "/curso-docker-fig04-go-build"
Type: exec.cachemount
$ docker run --rm --name curso-docker-mira --user 0 curso-docker-revisor:1.1.0 sh -c 'du -sh /root/.cache/go-build 2>&1'
du: cannot access '/root/.cache/go-build': No such file or directory
[código de salida: 1]
$ docker run --rm --name curso-docker-mira --user 0 curso-docker-revisor:1.0.0 sh -c 'du -sh /root/.cache/go-build 2>&1'
95M /root/.cache/go-build
Идентификаторът на първия ред и „1 second ago“ се променят; размерите може да варират с няколко мегабайта. docker buildx du показва колко място заема builder-ът на диска (disk usage), а --filter description~=... запазва записите, чието описание съдържа този текст. С --verbose показва всеки запис изцяло; grep запазва описанието и типа му, които ще ти трябват в 4.4.5. Двете docker run влизат във всеки образ като root (--user 0, само за да може да се чете /root) и измерват папката.
Три реда, три факта:
- В builder-а има кеш от 99.59 MB, от тип
exec.cachemount(монтиране на кеш), сidот скрипта. Там е онова, което Go е компилирал. - В образа
1.1.0папката дори не съществува. Кешът не е пътувал. - В образа
1.0.0, този от Урок 3, папката тежи 95 MB. Ето къде бяха излишните 100 MB.
А защо скриптът използва curso-docker-fig04-go-build, а не curso-docker-go-build от Dockerfile? Защото скриптът променя id, когато сглобява своето копие на проекта, за да има кеш само за себе си, да започне наистина на студено и да може да го изтрие накрая, без да засегне този на твоя проект. Това е същата идея, която ще видиш в 4.4.5.
А раздел 5 от същия скрипт го потвърждава от страната на образите:
$ docker history curso-docker-revisor:1.1.0 --format '{{.Size}}\t{{.CreatedBy}}' | head -9 | cut -c1-70
0B CMD ["revisor"]
0B EXPOSE [8080/tcp]
0B USER 10001:10001
10MB RUN /bin/sh -c go build -o /usr/local/bin/re…
77.8kB COPY internal/ internal/ # buildkit
41kB COPY cmd/ cmd/ # buildkit
12.3kB COPY go.mod ./ # buildkit
8.19kB WORKDIR /src
4.1kB WORKDIR /go
$ docker image ls --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep -E '^curso-docker-revisor:1\.[01]\.0 '
curso-docker-revisor:1.1.0 1.32GB
curso-docker-revisor:1.0.0 1.45GB
$ for i in curso-docker-revisor:1.0.0 curso-docker-revisor:1.1.0; do docker image inspect --format "$i {{.Size}}" $i; done
curso-docker-revisor:1.0.0 337298513
curso-docker-revisor:1.1.0 314341606
RUN спадна от 110 MB на 10 MB: сега слоят съдържа само изпълнимия файл. Трите слоя с код заедно са колкото предишния COPY . ., само разпределени. Това е твърдото число: слоят олекна със 100 MB, без да се махне нищо, от което revisor се нуждае.
Другите два реда отчитат размера на образа по два начина и е добре да не ги бъркаш. docker image inspect дава размера на съдържанието, компресирано така, както се съхранява и пренася: 337.3 MB срещу 314.3 MB, 23 MB по-малко. docker image ls дава 1.45 GB срещу 1.32 GB, около 130 MB по-малко. Разликата между двата начина идва от хранилището за образи на Docker Engine 29, което според документацията му пази образите „в компресиран и некомпресиран формат“ (Docker, „containerd image store“). Документацията не дава формула за това как се получава числото на docker image ls и няма да измисляме такава със сбор, който случайно прилича на него. Това, което можем да твърдим, е онова, което беше измерено: и двете числа намаляват в посоката, която съответства на един слой, по-малък със 100 MB, а надеждната мярка — тази, която не зависи от начина, по който брои всяка команда — е слоят от docker history. Това е икономия, която не търсеше, и тя дойде, защото направи добре онова, което търсеше.
4.4.4 Двата кеша са независими
Една подробност, която ще ти спести объркване: това са две отделни неща и се управляват поотделно. --no-cache иска от builder-а да не използва кеша на слоевете; за да бъде измерването на студено честно, раздел 1 от fig04_03.sh освен това изтрива своето монтиране преди началото, със собствения си id (4.4.5). И обратното: стъпка, която идва от кеша на слоевете, не изпълнява своя RUN, така че нито чете, нито пълни монтирането. Ако изградиш веднъж с всичко в кеша на слоевете, монтирането остава толкова празно или толкова пълно, колкото е било.
4.4.5 Собствен id и защо никога широко изтриване
Монтирането на кеш заема диск и един ден ще поискаш да го изтриеш. Командата е docker buildx prune, която изтрива кеш на builder-а, а опцията ѝ --filter позволява да избереш какво да се изтрие: по възраст (until), по тип (type, като един от типовете е exec.cachemount, точно този на монтиранията) или по текст от описанието (description) (docker buildx prune). Вече видя в 4.4.3 как изглежда описанието на монтиране на кеш: cached mount /root/.cache/go-build from exec … with id "/curso-docker-fig04-go-build". id се появява в края, след наклонена черта и в кавички. Затова id има значение: той е етикетът, който ти позволява да намериш твоя кеш и само него.
Ето я опасността. Широк филтър, като description~=go-build, хваща кеша на компилацията на всеки проект на Go на машината, който използва пътя по подразбиране. Не е хипотетично: на машината на тези изпълнения, докато се пишеше този урок, docker buildx du --filter 'description~=go-build' намери два записа — този от урока и този на друг проект, който се изграждаше по същото време. prune с този филтър би изтрил кеша на другия и следващото му изграждане би се забавило, без никой да знае защо. На споделена машина (сървър за изграждане, компютърът на екип или твоят с няколко проекта) широкото изтриване означава да изтриеш чуждото.
Правилото на този курс има две части:
- Всеки проект — свой
id.revisorизползваcurso-docker-go-build. Ако утре пакетираш друг проект на Go, даваш му друг. Безidвсички проекти, които използватtarget=/root/.cache/go-build, споделят един и същ кеш, защотоidпо подразбиране е пътят. - Изтриване с пълния
idи типа. Така го правят скриптовете от този урок на излизане:
docker buildx prune -f --filter type=exec.cachemount \
--filter "description~=[^a-z0-9-]curso-docker-fig04-go-build[^a-z0-9-]"
type=exec.cachemount ограничава изтриването до монтиранията на кеш. ~= сравнява с регулярен израз, а [^a-z0-9-] от всяка страна („всеки знак, който не е малка буква, цифра или тире“) изисква id да се появи цял: в описанието около id са наклонената черта отпред и кавичката отзад, и двете отговарят на условието. Така по-дълъг id, който го съдържа, като curso-docker-fig04-go-build-viejo, не се изтрива по невнимание. Защо да не се напишат направо наклонената черта и кавичките във филтъра? Защото с версията на Buildx от тези изпълнения (v0.31.1) филтър с интервали, кавички или / не намери нищо: това беше измерено с docker buildx du, преди да се използва. Добър навик е: изпробвай филтъра с du, който само гледа, преди да го използваш с prune, който изтрива.
Изтриване, което не съобщава, ако не е изтрило нищо, не доказва нищо, затова раздел 8 от fig04_03.sh изтрива пред очите ти и после търси отново:
## 8. Borro SOLO mi caché de compilación y compruebo que ya no está
$ docker buildx prune -f --filter type=exec.cachemount --filter 'description~=[^a-z0-9-]curso-docker-fig04-go-build[^a-z0-9-]' | tail -1
Total: 99.59MB
$ docker buildx du --filter 'description~=curso-docker-fig04-go-build'
ID RECLAIMABLE SIZE LAST ACCESSED
prune отчита какво е освободил (Total: 99.59MB, същият кеш от раздел 6), а следващият du вече не намира нищо: остава само заглавният ред на таблицата. За твоя проект същата команда с curso-docker-go-build изтрива кеша на revisor и нищо друго; следващото изграждане ще бъде бавно веднъж и ще го напълни отново.
4.5 Измерване: хронометърът и --progress=plain
4.5.1 Да видиш какво прави builder-ът
Когато изграждаш в терминал, BuildKit рисува екран, който се прерисува и оцветява. Удобно е за гледане, но не може да се запази, нито да се търси в него. Опцията --progress определя формата. Според справочника на docker buildx build стойността по подразбиране е auto, която използва режима tty (интерактивния, „с цвят и прерисуване“), ако изходът е терминал, и plain, ако не е; plain „отпечатва напредъка на изграждането като обикновен текст“ (docker buildx build, --progress). На сървър за изграждане, където няма терминал, винаги виждаш plain, така че е добре да умееш да го разчиташ.
Обикновеният текст има следната форма: всяка стъпка е число с # отпред. Първо се появява редът, който я назовава, като #10 [5/6] COPY internal/ internal/, а след това редовете с онова, което е направила, всички със същото число. Стъпката обикновено завършва с един от два реда: #10 CACHED, ако е дошла от кеша, или #10 DONE 0.5s, ако е направена, с времето си. Ако се провали, завършва с #10 ERROR: …, както ще видиш в 4.6.2. Стъпките [internal] са онези, които builder-ът прави сам: четене на Dockerfile, на .dockerignore, на контекста. Обърни внимание и на това, че номерираните стъпки стигат до [6/6], макар новият Dockerfile да има девет инструкции: FROM, WORKDIR, трите COPY и RUN са шестте стъпки, които BuildKit номерира, защото са тези, които създават слой с файлове. USER, EXPOSE и CMD само записват данни в конфигурацията на образа (4.1), нямат собствена стъпка и затова не се появяват като [7/9]. Не са пропуснати: има ги в резултата, както показва docker history с техните 0 B.
4.5.2 Скриптът за измерването
fig04_03.sh изгражда двата Dockerfile в четири ситуации, засича времето на всяко изграждане и обобщава записа от --progress=plain в таблица със стъпки. Това е най-дългият скрипт в урока, а разделите му от 5 до 8 са онези, които вече видя в 4.1.2, 4.3.3, 4.4.3 и 4.4.5:
#!/usr/bin/env bash
# fig04_03.sh — Capas y caché: el Dockerfile de la Lección 3 contra el de la
# Lección 4. Construye los dos en frío, otra vez sin cambios y después de
# cambiar un archivo; cronometra cada construcción, muestra qué pasos salieron
# de la caché (--progress=plain) y compara las capas con docker history.
# Borra al salir sus imágenes y su caché de compilación.
set -euo pipefail
PREFIJO="${PREFIJO:-curso-docker-}"
AQUI="$(cd "$(dirname "$0")" && pwd)"
TRABAJO="$(mktemp -d)"
# La caché de compilación de ESTE guion, con un id propio: así nunca toca la
# caché curso-docker-go-build de tu proyecto ni la de otro proyecto.
CACHE_ID="${PREFIJO}fig04-go-build"
L3="${PREFIJO}revisor:1.0.0"
L4="${PREFIJO}revisor:1.1.0"
# El proyecto: el código que entrega el curso + los archivos de esta lección.
cp -R "$AQUI/../revisor" "$TRABAJO/revisor"
cp "$AQUI/estado/.dockerignore" "$AQUI/estado/Dockerfile.leccion3" "$TRABAJO/revisor/"
sed "s/id=curso-docker-go-build/id=${CACHE_ID}/" "$AQUI/estado/Dockerfile" > "$TRABAJO/revisor/Dockerfile"
cd "$TRABAJO/revisor"
titulo() { printf '\n## %s\n' "$*"; }
# muestra: imprime el comando con «$ » y lo ejecuta (salida y error juntos)
muestra() {
printf '$ %s\n' "$*"
local rc=0
eval "$@" 2>&1 || rc=$?
[ "$rc" -ne 0 ] && echo "[código de salida: $rc]"
return 0
}
# t: milisegundos desde la época, para medir duraciones
t() { date +%s%3N; }
# construir ETIQUETA DOCKERFILE [opciones...]: construye con --progress=plain,
# guarda el registro en ../construccion.log e imprime cuánto tardó
construir() {
local etiqueta="$1" archivo="$2" inicio fin; shift 2
inicio=$(t)
docker build --progress=plain "$@" -t "$etiqueta" -f "$archivo" . > ../construccion.log 2>&1
fin=$(t)
echo "$etiqueta ($archivo${*:+ $*}): $(( fin - inicio )) ms"
}
# pasos: de ../construccion.log, cada paso [n/m] con su estado final
pasos() {
awk '
/^#[0-9]+ \[([a-z0-9-]+ )?[0-9]+\/[0-9]+\]/ {
if (!($1 in paso)) {
l=$0; sub(/^#[0-9]+ \[([a-z0-9-]+ )?/, "", l); split(l, p, "/"); i=p[1]+0
sub(/^[0-9]+\/[0-9]+\] /, "", l); sub(/--mount=[^ ]+ +/, "", l); paso[$1]=i; nombre[i]=l; if (i > k) k=i
}
}
/^#[0-9]+ (CACHED|DONE)/ { if ($1 in paso) estado[paso[$1]]=$2 ($2=="DONE" ? " " $3 : "") }
END { for (i=1; i<=k; i++) printf " paso %d %-10s %s\n", i, estado[i], substr(nombre[i], 1, 60) }
' ../construccion.log
}
# borrar_cache: borra SOLO la caché de compilación con este id exacto. El id
# va entre [^a-z0-9-] para que no atrape otro id que lo contenga.
borrar_cache() {
docker buildx prune -f --filter type=exec.cachemount \
--filter "description~=[^a-z0-9-]${CACHE_ID}[^a-z0-9-]" >/dev/null 2>&1 || true
}
trap 'docker rm -f "${PREFIJO}revisor" >/dev/null 2>&1; docker rmi -f "$L3" "$L4" >/dev/null 2>&1; borrar_cache; rm -rf "$TRABAJO"' EXIT
titulo "1. En frío: sin caché de capas y con la caché de compilación vacía"
borrar_cache
construir "$L3" Dockerfile.leccion3 --no-cache
pasos
construir "$L4" Dockerfile --no-cache
pasos
titulo "2. Otra vez, sin cambiar nada"
construir "$L3" Dockerfile.leccion3
pasos
construir "$L4" Dockerfile
pasos
titulo "3. Cambio un comentario en internal/reporte/tabla.go"
echo "// cambio de prueba $(date +%s)" >> internal/reporte/tabla.go
construir "$L3" Dockerfile.leccion3
pasos
construir "$L4" Dockerfile
pasos
titulo "4. Cambio un archivo que el .dockerignore deja fuera"
echo "notas de trabajo $(date +%s)" > NOTAS.md
construir "$L4" Dockerfile
pasos
titulo "5. Las capas de cada imagen: solo los pasos de la receta"
muestra "docker history $L3 --format '{{.Size}}\t{{.CreatedBy}}' | head -7 | cut -c1-70"
muestra "docker history $L4 --format '{{.Size}}\t{{.CreatedBy}}' | head -9 | cut -c1-70"
muestra "docker image ls --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep -E '^${PREFIJO}revisor:1\.[01]\.0 '"
muestra "for i in $L3 $L4; do docker image inspect --format \"\$i {{.Size}}\" \$i; done"
titulo "6. La caché de compilación vive en el constructor, no en la imagen"
muestra "docker buildx du --filter 'description~=${CACHE_ID}'"
muestra "docker buildx du --verbose --filter 'description~=${CACHE_ID}' | grep -E '^(Description|Type):'"
muestra "docker run --rm --name ${PREFIJO}mira --user 0 $L4 sh -c 'du -sh /root/.cache/go-build 2>&1'"
muestra "docker run --rm --name ${PREFIJO}mira --user 0 $L3 sh -c 'du -sh /root/.cache/go-build 2>&1'"
titulo "7. La imagen nueva hace lo mismo que la vieja"
docker run -d --name "${PREFIJO}revisor" -e REVISOR_SERVICIOS='propio http://127.0.0.1:8080/salud' "$L4" >/dev/null
for _ in $(seq 40); do docker exec "${PREFIJO}revisor" curl -fsS -o /dev/null http://127.0.0.1:8080/salud 2>/dev/null && break; sleep 0.5; done
muestra "docker exec ${PREFIJO}revisor curl -s http://127.0.0.1:8080/salud"
docker rm -f "${PREFIJO}revisor" >/dev/null
titulo "8. Borro SOLO mi caché de compilación y compruebo que ya no está"
muestra "docker buildx prune -f --filter type=exec.cachemount --filter 'description~=[^a-z0-9-]${CACHE_ID}[^a-z0-9-]' | tail -1"
muestra "docker buildx du --filter 'description~=${CACHE_ID}'"
Помощните функции в началото са тези от Урок 3 (titulo, muestra, t), плюс три нови. construir изгражда с --progress=plain, запазва записа във файл и засича времето. pasos е малка програма на awk, която чете този запис и за всяка номерирана стъпка [n/m] отпечатва номера, името и крайното ѝ състояние; маха от името опцията --mount=..., за да се побере редът, и номерира стъпките сама, защото, когато друго изграждане споделя идентични стъпки на същата машина, BuildKit може да покаже номера на стъпката от другото. borrar_cache е онази от 4.4.5.
Още три подробности. Промяната съдържа часа ($(date +%s)), за да е различна при всяко изпълнение: ако пишеш винаги един и същ текст, при второто пускане на скрипта това съдържание вече би било в кеша на слоевете и „промяната“ би излязла CACHED. (Това се случи на автора на този урок в първа версия на скрипта и е добър пример, че кешът помни съдържание, а не моменти.) Раздел 7 използва docker exec ... curl, за да говори с revisor отвътре на контейнера му, без да публикува портове. А скриптът, също като тези от Урок 3, изтрива на излизане образите, които е създал; ако искаш да запазиш 1.1.0, изгради го след това в проекта си с docker build -t curso-docker-revisor:1.1.0 ..
4.5.3 Числата
Разделите 1 и 2 от изхода:
$ PREFIJO=curso-docker-l04- ./fig04_03.sh 2>&1 | sed 's/curso-docker-l04-/curso-docker-/g'
## 1. En frío: sin caché de capas y con la caché de compilación vacía
curso-docker-revisor:1.0.0 (Dockerfile.leccion3 --no-cache): 9909 ms
paso 1 DONE 0.0s FROM docker.io/library/golang:1.27.3-trixie@sha256:604106102
paso 2 CACHED WORKDIR /src
paso 3 DONE 0.0s COPY . .
paso 4 DONE 7.1s RUN go build -o /usr/local/bin/revisor ./cmd/revisor
curso-docker-revisor:1.1.0 (Dockerfile --no-cache): 12207 ms
paso 1 DONE 0.0s FROM docker.io/library/golang:1.27.3-trixie@sha256:604106102
paso 2 CACHED WORKDIR /src
paso 3 DONE 0.0s COPY go.mod ./
paso 4 DONE 0.0s COPY cmd/ cmd/
paso 5 DONE 0.0s COPY internal/ internal/
paso 6 DONE 10.5s RUN go build -o /usr/local/bin/revisor ./cmd/revisor
## 2. Otra vez, sin cambiar nada
curso-docker-revisor:1.0.0 (Dockerfile.leccion3): 1176 ms
paso 1 DONE 0.0s FROM docker.io/library/golang:1.27.3-trixie@sha256:604106102
paso 2 CACHED WORKDIR /src
paso 3 CACHED COPY . .
paso 4 CACHED RUN go build -o /usr/local/bin/revisor ./cmd/revisor
curso-docker-revisor:1.1.0 (Dockerfile): 628 ms
paso 1 DONE 0.0s FROM docker.io/library/golang:1.27.3-trixie@sha256:604106102
paso 2 CACHED WORKDIR /src
paso 3 CACHED COPY go.mod ./
paso 4 CACHED COPY cmd/ cmd/
paso 5 CACHED COPY internal/ internal/
paso 6 CACHED RUN go build -o /usr/local/bin/revisor ./cmd/revisor
Времената се променят при всяко изпълнение; последователността от CACHED и DONE е стабилна. (WORKDIR /src излиза CACHED дори с --no-cache. Това е подробност от начина, по който BuildKit третира тази стъпка, която не добавя файлове от контекста, нито изпълнява нещо, и не променя важното: COPY и RUN наистина бяха направени наново.)
На студено двата компилират всичко от нулата. Времената от това изпълнение (9.9 и 12.2 s) зависят най-вече от шума на машината: при предишни изпълнения на същия скрипт, със същите изграждания и повече или по-малко заета машина, се получиха 12.5 срещу 7.1 s, 18.6 срещу 10.5 s и 20.2 срещу 23.0 s, като понякога печели единият, а понякога другият. Стабилен е порядъкът: от десет до двайсет секунди, цената на компилирането на всичко. Тази цена не я премахва никакъв ред: първото изграждане винаги се плаща.
Без промени двата идват изцяло от кеша за по-малко от секунда. Тук също няма разлика: кешът работи еднакво добре и с двата Dockerfile, когато нищо не се е променило.
Разликата се появява в раздел 3, с промяна, който видя в 4.2.2 и 4.3.3: 13.0 s срещу 1.8 s, а RUN — 10.1 s срещу 0.8 s. И точно този случай е важен, защото е ежедневният: променяш нещо и изграждаш отново.
Раздел 4 завършва със случай, който води към следващото:
## 4. Cambio un archivo que el .dockerignore deja fuera
curso-docker-revisor:1.1.0 (Dockerfile): 790 ms
paso 1 DONE 0.0s FROM docker.io/library/golang:1.27.3-trixie@sha256:604106102
paso 2 CACHED WORKDIR /src
paso 3 CACHED COPY go.mod ./
paso 4 CACHED COPY cmd/ cmd/
paso 5 CACHED COPY internal/ internal/
paso 6 CACHED RUN go build -o /usr/local/bin/revisor ./cmd/revisor
В папката беше добавен NOTAS.md и всичко дойде от кеша. Тук действат две бариери едновременно: .dockerignore го остави отвън (4.6), така че файлът дори не стигна до builder-а, и освен това никой селективен COPY в новия Dockerfile не го иска. Каквото не пътува или не се иска, не може да обезсили нищо.
4.6 Какво пътува в контекста
4.6.1 BuildKit иска само онова, което му трябва
В Урок 3 видя, че контекстът е папката, която даваш на docker build, и че .dockerignore казва какво не пътува. Липсва още едно парче: BuildKit не изпраща целия контекст. Сред подобренията спрямо предишния builder, които документацията му изброява, има две, които ще видиш веднага: „инкрементално прехвърляне само на файловете в контекста, които са се променили между изгражданията“ и „откриване и пропускане на прехвърлянето на неизползвани файлове в контекста“ (Docker, „BuildKit“).
fig04_04.sh го измерва. Засява в папката на проекта резервно копие от 50 MB и симулирана папка .git от 10 MB и изгражда три пъти:
#!/usr/bin/env bash
# fig04_04.sh — Qué viaja en el contexto. Siembra en la carpeta un respaldo de
# 50 MB y una carpeta .git falsa de 10 MB, y construye tres veces:
# 1) Dockerfile de esta lección (COPY selectivos), con .dockerignore;
# 2) Dockerfile de la Lección 3 (COPY . .), con .dockerignore;
# 3) Dockerfile de la Lección 3, SIN .dockerignore: falla a propósito.
# Borra al salir lo que crea.
set -uo pipefail
PREFIJO="${PREFIJO:-curso-docker-}"
AQUI="$(cd "$(dirname "$0")" && pwd)"
TRABAJO="$(mktemp -d)"
# La caché de compilación de ESTE guion, con un id propio: así nunca toca la
# caché curso-docker-go-build de tu proyecto ni la de otro proyecto.
CACHE_ID="${PREFIJO}fig04-go-build"
L4="${PREFIJO}revisor:1.1.0"
CON="${PREFIJO}revisor:con-ignore"
SIN="${PREFIJO}revisor:sin-ignore"
cp -R "$AQUI/../revisor" "$TRABAJO/revisor"
cp "$AQUI/estado/.dockerignore" "$AQUI/estado/Dockerfile.leccion3" "$TRABAJO/revisor/"
sed "s/id=curso-docker-go-build/id=${CACHE_ID}/" "$AQUI/estado/Dockerfile" > "$TRABAJO/revisor/Dockerfile"
cd "$TRABAJO/revisor"
titulo() { printf '\n## %s\n' "$*"; }
muestra() {
printf '$ %s\n' "$*"
local rc=0
eval "$@" 2>&1 || rc=$?
[ "$rc" -ne 0 ] && echo "[código de salida: $rc]"
return 0
}
# borrar_cache: borra SOLO la caché de compilación con este id exacto. El id
# va entre [^a-z0-9-] para que no atrape otro id que lo contenga.
borrar_cache() {
docker buildx prune -f --filter type=exec.cachemount \
--filter "description~=[^a-z0-9-]${CACHE_ID}[^a-z0-9-]" >/dev/null 2>&1 || true
}
trap 'docker rmi -f "$L4" "$CON" "$SIN" >/dev/null 2>&1; borrar_cache; rm -rf "$TRABAJO"' EXIT
titulo "La carpeta: el código, un respaldo de 50 MB y un .git falso de 10 MB"
head -c 52428800 /dev/urandom > respaldo.tar
mkdir -p .git && head -c 10485760 /dev/urandom > .git/objeto-falso
muestra "for f in respaldo.tar .git cmd internal go.mod .; do du -sh --apparent-size \$f; done"
titulo "1. Dockerfile de la Lección 4 (COPY selectivos), con .dockerignore"
muestra "docker build --progress=plain -t $L4 . > ../construccion.log 2>&1; echo \"construcción: código \$?\"; grep 'transferring context' ../construccion.log | tail -1"
titulo "2. Dockerfile de la Lección 3 (COPY . .), con .dockerignore"
muestra "docker build --progress=plain -t $CON -f Dockerfile.leccion3 . > ../construccion.log 2>&1; echo \"construcción: código \$?\"; grep 'transferring context' ../construccion.log | tail -1"
titulo "3. Dockerfile de la Lección 3, SIN .dockerignore"
mv .dockerignore dockerignore.apartado
muestra "docker build --progress=plain -t $SIN -f Dockerfile.leccion3 . > ../construccion.log 2>&1; echo \"construcción: código \$?\"; grep 'transferring context' ../construccion.log | tail -1"
grep -E -A4 '^#[0-9]+ ERROR|error obtaining' ../construccion.log | head -8
mv dockerignore.apartado .dockerignore
head -c 52428800 /dev/urandom записва 50 MB случайни байтове: файл, който тежи колкото истинско резервно копие и не може да се компресира. du -sh --apparent-size измерва размера на всяко нещо. От всяко изграждане скриптът запазва записа и търси реда transferring context, който казва колко е изпратил клиентът на builder-а. Скриптът е с set -uo pipefail, без -e, защото третото изграждане се проваля нарочно и скриптът трябва да продължи. Трите изграждания използват три различни тага (1.1.0, con-ignore и sin-ignore), за да се разчита в изхода кое какво е.
$ PREFIJO=curso-docker-l04- ./fig04_04.sh 2>&1 | sed 's/curso-docker-l04-/curso-docker-/g'
## La carpeta: el código, un respaldo de 50 MB y un .git falso de 10 MB
$ for f in respaldo.tar .git cmd internal go.mod .; do du -sh --apparent-size $f; done
50M respaldo.tar
10M .git
15K cmd
27K internal
41 go.mod
61M .
## 1. Dockerfile de la Lección 4 (COPY selectivos), con .dockerignore
$ docker build --progress=plain -t curso-docker-revisor:1.1.0 . > ../construccion.log 2>&1; echo "construcción: código $?"; grep 'transferring context' ../construccion.log | tail -1
construcción: código 0
#7 transferring context: 42.80kB done
## 2. Dockerfile de la Lección 3 (COPY . .), con .dockerignore
$ docker build --progress=plain -t curso-docker-revisor:con-ignore -f Dockerfile.leccion3 . > ../construccion.log 2>&1; echo "construcción: código $?"; grep 'transferring context' ../construccion.log | tail -1
construcción: código 0
#7 transferring context: 1.00kB done
Номерата на стъпките (#7) и времената може да се променят; прехвърлените размери са стабилни, ако пуснеш скрипта точно както е.
Папката тежи 61 MB, а кодът — около 42 kB. При първото изграждане пътуваха 42.80 kB: кодът и нищо друго. При второто пътува 1.00 kB, макар че Dockerfile от Урок 3 иска всичко с COPY . .. Двете изречения от документацията обясняват двете числа:
- „Пропускане на неизползваното“: при първото резервното копие и
.gitне пътуваха по две причини едновременно..dockerignoreги изключва, а освен товаCOPYискат самоgo.mod,cmd/иinternal/. - „Само онова, което се е променило“: при второто кодът вече беше от страната на builder-а от първото изграждане и не се беше променил. Изпратени бяха само разлики. Затова числото в реда
transferring contextзависи от онова, което си изградил преди в същата папка: първия път се изпраща кодът; следващите — почти нищо.
4.6.2 Без .dockerignore: грешката от урока
Третото изграждане е с Dockerfile от Урок 3, с COPY . ., без .dockerignore:
## 3. Dockerfile de la Lección 3, SIN .dockerignore
$ docker build --progress=plain -t curso-docker-revisor:sin-ignore -f Dockerfile.leccion3 . > ../construccion.log 2>&1; echo "construcción: código $?"; grep 'transferring context' ../construccion.log | tail -1
construcción: código 1
#6 transferring context: 62.93MB 0.1s done
#10 0.344 error obtaining VCS status: exit status 128
#10 0.344 Use -buildvcs=false to disable VCS stamping.
#10 ERROR: process "/bin/sh -c go build -o /usr/local/bin/revisor ./cmd/revisor" did not complete successfully: exit code: 1
------
> [4/4] RUN go build -o /usr/local/bin/revisor ./cmd/revisor:
0.344 error obtaining VCS status: exit status 128
0.344 Use -buildvcs=false to disable VCS stamping.
------
Номерата на стъпките и времената (0.344, 0.1s) се променят; останалото е стабилно.
Случиха се две неща. Първото, което очакваше: пътуваха 62.93 MB — резервното копие, .git и кодът. Това са 62-та MB от сцената: тези на папка без .dockerignore, с резервно копие и историята на Git. На твоя компютър това е секунда; на сървър за изграждане, който изпраща контекста по мрежата при всяко изграждане, се усеща. А онова, което пътува с COPY . ., попада в образа, като .env от Урок 3.
Второто е изненадата: изграждането се провали, и то не заради Docker. .git стигна до /src в образа и Go, докато компилираше, го видя. По подразбиране Go се опитва да запише в изпълнимия файл информацията от системата за контрол на версиите (кой commit, дали има промени, които още не са в commit) и го прави, когато основният пакет, неговият модул и текущата папка са в едно и също хранилище (документация на go, опция -buildvcs). За целта извика инструмента за контрол на версиите и понеже .git беше симулиран, инструментът се провали: exit status 128. Go спря и предложи -buildvcs=false.
Тази грешка ще я видиш в реалния живот с истински, но непълен .git, или в образ, в който инструментът git не може да прочете хранилището. А с пълно и изправно хранилище не се проваля, но прави нещо по-лошо, без да предупреди: записва данни от хранилището ти в изпълнимия файл и съдържанието на образа зависи от това дали в папката ти е имало промени, които още не са в commit.
4.6.3 .dockerignore от урока
Поправката не е -buildvcs=false. Тя е .git да не пътува. Ето .dockerignore, с който завършва урокът; той заменя този от Урок 3:
# fig04_02.dockerignore
# Lo que nunca viaja al daemon al construir.
.git
.env
*.tar
*.md
Dockerfile*
.dockerignore
compose.yaml
secretos/
Петте реда от Урок 3 остават, всеки със своята причина. Добавят се три:
*.md: бележките,README.md, дневникът.revisorне се нуждае от нито един файл на Markdown, за да се компилира, а в раздел 4 отfig04_03.shвече видя, че с този ред редактирането на бележките ти не обезсилва нищо.compose.yaml: файлът, който описва цялата система и който ще напишеш в Урок 9. Той описва как се стартира образът; няма работа вътре в него.secretos/: папката, в която от Урок 6 нататък ще живеят файловете със симулирани токени. Изключва се още от днес, преди да съществува, по същата причина, поради която в Урок 3.envбеше изключен, преди да има такъв: в деня, в който някой създаде тази папка,.dockerignoreвече ще я чака.
С този файл и селективните COPY между папката ти и образа има две бариери: онова, което .dockerignore пуска да пътува, и от него — само онова, което някой COPY иска.
4.7 Проверка преди изграждане
Навикът от Урок 3 не се губи, защото Dockerfile се е променил. fig04_06.sh проверява новия Dockerfile с docker build --check, който пуска проверките, без да изгражда нищо (Docker, „Build checks“):
#!/usr/bin/env bash
# fig04_06.sh — Revisar el Dockerfile de la lección antes de construir, como en
# la Lección 3. No construye nada ni crea contenedores.
set -euo pipefail
AQUI="$(cd "$(dirname "$0")" && pwd)"
TRABAJO="$(mktemp -d)"
trap 'rm -rf "$TRABAJO"' EXIT
cp -R "$AQUI/../revisor" "$TRABAJO/revisor"
cp "$AQUI/estado/Dockerfile" "$AQUI/estado/.dockerignore" "$TRABAJO/revisor/"
cd "$TRABAJO/revisor"
docker build --check . 2>&1 | tail -1
$ ./fig04_06.sh
Check complete, no warnings found.
Този изход е стабилен. tail -1 оставя само присъдата; без него би видял същите стъпки [internal] като в Урок 3. RUN --mount не пречи на проверката: това е действащ синтаксис на Dockerfile, а редът # syntax=docker/dockerfile:1 в началото казва на BuildKit коя версия на синтаксиса на Dockerfile да използва, за да го прочете. Това предполага, че BuildKit е активен, което е нормалният случай в Docker Engine 29; при наследения builder (legacy builder), който не използва BuildKit, RUN --mount не се разбира. Проверките на --check съществуват от версия 1.8 на синтаксиса на Dockerfile и изискват Buildx 0.15 или по-нов (Docker, „Build checks“); с по-стари инструменти командата не ги разпознава.
4.8 Какво ще видиш в други проекти
COPY go.mod go.sum ./, последвано отRUN go mod download. Това е най-известният трик за проекти на Go със зависимости: изтеглянето им в слой, който се прави наново само когато се промени списъкът. Добра идея е, когато има зависимости.revisorняма нито една (затова нямаgo.sum), така че този слой не би имал какво да изтегли. Ако пакетираш проект със зависимости, тази двойка редове отива точно там, където тук еCOPY go.mod ./, а ръководството на Docker използва и второ монтиране на кеш,target=/go/pkg/mod, за изтеглените модули (Docker, „Optimize cache usage“).- Всичко в един гигантски
RUNс&&. Има смисъл при инсталиране на пакети и почистване в същия слой (спомни си Инженерна бележка 4.1: каквото се изтрие в друг слой, продължава да тежи). Няма смисъл за смесване на стъпки, които се променят с различно темпо. DOCKER_BUILDKIT=0. Включва builder-а отпреди BuildKit (legacy builder), който няма монтирания на кеш, нито инкременталните прехвърляния от 4.6. BuildKit е builder-ът по подразбиране на Docker Engine (Docker, „BuildKit“); ако някое ръководство ти казва да го изключиш, не се доверявай на ръководството. Не го бъркай сdocker builder, който е само групата команди за управление на изгражданията, катоdocker builder prune(docker builder).docker system prune -aилиdocker builder prune -a„за освобождаване на място“. Изтриват кеша на всички проекти на машината. Вече знаеш защо не (4.4.5).
Грешката, която ще видиш
Този урок има една основна грешка и един симптом, който не дава грешка, но струва време.
1. .git, който чупи компилацията.
#10 0.344 error obtaining VCS status: exit status 128
#10 0.344 Use -buildvcs=false to disable VCS stamping.
#10 ERROR: process "/bin/sh -c go build -o /usr/local/bin/revisor ./cmd/revisor" did not complete successfully: exit code: 1
Чете се отгоре надолу: Go се опита да получи състоянието на системата за контрол на версиите (VCS status), инструментът се провали с код 128 и Go ти предлага да не записваш тази информация. Последният ред е на Docker: RUN завърши с код 1 и изграждането спря. Причината почти никога не е в кода ти: тя е, че .git е стигнал до образа. Провери в този ред: в корена на контекста ли е .dockerignore, до Dockerfile? Пише ли в него .git? Има ли COPY . . в Dockerfile? Поправката е .git да не пътува. -buildvcs=false също премахва грешката, но оставя .git да пътува и да влиза в образа: това е да изключиш алармата, без да изгасиш пожара.
2. Всичко се изгражда наново, макар да си променил малко. Няма съобщение за грешка; има само чакане. Диагностицира се с --progress=plain: търси първата стъпка след FROM, която казва DONE вместо CACHED. Това е стъпката, чието съдържание се е променило, а всичко отдолу се е повторило по правило 3. Ако тази първа стъпка е COPY . ., вече знаеш поправката (4.3). Ако е COPY на файл, който не си пипал, потърси какво друго се е променило в този път: генериран файл, бележки, резервно копие. Онова, което не бива да обезсилва кеша, не бива и да пътува: то отива в .dockerignore.
Навикът за сигурност от този урок
Контекстът на изграждане не съдържа .git, .env, резервни копия, нито тайни, а проверката на Dockerfile се пуска винаги. Навикът предотвратява две злополуки. Първата е тази от 4.6: .git, .env или резервно копие, които пътуват до сървър за изграждане и, с COPY . ., остават в образ, който после се публикува. .git освен това може да пази тайни, които някой е изтрил от кода, но не и от историята. Втората е по-тиха: кешът на компилацията с id на друг проект или широко изтриване, което отнася кеша на всички. Собствен id и изтриване с пълния id държат всеки проект в собствената му лента.
И един бонус за сигурността, който дава монтирането на кеш: онова, което компилаторът оставя в кеша си, не остава в образа. По-малко файлове вътре означава по-малка повърхност за атака и по-малко неща за обясняване в деня, в който някой провери какво съдържа образът ти.
Често допускани грешки
COPY . . и после компилиране. Всяка промяна, дори да е коментар или бележка, повтаря цялата компилация. Копирай първо онова, което се променя рядко, а накрая — онова, което се променя често.
Да разчиташ, че кешът „ще разбере“ какво се е променило. Кешът е верига: къса се на първото звено, което се промени, и всичко отдолу се повтаря. Редът в Dockerfile е твоят инструмент това звено да е възможно най-долу.
Изтриване на кеша на компилацията с rm -rf в края на RUN. Смалява слоя, но изхвърля и онова, което ускорява следващото изграждане. Монтирането на кеш дава и двете неща: малкия слой и живия кеш.
Използване на -buildvcs=false, за да се махне грешката от .git. Маха симптома и оставя .git вътре в образа.
Измерване само на общото време. Число без таблицата със стъпките не казва какво е направено наново. --progress=plain го казва.
Използване на id по подразбиране или на общ id за монтирането на кеш. Различни проекти накрая споделят — и си презаписват — един и същ кеш.
Почистване с prune -a или с широк филтър. Отнася кеша на други проекти на същата машина. Изтрива се с типа и пълния id.
Махане на редове от .dockerignore, защото „със селективни COPY вече не е нужно“. Нужно е в деня, в който някой отново напише COPY . ..
Упражнения
Прави ги подред и записвай прогнозата си в дневника, преди да пуснеш всяко. Накрая изтрий онова, което си създал: всичко носи префикса curso-docker-, а кешът на компилацията от упражненията се изтрива с командата от 4.4.5 и собствения му id.
Упражнение 1 — Предвиди кеша
С Dockerfile от урока и предишно изграждане в кеша предвиди за всяка промяна кои стъпки ще кажат CACHED и кои DONE. Направи го с молив, като използваш трите правила от 4.2.1, а после го провери с fig04_05.sh, който прилага четирите промени една след друга:
a) Променяш само датата на internal/reporte/tabla.go с touch, без да пипаш съдържанието му.
b) Добавяш коментар в края на cmd/revisor/main.go.
c) Добавяш коментар в края на go.mod.
d) Добавяш коментар (# ...) в края на Dockerfile.
И още един въпрос: в случай c очакваш ли RUN go build да отнеме много, или малко време? Защо?
Упражнение 2 — .dockerignore като единствена бариера (проект)
Копирай programas/revisor/ в своя папка и ѝ сложи .dockerignore от урока, а като Dockerfile.leccion3 — Dockerfile от Урок 3, онзи с COPY . .. Нарочно: с COPY . . .dockerignore е единствената бариера. Изгради веднъж curso-docker-revisor:1.0.0-ej2 с -f Dockerfile.leccion3 --progress=plain и запиши реда transferring context. После създай в папката README.md, папка secretos/ със симулиран файл token.txt и файл datos.json от 5 MB (head -c 5242880 /dev/urandom > datos.json). Преди да изградиш отново, предвиди кои от трите ще пътуват. Изгради отново и погледни реда пак. После добави в .dockerignore каквото е нужно и изгради за трети път. Упражнението се самопроверява: готов си, когато прехвърленият контекст отново е от няколко килобайта и docker run --rm curso-docker-revisor:1.0.0-ej2 ls -A /src показва само cmd, go.mod и internal. Накрая docker rmi curso-docker-revisor:1.0.0-ej2 и изтрий папката.
Упражнение 3 — Поправи реда
Един колега поиска да пренареди Dockerfile-а на revisor, за да се възползва от кеша, и го остави така. Има проблеми с реда, а един от тях е сериозен:
# syntax=docker/dockerfile:1
FROM golang:1.27.3-trixie
WORKDIR /src
COPY internal/ internal/
COPY cmd/ cmd/
RUN go build -o /usr/local/bin/revisor ./cmd/revisor
COPY go.mod ./
USER 10001:10001
EXPOSE 8080
CMD ["revisor"]
- Преди да изградиш: това изграждане работи ли, или се проваля? Помисли от какво се нуждае
go build, за да компилира модул. - Открий проблемите с реда и с кеша и напиши поправената версия.
- Изгради своята версия два пъти, втория път след промяна на коментар в
internal/reporte/tabla.go, и провери с--progress=plain, чеCOPY go.mod ./иCOPY cmd/ cmd/казватCACHED.
Упражнение 4 — Обясни го на колежката си
Колежката ти от сцената те пита: „Щом сега RUN go build казва DONE всеки път, когато променя кода, какво печелим с --mount? Пак се повтаря.“ Отговори ѝ в най-много пет изречения, като използваш думите кеш на слоевете, монтиране на кеш, builder и образ.
Решения
Решение 1
Реалният изход на fig04_05.sh:
$ PREFIJO=curso-docker-l04- ./fig04_05.sh 2>&1 | sed 's/curso-docker-l04-/curso-docker-/g'
## a) Solo cambia la fecha de un archivo (touch), no su contenido
paso 1 DONE 0.0s FROM docker.io/library/golang:1.27.3-trixie@sha256:604106102
paso 2 CACHED WORKDIR /src
paso 3 CACHED COPY go.mod ./
paso 4 CACHED COPY cmd/ cmd/
paso 5 CACHED COPY internal/ internal/
paso 6 CACHED RUN go build -o /usr/local/bin/revisor ./cmd/revisor
## b) Un comentario nuevo en cmd/revisor/main.go
paso 1 DONE 0.0s FROM docker.io/library/golang:1.27.3-trixie@sha256:604106102
paso 2 CACHED WORKDIR /src
paso 3 CACHED COPY go.mod ./
paso 4 DONE 0.0s COPY cmd/ cmd/
paso 5 DONE 0.0s COPY internal/ internal/
paso 6 DONE 0.4s RUN go build -o /usr/local/bin/revisor ./cmd/revisor
## c) Un comentario nuevo en go.mod
paso 1 DONE 0.0s FROM docker.io/library/golang:1.27.3-trixie@sha256:604106102
paso 2 CACHED WORKDIR /src
paso 3 DONE 0.0s COPY go.mod ./
paso 4 DONE 0.0s COPY cmd/ cmd/
paso 5 DONE 0.0s COPY internal/ internal/
paso 6 DONE 0.3s RUN go build -o /usr/local/bin/revisor ./cmd/revisor
## d) Un comentario nuevo en el Dockerfile
paso 1 DONE 0.0s FROM docker.io/library/golang:1.27.3-trixie@sha256:604106102
paso 2 CACHED WORKDIR /src
paso 3 CACHED COPY go.mod ./
paso 4 CACHED COPY cmd/ cmd/
paso 5 CACHED COPY internal/ internal/
paso 6 CACHED RUN go build -o /usr/local/bin/revisor ./cmd/revisor
a) Всичко CACHED. Датата на модификация не се брои за кеша (правило 1); само съдържанието.
b) От COPY cmd/ надолу. cmd/ се е променил; internal/ — не, но идва след него, така че се повтаря по правило 3 (затова казва DONE, макар да няма нищо различно); а RUN — също.
c) От COPY go.mod надолу: всичко. go.mod е първият COPY, така че промяна там обезсилва цялата верига. Затова първо отива само онова, което почти никога не се променя.
d) Всичко CACHED. Кешът се изчислява по инструкция, а коментарът не е инструкция: никоя стъпка не се е променила. (Промяна в инструкция обаче обезсилва тази стъпка и долните.)
Допълнителният въпрос: в случай c RUN се повтаря, но отнема малко (0.3 s в това изпълнение), защото монтирането на кеш вече съдържа всички компилирани пакети, а коментар в go.mod не променя онова, което Go трябва да компилира. Кешът на слоевете каза „повтори стъпката“; кешът на Go каза „но почти няма какво да се прави“. Ако твоите времена излязат различни, това е наред; онова, което трябва да съвпада, е колоната с CACHED и DONE.
Решение 2
При второто изграждане пътува datos.json и редът transferring context го издава: минава от килобайти на около 5 MB. README.md не пътува (изключва го *.md), нито secretos/ (има си свой ред). С COPY . . datos.json освен това влиза в образа: ls -A /src го показва. Поправката е още един ред в .dockerignore: datos.json, или *.json, ако revisor не се нуждае от никакъв JSON, за да се компилира, което е точно така. С това третото изграждане отново прехвърля няколко килобайта, а в /src остават cmd, go.mod и internal. Ако числото не спада, провери дали .dockerignore е в корена на контекста, до Dockerfile файловете.
Обърни внимание, че с Dockerfile от урока, този със селективните COPY, datos.json нямаше да пътува дори без този ред, защото никой COPY не го иска. Това е Инженерна бележка 4.4 в действие: с COPY . . .dockerignore е единствената бариера; със селективни COPY е втората. Пиши го въпреки това.
Решение 3
- Проваля се.
go buildсе нуждае отgo.mod, за да знае кой модул компилира, а в този Dockerfilego.modпристига следRUN. Компилирането безgo.modдава тази грешка, измерена в образаgolang:1.27.3-trixieс минимален пакетmainвcmd/revisor:go: go.mod file not found in current directory or any parent directory; see 'go help modules'. В изграждането ще я видиш във формата наRUN, който се проваля, както в 4.6.2. Това съобщение се получава отfig04_07.sh, който компилира същия минимален пакет безgo.modв образа на Go и оставя изхода си до себе си, вfig04_07.salida.txt. - Проблемите:
go.modе следRUN(грешка), а трябва да е първи (кеш);internal/е предиcmd/, обратно на реда от урока (по-малко сериозно, но онова, което се променя повече, трябва да е отдолу); аRUNняма монтиране на кеш, така че всяка промяна компилира от нулата. Поправената версия еfig04_01.Dockerfile. - С поправената версия, след промяната в
tabla.go, таблицата казваCACHEDприWORKDIR,COPY go.mod ./иCOPY cmd/ cmd/, иDONEприCOPY internal/ internal/и приRUN, както в 4.3.3.
Решение 4
Един възможен отговор: „Кешът на слоевете решава дали дадена стъпка се повтаря, и понеже кодът се е променил, RUN go build се повтаря: това нищо не го предотвратява. Онова, което прави монтирането на кеш, е, че когато стъпката се повтаря, Go намира там всичко, което е компилирал предишния път, и компилира наново само пакета, който си пипнала. Този кеш се пази от builder-а, отделно, и не влиза в образа. Затова RUN спадна от 10.1 на 0.8 секунди, а слоят на компилацията тежи със 100 MB по-малко. Повтаря се, да, но почти безплатно.“
Как да разбера дали се справих
Отбележи всяка точка само ако си я проверил на своя компютър:
-
docker build --check .с Dockerfile от урока завършва сCheck complete, no warnings found. -
docker history curso-docker-revisor:1.1.0показва три отделни слояCOPYиRUNот около 10 MB. -
docker image ls curso-docker-revisorпоказва1.1.0по-лек от1.0.0(при тези изпълнения 1.32 GB срещу 1.45 GB). - След промяна на коментар в
internal/--progress=plainпоказваCACHEDприCOPY go.mod ./и приCOPY cmd/ cmd/, аRUNотнема по-малко от няколко секунди. - Добавянето или редактирането на файл
.mdв папката не обезсилва нито една стъпка. - С голям файл в папката редът
transferring contextостава в килобайти. - Умееш да разчетеш грешката
error obtaining VCS statusи я поправяш с.dockerignore, а не с-buildvcs=false. - Изтри кеша си на компилацията с
docker buildx prune, като филтрира поtype=exec.cachemountи по пълнияid, аdocker buildx du --filter 'description~=curso-docker-go-build'вече не го показва. - Накрая
docker ps -a --filter name=curso-docker-не показва нищо, а от образите наrevisorостава само онзи, който искаш да запазиш.
Обобщение
Отговори, без да гледаш урока:
- Какво е слой? Кои инструкции добавят файлове и кои само данни? С каква команда ги виждаш?
- Какво гледа кешът, за да реши дали
COPYсе повтаря? А заRUN? Брои ли се датата на файла? - Какво става със стъпките отдолу, когато една бъде обезсилена? Какво следствие има това за реда в Dockerfile?
- Какво прави
RUN --mount=type=cache? Къде живее този кеш и защо не остава в образа? - За какво служи
idна монтирането на кеш и защо е опасенpruneс широк филтър? - Какво означават
CACHEDиDONEв--progress=plain? Кога виждашplain, дори да не си го поискал? - Кои файлове изпраща клиентът на builder-а? Защо второто изграждане прехвърля по-малко от първото?
- Какво причинява грешката
error obtaining VCS statusи коя е правилната поправка?
Източници
Всички са посетени на 21 октомври 2026 г.
- Docker, „Docker build cache“: по един слой на инструкция и обезсилването по веригата.
- Docker, „Build cache invalidation“: правилата за
COPY,ADDиRUNи това, че датата на модификация не се брои. - Docker, „Optimize cache usage“: редът на слоевете, малкият контекст, монтиранията на кеш и примерът им за Go.
- Справочник на Dockerfile,
RUN --mount=type=cache: опциитеid,target,sharingи останалите, иidпо подразбиране. - Docker, „Understanding the image layers“: какво е слой и защо се използва повторно.
docker image history: командата,--formatи--no-trunc.docker buildx build, опция--progress: режимитеauto,tty,plain,quietиrawjson.docker buildx prune: филтритеuntil,id,descriptionиtype, с типаexec.cachemount.- Docker, „BuildKit“: инкременталното прехвърляне на контекста и пропускането на неизползваните файлове.
docker builder: групата команди за управление на изгражданията.- Docker, „containerd image store“: че хранилището пази образите компресирани и некомпресирани.
- Docker, „Build checks“:
docker build --check. - Документация на командата
go: кешът на компилацията („Build and test caching“) и опцията-buildvcs. - Docker Engine 29, бележки към версиите: версиите от „За изпълненията“.
- Colima: инструментът, с който бяха изпълнени програмите в macOS.
Термини от този урок
| кеш на изграждането (build cache) | резултатите от предишни стъпки, които builder-ът пази, за да не ги повтаря |
| кеш на слоевете | частта от кеша, която решава дали цяла стъпка се повтаря, или идва вече готова |
| слой (layer) | наборът от промени във файловете, който оставя една стъпка от Dockerfile |
CACHED |
в --progress=plain: стъпката е дошла от кеша |
| контекст на изграждане | файловете, които изграждането може да използва; BuildKit прехвърля само поисканите и променените |
DONE |
в --progress=plain: стъпката е направена наистина, с времето си |
| обезсилване | когато дадена стъпка вече не може да дойде от кеша, защото нещо от онова, което използва, се е променило |
| монтиране на кеш (cache mount) | папка, която builder-ът пази отделно и монтира в RUN; съдържанието ѝ не остава в образа |
--progress=plain |
форматът с обикновен текст на изграждането, стъпка по стъпка |
| VCS (version control system) | системата за контрол на версиите, като Git |
Предпочитате имейл? Пишете ни на hola@habil.mx