JavaRush /Курсы /Docker for Spring /Конфигурация контейнерного запуска

Конфигурация контейнерного запуска

Docker for Spring
11 уровень , 2 лекция
Открыта

1. Три канала конфигурации: env, -D и args

Когда вы впервые слышите «env vars», «system properties» и «application args», мозг естественно пытается упростить: “да какая разница, всё же просто значения”. И это правда — но только наполовину. Разница появляется ровно в тот момент, когда приложение перестаёт жить «внутри IDE на вашей машине» и начинает жить как процесс в контейнере: вы внезапно видите, что значения можно передать в разных местах, и они ещё и перекрывают друг друга.

Чтобы не превращать конфигурацию в гадание на кофейной гуще, полезно прямо сейчас построить очень прагматичную картину:

  • env vars — это то, что вы обычно передаёте контейнеру «снаружи» как часть окружения;
  • system properties — это то, что вы передаёте JVM через -D... (то есть это “внутренний словарь JVM”);
  • application args — это то, что вы передаёте самому Spring Boot приложению как аргументы --key=value.

Spring Boot умеет читать все три, но читать — не значит «вы никогда не перепутаете». Перепутаете. Мы все перепутывали. И это нормально, пока у вас есть чёткая схема в голове.

Этими же каналами можно передавать и spring.profiles.active, и простой учебный app.mode. Здесь важно поймать именно способ доставки значения, а не спорить, какой ключ сейчас удобнее.

Ниже — простая картинка, как эти каналы входят в приложение:

flowchart TD
  A["Host: терминал разработчика"] -->|docker run -e ...| B[Container env vars]
  A -->|java -D...| C[JVM system properties]
  A -->|--key=value| D[Spring Boot application args]
  B --> E[Spring Boot property sources]
  C --> E
  D --> E
  E --> F[Итоговое значение свойства]

Обратите внимание на важный момент: код приложения почти никогда не должен знать, откуда конкретно пришло значение. Он должен работать с итогом. Именно поэтому мы в примерах часто читаем значения через @Value("${...}"): это быстрый способ увидеть “что победило”.

2. Env vars: контейнерная «любовь с первого запуска»

Env vars (переменные окружения) в контейнерном мире любят по очень простой причине: их удобно передавать снаружи и удобно менять без пересборки. Вы меняете не образ, а параметры запуска. Это идеально совпадает с нашим правилом “same image, different runtime config”. И да — тут обычно новички совершают первую большую ошибку: пытаются «прошить» режим в Dockerfile через ENV, а потом удивляются, почему им приходится копировать Dockerfile для каждого режима.

Как env vars попадают в Spring Boot приложение

Внутри контейнера переменные окружения — это просто набор строк KEY=VALUE. Spring Boot читает их и может сопоставить с именами свойств (как именно сопоставить — отдельная тема следующей лекции про relaxed binding, сегодня нам важна идея и практика).

Возьмём один очень дружелюбный для контейнера пример — порт. В Spring Boot свойство — server.port, а “типичная” переменная окружения — SERVER_PORT. Это как раз тот случай, где даже без глубоких правил именования всё выглядит логично.

Если у вас уже есть небольшая runtime-ручка, для этой проверки достаточно одного поля:

@Value("${server.port}")
private String port;

Наружу его можно отдать тем же способом, что и остальные диагностические значения. Нам здесь важен не новый controller, а сам факт, что Spring честно показывает победивший server.port.

Теперь запуск локально (вне Docker) может выглядеть так: вы задаёте env var в вашей OS и запускаете приложение обычным способом. На Linux/macOS это будет одна команда, на Windows — другая, но идея одинаковая: значение должно оказаться в окружении процесса, который запускает Java.

Если же вы запускаете в Docker, вы передаёте env var контейнеру:

# --rm: удалить контейнер после остановки (для локальных экспериментов удобно)
# -p 8080:8080: проброс порта с хоста в контейнер (сеть Docker)
# -e SERVER_PORT=8080: переменная окружения внутри контейнера (конфиг приложения)
docker run --rm \
  -p 8080:8080 \
  -e SERVER_PORT=8080 \
  docker-java-catalog-service

