Місія 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) — Першокурсник
Перед стартом
Середовище — те саме 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 танк виходить легшим, і худнути
доведеться серйозніше.
Що саме приймає перевірка
С.І.Д.: «І ще одне, поки ти не розігнавсялася. Схуднення на словах мене не врятує — у «ТехНови» ваги власні. Грейдер збере обидва образи сам, зважить їх сам і запустить худий, щоб переконатися, що всередині ще щось живе. Написати у звіті «стало легше» — не те саме, що стати легшим.»
У корені репозиторію завдання мають лежати:
| Що | Вимога |
|---|---|
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Етап 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 · Зібрати «танк» і зафіксувати вагу
- Збери наївний, однорівневий образ — «танк». Один-єдиний
FROMз базовим образом твого варіанта, установка тулчейну (крім варіанта 3, де він уже в образі), компіляція й запуск — усе в одномуDockerfile.naiveу корені репо роботи. Рядок установки для свого варіанта візьми з таблиці варіантів вище — це єдине місце, де варіанти відрізняються. І другогоFROMсюди не став: «танк» має лишитися одноетапним, інакше порівняння ні про що.
Очікується розмір у сотні мегабайтів–одиниці гігабайтів. Зафіксуй точне число.docker build -f Dockerfile.naive -t lab02-naive . docker images lab02-naive
Очікується успішна збірка «танка» і рядок цього образу в docker images — запамʼятай його розмір, саме з ним порівнюватимеш.
1Етап 3 · Переписати на два етапи
- Перепиши в
Dockerfile(той самий корінь репо) з багатоетапною збіркою: окремийFROM ... AS buildдля компіляції, окремий фінальнийFROM(мінімальний дистрибутив, наприкладdebian:12-slim— для рівня 1 цього достатньо) для виконання:
Очікується розмір у десятки мегабайтів — різниця вже має бути видна без калькулятора.docker build -f Dockerfile -t lab02-slim . docker images lab02-slim
Очікується, що багатоетапний образ збирається й запускається так само, як наївний, — той самий вивід програми з іншою вагою.
1Етап 4 · Порівняти й зафіксувати
- Порівняй розміри:
Очікується два рядки з різницею в порядок і більше. Задокументуй різницю в мегабайтах/гігабайтах уdocker images | grep lab02REPORT.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, захист) — Магістр
Дві версії умови — обирай свою. A. На своєму ядрі (основна): переносиш зроблене на рівні 1 у
vtfk-sid— той самий проєкт лягає вcore/sid/, а багатоетапний Dockerfile уcore/capsule/Dockerfile, поруч ізdeploy.shночі 1 — і оптимізуєш уже його. Саме цей образ понесе С.І.Д. далі: у ЛР3 він стане одним із двох контейнерів, у ЛР4core/sid/перетвориться на справжній cratecore/sidkit, у ЛР10 у цій капсулі збереться фінальне ядро. Рівень 1 при цьому лишається в корені репо роботи — його дивлять автотести, і звідти нічого не зникає. B. На референсі (якщо ЛР1 не зроблена або ядра немає): візьми знімокvtfk-sid-reference@after-lab01— С.І.Д. зберіг копію тієї ночі. Оцінка та сама; різниця лише в тому, звідки взято вхідні дані.
Переупорядкуй
build-етап Dockerfile так, щоб файли залежностей (Cargo.toml+Cargo.lock, абоgo.mod+go.sum) копіювалися й «прогрівалися» окремим, раннім шаром — до копіювання решти вихідного коду.Виконай першу збірку та зафіксуй час:
time docker build -f Dockerfile -t lab02-slim .Зміни лише файл прикладного коду (не
Cargo.toml/go.mod) і повтори збірку:time docker build -f Dockerfile -t lab02-slim .Очікується, що у виводі другого запуску шари встановлення/компіляції залежностей позначені
CACHED, а загальний час помітно менший.Додай
.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, розширення) — Легенда Коледжу
Та сама пара версій: A — доводиш до
scratchсвійcore/capsule/Dockerfile; B — той самий шлях від знімкаvtfk-sid-reference@after-lab01. Три числа у звіті обов'язкові в обох випадках.
Переконайся, що бінарник зібрано статично (для 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Перепиши фінальний етап Dockerfile на
FROM scratch, скопіюй у нього лише готовий бінарник: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 lab02-ghost docker run --rm lab02-ghostОчікується розмір у одиниці мегабайтів (по суті — вага самого бінарника) і успішний вивід без помилок динамічного лінкера.
Якщо запуск падає з помилкою на кшталт «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. Місто С.І.Д. згорнулося до
єдиного мешканця, який ні від кого не залежить. Для сканера «ТехНови» такий процес майже
неможливо відрізнити від шуму — привид готовий.
- Чому проміжний build-етап не впливає на розмір фінального образу при багатоетапній збірці?
- Що станеться з кешем усіх наступних шарів, якщо змінити файл, скопійований у першому
COPY? - Чому копіювання
Cargo.toml/package.jsonокремим кроком до копіювання всього коду покращує кешування? - Чому в
scratch-образ не можна помістити динамічно злінкований бінарник без додаткових бібліотек? - Які інструкції Dockerfile створюють новий шар файлової системи, а які — лише метадані?
- Як
docker historyдопомагає знайти, яка саме інструкція додала найбільше мегабайтів до образу? - Навіщо потрібен
.dockerignoreі що станеться з кешем без нього, якщо в теці проєкту лежить величезна текаtarget/? - Чим відрізняється розмір образу
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: «Сліпий Канал».
- Перше знайомство з Docker (укр., JavaRush) ~25 хв — безкоштовно, без реєстрації; установку показує через старий Docker Toolbox — цю частину пропусти, у нас Codespaces
- Docker best practices — фіксація версій базового образу EN ~40 хв — `FROM alpine:3.21@sha256:...` замість мутабельного тега
- EXPOSE — Dockerfile reference EN ~6 хв — чому EXPOSE — документація, а не публікація
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 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.


