MAATRIX / Блог / Антипаттерн: latest вместо версии образа

Антипаттерн: latest вместо версии образа

MAATRIX

Если во всех ваших docker-compose.yml и Dockerfile стоит image: postgres:latest или FROM node:latest — вы уже наступили на грабли, просто ещё не знаете об этом. Классический сценарий: сервис месяцами работал стабильно, потом кто-то сделал docker-compose pull && docker-compose up -d для рутинного обновления — и прод лёг, потому что подтянулась мажорная версия базы данных с несовместимым форматом данных. Разберём, почему latest — это ловушка, а не удобство, и как зафиксировать версии так, чтобы обновления происходили по вашему решению, а не по воле тега.

Как это выглядит на практике

Вот типичный docker-compose.yml, который встречается в половине проектов, поднятых "на скорую руку" и так и оставшихся в проде:

version: "3.9"

services:
  db:
    image: postgres:latest
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data

  cache:
    image: redis:latest
    restart: unless-stopped

  app:
    image: myregistry.local/myapp:latest
    restart: unless-stopped
    depends_on:
      - db
      - cache

volumes:
  pgdata:

Логика, которая обычно стоит за этим, звучит примерно так: "latest — значит всегда последняя версия, зачем мне вручную следить за апдейтами базы, пусть само подтягивает свежее". На собеседовании такое объяснение сходит за разумное. В реальной эксплуатации это ровно то место, где инфраструктура превращается в лотерею.

То же самое часто встречается и в Dockerfile для собственных образов:

FROM node:latest
FROM python:latest
FROM nginx:latest

Автор Dockerfile хочет одного — чтобы образ "всегда собирался на свежем". Получает он другое: сегодняшняя сборка и сборка через три месяца могут различаться базовым слоем настолько, что упадёт даже npm install, потому что в новом мажоре Node изменилось поведение нативных зависимостей.

Что такое latest на самом деле

Ключевое заблуждение — считать latest версией. Это не версия. Это просто тег — текстовая метка, указывающая на конкретный образ в реестре в конкретный момент времени. Большинство официальных образов (postgres, redis, nginx, node и так далее) обновляют тег latest так, чтобы он указывал на самую свежую стабильную ветку выпуска, но это соглашение майнтейнеров образа, а не гарантия Docker как платформы.

Практическое следствие: docker pull postgres:latest, выполненный сегодня, и docker pull postgres:latest, выполненный через полгода на другом сервере (или на том же после docker system prune и повторного пуша) — это два разных запроса к реестру, которые могут вернуть два совершенно разных образа. Один — PostgreSQL 16.3, другой — уже 17.1, потому что за это время вышел новый мажорный релиз и тег latest в реестре теперь указывает на него.

Отдельная деталь для собственных образов в приватном реестре: если вы сами публикуете myapp:latest при каждом CI-пуше, то тег двигается ещё быстрее и совершенно не отражает, что реально работает на проде. latest в приватном реестре — это "то, что было собрано последним", а не "то, что задеплоено сейчас". Это два разных состояния, и путать их — прямой путь к путанице между окружениями.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Проблема 1: непредсказуемые breaking changes при передеплое

Самый частый сценарий — рутинное обслуживание. Администратор заходит на сервер раз в квартал, чтобы обновить систему, и заодно гоняет:

docker-compose pull
docker-compose up -d

Если в compose-файле postgres:latest, эта команда молча подтягивает всё, что реестр сейчас считает последним стабильным релизом — включая мажорные обновления. PostgreSQL, MySQL, MongoDB и другие СУБД регулярно меняют формат данных между мажорными версиями и требуют явной миграции (pg_upgrade и подобные инструменты) — простой перезапуск контейнера с новым мажором может не дать серверу вообще стартовать, потому что формат данных на диске не совпадает с ожидаемым новой версией движка.

То же самое актуально для рантаймов приложений. Мажорное обновление Node.js или Python может убрать API, на которые опирается ваш код, изменить поведение по умолчанию (например, работу с TLS или кодировками) или просто требовать пересборки нативных модулей. Проблема не в том, что новая версия "плохая" — проблема в том, что apгрейд произошёл без вашего ведома, в момент, когда вы делали что-то совершенно другое, и без чтения changelog, где обычно прямым текстом написано про breaking changes.

Проблема 2: невозможность воспроизвести окружение

Воспроизводимость — одно из базовых свойств инфраструктуры, ради которого вообще существуют Docker и IaC-практики: один и тот же конфиг должен давать один и тот же результат на любой машине и в любой момент времени. Тег latest ломает это свойство напрямую.

