Docker Compose: отличия параметров build и image
Узнайте, почему Docker Compose игнорирует изменения в Dockerfile при использовании параметра image. Разбираем разницу между локальной сборкой и загрузкой готового образа на VPS.
Docker Compose: build или image — краткий ответ
В файле Docker Compose параметр image: указывает имя образа для загрузки из реестра, а build: предписывает Compose собрать образ на текущем сервере из Dockerfile. Если задать только image:, Compose загрузит указанный тег и запустит его. Если задать только build:, Compose соберет образ локально, присвоив ему имя на основе названия проекта и сервиса. Если задать оба параметра, Compose выполнит локальную сборку, а затем присвоит результату тег из image:; это стандартный способ сборки образа с последующей отправкой в реестр под выбранным именем.
В этом заключается основное различие. Все, что описано ниже, касается практического применения на сервере. Предполагается, что Docker Engine и плагин Compose уже установлены; в руководстве запуск Docker на VPS описан этот процесс.
Три основные формы использования
Загрузите опубликованный тег и запустите его. Dockerfile на этом этапе не используется.
services:
web:
image: nginx:1.27
restart: unless-stopped
ports:
- "80:80"Выполните сборку из Dockerfile в текущем каталоге. Ничего не загружается, кроме базового образа, указанного в FROM.
services:
web:
build: .
restart: unless-stopped
ports:
- "80:80"Выполните локальную сборку и присвойте результат тегу. docker compose push затем может отправить этот конкретный тег в реестр.
services:
web:
build:
context: .
dockerfile: Dockerfile
image: registry.example.com/acme/web:1.4.2
restart: unless-stopped
ports:
- "80:80"context — это каталог, который передается сборщику. dockerfile разрешается относительно этого контекста, поэтому context: . с dockerfile: docker/prod.Dockerfile — это стандартная и корректная практика. Выполните docker compose images, чтобы увидеть имя образа и ID образа для каждого контейнера сервиса; это самый быстрый способ подтвердить, какую именно из этих трех форм вы использовали.
Почему docker compose up не выполняет пересборку после изменения Dockerfile?
Потому что up проверяет наличие образа, а не его актуальность.
Когда Compose запускает сервис, содержащий секцию build:, он ищет образ в локальном хранилище. Если образ с таким именем уже существует, Compose использует его. Он не читает ваш Dockerfile, не сравнивает исходные файлы и не проверяет временные метки. Спецификация Compose определяет это правило через атрибут pull_policy, и поведение по умолчанию подразумевает сборку образа только в том случае, если он отсутствует. Наличие образа считается достаточным условием.
Таким образом, вы редактируете app.py, запускаете docker compose up -d, видите, что Compose сообщает о работающем контейнере, но сервис продолжает использовать старый код. Ошибок не возникает, поэтому система не выдает предупреждений. Это самая частая причина жалоб в духе «мои изменения не вступили в силу» при работе с Compose. Признаком проблемы служит статус, который Compose выводит рядом с именем контейнера: контейнер, который Compose заменил, помечается как recreated или started, а контейнер, который Compose решил не трогать, помечается как running.
Две проверки помогут прояснить ситуацию. docker compose images выводит ID образа, который использует каждый контейнер: запишите его до развертывания и сравните после. В выводе docker image ls есть колонка CREATED; если образ был создан до вашего последнего коммита, он является устаревшим, независимо от того, что вывело сообщение о развертывании.
Какие флаги принудительно запускают пересборку
docker compose up -d --buildсначала выполняет сборку, а затем пересоздает любой контейнер, чей образ изменился. Это флаг, который ищет большинство пользователей.docker compose build webсобирает один сервис и ничего не запускает. Используйте его вместе сdocker compose up --no-deps -d web, чтобы заменить только этот контейнер, оставив остальной стек запущенным.docker compose build --no-cache webотбрасывает все кэшированные слои и выполняет сборку с первой инструкции.docker compose build --pullпытается загрузить более новую версию базового образа, указанного вFROM, поэтому динамический тег, такой какnode:22, получит актуальное содержимое вместо копии, загруженной вами в марте.docker compose up -d --force-recreateпересоздает контейнеры из уже используемого ими образа. Он никогда не выполняет сборку. Использование этого флага вместо--build— частый тупиковый путь.
Вы также можете перенести решение в файл. Согласно спецификации Compose, pull_policy: build означает, что Compose собирает образ и пересобирает его, если он уже существует. Каждая команда up в этом случае требует выполнения сборки, что удобно на ноутбуке, но редко требуется на сервере.
services:
web:
build: .
image: registry.example.com/acme/web:dev
pull_policy: buildСтоит знать еще об одном взаимодействии. docker compose pull пытается загрузить образы для сервисов, у которых есть секция build; если загрузка не удается, он сообщает, что образ должен быть собран. Передайте --ignore-buildable, чтобы пропустить такие сервисы без вывода сообщений.
Как кэш сборки определяет время развертывания
Каждая инструкция в Dockerfile создает слой, и сборщик повторно использует кэшированный слой, если эта инструкция и её входные данные не изменились. Для COPY входными данными является содержимое копируемых файлов. Как только один слой не находит соответствия в кэше, все последующие слои пересобираются, так как каждый слой строится на основе файловой системы, созданной предыдущим.
Это правило определяет, займет ли развертывание секунды или минуты. Располагайте инструкции в Dockerfile от тех, что меняются редко, к тем, что меняются при каждом коммите.
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]npm ci находится выше COPY . ., поэтому изменение исходного файла оставляет слой с установкой зависимостей в кэше, а сборка возобновляется с шага копирования. Поменяйте эти две строки местами, и изменение одного символа приведет к переустановке всех зависимостей, так как COPY . . делает недействительным слой, на котором строится npm ci. Тот же принцип применим к pip install -r requirements.txt и go mod download.
--no-cache — подходящий инструмент, если вы подозреваете, что устаревший слой скрывает исправление. Использовать его по умолчанию не стоит, так как это отменяет преимущество повторного использования слоев, ради которого и выстраивается порядок инструкций в Dockerfile.
Один параметр, который задается в образе, но может быть переопределен в Compose: CMD в Dockerfile — это то, что образ запускает по умолчанию, а ключ command: в описании сервиса заменяет его. Как взаимодействуют command и entrypoint важно здесь, так как переопределение в Compose может привести к тому, что свежесобранный образ будет вести себя точно так же, как старый.
Контекст сборки и .dockerignore
context: . означает, что Compose упаковывает указанный каталог и отправляет его сборщику до выполнения первой инструкции. Передается всё содержимое, включая .git и любые каталоги с данными, которые вы храните рядом с исходным кодом. Если сборка зависает на этапе передачи контекста (transferring context) в проекте, который не менялся, это означает, что контекст слишком велик.
Файл .dockerignore в корне контекста исключает пути из этой передачи. Синтаксис близок к .gitignore.
.git
node_modules
*.log
data/
.envЭто дает два преимущества. Объем передаваемых данных сокращается, поэтому каждая сборка начинается быстрее. Кроме того, COPY . . больше не сможет скопировать .env в образ, откуда любой, кто скачает этот образ, сможет прочитать содержимое файла.
Случай медленной сборки, которая со временем становится еще медленнее, связан с bind mount. Именованный том (named volume) находится вне каталога проекта, но bind mount, такой как ./data:/var/lib/postgresql/data, располагается внутри контекста сборки, поэтому сборки замедляются каждую неделю по мере роста базы данных. Одна строка в .dockerignore решает эту проблему. В Bind mounts против именованных томов рассматриваются более широкие аспекты этого выбора.
Аргументы сборки несут меньшую, но аналогичную угрозу. Значения, переданные через args:, видны в истории образа любому, у кого есть этот образ, поэтому указывайте там только номера версий, но никогда не токены. В Env-файлы и секреты в Compose описано, где следует хранить учетные данные.
Стоит ли выполнять сборку на VPS или собирать в другом месте и загружать образ?
Сборка на сервере, который обслуживает ваш трафик, является стандартным вариантом, так как это кратчайший путь: git pull, затем docker compose up -d --build. Это допустимо на небольшом сервере, от которого пока никто не зависит. Однако такой подход становится неприемлемым по двум причинам, которые можно измерить, и одной, которая проявляется только в критический момент.
Память. Процесс сборки запускает компиляторы и сборщики (bundlers) параллельно с работающим приложением, а они потребляют больше всего памяти в большинстве стеков. На VPS с 1 GB оперативной памяти JavaScript-сборщик или компиляция Rust часто становятся самыми ресурсоемкими процессами. Когда ядру не хватает памяти, оно завершает самый «тяжелый» процесс: либо сборка прерывается с Killed и кодом выхода 137, либо завершается база данных, и сайт падает в процессе развертывания. dmesg -T | grep -i oom выводит строку с именем процесса, поэтому вы сможете точно определить, что именно произошло, вместо того чтобы гадать.
Диск. Каждая сборка оставляет после себя слои, а система сборки хранит собственный кэш отдельно от ваших образов. docker system df показывает и то, и другое, при этом строка кэша сборки постоянно растет. Очищайте место с помощью docker image prune для удаления «висящих» образов и docker builder prune для удаления кэшированных слоев. Переполнение диска останавливает не только сборку. База данных также перестает записывать данные, и этот сбой обходится гораздо дороже, чем медленное развертывание.
Воспроизводимость. Образ, собранный на сервере, существует только на этом сервере. Откат к предыдущей версии означает необходимость снова переключиться на старый коммит и выполнить сборку, но нет гарантии, что результат будет идентичен предыдущему, так как базовый тег мог измениться, а вместе с ним и зеркала пакетов. Сборка в другом месте и отправка тега превращают откат в простую правку: укажите image: на предыдущий тег и выполните docker compose up -d.
Наиболее надежная схема проста. Ваша система непрерывной интеграции (CI) выполняет сборку и отправляет registry.example.com/acme/web:<git-sha>, а файл Compose на VPS содержит image: без ключа build: вообще. Развертывание в таком случае сводится к двум командам, которые практически не потребляют память.
docker compose pull
docker compose up -dВыполните docker login registry.example.com один раз на сервере, и после этого Compose сможет загружать приватные теги.
Сохраните секцию сборки для разработки, а не удаляйте её, поместив в файл с собственным именем.
# compose.dev.yaml
services:
web:
build:
context: .
pull_policy: builddocker compose -f compose.yaml -f compose.dev.yaml up -d --buildНазовите этот файл compose.dev.yaml, а не compose.override.yaml. Compose автоматически загружает файл переопределения, если он присутствует, поэтому случайно скопированный на сервер файл переопределения может незаметно запустить сборку прямо на нем. Объединение нескольких файлов Compose объясняет, как при слиянии разрешаются конфликты ключей.
Ловушка архитектуры при сборке на сторонних системах
Образ содержит информацию об архитектуре процессора, для которой он был собран. Если собрать образ на ноутбуке с Apple Silicon, отправить его в реестр, а затем вытянуть этот тег на VPS с архитектурой x86_64, Docker выдаст предупреждение о том, что платформа запрашиваемого образа не совпадает с платформой хоста. Процесс завершится с ошибкой exec format error, которая выглядит как поврежденный бинарный файл, хотя таковым не является. Выполняйте сборку явно под целевую платформу:
docker buildx build --platform linux/amd64 \
-t registry.example.com/acme/web:1.4.2 --push .Такое же несоответствие возникает в обратной ситуации, если ваш ноутбук работает на x86, а вы запускаете ARM VPS вместо x86. Сборка в CI-системе на той же архитектуре, на которую вы планируете развертывание, полностью устраняет эту проблему.
Что проверить после развертывания
docker compose imagesвыводит образ и тег для каждого запущенного контейнера. Измененный ID образа подтверждает, что новая сборка введена в эксплуатацию.docker compose configвыводит итоговый файл после подстановки переменных, что позволяет увидеть финальное имя образа, которое будет использовать Compose до запуска.docker compose logs -f webв течение первых 30 секунд после замены. Контейнер, который запускается и сразу завершается, будет перезапускаться в цикле, и этот процесс не будет заметен, если не проверить логи.docker image lsотображает столбец CREATED. Если образ старше вашего последнего коммита, значит, пересборка не выполнялась.
Если вы еще формируете файл, для которого выполняются эти проверки, в основах файла Compose на VPS описаны ключевые параметры, а в шпаргалке по командам Compose перечислены остальные подкоманды.
FAQ
Можно ли использовать build и image в одном сервисе?
Да, это стандартная конфигурация для проекта, который вы собираете самостоятельно. Compose выполняет сборку из секции build: и помечает результат тегом из значения image:. Этот тег docker compose push отправляет в реестр, и именно его загружает другая машина. Без ключа image: Compose всё равно выполнит сборку, но присвоит образу имя на основе проекта и сервиса, а также выдаст предупреждение о том, что отсутствие атрибута не позволит отправить образ в реестр.
Почему docker compose up не подхватывает изменения в Dockerfile?
Потому что up проверяет только наличие образа с таким именем. Если образ уже существует, Compose запускает его и не сравнивает с Dockerfile или исходным кодом. Выполните docker compose up -d --build или запустите docker compose build web, а затем docker compose up --no-deps -d web, чтобы заменить конкретный сервис. Установка pull_policy: build для сервиса заставляет выполнять up при каждом запуске, что удобно для среды разработки.
В чем разница между --build и --force-recreate?
--build пересобирает образ, а затем пересоздаёт контейнеры, образ которых изменился. --force-recreate пересоздаёт контейнеры из уже имеющегося образа, поэтому изменения в коде не будут применены. Если изменения внесены в исходный код или Dockerfile, используйте флаг --build. --force-recreate предназначен для сброса состояния самого контейнера, например, для очистки его записываемого слоя при сохранении того же образа.
Стоит ли собирать Docker-образы на VPS или в другом месте?
Собирайте образы в другом месте и загружайте тег, когда сервер уже обслуживает трафик. Процесс сборки конкурирует с приложением за оперативную память, и на небольшом VPS это приводит к тому, что ядро завершает самый ресурсоёмкий процесс — это может быть как сборка, так и база данных. Сборка также оставляет кэш на диске, который не очищается автоматически. Сборка на сервере допустима для небольшого проекта без пользователей, а перенос процесса в будущем не потребует усилий, если вы вынесете секцию build: в отдельный файл Compose для разработки.
Как предотвратить заполнение диска кэшем сборки Docker?
Выполните docker system df, чтобы увидеть, сколько места занимают образы и кэш сборки. docker builder prune удаляет кэшированные слои, а docker image prune удаляет «висящие» образы, оставшиеся от предыдущих сборок. Добавление -a к любой из этих команд действует более агрессивно и заставляет следующую сборку выполняться «с нуля». Не настраивайте автоматический запуск docker system prune -af --volumes на сервере, так как --volumes удаляет любые тома, которые не используются ни одним контейнером в данный момент, а остановленный для обслуживания стек хранит вашу базу данных именно в таком томе.