Здесь есть тонкость, на которую новичок почти всегда наступает: -p 8080:8080 публикует порт контейнера наружу, но не меняет порт, на котором приложение слушает внутри контейнера. То есть “публикация порта” и “настройка server.port” — это две независимые вещи (и это не баг, а нормальная модель сети в Docker).

Env vars и Docker-запуск

Если говорить по-честному, env vars — это “самый ленивый способ сделать правильно”. Они хорошо сочетаются с тем, как запускается контейнер:

  • вы не переписываете ENTRYPOINT;
  • вы не усложняете команду запуска Java;
  • вы просто говорите: “контейнер, вот тебе значения”.

В Docker-контексте это ещё удобно потому, что env vars легко читать глазами: вы открыли команду запуска и видите, что происходило. Это особенно ценно, когда образ один, а режимов много.

И тут же — типичный анти‑паттерн: «давайте запишем это в Dockerfile через ENV». Технически это работает, но вы тихо превращаете образ в «окружение». А мы как раз пытаемся сделать обратное: образ — это код, окружение — это запуск.

3. System properties (-D...): значения внутри JVM

System properties — это свойства, которые живут внутри JVM и задаются обычно так: -Dkey=value. Новичку они кажутся “ещё одним способом передать то же самое”, но в контейнерной жизни они имеют свои характерные роли и свои характерные ловушки.

Главное различие в ощущении такое: env vars — это «положили бумажку на стол контейнеру», system properties — это «положили бумажку прямо в карман JVM перед стартом». Spring Boot умеет читать и то, и другое, но при конфликтах у system properties обычно приоритет выше (мы это уже обсуждали на уровне общей лестницы приоритетов).

Запуск с -D... локально

Представим, что мы запускаем уже собранный jar. Упрощённая команда:

java -Dserver.port=8082 -jar app.jar

Если одновременно в application.yml был server.port: 8080, а вы передали -Dserver.port=8082, то приложение стартует на 8082, потому что system property перекрывает file-based конфиг.

Важно не перепутать “system property” с “любой опцией JVM”. Например, -Xmx256m — это настройка памяти JVM, но не system property. -D... — именно “key/value”, который попадёт в System.getProperties().

System properties в контейнере без правок Dockerfile

В контейнерном мире не хочется каждый раз переписывать команду запуска Java. И вот здесь всплывает практичная вещь: JVM читает переменную окружения JAVA_TOOL_OPTIONS и добавляет её к своим опциям. Это удобно, потому что вы продолжаете жить в “env var world”, но фактически влияете на JVM‑уровень.

Пример (чисто демонстрационный):

# JAVA_TOOL_OPTIONS будет прочитан JVM и добавлен к параметрам запуска
# Здесь мы через env var создаём system property -Dserver.port=8082
docker run --rm \
  -p 8082:8082 \
  -e JAVA_TOOL_OPTIONS="-Dserver.port=8082" \
  docker-java-catalog-service

С точки зрения Spring Boot это будет выглядеть как “server.port пришёл из system properties”.

Есть важный психологический момент: вы вроде “передали переменную окружения”, а реально создали system property, которое имеет более высокий приоритет. Именно так появляются ситуации “я передал SERVER_PORT, но почему-то оно игнорируется”. Ответ часто такой: потому что где-то в JAVA_TOOL_OPTIONS (или прямо в команде java) вы уже задали -Dserver.port, и оно перекрыло env var.

Когда system properties уместны

В обычной команде разработки часто есть негласное правило: “всё, что относится к Spring Boot конфигурации, отдаём env vars или --... аргументам, а system properties оставляем для JVM‑вещей”. Это не закон природы, но как минимум снижает вероятность хаоса.

Если вы начинающий, вы можете взять простую дисциплину: env vars — главный канал для Docker‑запуска, system properties — инструмент для ситуаций, когда вам действительно нужно воздействовать на JVM‑уровень или вы явно хотите приоритет “выше env vars”.

4. Application args (--key=value): высокий приоритет