Представьте: вы разворачиваете стек на тестовом сервере сегодня, всё работает. Через полгода нужно поднять точно такой же стек на новом продакшен-сервере — тот же docker-compose.yml, ни строчки не менялось. Но docker-compose pull на новом сервере подтянет актуальный на тот момент latest, который может отличаться от того, что был подтянут полгода назад на тестовом. Формально конфиг идентичен, а реально это два разных окружения с разными версиями зависимостей — и расхождение это никак не видно, пока не появится баг, который воспроизводится на одном сервере и не воспроизводится на другом.

Это же бьёт по CI/CD. Если пайплайн собирает образ на базе FROM python:latest, то сборка, прошедшая вчера, и сборка с тем же коммитом, запущенная сегодня, могут дать разные бинарные артефакты. Тестировали вы одно, задеплоили — по факту — другое. Отладка таких расхождений съедает часы, потому что первое, что проверяют инженеры — это код и конфиги, а не то, что теги образов "поехали" сами по себе.

Проблема 3: откат при проблеме превращается в гадание

Когда что-то в проде ломается после обновления, первый инстинкт — откатиться на предыдущую версию. С зафиксированными версиями это тривиально: смотрите в git-историю compose-файла, видите postgres:16.2 вместо postgres:16.3, откатываете тег, поднимаете контейнер.

С latest этой опоры нет вообще. Какая версия работала вчера? Никто не записывал — тег всегда был один и тот же, latest, менялось лишь то, на что он указывает внутри реестра. Единственный способ узнать, что реально крутилось на проде — смотреть docker inspect уже остановленного или ещё живого контейнера и вытаскивать оттуда digest образа, если контейнер ещё не пересоздан. Если он уже пересоздан (а часто это первое, что делают в панике — "перезапустить и посмотреть") — история потеряна безвозвратно, и откатываться приходится буквально наугад, перебирая версии из changelog проекта.

Подробный разбор именно этого сценария и пошаговых действий при откате — в статье что делать, если обновление положило прод. Но правило простое: если версия не зафиксирована явно, откат — это не "одна команда", а расследование.

Правильный подход: фиксация версии

Базовое правило — никогда не используйте latest в конфигах, которые должны работать предсказуемо. Вместо этого указывайте конкретный тег версии:

services:
  db:
    image: postgres:16.3
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data

  cache:
    image: redis:7.4.0
    restart: unless-stopped

  app:
    image: myregistry.local/myapp:2026.08.3
    restart: unless-stopped

Для собственных образов используйте семантическое версионирование (myapp:1.4.2) или дату сборки/номер коммита (myapp:2026.08.3 или myapp:git-a1b2c3d), но не полагайтесь на latest как на единственный тег в CI. Пусть пайплайн публикует и версионный тег, и — если нужно для внутренних нужд — latest как дополнительный указатель, но деплой всегда идёт по версионному тегу, никогда по latest.

Для Dockerfile то же самое правило:

# Плохо
FROM node:latest

# Хорошо
FROM node:20.17.0-bookworm-slim

Указание конкретной версии базового образа делает сборку воспроизводимой: через год тот же Dockerfile соберёт образ на той же версии Node, а не на той, что окажется актуальной в момент сборки.

Ещё более жёсткая фиксация: digest вместо тега

Даже версионный тег вроде postgres:16.3 — не абсолютная гарантия побитовой идентичности. В редких случаях майнтейнеры пересобирают образ под тем же тегом (например, чтобы подтянуть патч безопасности в базовом слое ОС), и postgres:16.3 сегодня и postgres:16.3 через месяц физически могут быть разными слоями, пусть и с той же версией PostgreSQL внутри.

Для критичных продакшен-систем, где важна не просто "та же версия", а именно тот же самый образ, побайтово, используется фиксация по digest:

services:
  db:
    image: postgres@sha256:9b8b2c1f3e4a5d6c7b8a9f0e1d2c3b4a5968778695a4b3c2d1e0f9a8b7c6d5e4

Digest — это криптографический хэш содержимого образа. В отличие от тега, он не может "переехать" на другой образ: если содержимое меняется, меняется и digest. Узнать digest уже запущенного или скачанного образа:

docker inspect --format='{{index .RepoDigests 0}}' postgres:16.3

или после пула:

docker pull postgres:16.3
docker images --digests postgres

Минус подхода — читаемость. postgres@sha256:9b8b2c1f... ничего не говорит человеку о том, какая версия там внутри, поэтому на практике digest обычно комментируют рядом версией-тегом:

services:
  db:
    # postgres:16.3, зафиксировано 2026-08-20
    image: postgres@sha256:9b8b2c1f3e4a5d6c7b8a9f0e1d2c3b4a5968778695a4b3c2d1e0f9a8b7c6d5e4

Используйте digest там, где цена ошибки высока — платёжные системы, регулируемые данные, инфраструктура, от которой напрямую зависит выручка. Для менее критичных внутренних сервисов обычно достаточно версионного тега — digest добавляет операционную сложность (нужно вручную обновлять хэш при каждом сознательном апгрейде), которая оправдана не везде.

Как обновляться осознанно, а не через latest

Отказ от latest не означает "никогда не обновляться" — означает "обновляться по своему решению, а не случайно". Практическая методика:

  1. Тестовое окружение сначала. Заведите отдельный стек (staging или локальный) с теми же образами, что и прод, но где обновление версии тестируется первым. Это может быть тот же compose-файл с другим .env и отдельным volume.
  2. Читайте changelog перед апгрейдом. У PostgreSQL, Redis, большинства баз и рантаймов есть release notes с разделом "Breaking Changes" или "Upgrade Notes" — там прямо написано, что может сломаться. Пять минут чтения экономят часы отладки на проде.
  3. Обновляйте по одному компоненту. Не тяните разом все версии в compose-файле — меняете тег одного сервиса, тестируете, только потом переходите к следующему. Так проще локализовать, что именно вызвало проблему, если она возникла.
  4. Автоматизируйте отслеживание новых версий, но не автоприменение. Инструменты вроде Renovate умеют мониторить реестр и открывать pull request с предложением обновить тег версии — вы видите diff, читаете, к какой версии переходите, и осознанно мёржите. Это принципиально другая модель, чем latest: обновление видимо, обсуждаемо и обратимо через git-историю. Подробнее о настройке — в статье Renovate: пошаговая установка.
  5. Фиксируйте версию в git вместе с остальным конфигом. Если compose-файл лежит в репозитории (а он должен), смена версии образа — это обычный коммит с понятным сообщением: "postgres: 16.2 → 16.3, security patch". История версий становится частью истории проекта, а не живёт только в голове того, кто когда-то делал docker-compose pull.
  6. Делайте бэкап перед апгрейдом мажорной версии базы. Даже с зафиксированной версией мажорный переход (например, PostgreSQL 16 → 17) требует явной миграции данных и отдельного плана отката на случай проблем — сама по себе фиксация тега не отменяет необходимости резервной копии перед таким шагом.

Разбор смежных ошибок конфигурации продакшен-стека — в статье про частые ошибки docker-compose на проде. Если в проекте вдобавок используются контейнеры с root-правами по умолчанию — это отдельный, не менее частый антипаттерн, разобранный в материале все контейнеры под root.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Если я никогда не обновляю образы вручную, разве latest не удобнее?

Нет — даже если вы не трогаете сервер месяцами, любая пересборка (новый сервер, переустановка, восстановление из бэкапа конфига без volume) подтянет актуальный на тот момент latest, а не тот, что реально работал раньше. Удобство иллюзорно: вы просто откладываете момент, когда несовпадение версий вас удивит.

Что если официального образа с нужной версией уже нет в реестре?

У большинства проектов (PostgreSQL, Redis, nginx, node и т.д.) старые версионные теги хранятся в реестре годами — Docker Hub и другие реестры не удаляют старые теги при выходе новых. Если тег всё же убрали, это будет явная ошибка при пуле, а не молчаливая подмена версии — что само по себе безопаснее, чем незаметный переход через latest.

Нужно ли фиксировать версии для локальной разработки, а не только для прода?

Да, и это особенно важно для команды — если у одного разработчика локально postgres:latest тянет 17-ю версию, а у другого этот же latest был подтянут раньше и указывает на 16-ю, баги, специфичные для версии, будут воспроизводиться у одного и не воспроизводиться у другого. Фиксированная версия в compose-файле, коммитнутая в репозиторий, убирает это расхождение для всей команды разом.

Как быть с образами, у которых нет чёткого семантического версионирования тегов?

Ориентируйтесь на дату или хэш коммита в теге, если проект их публикует (например, myimage:2026-08-20 или myimage:abc1234), либо фиксируйте по digest — это работает для любого образа независимо от схемы версионирования майнтейнера.

Digest усложняет обновления — стоит ли использовать его везде?

Нет, для большинства сервисов достаточно версионного тега — он читаем и обновляется через обычный docker-compose pull после сознательной правки файла. Digest имеет смысл точечно, для компонентов, где цена расхождения образа критична, а не как повсеместная политика.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →