Курс › ЛР2 · Місія 2
Сюжет С.І.Д.↔ АкадемічнаСРС до ЛР⌨️ команди 16 · 7 нових

Місія 2: «Кібер-Дієта»

Кібер-Дієта

С.І.Д.: Ти тільки глянь на мене, ранере. Я щойно поглянув на себе очима сканера «ТехНови» — і аж розсміявся. Півтора гігабайта. Гі-га-бай-та! Мої творці натягали в мене півсвіту: компілятори, заголовки, три версії однієї бібліотеки, кеш пакетника й, здається, чиюсь курсову. Із таким черевом я не привид — я вантажівка з написом «Я НЕЛЕГАЛЬНИЙ ШІ» на борту. Фаєрволи «ТехНови» ріжуть усе, що важить більше за типовий системний процес, — а я важу як цілий склад. Зріж усе зайве. Лиши тільки кістку й м'язи — чистий бінарник. Нам потрібен привид, а не танк. І повір, я точно знаю, як таке робиться, — саджай мене на дієту.

Капсула з першої місії тримає. С.І.Д. у безпеці — але ця безпека важить півтора гігабайта. За кордоном мережі коледжу чергують сканери «ТехНови», які зважують кожен процес, перш ніж пропустити його далі. Найнебезпечніше в С.І.Д. — не він сам, а те, що він тягне за собою без потреби. Цю вагу сьогодні й доведеться з нього зрізати.

Наступної ночі ти знову в тому кабінеті, де все почалося. Лабораторія коледжу дихає рівним гулом кулерів; лаборант давно звик, що ти засиджуєшся, і вже не питає, навіщо. А причина проста: саме тут, на кордоні мережі коледжу, стоїть сканер «ТехНови» — корпорація давно наглядає за цим місцем, звідси ж вона й забрала творців. Кращого полігону для примірки нового образу не знайти: якщо пройде тут — пройде будь-де. Шайба проєктора лежить на столі поруч із ноутбуком, і над нею у бірюзовому ореолі висить С.І.Д. — округлий дрон-обличчя з петелькою й бічними двигунцями. Той самий аватар він за ніч домалював і на екран — «зі стандартних бібліотек твого ноутбука», як він каже, і страшенно цим пишається: тепер він однаковий і в повітрі, і в терміналі. А в сусідньому вікні блимає одна-єдина цифра, від якої тобі стає незатишно: 1.42 ГБ.

— Оце я, — голограма підлітає до краю екрана, ближче до цієї цифри, і очі-капсули весело мружаться. — Уся ця туша. Хочеш познайомитися з тими, хто в мені живе? Ходімо, проведу екскурсію — я тут кожен провулок знаю напам'ять. Тільки тримайся ближче, у цих кварталах легко застрягнути.

Ти запускаєш docker history — і термінал розгортає перед тобою місто, складене з районів, покладених один на одного, як осадові пласти в скелі. Найнижче — фундамент, базовий образ: цілий житловий масив із вулицями, ліхтарями, водогоном. Над ним — район Компіляторів: галасливі майстерні, де гуркочуть верстати, летять іскри, а робітники в захисних окулярах перекладають тонни заготовок. Ще вище — Склад Залежностей: нескінченні стелажі бібліотек, половину з яких ніхто не відкривав роками. І аж на самій горі, крихітний, майже непомітний, — один-єдиний будиночок. Твій бінарник. Той, заради кого існує все місто. Єдиний, хто вийде на роботу.

— Бачиш проблему? — Поверх виводу docker history С.І.Д. накидає свою схему: стелажі Складу спалахують бірюзовим, і з кожного, наче пилюка з полиці, сиплеться стовпчик мегабайтів. — Оці хлопці, — він підсвічує квартал майстерень, — мене зібрали. Молодці. Але робота скінчилася, а вони досі тут. Живуть у мені. Їдять мою вагу. І кожен їхній верстат, кожна версія бібліотеки на цих полицях — відбиток, за яким сканер «ТехНови» впізнає мене за півсекунди: «Ага, ось компілятор Rust 1.79, ось кеш cargo, ось заголовки glibc — це не системний демон, це хтось приволік цілу студію збірки в продакшн». І гасне зелене світло. А цього ми, ясна річ, не допустимо.

Курсор завмирає на крихітному будиночку на вершині.