Application args — это аргументы, которые вы передаёте приложению в момент запуска, и Spring Boot умеет превращать аргументы вида --key=value в свойства. Именно поэтому в Boot‑мире так популярны команды вроде --server.port=9090. Они читаются как “на этом запуске делай так”.

Тут у новичка часто ломается логика, потому что внешне -Dserver.port=9090 и --server.port=9090 выглядят почти одинаково, но это вообще разные дороги. Первое заходит через JVM, второе — через аргументы приложения.

Запуск jar с application args

Классический пример:

# После -jar всё, что начинается с --, будет считаться аргументами приложения (Spring Boot)
java -jar app.jar --server.port=8083 --app.mode=standalone

Spring Boot возьмёт эти значения и применит их как свойства. Если где-то ниже (в application.yml или env vars) тоже были значения, application args почти всегда окажутся выше по приоритету.

Запуск Docker-контейнера с application args

Вот тут становится особенно интересно (и полезно), потому что это напрямую завязано на то, как у вас устроены ENTRYPOINT и CMD.

Если ваш Dockerfile устроен в нормальном стиле (exec-form ENTRYPOINT), например:

# Exec-form ENTRYPOINT: всё, что вы допишете в docker run после имени образа,
# станет аргументами для java (а значит, и для Spring Boot)
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

…то всё, что вы добавите в конце docker run ... image ..., будет дописано как аргументы к этому ENTRYPOINT.

Пример:

# Всё после имени образа docker-java-catalog-service — это уже args приложения
docker run --rm \
  -p 8083:8083 \
  docker-java-catalog-service \
  --server.port=8083 --app.mode=standalone

То есть по сути мы сделали “контейнерный запуск” и “Boot‑аргументы” одной строкой. Это выглядит чуть длиннее, чем env vars, но часто очень удобно для разового запуска “проверить гипотезу”, не меняя ни Dockerfile, ни переменные окружения.

Application args и конфигурационные «призраки»

Если вы один раз где-то сохранили команду запуска (в истории терминала, в IDE run configuration, в скрипте), добавили --server.port=..., а потом забыли про это — вы будете мучительно смотреть на application.yml и думать, что Spring Boot вас игнорирует.

Он не игнорирует. Он просто честно выполняет правило: “самое сильное значение побеждает”. А application args — очень сильный канал.

5. Взаимодействие env vars, -D и args

Когда мы говорим “приоритет источников”, важно понимать одну вещь: приоритет работает не «файлом целиком», а ключом. То есть можно иметь десяток свойств, и каждое может прийти из разных каналов. В итоге получится “сборная солянка”, но не в плохом смысле: это нормальная модель внешней конфигурации.

Давайте зафиксируем упрощённую лестницу (ровно ту, которая нам нужна в этом дне курса, без редких источников и тонких нюансов):

Канал Пример записи Где задаём Приоритет (в рамках сегодняшней модели)
File-based config
server.port: 8080
application.yml внутри jar
ниже
Env vars
SERVER_PORT=8081
docker run -e ...
выше файла
System properties
-Dserver.port=8082
java ...
или
JAVA_TOOL_OPTIONS
выше env vars
Application args
--server.port=8083
аргументы запуска приложения
выше всего из перечисленного

Теперь представим ситуацию (да, это “умственный эксперимент”, но он очень жизненный):

- в application.yml написано server.port=8080;
- вы запускаете контейнер с -e SERVER_PORT=8081;
- вы ещё и добавили -e JAVA_TOOL_OPTIONS="-Dserver.port=8082";
- и в конце вы передали --server.port=8083.

Победит 8083, потому что application args в этой цепочке самые приоритетные.

Чтобы увидеть конфликт вживую, хватает той же диагностической ручки, которая уже показывает mode и server.port. Меняется способ передачи значения, а проверяете вы всё тот же итог.

И важное наблюдение: диагностическая ручка не знает, откуда пришёл app.mode. Она видит итог. Это нормально. Это даже хорошо: мы не хотим писать код “если пришло из env vars — делай одно, если из args — делай другое”. Мы хотим написать код, который работает с конечной конфигурацией.

Частый Docker‑подвох с портами

Эта тема уже мелькала в курсе раньше, но именно в день про конфигурацию она начинает стрелять особенно больно. Новичок меняет SERVER_PORT, но забывает поменять -p, или наоборот. В итоге симптомы выглядят как “приложение не поднимается”, “контейнер сломан”, “Spring Boot не слушает порт”, хотя на самом деле всё работает — просто вы смотрите не туда.

Если внутри контейнера приложение слушает 8083, то публикация должна быть вида -p 8083:8083 (или любой hostPort:8083, если вы хотите поменять порт на хосте). Например:

# Внутри контейнера приложение слушает 8083 (SERVER_PORT=8083)
# Снаружи (на хосте) мы публикуем это как localhost:9090
docker run --rm \
  -e SERVER_PORT=8083 \
  -p 9090:8083 \
  docker-java-catalog-service

В этом примере приложение внутри контейнера слушает 8083, а с хоста вы заходите на localhost:9090. Это может быть удобно, но только если вы явно понимаете, что делаете.

И вот здесь рождается полезное правило для учебного проекта: внутренний порт приложения лучше держать стабильным, а если нужно — менять внешний порт публикации. То есть чаще менять -p, чем server.port. Но понимать оба подхода — обязательно, потому что жизнь не всегда идеальна.

6. Типичные ошибки конфигурации

Ошибка №1: путать -D... и --... и считать их одним и тем же.
Снаружи оба варианта выглядят как “я задал server.port”, но первый вариант — system property JVM, второй — аргумент приложения Spring Boot. В результате вы можете случайно получить другое поведение приоритета, а ещё — другой способ передачи в Docker (например, JAVA_TOOL_OPTIONS может перекрывать всё незаметно). Лечится просто: держите в голове, что -D — это карман JVM, а -- — это слова, которые вы сказали Spring Boot на старте.

Ошибка №2: ожидать, что изменение application.yml всегда даёт эффект.
Если вы хоть раз запускали приложение с --server.port=..., то можете вообще не увидеть эффекта от редактирования файла. И это не магия и не “Boot странный”, это честный порядок property sources. Самый простой способ отладки — временно вывести итоговое значение через маленький endpoint вроде /api/runtime и убедиться, что вы реально смотрите на итоговую конфигурацию, а не на файл “как он написан”.

Ошибка №3: “я пробросил порт -p, значит server.port тоже поменялся”.
Docker не трогает ваш процесс и не переучивает Spring Boot слушать другой порт. -p — это про сеть Docker, а server.port — про приложение. Если вы поменяли одно, а другое оставили прежним, контейнер может быть жив, но вы будете стучаться в закрытую дверь (иногда даже очень уверенно). Помогает дисциплина: фиксируем, что слушает приложение, и отдельно фиксируем, что вы публикуете наружу.

Ошибка №4: задавать одно и то же свойство сразу через env vars, JAVA_TOOL_OPTIONS и --..., а потом гадать, “кто победил”.
В какой-то момент новичок начинает “на всякий случай” прокидывать значение всеми способами. Это почти гарантированно создаёт ситуацию, где вы уже не можете объяснить поведение. На практике полезно выбрать один главный канал “по умолчанию” для вашего проекта (часто это env vars для Docker‑запуска), а остальные держать как осознанные инструменты для редких случаев.

Ошибка №5: пытаться «закодировать окружение» в Dockerfile через ENV.
ENV в Dockerfile — это не зло, но это дефолт образа. Когда вы пишете туда SPRING_PROFILES_ACTIVE=postgres, вы делаете так, что ваш образ “по умолчанию” — postgres. Потом вам нужен standalone, и вы начинаете либо пересобирать образ, либо копировать Dockerfile, либо писать костыли. А мы как раз строим модель, где образ один, а режимы приходят при запуске. Поэтому ENV лучше держать как “разумный default”, а конкретные режимы передавать снаружи.

1
Задача
Docker for Spring, 11 уровень, 2 лекция
Недоступна
Один и тот же `server.port` через три канала запуска
Один и тот же `server.port` через три канала запуска
1
Задача
Docker for Spring, 11 уровень, 2 лекция
Недоступна
Один ключ сразу в env vars, system properties и application args
Один ключ сразу в env vars, system properties и application args
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