Ви прокидаєтесь, дивитесь дашборди, і ваша «нудна» служба вже не така нудна. Контейнер перезапустився після рутинного оновлення хоста,
підтягнув інший образ, ніж учора, і тепер ваш процес входу поводиться як трагедія на сцені.
Це тихий жах мутованих тегів контейнерів: усе виглядає зафіксовано — поки ні. Виправимо це правильно: не дозволяти контейнерам
змінюватися під час оновлень, не перетворюючи ваш кластер на музей незмінних, непатчених образів.
Що ви насправді намагаєтеся контролювати
«Не дозволяти контейнерам завантажуватися по оновленнях» зазвичай означає одну з трьох речей, і варто чітко визначити, яку саме — бо виправлення різняться:
1) Не змінювати біти під час перезапуску
Ви погоджуєтеся, що контейнери можуть перезапускатися (оновлення ядра, рестарт демонів, ребут нод, пересадка оркестратором), але хочете, щоб перезапущений
контейнер використовував точно ті самі байти образу, що й раніше. Це проблема фіксації образу.
2) Не перезапускати контейнери під час оновлень хоста
Це питання обслуговування й оркестрації: дренуйте ноди, чергуйте ребути, налаштовуйте поведінку systemd/docker при оновленнях пакунків та уникайте «корисної» автоматизації,
яка перезавантажує все підряд при апгрейдах пакунків. Фіксація допомагає, але не запобігає перезапускам.
3) Не розгортати нові версії додатка без явної зміни
Це інженерія релізів: відокремте збірку від деплою, вимагайте зміни конфігурації (коміт у Git / запит на зміни), щоб розгорнути новий дайджест,
і зробіть відкати буденною операцією.
Ця стаття зосереджена на (1) і (3), з достатньою операційною реальністю для (2), щоб вас не застали зненацька.
Факти та коротка історія: чому це повторюється
Трохи контексту допоможе, бо ця проблема — не лише людська помилка; це властивість того, як працюють образи, теги та pull.
- Факт 1: Docker‑теги — це змінні вказівники. Реєстр може перемістити
:latest(або:1.2) на новий дайджест у будь‑який момент. - Факт 2: Єдина незмінна ідентифікація, якій можна довіряти для «однакових байтів», — це контент‑дайджест (наприклад,
@sha256:…). - Факт 3: Початковий UX Docker зробив теги схожими на версії, і це навчило індустрію трактувати змінні рядки як незмінні факти.
- Факт 4: API реєстру v2 (середина 2010‑х) формалізував маніфести й дайджести, тому сьогодні ми можемо фіксувати образи за дайджестом без костилів.
- Факт 5: «Latest» — не версія; це маркетинговий термін. Реєстри ніколи не обіцяли семантики для нього, і вони тримають цю обіцянку.
- Факт 6: Багатоархітектурні образи ускладнили «той самий тег»: один тег може мапитися на різні маніфести залежно від архітектури.
- Факт 7: Поведінка pull змінювалася з часом у Docker Engine, версіях Compose та оркестраторах — тому «раніше так не було» може бути правдою.
- Факт 8: Сучасні фічі ланцюга постачання (SBOM, атестації походження, перевірка підписів) припускають, що ви можете назвати незмінний артефакт. Дайджести — це таке ім’я.
- Факт 9: «Відтворювані збірки» все ще рідкість у світі контейнерів. Навіть якщо Dockerfile не змінився, повторна збірка часто дає інший дайджест.
Якщо шукати винуватця — то це не Docker. Це поєднання змінних вказівників і автоматизації, яка припускає, що вказівники стабільні.
Режими відмов: як контейнери «оновлюються самі»
Pull‑on‑recreate: найпоширеніша капость
Контейнер не змінює свій образ мовчки під час роботи. Набагато банальніше: щось його відтворює (Compose up, Swarm update,
Kubernetes rollout, reboot ноди), і рантайм підтягує те, на що тег вказує в цей момент.
Якщо ви вказали image: vendor/app:1.4 і вендор перемітив тег 1.4 з патчем безпеки, ваш наступний recreate підтягне нові байти.
Це може бути добре. А може бути катастрофою. Це точно не підпорядковано вашому change management.
Оновлення хоста, що перезапускають Docker‑daemon
Оновлення пакунків можуть перезапустити dockerd. Залежно від політик рестарту, контейнери повернуться. Якщо оркестратор їх відтворює, теги знову резолвляться.
Якщо ви зафіксували дайджест і нода має образ локально, отримаєте ті ж байти. Якщо покладаєтеся на теги і дозволяєте pull — ризикуєте рулеткою.
Завдання «очищення», що видаляють образи
Прюнинг образів — корисна справа, поки не стає проблемою. Якщо нода чистить невикористані образи, а пізніше треба відтворити контейнер, доведеться знову підвантажити.
Ось тоді теги можуть змінитися під вами.
Перетегування на стороні реєстру та культура force‑push
Деякі організації трактують теги як гілки і перезаписують їх. Якщо ваше розгортання посилається на теги, ви підписалися на «те, що зараз на цій гілці», чи хотіли того чи ні.
Жарт №1: Використовувати :latest у продакшні — це як назвати єдину резервну копію final_final_really_final.zip. Це настрій, а не план.
Дефолти оркестратора, що заохочують pull
Kubernetes з imagePullPolicy: Always завжди спілкуватиметься з реєстром, навіть якщо дайджест уже присутній. Compose має свої дефолтні поведінки pull залежно від команди.
Ручки існують, але дефолти оптимізовані для зручності, а не для розбору інцидентів.
Варіанти фіксації: теги, дайджести та підписані посилання
Варіант A: Фіксувати за дайджестом (рекомендовано для «не змінювати біти»)
Використовуйте repo/name@sha256:…. Цей дайджест ідентифікує маніфест (часто індекс для кількох архітектур), який у свою чергу посилається на шари образу. За визначенням він незмінний.
Якщо підтягуєте за дайджестом, отримаєте ті самі байти завтра, за тиждень і під час відкату о 3 ранку.
Недолік — ергономіка для людей. Дайджести довгі, негарні й мало що кажуть у код‑ревʼю. Тому відповідальна фіксація поєднує обох:
дружній для людини тег для наміру і дайджест для незмінності.
Варіант B: Фіксувати «незмінними тегами» (прийнятно за гарантії незмінності)
Якщо ваш реєстр і політика організації гарантують, що теги на кшталт 1.2.3 ніколи не перезаписуються, то фіксація семвер‑тегом може працювати.
Але розумійте, на що ви покладаєтеся: на соціальний контракт, а не на криптографію.
Варіант C: Фіксувати за дайджестом і перевіряти підписи (найкраще, якщо можете дозволити)
Дайджести вирішують «однакові байти». Вони не вирішують «байти, яким варто довіряти». Тут приходять підписи та походження: ви можете вимагати, щоб дайджест був підписаний CI‑ідентичністю і супроводжувався SBOM/атестацією.
Не потрібно одразу робити все. Почніть з фіксації дайджестом. Додайте перевірку підписів, коли базове перестане вас будити вночі.
Що означає «відповідально»
Відповідальна фіксація — це не «ніколи не оновлювати». Це «оновлення відбуваються тільки коли ми вирішуємо, і ми можемо пояснити, які байти запущені».
Ви хочете:
- Незмінні посилання в продакшні (дайджести).
- Процес промоції, який переміщує протестовані артефакти далі.
- Відкат, який повторно використовує відомі‑добрі дайджести.
- Політики збереження в реєстрі, що не видаляють те, що використовує продакшн.
Docker Compose і Swarm: припиніть несподівані pulls
Compose: зробіть відтворення детермінованим
Compose часто використовують як оркестратор, але він поводиться як інструмент для зручності. Це нормально — поки ви не запускаєте його під автоматизацією за розкладом.
Тоді починаються «чому він підтягнув?» сюрпризи.
Практична стратегія для Compose:
- Посилайтеся на образи за дайджестом у
compose.yamlдля продакшну. - Використовуйте явний
docker compose pullпід час вікон обслуговування або CI‑деплоїв. - Уникайте поведінки «завжди підвантажувати», якщо ви дійсно цього не хочете.
Swarm: розумійте оновлення сервісів і дайджести
Сервіси Swarm можуть стежити за тегами, але вони також записують розвʼязані дайджести. Вам все одно треба бути явним щодо оновлень, щоб контролювати, коли приймається новий дайджест.
Кут зору Kubernetes: та сама проблема, інші ручки
Користувачі Kubernetes люблять думати, що драма з Docker‑тегами — для менших команд. Потім Deployment посилається на myapp:stable, нода дрениться,
і раптом «stable» означає «сюрприз».
Контролі в Kubernetes такі:
- Фіксуйте за дайджестом у Pod‑spec:
image: repo/myapp@sha256:… - Налаштуйте
imagePullPolicyсвідомо. Для дайджестів зазвичай підходитьIfNotPresent, але остерігайтеся розбігання кешів нод. - Використовуйте політики admission, щоб відхиляти змінні теги в певних неймспейсах.
Якщо ви фіксуєте за дайджестом, пересадка не змінює байтів. Якщо фіксуєте за тегом — пересадки стають релізами.
Реєстри, збереження та пастка з дайджестами
Фіксація за дайджестом вводить новий режим відмов: ваш реєстр може робити garbage‑collect маніфестів без тегів. Багато систем збереження вважають «без тега» безпечним для видалення.
Але коли ви розгортаєте за дайджестом, ви можете перестати посилатися на тег, і реєстр подумає, що артефакт мертвий.
Відповідальний патерн: зберігайте тег, що відстежує промотований артефакт (навіть якщо продакшн використовує дайджест), або налаштуйте збереження реєстру
так, щоб маніфести, на які посилається продакшн‑дайджест, не видалялися. Це менш блискуче, ніж підписи, але більш ймовірно запобіжить вашому наступному простою.
Також: якщо ви використовуєте багатоархітектурний індекс‑дайджест, видалення маніфесту однієї архітектури може поламати pulls на цій архітектурі, навіть якщо «на amd64 все працює».
Реєстр не переймається вашими почуттями.
Практичні завдання: команди, виводи та рішення (12+)
Це реальні речі, які ви можете зробити на хості сьогодні. Кожне завдання містить: команду, що означає вивід, і рішення, яке слід ухвалити.
Завдання 1: Подивіться, яким образом фактично користується запущений контейнер
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.ID}}\t{{.Status}}'
NAMES IMAGE ID STATUS
api myco/api:1.9 7c2d1a9f01b2 Up 3 days
worker myco/worker:1.9 1a0b9c3f9e44 Up 3 days
Значення: Ви бачите налаштований референс (часто тег), а не обов’язково дайджест, до якого він резолвився.
Рішення: Якщо тут теги, не припускайте незмінності. Перейдіть до інспекції дайджестів далі.
Завдання 2: Перевірте Image ID і RepoDigests контейнера
cr0x@server:~$ docker inspect api --format 'ImageID={{.Image}} RepoDigests={{json .RepoDigests}}'
ImageID=sha256:5c0c0b9d2f2d8e0e5a11c8b3c9fb4f0d0a4c3cc0f55b9c8e0d1f0f2a3b4c5d6e RepoDigests=["myco/api@sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a"]
Значення: .Image — локальний content ID; RepoDigests показує мапінг на дайджест реєстру.
Рішення: Якщо RepoDigests присутній — зафіксуйте його і привʼяжіть у конфігурації. Якщо відсутній — можливо, ви використовуєте локально збудований образ або тягнули без метаданих дайджеста.
Завдання 3: Резолв тега в дайджест без підвантаження шарів (inspect manifest)
cr0x@server:~$ docker buildx imagetools inspect myco/api:1.9
Name: myco/api:1.9
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a
Manifests:
Name: myco/api:1.9@sha256:4f3c2b1a...
Platform: linux/amd64
Name: myco/api:1.9@sha256:7a6b5c4d...
Platform: linux/arm64
Значення: Тег наразі вказує на індекс‑дайджест; пер‑архітектурні маніфести відрізняються.
Рішення: Фіксуйте за індекс‑дайджестом, якщо у вас змішані архітектури; фіксуйте за архітектурно‑специфічним дайджестом для абсолютного детермінізму на платформі.
Завдання 4: Підтвердіть, чи Compose відтворить контейнери (і отже може підвантажувати)
cr0x@server:~$ docker compose up -d --no-build
[+] Running 2/2
✔ Container stack-api-1 Started
✔ Container stack-worker-1 Started
Значення: «Started» означає, що існуючі контейнери були запущені; «Recreated» значить, що створено нові контейнери.
Рішення: Якщо ви бачите «Recreated» несподівано — зʼясуйте, чому (дріфт конфігу, зміна змінних середовища, зміна bind‑mount). Recreate — це місце, де тег‑дріфт кусає.
Завдання 5: Примусіть Compose використовувати тільки локальний кеш (безпечна перевірка дріфту тегів)
cr0x@server:~$ docker compose up -d --pull never
[+] Running 2/2
✔ Container stack-api-1 Started
✔ Container stack-worker-1 Started
Значення: Compose відмовився робити pull. Якби потрібного образу не було локально, він помилився б.
Рішення: Використовуйте це в продакшн‑автоматизації, щоб не допустити «ой, він підтягнув щось нове, бо кеш був почищений».
Завдання 6: Виявити, чи тег змінився від останнього разу (порівняння дайджестів)
cr0x@server:~$ docker buildx imagetools inspect myco/api:1.9 --format '{{.Digest}}'
sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a
Значення: Це поточний дайджест для тега.
Рішення: Збережіть його в Git (або в системі розгортання). Якщо він змінюється без планованого релізу — розглядайте це як подію upstream.
Завдання 7: Зафіксувати образ за дайджестом у Compose
cr0x@server:~$ cat compose.yaml
services:
api:
image: myco/api@sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a
restart: unless-stopped
Значення: Розгортання тепер вказує на незмінний артефакт.
Рішення: Ви обміняли «зручні оновлення» на «контрольовані оновлення». Це сенс. Побудуйте промоційний пайплайн, щоб не застрягти назавжди.
Завдання 8: Перевірити, який дайджест працює після деплою
cr0x@server:~$ docker compose ps --format json | jq -r '.[].Name + " " + .Image'
stack-api-1 myco/api@sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a
Значення: Ваш рантайм‑референс тепер містить дайджест.
Рішення: Якщо ви все ще бачите теги, ваш шлях деплою переписує референс. Виправте це перед оголошенням перемоги.
Завдання 9: Знайти недавні pulls образів і пов’язати з інцидентами
cr0x@server:~$ journalctl -u docker --since "24 hours ago" | grep -E "Pulling|Downloaded|Digest"
Jan 03 01:12:44 server dockerd[1123]: Pulling image "myco/api:1.9"
Jan 03 01:12:49 server dockerd[1123]: Downloaded newer image for myco/api:1.9
Jan 03 01:12:49 server dockerd[1123]: Digest: sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a
Значення: Логи демона показують активність pull по тегу і розвʼязаний дайджест.
Рішення: Якщо інцидент почався одразу після pull — припускайте дріфт образу, поки не доведено інше. Зафіксуйте дайджест для постмортему й відкату.
Завдання 10: Побачити, які образи присутні і чи може pruning зашкодити
cr0x@server:~$ docker images --digests --format 'table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.Size}}' | head
REPOSITORY TAG DIGEST ID SIZE
myco/api 1.9 sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a 5c0c0b9d2f2d 312MB
myco/worker 1.9 sha256:1a2b3c4d5e6f... 9aa0bb11cc22 188MB
Значення: Дайджести тут показують, що рантайм може використати, якщо не треба буде тягнути.
Рішення: Якщо ваші ноди постійно прюнять, вважайте, що «ми знову підвантажимо», і фіксуйте за дайджестом, щоб уникнути дріфту тегів.
Завдання 11: Dry‑run pull, щоб побачити, чи тег зміниться (без деплою)
cr0x@server:~$ docker pull myco/api:1.9
1.9: Pulling from myco/api
Digest: sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a
Status: Image is up to date for myco/api:1.9
docker.io/myco/api:1.9
Значення: «Up to date» означає, що тег наразі резолвиться в те, що вже є локально.
Рішення: Якщо дайджест змінюється тут — зупиніться і розглядайте це як новий реліз. Не дозволяйте технічному обслуговуванню перетворитися на неперевірений деплой.
Завдання 12: Доведіть собі, що теги змінні (неприємна демонстрація)
cr0x@server:~$ docker image inspect myco/api:1.9 --format '{{index .RepoDigests 0}}'
myco/api@sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a
Значення: Це те, на що 1.9 мапиться зараз. Це не гарантія щодо наступного тижня.
Рішення: Якщо вам важливі детерміновані рестарти — припиніть розгортати теги у продакшні. Крапка.
Завдання 13: Визначте контейнери, які ймовірно зміняться при наступному рестарті (використовуючи теги)
cr0x@server:~$ docker ps --format '{{.Names}} {{.Image}}' | grep -v '@sha256:' | head
api myco/api:1.9
nginx nginx:latest
Значення: Усе без @sha256: базується на тегах і тому змінне.
Рішення: Поставте ці сервіси в список ремедіації. Почніть із сервісів, відкритих в інтернет, і шляхів автентифікації — ви знаєте, які руйнують вихідні дні.
Завдання 14: Перевірте політики рестарту, які підсилюють рестарти демона
cr0x@server:~$ docker inspect api --format 'Name={{.Name}} RestartPolicy={{.HostConfig.RestartPolicy.Name}}'
Name=/api RestartPolicy=unless-stopped
Значення: Політики рестарту визначають, що повернеться після рестарту демона.
Рішення: Зберігайте політики рестарту, але поєднуйте їх із фіксацією за дайджестом. «Always restart» + «змінний тег» — це якраз шлях до несподіваних redeploy.
Завдання 15: Перевірте ризик збереження в реєстрі (список локальних дайджестів у використанні)
cr0x@server:~$ docker ps -q | xargs -n1 docker inspect --format '{{.Name}} {{json .RepoDigests}}' | head -n 3
/api ["myco/api@sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a"]
/worker ["myco/worker@sha256:1a2b3c4d5e6f..."]
/metrics ["prom/prometheus@sha256:7e6d5c4b3a2f..."]
Значення: Це реальний набір артефактів, від яких залежить продакшн.
Рішення: Переконайтеся, що ваш реєстр зберігає ці дайджести. Якщо політика retention видаляє «без тега», переконайтеся, що ці дайджести залишаються тегованими або мають винятки.
Завдання 16: Виявити «корисну» автоматизацію на кшталт Watchtower (або еквівалентів)
cr0x@server:~$ docker ps --format '{{.Names}} {{.Image}}' | grep -i watchtower
watchtower containrrr/watchtower:1.7.1
Значення: На хості працює авто‑апдейтер.
Рішення: Або видаліть його з продакшну, або звузьте обсяг його дії, або зробіть його обізнаним про дайджести з gate на схвалення. Неконтрольовані оновлення — протилежність SRE.
Швидкий playbook діагностики
Коли підозрюєте «контейнери змінилися після оновлень», не починайте з суперечок про теги в чаті. Швидко перевірте факти й ізолюйте вузьке місце:
це дріфт образу, дріфт конфігу чи тиск ресурсів?
По‑перше: чи змінився запущений дайджест?
- Перевірте поточні
RepoDigestsконтейнера черезdocker inspect. - Порівняйте з останнім відомим добрим дайджестом (з Git, changelog або нотаток інциденту).
- Якщо дайджести відрізняються — поводьтеся, як з деплоєм. Почніть відкат або виправлення вперед.
По‑друге: що спричинило рестарт/відтворення?
- Перегляньте
journalctl -u dockerна предмет рестартів демона і pull. - Перевірте події Compose/Swarm/Kubernetes щодо пересадки і відтворення.
- Шукайте логи оновлення пакунків, що могли перезапустити docker/containerd.
По‑третє: якщо дайджест не змінився, що ще могло змінитись?
- Дріфт конфігу: змінні середовища, змонтовані файли конфігів, секрети, DNS, сертифікати.
- Зміни на хості/ядрі: поведінка cgroup, iptables/nft, зміни storage driver.
- Обмеження ресурсів: дисковий простір, inodes, CPU throttling.
По‑четверте: вирішіть, заморозити чи йти вперед
- Якщо ви використовуєте теги: заморозьте, переключившись на дайджест, потім безпечно розбирайтеся з upstream‑рухом тегів.
- Якщо ви вже зафіксовані: ваш інцидент, ймовірно, не «таємниче оновлення». Працюйте далі по справжній причині.
Типові помилки: симптом → корінь → виправлення
1) «Нічого не змінювалося», але поведінка змінилася після ребуту
Симптом: Після ребуту нод контейнери поводяться інакше; банери версій не відповідають очікуванням.
Корінь: Контейнери були відтворені й підтягли інший дайджест, бо тег змістився, або локальний образ був прюндений.
Виправлення: Фіксуйте за дайджестом у продакшн‑конфігах. Використовуйте docker compose up --pull never в автоматизації. Не займайтеся прюнингом, не розуміючи наслідків повторного деплою.
2) Відкат не спрацював
Симптом: Ви «відкотились» до :stable, але баг залишився або змінився.
Корінь: Ви відкотилися до тега, який уже просунувся; теги не є знімками.
Виправлення: Відкатуйтеся до зафіксованого дайджесту. Зберігайте дайджести для кожного релізу в Git або в системі розгортання.
3) Лише ARM‑ноди зламані
Симптом: amd64 працює; arm64 ноди після пересадки потрапляють у crashloop.
Корінь: Багатоархітектурний індекс змінився або маніфест однієї архітектури видалили через retention/GC.
Виправлення: Фіксуйте на індекс‑дайджесті і захистіть його від GC; перевіряйте наявність маніфестів кожної архітектури перед промоцією.
4) «Ми зафіксували», але все одно був дріфт
Симптом: У Compose‑файлі вказано дайджест, але ноди підтягують щось інше.
Корінь: Обгортка‑скрипт або шаблонізатор переписав референс образу на тег у рантаймі, або використано інший Compose‑файл.
Виправлення: Друкуйте ефективний конфіг у CI/CD і стверджуйте наявність @sha256:. Розглядайте «зрендерений конфіг» як артефакт деплою.
5) Продакшн більше не може підтягнути зафіксований дайджест
Симптом: Нова нода падає з «manifest unknown» для дайджеста, який ви розгорнули місяць тому.
Корінь: Реєстр видалив маніфест за політикою retention, бо він був без тега, або репозиторій почистили агресивно.
Виправлення: Тримайте «релізні теги», що вказують на промотовані дайджести, або налаштуйте винятки retention для продакшн‑дайджестів. Регулярно аудіруйте політику retention.
6) Команда з безпеки ненавидить фіксацію
Симптом: «Ви зафіксували образи, отже ви ніколи не будете патчити.»
Корінь: Фіксація була впроваджена як заборона на зміни, а не як контрольований workflow промоції.
Виправлення: Фіксуйте в продакшні, але оновлюйте через CI‑керовану промоцію. Додайте планові перебудови, сканування та явні розгортання.
Три корпоративні міні‑історії з поля бою
Міні‑історія 1: Інцидент через неправильне припущення
Середня B2B SaaS компанія використовувала Docker Compose на кількох потужних VM. Вони всюди посилалися на myco/api:2.3 і вважали, що «2.3 означає 2.3».
Команда вендора, що володіла образом, думала інакше: вони трактували 2.3 як лінію мінорних релізів і регулярно перебудовували її з патченими базовими образами.
В один вівторок інфраструктурна команда розгорнула оновлення безпеки на хості. Docker перезапустився. Контейнери перезапустилися. API повернувся — здебільшого.
Симптом був дивним: частина клієнтів отримувала 401, потім повтори працювали, потім знову падали. Схоже на rate limiting або проблему кешу токенів.
On‑call робив звичні речі: перевіряв логи апки, Redis, балансувальник навантаження. Графіки вказували на підвищену латентність, а не на тотальні відмови.
Тим часом клієнти ескалували, бо «періодичні проблеми з автентифікацією» швидко підривають довіру.
Прорив був смішно простим. Хтось порівняв дайджести на одній і іншій ноді й виявив різницю.
Половина флота підтягнула новозбудований 2.3; інша половина все ще мала закешований попередній дайджест і не потребувала pull.
Кластер став невідповідною canary‑тестовою платформою, про яку ніхто не знав.
Виправлення зайняло менше часу, ніж пояснення: вони зафіксували продакшн на дайджесті, розгорнули всю флота на один відомий артефакт і стабілізувалися.
Довгострокова зміна була культурною: «тег = версія» стало забороненим допущенням, якщо реєстр не забезпечував незмінність.
Міні‑історія 2: Оптимізація, що вдарила по вас
Інша компанія мала здраву ціль: зменшити використання диску на нодах. Їхні контейнери створювали багато churn образів, і команда збереження скаржилася
на роздутий root‑том і повільні бекапи. Отже вони додали агресивне нічне очищення: docker system prune -af на кожній ноді.
Спершу все було добре. Сигнали про диск зникли. Кожен похлопав один одному по спині і повернувся до ігнорування планування ємності. Потім тихо апгрейднули залежність:
нода перезавантажилася після патчу ядра, і кілька сервісів відтворилися. Ці сервіси були теговими, і зараз усі образи потрібно було підтягнути заново.
Реєстр мав обмеження по ліміту, і мережевий шлях до нього не був таким широким, як уявляли. Роллінгові рестарти стали повільними рестартами.
Більш важливо, один upstream тег перемістився на дайджест із бібліотекою, що змінила налаштування TLS. Сервіс не впав, але перестав спілкуватися з upstream, що використовував застарілі шифри.
Інцидент не був «очищення диска зламало прод». Інцидент — «очищення диска перетворило кожен рестарт у переDeploy».
Вирішили розділити зони відповідальності: зберегти очищення, але фіксувати образи за дайджестом і тримати невеликий кеш відомих продакшн‑дайджестів на кожній ноді.
Вони також перестали вважати реєстри безкінечно доступними й швидкими.
Жарт №2: docker system prune -af в cron — операційний еквівалент гоління бензопилою. Швидко — але тільки поки не стане небезпечно.
Міні‑історія 3: Нудна, але правильна практика, яка врятувала день
Регульований ентерпрайз запускав сотні контейнерів, і їхній change control був… назвемо це «ентузіазмом». Інженери бурчали про паперову тяганину,
але в процесу релізів була одна недооцінена риса: кожен артефакт розгортання включав розвʼязані дайджести образів, збережені поруч із конфігом.
Одного ранку критичний сервіс почав викидати segmentation faults після того, як падіння ноди спричинило пересадку. Всі підозрювали
«поганий хардвер» або «регресію ядра». Сигнатура крашу була в нативній залежності, тож почалися звинувачення.
SRE on‑call зробив щось геть несексуальне: порівняв дайджести в записі деплою з дайджестами, що працювали на новій ноді.
Вони не співпали. Нода підтягнула тег, який просунувся за ніч, бо конвеєр знову опублікував «stable» після тестів.
Оскільки організація мала записаний дайджест, відкат був миттєвим і точним: redeploy попереднього дайджеста. Без гадання. Без «випробувати тег з минулого тижня».
Сервіс швидко відновився, і інцидент перейшов від «паніка інфраструктури» до «помилка в контролі релізів».
Постмортем був не гламурним: вимагати фіксацію дайджестами, припинити використання «stable» в продакшні і вимагати схвалень для перетегувань.
Нудні практики не потрапляють на конференції, але вони повертають вам сон.
Контрольні списки / покроковий план
Покроково: переведіть Compose‑деплой з тегів на дайджести (без хаосу)
- Проведіть інвентаризацію того, що працює. Зберіть імена контейнерів, поточні образи та поточні repo‑digests.
- Розвʼяжіть теги в дайджести. Для кожного тега в продакшні зафіксуйте, якому дайджесту він зараз відповідає.
- Оновіть Compose‑файли. Замініть
repo:tagнаrepo@sha256:…. - Розгорніть з вимкненими pulls. Використовуйте
docker compose up -d --pull never, щоб випадково не прийняти переміщений тег. - Перевірте фактичний референс. Підтвердіть появу
@sha256:уdocker compose psтаdocker inspect. - Збережіть дайджести в Git/записах змін. Щоб можна було відповісти «що працює?» без SSH у прод.
- Виправте політику retention. Переконайтеся, що промотовані дайджести не видаляються.
- Побудуйте workflow промоції. CI збирає один раз, тегує незмінно, і «промотує», переміщуючи релізний тег або оновлюючи конфіг з дайджестом.
Список для продакшну: перед патчем хостів
- Підтвердіть, що в продакшні використовуються дайджести, а не теги.
- Переконайтеся, що нодам вистачає диску, щоб не доводилося робити аварійний prune.
- Підтвердіть доступність реєстру та креденшелів з нод.
- Вимкніть авто‑апдейтери на продакшн‑нодах (або обмежте їх для не критичних стеків).
- Сплануйте рестарт демона: знайте, які контейнери перезапустяться і в якому порядку.
Список релізу: перед тим, як перевести новий образ у продакшн
- Промотуйте конкретний дайджест, що пройшов тести; не промотуйте тег, який може зрушити.
- Перевірте, що дайджест існує для потрібних архітектур.
- Скануйте образ і зберігайте SBOM/похідні дані, якщо цього вимагає організація.
- Тримайте готовий (і протестований) відкатний дайджест.
Операційний принцип, який варто надрукувати
перефразована ідея
від Gene Kim: робити зміни меншими й контрольованішими знижує ризик і пришвидшує відновлення.
Фіксація за дайджестом робить рестарти буденними; контрольована промоція робить оновлення свідомими.
Питання та відповіді (FAQ)
1) Якщо контейнери не змінюються під час роботи, чому мій додаток змінився «без деплою»?
Бо щось відтворило контейнер (рестарт демона, ребут ноди, пересадка оркестратором), а ваш конфіг посилався на змінний тег.
Це відтворення фактично виконало деплой.
2) Чи достатньо фіксації за дайджестом для безпеки?
Це достатньо для детермінізму. Безпека — окрема справа: потрібні вчасні перебудови, сканування та контрольована промоція.
Дайджести допомагають безпеці тим, що чітко вказують, який артефакт ви сканували і затвердили.
3) Чи можу я і далі використовувати теги в деві?
Так. Дев — місце, де зручність важлива. Використовуйте теги на кшталт :latest, якщо це пришвидшує ітерацію. Але зробіть межу промоції жорсткою:
продакшн має приймати тільки дайджести (або теги, що гарантовано незмінні).
4) Що поганого в тегах :1.2.3?
Нічого — якщо ваш реєстр і політика організації забезпечують незмінність і ви це аудитуєте. Якщо незабезпечено — ви довіряєте, що ніхто ніколи не перетегує, не force‑push або «не перебудує 1.2.3 з швидким фіксом».
5) Чи зламає фіксація за дайджестом автоматичне патчення базових образів?
Це змінює процес із «мовчазного» на «явний». Ви маєте перебудовувати й промотувати нові дайджести за розкладом. Це краще, ніж прокидатися через несподіваний апгрейд.
6) Як безпечно робити відкат?
Відкочуйте, розгортаючи попередній відомий добрий дайджест, а не «відкат до тега». Зберігайте дайджести для релізів, щоб відкат був однією зміною конфігу.
7) Чи Compose завжди робить pull при up?
Не завжди. Поведінка залежить від команди й прапорів. Якщо хочете заборонити pull у продакшні — використайте --pull never.
Якщо хочете оновити свідомо — запускайте docker compose pull окремим кроком.
8) Що з Kubernetes — чи завжди слід використовувати дайджести там теж?
Для продакшну — так, якщо у вас немає суворої незмінності тегів і admission control. Фіксація за дайджестом запобігає ситуації «пересадка = реліз».
Якщо ви використовуєте теги, трактуйте кожну подію ноди як потенційний rollout.
9) Чому зафіксований дайджест не підтягнувся на новій ноді через місяці?
Ймовірно, реєстр видалив маніфест через retention або garbage collection (часто тому, що він був без тега). Виправте політику retention і тримайте релізні теги, що вказують на промотовані дайджести.
10) Який дайджест фіксувати: індекс чи архітектурно‑специфічний?
Якщо ви працюєте на змішаних архітектурах — фіксуйте індекс‑дайджест, щоб кожна нода отримувала коректний маніфест платформи.
Якщо ви працюєте на одній архітектурі й прагнете максимальної детермінованості — фіксуйте платформ‑специфічний дайджест.
Висновок: наступні кроки на цей тиждень
Якщо ви запам’ятаєте одну річ: теги — зручні імена, а не незмінні версії. Якщо конфіг продакшну посилається на теги, ваше наступне оновлення хоста може перетворитися на деплой.
Це не «DevOps». Це гра з кращим брендингом.
Практичні кроки:
- Проведіть інвентаризацію продакшн‑контейнерів, що досі працюють на тегах (без
@sha256:). - Виберіть одну критичну службу і зафіксуйте її за дайджестом у конфігурації.
- Оновіть автоматизацію, щоб за замовчуванням deploy робився з відключеним pull (
--pull never) і щоб промоція нових дайджестів була явною. - Аудитуйте retention реєстру, щоб зафіксовані дайджести були доступні не днями, а місяцями.
- Зберігайте дайджести для релізів, щоб відкат був зміною, а не пошуком.
Вам не потрібні ідеальні інструменти ланцюга постачання, щоб припинити несподівані оновлення. Потрібні детермінізм, дисципліна і відмова трактувати змінні вказівники як контракти.