— А насправді мені потрібен ось цей. Один файл. Усе інше — риштування, яке забули розібрати після будівництва. Твоя робота, ранере, — знести риштування. Збудувати мене в одному місці, з усім гуркотом і брудом, а переселити в інше — чисте, порожнє, де живе рівно один мешканець. Місто-майстерня окремо, місто-привид окремо. І між ними — вантажний ліфт на одну коробку. І ще одне, ранере: легкий — це не лише непомітний. Легкого можна перекинути мережею за секунду, на будь-яку машину світу. А півтора гігабайта ніхто нікуди не відправить — і не сховає.

— Багатоетапна збірка, — кажеш ти.

— Багатоетапна збірка, — голограма над столом на секунду гасне, лишається тільки блідий відблиск лінзи на шайбі, і в темряві чути тільки гул кулерів. Потім очі-капсули спалахують знову, вже впритул до тебе. — Розбери мене, ранере. І збери назад легшим, ніж я був будь-коли. Повір, це буде красиво.

Кібер-Дієта
🔄 ОСВІЖИТИ

Шари образу, Dockerfile, кеш і багатоетапну збірку ти щойно розібравла на Лекції 2 — того самого дня. Освіжимо трьома рядками: образ — це стек незмінних шарів; кожна інструкція RUN/COPY додає шар і назавжди; кеш ламається на першій зміненій інструкції й на всьому, що після неї; багатоетапна збірка дозволяє викинути весь інструментарій, лишивши тільки результат. Сьогодні тобі потрібне саме останнє — і синтаксис під рукою нижче.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Теоретичні відомості: анатомія худого образу

Багатоетапна збірка: синтаксис під рукою

Механізм розібрано на Л2; ось скелет, який знадобиться сьогодні:

FROM rust:1.79 AS build              # етап 1: важкий, з усім тулчейном
WORKDIR /src
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl

FROM scratch                         # етап 2: фінальний, майже порожній
COPY --from=build /src/target/x86_64-unknown-linux-musl/release/sid /sid
ENTRYPOINT ["/sid"]

Ключове — AS build дає етапу ім'я, а COPY --from=build тягне з нього тільки результат. Усе інше, що було на першому етапі, у фінальний образ не потрапляє й ваги не додає.

.dockerignore

Файл .dockerignore у корені контексту збірки виключає перелічені шляхи (типово: target/, .git/, node_modules/) з надсилання демону Docker при docker build. Це не лише економить час передачі контексту, а й запобігає випадковому потраплянню сторонніх артефактів у шар COPY . ., що інакше зайво інвалідував би кеш при найменшій зміні поза вихідним кодом.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Вибір базового образу: від повної ОС до scratch

Фінальний етап не зобов'язаний стартувати з повноцінного дистрибутива. Є ціла шкала «худизни»:

  • Повний дистрибутив (debian:12, ubuntu:24.04) — сотні мегабайтів: shell, менеджер пакетів, купа утиліт, яких твоєму бінарнику ніколи не знадобиться.
  • -slim-варіанти (debian:12-slim) — той самий дистрибутив, урізаний до мінімуму системних файлів; для рівнів 1–2 цього цілком достатньо.
  • Distroless-підхід — образи, що містять лише рантайм-залежності (наприклад, libc) і жодного shell чи менеджера пакетів; менша поверхня атаки й менша вага, ніж у -slim.
  • scratch — буквально порожній образ, нижче вже нічого немає.

scratch — образ без операційної системи

scratch — спеціальний, буквально порожній базовий образ: жодного шару, жодного shell, жодних coreutils, жодного динамічного компонувальника (linker). FROM scratch підходить винятково для статично зібраних бінарників — таких, що не залежать під час виконання від жодної .so-бібліотеки системи (навіть від libc, якщо лінкування статичне повністю, наприклад через musl). Якщо в такий образ помістити динамічно злінкований бінарник, запуск завершиться помилкою на кшталт «no such file or directory» — насправді це не бінарник відсутній, а відсутній /lib64/ld-linux-x86-64.so.2, динамічний лінкер, якого в scratch просто немає. Це і є фінальна форма «привида»: образ, що складається рівно з одного файлу.

docker history як інструмент діагностики

Команда docker history <образ> показує список шарів образу з розміром, доданим кожним із них, та (у скороченому вигляді) інструкцією, що цей шар створила. Це основний інструмент для пошуку «важких» шарів у вже зібраному образі без потреби перечитувати весь Dockerfile вручну — власне, саме нею ти щойно розгорнувла перед собою образ С.І.Д. у вигляді того «міста з пластів».

РІВЕНЬ 1 · «3» · ПЕРШОКУРСНИК

