Полтора года мы собирали образы на том же сервере, где они и запускались. Работало до момента, когда сборка упёрлась в память: контейнер с mvn package выел 1,5 ГБ, ядро OOM-killer выбрало самый жирный процесс и убило заодно базу. Деплой, который роняет прод, — это не деплой.

Что плохого в сборке на сервере

  • Память и процессор делятся с рабочими сервисами. Сборка — самый ресурсоёмкий процесс в жизни сервера.
  • Тег latest пересобирается непредсказуемо. «На сервере сейчас какой образ?» — вопрос без ответа.
  • Нет повторяемости. Зависимости кэшируются локально, и два запуска на двух машинах дают разные результаты.

Как делаю теперь

Сборка в CI, на сервер приходит только готовый артефакт по неизменяемому тегу (хэш коммита), и запускается он через docker compose с явным тегом.

services:
  app:
    image: registry.example.com/app:${TAG:?TAG is required}
    restart: unless-stopped
    mem_limit: 768m
    environment:
      - JAVA_TOOL_OPTIONS=-XX:MaxRAMPercentage=70

Три детали:

  • ${TAG:?TAG is required} — compose откажется стартовать без явного тега. Никаких «взял что было».
  • mem_limit — контейнер не имеет права вытеснить базу. Правильный лимит защищает от того, чтобы одна утечка превратилась в отказ всего сервера.
  • MaxRAMPercentage=70 — JVM в контейнере читает лимит и сам держится в нём, вместо того чтобы гадать по памяти хоста.

Про кэш сборки

Если сборка в CI каждый раз с нуля — это минус десять минут жизни. Кэш слоёв (cache-from/cache-to) решается за одну строку в CI-конфиге, но работает только при правильном порядке инструкций в Dockerfile: сначала зависимости, потом исходники. Копирование всего проекта первой строкой обнуляет кэш при любом изменении.

Когда собирать локально на сервере всё же нормально

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