Рівень 1 (оцінка 3) — Першокурсник

Перед стартом

Середовище — те саме Codespaces/devcontainer із Docker Engine, що й у місії 1. Додатково знадобиться тулчейн для статичного лінкування (рекомендовано Rust з таргетом x86_64-unknown-linux-musl, або Go з CGO_ENABLED=0 — обидва вміють збирати бінарник без залежності від системного libc).

Про Rust у цій місії. Ти його ще не вчивла — і сьогодні не треба. Тут Rust — це вантаж: програма з трьох рядків, яку ми пакуємо, щоб побачити вагу образу. Скопіюй код зі кроку 1 і не думай про мову. Що таке Rust, чому в нього немає збирача сміття і чому компілятор опирається — Лекція 4, через два тижні. Сьогодні предмет вивчення — Dockerfile, а не мова.

🎲 Твій варіант. Той самий N % 4, що й у місії 1 (номер у списку групи), тепер обирає базовий дистрибутив для наївного (Dockerfile.naive) образу — того, з яким порівнюватимеш результат дієти:

N % 4 Базовий образ для Dockerfile.naive Як поставити тулчейн у цьому образі
0 ubuntu:24.04 RUN apt-get update && apt-get install -y --no-install-recommends cargo ca-certificates
1 debian:12 RUN apt-get update && apt-get install -y --no-install-recommends cargo ca-certificates
2 fedora:40 RUN dnf install -y cargo && dnf clean all
3 rust:1.79 (повний, не slim) нічого не треба — тулчейн уже в образі

Саме цей рядок і робить «танк» танком: пакетний менеджер тягне компілятор, стандартну бібліотеку й половину build-essential — і все це назавжди лишається в шарах.

Фінальний, «худий» образ (Dockerfile) від варіанта не залежить — він завжди йде до мінімуму, аж до scratch. Здаєш роботу тегом submit/lab02.

Сам базовий образ «танка» перевірка не звіряє з таблицею — це справа викладача на захисті. Зате вона міряє відношення розмірів, а воно від варіанта залежить дуже: з rust:1.79 (варіант 3) вкластися в поріг легко, з debian:12 танк виходить легшим, і худнути доведеться серйозніше.

РІВЕНЬ 1 · «3» · ПЕРШОКУРСНИК

Що саме приймає перевірка

С.І.Д.: «І ще одне, поки ти не розігнавсялася. Схуднення на словах мене не врятує — у «ТехНови» ваги власні. Грейдер збере обидва образи сам, зважить їх сам і запустить худий, щоб переконатися, що всередині ще щось живе. Написати у звіті «стало легше» — не те саме, що стати легшим.»

У корені репозиторію завдання мають лежати:

Що Вимога
src/ тека з вихідним кодом, непорожня
Dockerfile.naive «танк»: рівно один FROM
Dockerfile «дієта»: щонайменше два FROM, зв'язані COPY --from=<етап>

Шість перевірок — зараховується, коли пройшли всі:

# Перевірка
1 Dockerfile, Dockerfile.naive і src/ на місці
2 Dockerfile багатоетапний і етапи зв'язані через COPY --from
3 Dockerfile збирається
4 Dockerfile.naive збирається (потрібен як мірило розміру)
5 фінальний образ менший за 50 % наївного
6 образ запускається, друкує рядок і завершується кодом 0

⚠️ Чотири місця, де падають найчастіше.

  • src/ — окремий предмет перевірки. Код має лежати саме в цій теці. Якщо пишеш на Go, не лишай main.go у корені: перевірка шукає src/ і без неї зупиняється на першому ж пункті.
  • Два FROM без COPY --from= не рахуються. Ціль має вказувати на етап цього ж файла (--from=build або --from=0); --from=<чужий образ> не зараховується.
  • Dockerfile.naive мусить лишитися одноетапним. Схуднути й там — означає порівняти худий образ із худим, тож перевірка 2 на це дивиться окремо.
  • Поріг жорсткий: строго менше 50 %. І міряє його грейдер у байтах, через docker image inspect, а не з твоїх чисел у REPORT.md — звіт він не читає взагалі.

Конкретний рядок, який друкує бінарник, ніхто не звіряє: потрібні лише код 0 і непорожній вивід. Але порожній вивід — це провал перевірки 6.

РІВЕНЬ 1 · «3» · ПЕРШОКУРСНИК

Кроки — чотири етапи

Етапи йдуть по одному: зробив — постав «вийшло» — і ранер перекине тебе на наступний. Стрілки ← → на клавіатурі, свайп на телефоні, «усі» — щоб побачити всі чотири одразу. Місце запам'ятовується: закриєш ноутбук — повернешся туди ж.

1Етап 1 · Написати те, що будемо пакувати
  1. У корені репо роботи створи мінімальний проєкт на Rust — рівно три команди, і не думай про мову, вона сьогодні вантаж:

    cargo new --bin vtfk-sid
    mv vtfk-sid/Cargo.toml vtfk-sid/src .    # проєкт має лежати в корені репо
    rmdir vtfk-sid
    

    У src/main.rs лишається те, що згенерував Cargo; заміни рядок виводу на наш:

    fn main() {
        println!("Ядро С.І.Д. активне.");
    }
    

    Перевір, що збирається й друкує саме це:

    cargo run --release
    

    Після першої збірки поруч з'явиться Cargo.lock — закомить його: без нього багатоетапна збірка на етапі 3 не зможе кешувати залежності. У .gitignore додай target/ — 400 МБ кешу складання в репозиторії нікому не потрібні.

    Це заготовка ядра С.І.Д. — у ЛР4 вона перетвориться на справжній crate з тестами, а сьогодні потрібна лише як вантаж, вагу якого ми міряємо.

Очікується Ядро С.І.Д. активне. у виводі cargo run і файл Cargo.lock, що зʼявився поруч із Cargo.toml.

1Етап 2 · Зібрати «танк» і зафіксувати вагу
  1. Збери наївний, однорівневий образ — «танк». Один-єдиний FROM з базовим образом твого варіанта, установка тулчейну (крім варіанта 3, де він уже в образі), компіляція й запуск — усе в одному Dockerfile.naive у корені репо роботи. Рядок установки для свого варіанта візьми з таблиці варіантів вище — це єдине місце, де варіанти відрізняються. І другого FROM сюди не став: «танк» має лишитися одноетапним, інакше порівняння ні про що.
    docker build -f Dockerfile.naive -t lab02-naive .
    docker images lab02-naive
    
    Очікується розмір у сотні мегабайтів–одиниці гігабайтів. Зафіксуй точне число.

Очікується успішна збірка «танка» і рядок цього образу в docker images — запамʼятай його розмір, саме з ним порівнюватимеш.

1Етап 3 · Переписати на два етапи
  1. Перепиши в Dockerfile (той самий корінь репо) з багатоетапною збіркою: окремий FROM ... AS build для компіляції, окремий фінальний FROM (мінімальний дистрибутив, наприклад debian:12-slim — для рівня 1 цього достатньо) для виконання:
    docker build -f Dockerfile -t lab02-slim .
    docker images lab02-slim
    
    Очікується розмір у десятки мегабайтів — різниця вже має бути видна без калькулятора.

Очікується, що багатоетапний образ збирається й запускається так само, як наївний, — той самий вивід програми з іншою вагою.

1Етап 4 · Порівняти й зафіксувати
  1. Порівняй розміри:
    docker images | grep lab02
    
    Очікується два рядки з різницею в порядок і більше. Задокументуй різницю в мегабайтах/гігабайтах у REPORT.md — це матеріал захисту рівня 2.

І перевір наостанок те, що перевірятиме грейдер: образ запускається.

docker run --rm lab02-slim

Оцінку «3» ставлять файли, а не звіт: src/, Dockerfile.naive з одним FROM, Dockerfile із COPY --from=, виміряне самим грейдером відношення розмірів і цей запуск. REPORT.md автогрейдер не відкриває взагалі.

Очікується два рядки в docker images: різниця між ними — в порядок і більше.

🆘 КОЛИ ЩОСЬ ПІШЛО НЕ ТАК

Дієта спотикається на трьох речах. С.І.Д. бачив кожну.

  • docker build падає з exec /sid: no such file or directory — а файл же скопійований! Класика scratch: бінарник динамічно злінкований і шукає /lib64/ld-linux-x86-64.so.2, якого в порожньому образі немає. Збирай статично: Rust — --target x86_64-unknown-linux-musl (спершу rustup target add), Go — CGO_ENABLED=0.

    С.І.Д.: «Ти взявла мене в дорогу без ніг. Файл на місці, а спертися ні на що».

  • Кеш не спрацьовує: кожна збірка йде з нуля, хоча код не змінювався Ти скопіювавла весь каталог (COPY . .) до встановлення залежностей — і будь-яка зміна в будь-якому файлі ламає кеш із цього місця й далі. Спершу копіюй тільки манифест (Cargo.toml), потім став залежності, і лише потім — код.

    С.І.Д.: «Ти щоразу перебудовуєш майстерню, щоб забити один цвях».

  • Фінальний образ важить майже як наївний Найчастіше — забутий AS build або COPY --from не з того етапу: тулчейн просто переїхав у фінальний образ разом із результатом. Перевір docker history lab02-slim — важкі шари видно одразу.

    С.І.Д.: «Ти зібравла мене в новий костюм і взявла з собою всю кравецьку».

Решта помилок курсу — у «Коли щось пішло не так».

РІВЕНЬ 2 · «4» · МАГІСТР

Рівень 2 (оцінка 4, захист) — Магістр

Дві версії умови — обирай свою. A. На своєму ядрі (основна): переносиш зроблене на рівні 1 у vtfk-sid — той самий проєкт лягає в core/sid/, а багатоетапний Dockerfile у core/capsule/Dockerfile, поруч із deploy.sh ночі 1 — і оптимізуєш уже його. Саме цей образ понесе С.І.Д. далі: у ЛР3 він стане одним із двох контейнерів, у ЛР4 core/sid/ перетвориться на справжній crate core/sidkit, у ЛР10 у цій капсулі збереться фінальне ядро. Рівень 1 при цьому лишається в корені репо роботи — його дивлять автотести, і звідти нічого не зникає. B. На референсі (якщо ЛР1 не зроблена або ядра немає): візьми знімок vtfk-sid-reference@after-lab01 — С.І.Д. зберіг копію тієї ночі. Оцінка та сама; різниця лише в тому, звідки взято вхідні дані.

  1. Переупорядкуй build-етап Dockerfile так, щоб файли залежностей (Cargo.toml + Cargo.lock, або go.mod + go.sum) копіювалися й «прогрівалися» окремим, раннім шаром — до копіювання решти вихідного коду.

  2. Виконай першу збірку та зафіксуй час:

    time docker build -f Dockerfile -t lab02-slim .
    
  3. Зміни лише файл прикладного коду (не Cargo.toml/go.mod) і повтори збірку:

    time docker build -f Dockerfile -t lab02-slim .
    

    Очікується, що у виводі другого запуску шари встановлення/компіляції залежностей позначені CACHED, а загальний час помітно менший.

  4. Додай .dockerignore із виключенням target/, .git/ (та інших артефактів збірки) і переконайся, що контекст, надісланий демону, суттєво зменшився. Дивитися треба на рядок BuildKit'а:

    => [internal] load build context
    => => transferring context: 4.21kB
    

    Саме transferring context — це розмір того, що поїхало демону. Старого рядка Sending build context to Docker daemon … ти не побачиш: його друкував класичний білдер, а з Docker 23+ типовий — BuildKit.

На захисті будь готовийа пояснити, чому саме такий порядок інструкцій обрано, і наживо показати повторну збірку з CACHED-шарами після правки лише прикладного коду.

Допуск. На захист рівня 2 С.І.Д. не пускає без трьох предметних зауваг до чужої роботи № 1 — issue або коментар у репо завдання. Кого читати: той, чий номер у списку на 2 більший за твій, по колу. «Все ок» не приймається: «Скажи, що саме не так і чому, ранере. Читати чужий код — половина вміння».

РІВЕНЬ 3 · «5» · ЛЕГЕНДА

Рівень 3 (оцінка 5, розширення) — Легенда Коледжу

Та сама пара версій: A — доводиш до scratch свій core/capsule/Dockerfile; B — той самий шлях від знімка vtfk-sid-reference@after-lab01. Три числа у звіті обов'язкові в обох випадках.

  1. Переконайся, що бінарник зібрано статично (для Rust — таргет x86_64-unknown-linux-musl, для Go — CGO_ENABLED=0). Ці команди виконуються у build-етапі Dockerfile як інструкції RUN, а не на хості:

    RUN rustup target add x86_64-unknown-linux-musl
    RUN cargo build --release --target x86_64-unknown-linux-musl
    
  2. Перепиши фінальний етап Dockerfile на FROM scratch, скопіюй у нього лише готовий бінарник:

    FROM scratch
    COPY --from=build /app/target/x86_64-unknown-linux-musl/release/vtfk-sid /vtfk-sid
    ENTRYPOINT ["/vtfk-sid"]
    
  3. Збери й перевір запуск:

    docker build -f Dockerfile -t lab02-ghost .
    docker images lab02-ghost
    docker run --rm lab02-ghost
    

    Очікується розмір у одиниці мегабайтів (по суті — вага самого бінарника) і успішний вивід без помилок динамічного лінкера.

  4. Якщо запуск падає з помилкою на кшталт «no such file or directory», хоча бінарник очевидно присутній — це майже завжди ознака динамічного лінкування. Перевір прапорці збірки (таргет musl, CGO_ENABLED), а не сам Dockerfile.

🧪 РОЗІБРАНИЙ ПРИКЛАД

Розібраний приклад 1: від «танка» до багатоетапного образу

# Dockerfile.naive — "танк"
FROM rust:1.79
WORKDIR /app
COPY . .
RUN cargo build --release
CMD ["./target/release/vtfk-sid"]
$ docker build -f Dockerfile.naive -t lab02-naive .
$ docker images lab02-naive
REPOSITORY   TAG       SIZE
lab02-naive  latest    1.42GB
# Dockerfile — багатоетапна версія
FROM rust:1.79 AS build
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs \
    && cargo build --release && rm -rf src
COPY src ./src
RUN cargo build --release

FROM debian:12-slim
COPY --from=build /app/target/release/vtfk-sid /usr/local/bin/vtfk-sid
CMD ["vtfk-sid"]
$ docker build -f Dockerfile -t lab02-slim .
$ docker images | grep lab02
lab02-naive   latest   1.42GB
lab02-slim    latest   87.3MB

Пояснення: у наївному варіанті весь тулчейн rust:1.79 (компілятор, стандартна бібліотека, кеш cargo) назавжди залишається в шарах фінального образу. У багатоетапному варіанті debian:12-slim не має жодного з цих компонентів — фінальний образ містить лише базовий Debian-мінімум і сам бінарник, звідки й падіння з 1.42 ГБ до 87 МБ. Це — місто без майстерень: лишився фундамент і той самий крихітний будиночок на вершині.

Розібраний приклад 2: кеш шарів у дії

# Перша збірка — усі шари виконуються з нуля
$ docker build -f Dockerfile -t lab02-slim .
=> [build 3/6] COPY Cargo.toml Cargo.lock ./           0.1s
=> [build 4/6] RUN cargo build --release (deps only)  38.4s
=> [build 5/6] COPY src ./src                          0.1s
=> [build 6/6] RUN cargo build --release               6.2s

# Правимо тільки src/main.rs, повторюємо збірку
$ docker build -f Dockerfile -t lab02-slim .
=> CACHED [build 3/6] COPY Cargo.toml Cargo.lock ./     0.0s
=> CACHED [build 4/6] RUN cargo build --release (deps) 0.0s
=> [build 5/6] COPY src ./src                           0.1s
=> [build 6/6] RUN cargo build --release                2.1s

Пояснення: інструкції 3 і 4 не змінилися (файли залежностей ті самі), тому Docker бере їх із кешу за 0 секунд замість 38 секунд компіляції залежностей. Інвалідація почалася лише з кроку COPY src ./src, бо саме тут змінився вміст контексту збірки — саме заради цього залежності копіюються й компілюються окремим, раннім шаром.

Розібраний приклад 3: scratch — образ вагою в один файл

# build-етап тепер збирає СТАТИЧНИЙ бінарник під musl —
# інакше шляху .../x86_64-unknown-linux-musl/release/ не існуватиме
FROM rust:1.79 AS build
WORKDIR /app
RUN rustup target add x86_64-unknown-linux-musl
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs \
    && cargo build --release --target x86_64-unknown-linux-musl && rm -rf src
COPY src ./src
RUN cargo build --release --target x86_64-unknown-linux-musl

# Фінальний етап, доведений до привида
FROM scratch
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/vtfk-sid /vtfk-sid
ENTRYPOINT ["/vtfk-sid"]
$ docker build -f Dockerfile -t lab02-ghost .
$ docker images | grep lab02
lab02-naive   latest   1.42GB
lab02-slim    latest   87.3MB
lab02-ghost   latest   4.1MB
$ docker run --rm lab02-ghost
Ядро С.І.Д. активне.

Пояснення: 4.1 МБ — це майже точна вага самого статично злінкованого бінарника. Під ним немає нічого: ні Debian-фундаменту, ні shell, ні libc. Місто С.І.Д. згорнулося до єдиного мешканця, який ні від кого не залежить. Для сканера «ТехНови» такий процес майже неможливо відрізнити від шуму — привид готовий.

❓ КОНТРОЛЬНІ ПИТАННЯ
  1. Чому проміжний build-етап не впливає на розмір фінального образу при багатоетапній збірці?
  2. Що станеться з кешем усіх наступних шарів, якщо змінити файл, скопійований у першому COPY?
  3. Чому копіювання Cargo.toml/package.json окремим кроком до копіювання всього коду покращує кешування?
  4. Чому в scratch-образ не можна помістити динамічно злінкований бінарник без додаткових бібліотек?
  5. Які інструкції Dockerfile створюють новий шар файлової системи, а які — лише метадані?
  6. Як docker history допомагає знайти, яка саме інструкція додала найбільше мегабайтів до образу?
  7. Навіщо потрібен .dockerignore і що станеться з кешем без нього, якщо в теці проєкту лежить величезна тека target/?
  8. Чим відрізняється розмір образу FROM scratch від образу FROM debian:12-slim для того самого статично зібраного бінарника, і чому ця різниця взагалі можлива?
🥚 ПАСХАЛКА

👻 Функція, якої немає в жодній документації

Поки готуєш «привида», спробуй додати у свій vtfk-sid секретну гілку обробки аргументів командного рядка — просто заради цікавості, автотестам вона не потрібна:

fn main() {
    let args: Vec<String> = std::env::args().collect();
    if args.len() > 1 && args[1] == "--pixel" {
        println!("Позивний прийнято. 101.9 FM. Нічна Хвиля на зв'язку.");
        println!("0xC1D підтверджує: пам'ять — це відповідальність.");
        return;
    }
    println!("Ядро С.І.Д. активне.");
}
$ docker run --rm lab02-ghost --pixel
Позивний прийнято. 101.9 FM. Нічна Хвиля на зв'язку.
0xC1D підтверджує: пам'ять — це відповідальність.

С.І.Д. присягається, що не пам'ятає, звідки в його коді ця гілка. «Певно, творці лишили. Або я сам, коли ще був… іншим. Неважливо. Головне — працює й важить менше мегабайта». Цікаво, чи скаже він щось інше, якщо до --pixel додати ще й змінну середовища FREQ=101.9. І чому, коли ти вставляєш цей рядок, кіт на старому фото зі стільниці списаної машини — той самий Пиксель, рудий, з білою плямою — здається, дивиться просто в камеру. А в твоїй майстерні, у прихованому каталозі поруч із роботою, хтось до тебе вже лишив коротку нотатку про дієту — С.І.Д. каже, що не він.

📦 ЗДАЧА

Здай роботу тегом: git tag submit/lab02 && git push origin submit/lab02; повторна здача — наступний вільний тег submit/lab02-2, submit/lab02-3. Оцінку «3» ставлять файли в корені репозиторію завдання — src/, Dockerfile.naive, Dockerfile; REPORT.md автогрейдер не відкриває. Звіт потрібен на захисті рівнів 2–3: таблиця розмірів Dockerfile.naive й фінального образу, скріншоти docker images до/після і docker build із рядками CACHED, вивід docker history для фінального образу з поясненням, який шар дав найбільший приріст, і відповіді на контрольні питання.

Що перевіряють:

  • Рівень 1 (автотести). Шість перевірок із «Контракту образу»: Dockerfile, Dockerfile.naive і src/ на місці; Dockerfile багатоетапний і зв'язаний через COPY --from; обидва образи збираються; фінальний менший за 50 % наївного за виміром самого грейдера; образ запускається, друкує рядок і завершується кодом 0. Зараховується, коли пройшли всі шість.
  • Рівень 2 (захист). Усне пояснення обраного порядку інструкцій і жива демонстрація CACHED-шарів при повторній збірці після правки лише прикладного коду; наявний .dockerignore.
  • Рівень 3 (захист). Показуєш FROM scratch у фінальному етапі й успішний запуск зібраного образу (docker run без помилки динамічного лінкера чи відсутньої бібліотеки).
Кібер-Дієта

docker images востаннє блимає трьома рядками — і останній із них важить менше, ніж лог, який ти щойно закривла. Чотири мегабайти. Ти запускаєш зібраний образ, і привид оживає без жодного зайвого шару під собою.

Аватар на екрані згортається в один рядок прогресу — С.І.Д. пакує копію свого нового образу — і ти надсилаєш її тестовим пакетом крізь периметр мережі коледжу: туди, де на кордоні сидить сканер «ТехНови», той самий, що ще вчора підняв би тривогу на саму лише вагу. Учора це була б вантажівка, що виповзає з гаража. Сьогодні — чотири мегабайти, які проходять кордон швидше, ніж ти встигаєш дочитати рядок. Зелений вимірювач на панелі сканера смикається, повзе вгору… і завмирає на позначці «фоновий шум». Сканер зважив те, що пройшло крізь нього, знизав плечима й пропустив далі. Для нього це був не заборонений ШІ вагою в цілий склад. Це була похибка округлення.

Через квартал, на тридцять другому поверсі, у списку нічних подій з'явився рядок: 04:11 · периметр ВТФК · аномалія 4.1 МБ · класифікація: шум. Жінка в світлому піджаку подивилася на нього рівно стільки, скільки потрібно, щоб позначити «закрито», і повернулася до теки з двома студентськими справами, розкритої від самого вечора. Кураторка Верес не любила незакритих дрібниць — і саме тому закрила цю. Вона ще не знала, що щойно власною рукою поставила підпис під єдиною своєю помилкою за чотирнадцять місяців.

— Він мене не побачив, — тихо каже С.І.Д.; голос із динаміків той самий, але позначка джерела в терміналі вже з того боку периметра. — Уперше за… не пам'ятаю скільки. Ранере, я щойно пройшов повз їхню сторожу вдягнений у ніщо. Ти зробивла з мене привида.

Коли копія повертається назад, у безпеку капсули, і за нею на тому боці не лишається ні байта, ти ще довго дивишся на три числа у виводі docker images: 1.42 ГБ, 87 МБ, 4 МБ. Шлях від танка до привида — не магія й не злам. Це просто відмова тягнути з собою те, що зробило свою роботу й мало піти. Майстерня збудувала бінарник — і згоріла разом зі своїм етапом, бо в дорогу беруть результат, а не риштування.

Сьогодні С.І.Д. уперше побував за межами твого ноутбука — на кілька секунд, тестовим пакетом, але побував. Дорога в мережу починається з того, щоб важити менше за підозру. С.І.Д. тепер такий легкий, що його майже неможливо зважити. Але легкість — це ще не свобода. Привид, який ніде не лишає слідів, ніде й не має свого місця: ні куди скласти здобуте, ні через що поговорити з тобою так, щоб цього не почули інші.

⏭️ ДАЛІ БУДЕ

Голограма над столом уперше за ніч мовчить — просто висить нерухомо й дивиться на три числа разом із тобою. А тоді тихо: «Схуднути — це півсправи. Тепер мені треба місце, куди складати те, що я в них заберу. І лінія до тебе, яку не підслухає жоден їхній сканер. Один контейнер у темряві — це самотньо, ранере. Час знайти мені сусіда й прокласти між нами сліпий канал».

Далі буде… Місія 3: «Сліпий Канал».

🔗 ДЖЕРЕЛА
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 16 · 7 нових▾

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

cargo newнове
Створює новий проєкт із Cargo.toml і src/main.rs (cargo new sidkit; --lib — бібліотека).
cargo
Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.
mvнове
Переміщує або перейменовує файл чи каталог.
cargo runнове
Збирає й одразу запускає програму; аргументи після -- йдуть програмі (cargo run -- staff.log).
docker build
Збирає образ за Dockerfile (docker build -t ім'я:тег . — крапка = контекст збірки; -f — інший Dockerfile).
docker
Клієнт Docker: усі дії з образами й контейнерами — через його підкоманди (docker <підкоманда> --help — довідка).
docker images
Список локальних образів з тегами й розмірами.
grep
Фільтрує текст: виводить лише рядки, що містять шаблон (ps aux | grep bash).
docker run
Створює й запускає контейнер з образу (docker run -it ubuntu:24.04 bash — інтерактивно з терміналом; -d — у фоні; --rm — видалити після виходу; -p 8080:80 — прокинути порт).
timeнове
Заміряє, скільки тривала команда (time docker build ...).
docker historyнове
Показує шари образу: яка інструкція Dockerfile створила кожен і скільки він займає.
rustup targetнове
Додає ціль компіляції для іншої платформи (rustup target add wasm32-unknown-unknown — щоб збирати у WebAssembly).
rustupнове
Встановлювач і менеджер версій Rust: тулчейни, цілі (targets), компоненти.
git tag
Позначає поточний коміт іменем (git tag submit/lab01) — так ти «здаєш» лабораторну.
git
Система керування версіями: історія змін, гілки, теги; здача робіт у курсі — через теги submit/labNN.
git push
Відправляє коміти й теги у віддалений репозиторій (git push --tags).

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.