1. Профиль cache: смысл и роль
Когда новички впервые слышат слово «профиль», часто появляется мысль: «Ага, это как ещё одна папка с настройками, которую никто не читает». В реальном проекте профиль — это не декорация, а способ сказать приложению, что мир вокруг него изменился. И в Docker/Compose это особенно важно: зависимости появляются и исчезают, а мы хотим, чтобы приложение включало нужные куски инфраструктуры только тогда, когда они реально доступны.
Здесь профиль cache решает две прикладные задачи, и обе очень контейнерные по сути.
Первая задача — сделать Redis опциональной зависимостью, а не «новым обязательным смыслом жизни приложения». У нас должен оставаться режим postgres без кэша, и он обязан работать даже если Redis вообще не поднят. Это защищает нас от ситуации «всё сломалось, потому что я хотел ускорить чтение, а не переписать архитектуру».
Вторая задача — вписать Redis в принцип курса same image, different runtime config. То есть мы не собираем «образ с Redis» и «образ без Redis». Образ один, а поведение меняется через SPRING_PROFILES_ACTIVE=postgres,cache и конфигурацию, которая включается ровно в этом режиме.
Само включение кэширования мы уже вынесли в отдельную конфигурацию под профилем cache. Этого достаточно: app-side abstraction подготовлена, @Cacheable-методы уже есть. Теперь профиль нужен не для новой магии внутри кода, а чтобы тот же слой начал жить с внешним Redis и правильным runtime wiring.
Это маленькое различие полезно держать в голове. Пока cache не активен, приложение просто работает без этого дополнительного слоя. Когда профиль включён, к чтениям добавляется Redis-backed кэш, а основной image и основной HTTP-контракт не меняются.
2. Service name в Compose: Redis по имени сервиса
В Compose есть одна «магия», которую нужно не бояться, а приручить: встроенный DNS. Он делает ровно одну вещь, зато очень полезную: позволяет контейнерам находить друг друга по имени сервиса. Это как телефонная книга внутри маленького «города Compose»: написали в YAML redis — значит внутри сети появилось имя redis, которое резолвится в IP этого контейнера.
Именно поэтому в контейнерном мире localhost — это не «компьютер», а «я сам». Внутри контейнера localhost указывает на этот же контейнер. Если приложение попытается подключиться к localhost:6379, оно будет честно искать Redis у себя внутри (а у себя внутри у него обычно только Spring Boot и тоска).
Нам нужна следующая картина:
flowchart TD
Browser["Ваш браузер (host)"] -->|HTTP :8080| App["app (Spring Boot)"]
App -->|JDBC| PG["postgres"]
App -->|Redis protocol| R["redis"]
subgraph "Compose network (встроенная сеть)"
App
PG
R
end
Обратите внимание на тонкость: браузер ходит на localhost:8080 на хост-машине, потому что мы публикуем порт приложения наружу. Но внутри Compose-сети приложение ходит в базу и Redis по именам postgres и redis. Это два разных «мира», и их очень важно не путать.
Ещё одна важная деталь для будущей диагностики: service name — это не container_name. Мы не хотим привязываться к container_name, потому что он часто превращается в костыль. Compose и без него отлично умеет: redis — значит redis.
3. Spring Boot: application-cache.yml и env vars
На этом шаге мы делаем то, ради чего вообще существует Docker в этом курсе: подключаем новую зависимость без изменения образа. То есть наш Dockerfile не должен превращаться в «швейцарский нож», где на каждый режим появляется отдельный RUN echo "...redis...". Вместо этого мы используем то, что уже выучили: профили и внешнюю конфигурацию.
Но тут есть одна техническая ступенька, которую легко пропустить. spring-boot-starter-cache дал нам аннотации и cache abstraction, но не научил приложение разговаривать с Redis. Для Redis-backed варианта на classpath нужна Redis-интеграция — обычно spring-boot-starter-data-redis. Если эта зависимость уже есть в проекте, то application-cache.yml не создаёт магию из воздуха, а просто говорит Spring: используй Redis как backend для уже подготовленного cache-layer.
В проекте у нас есть профильный файл application-cache.yml. В нём мы описываем две вещи: «кэш теперь Redis» и «куда подключаться».
spring:
cache:
# Сообщаем Spring, что реализация кэша — Redis, а не in-memory
type: redis
# Явно фиксируем имена кэшей, которые будет использовать приложение
cache-names: catalog-items, catalog-item-by-id
data:
redis:
# По умолчанию — имя сервиса в Compose-сети; можно переопределить env-переменной
host: ${SPRING_DATA_REDIS_HOST:redis}
# Порт по умолчанию стандартный; тоже можно переопределить env-переменной
port: ${SPRING_DATA_REDIS_PORT:6379}
Здесь есть очень аккуратная идея: мы даём значения по умолчанию, которые подходят для Compose (redis:6379), но при этом оставляем возможность переопределить их env-переменными. Это полезно по двум причинам. Во-первых, вы можете запускать этот же образ в другом окружении, где Redis называется иначе (например, redis-cache, или вообще это внешний Redis в облаке). Во-вторых, это подчёркивает принцип курса: конфигурация меняется снаружи, образ остаётся один.
Чтобы не путаться, полезно держать в голове мини-таблицу соответствий, потому что начинающие разработчики часто путают «как называется свойство в YAML» и «как назвать переменную окружения»:
| Что настраиваем | Spring property | Env var (relaxed binding) | Типичное значение в Compose |
|---|---|---|---|
| Хост Redis | spring.data.redis.host | SPRING_DATA_REDIS_HOST | redis |
| Порт Redis | spring.data.redis.port | SPRING_DATA_REDIS_PORT | 6379 |
| Активные профили | spring.profiles.active | SPRING_PROFILES_ACTIVE | postgres,cache |
Обратите внимание на запятые в профилях: postgres,cache — это не «две переменные», а одна строка со списком профилей. Spring Boot спокойно это понимает.
И ещё один момент, который я проговорю вслух, потому что он экономит часы жизни. Мы не добавляем в код никаких проверок «если Docker, то хост такой». Это путь в “docker-ветку логики” в коде, а потом в “а почему на проде не так”. У нас есть профиль и конфигурация — этого достаточно.
4. Redis в compose.yaml: отдельный сервис
Когда хочется «быстрее», бывает соблазн сделать странное: поставить Redis прямо внутрь контейнера приложения. Это примерно как поселить почтальона в вашу квартиру, потому что так письма быстрее приходят. Сначала кажется удобно, потом становится страшно, когда вы понимаете, что у почтальона свои жизненные циклы, порты, логи и характер.
В Compose Redis должен быть отдельным сервисом. Минимальная версия выглядит вот так:
services:
redis:
image: redis:8.6.0
healthcheck:
# Проверяем готовность реальной командой Redis (а не "контейнер запущен")
test: ["CMD", "redis-cli", "ping"]
# Как часто проверяем
interval: 5s
# Таймаут на одну проверку
timeout: 3s
# Сколько неудачных попыток подряд считаем "ещё не готов"
retries: 5
# Сколько ждём перед началом проверок после старта контейнера
start_period: 10s
Мы сразу добавили healthcheck, хотя формально можно было бы «пока без него». Но у нас в курсе уже есть привычка: зависимости должны быть наблюдаемыми, иначе вы снова вернётесь в мир гаданий «ну вроде он уже поднялся… наверное».
Теперь нужно связать приложение с Redis через профиль. В app сервисе мы активируем postgres,cache и (опционально) прокидываем адрес Redis env-переменными. Да, они совпадают с дефолтами из application-cache.yml, но это нормально: мы делаем wiring явным, чтобы новичок видел, где что включается.
services:
app:
environment:
# Включаем и базу, и кэш — это два независимых профиля
SPRING_PROFILES_ACTIVE: postgres,cache
# Redis внутри Compose-сети доступен по имени сервиса "redis"
SPRING_DATA_REDIS_HOST: redis
# Стандартный Redis-порт
SPRING_DATA_REDIS_PORT: 6379
И вот здесь есть важный “Compose-момент”: мы не обязаны публиковать порт Redis на host-машину. Приложение общается с Redis внутри сети Compose. Публикация порта нужна обычно только если вы хотите подключаться к Redis с хоста вручную (например, локальным redis-cli). Для курса это не обязательно, а иногда даже вредно, потому что у студентов на машине уже может быть Redis и начнутся конфликты портов.
5. Readiness Redis: healthcheck и depends_on
Когда вы добавляете вторую инфраструктурную зависимость в стек, старые проблемы возвращаются в новом костюме. И главная из них — «порядок старта». Redis обычно поднимается быстро, но Docker-опыт показывает: «обычно» — это слово, которое ломает демо на занятии.
Поэтому мы повторяем паттерн, который уже выучили на PostgreSQL: healthcheck + depends_on.condition: service_healthy. Это не просто удобство. Это превращает Compose-стенд из «иногда работает» в «работает по правилам».
Дополняем app сервис зависимостями:
services:
app:
depends_on:
postgres:
# Ждём, пока Postgres пройдёт healthcheck
condition: service_healthy
redis:
# Ждём, пока Redis начнёт отвечать на ping (PONG)
condition: service_healthy
Получается простая логика: пока Postgres не готов и Redis не отвечает PONG, приложение не стартует. И это именно то, что нам нужно в учебном стенде: минимум случайности, максимум повторяемости.
Здесь полезно понимать границу: depends_on в режиме service_healthy не делает ваш стенд «продакшен-оркестрацией». Он всего лишь делает запуск честным. Мы не усложняем стек, мы убираем гадания.
А redis-cli ping в healthcheck — это почти идеальный пример проверки готовности, потому что он проверяет ровно то, что нам нужно: «Redis жив и отвечает на команду». И заодно это команда, которую вы сможете выполнить руками в диагностике.
6. Быстрые проверки связки
После правок в конфигурации и Compose хочется получить не философию, а уверенность: «оно правда работает». На этом этапе нам не нужно ещё доказывать hit/miss; сначала важно убедиться, что инфраструктура поднялась и приложение стартовало в правильном режиме.
Начинаем с обычного запуска:
# Поднимаем стенд и пересобираем образ приложения при необходимости
docker compose up --build
Потом смотрим состояние:
# Проверяем статусы контейнеров и (если есть) healthcheck-статус
docker compose ps
Если Redis healthy, вы увидите в статусе что-то вроде healthy (формат зависит от Docker Desktop/CLI). Если не healthy — смотрим логи Redis:
# Смотрим, что именно не так с Redis (например, не стартует или не проходит healthcheck)
docker compose logs -f redis
И наконец — проверка, которую понимают даже люди, которые не любят YAML (такие люди существуют, просто они обычно не пишут Compose-файлы добровольно). Проверяем Redis изнутри контейнера Redis:
# Выполняем ping внутри контейнера Redis: ожидаем ответ PONG
docker compose exec redis redis-cli ping
# PONG
PONG — это маленькое счастье. Оно означает: контейнер жив, сеть работает, и Redis отвечает.
Дальше проверяем приложение. Логи приложения почти всегда содержат строку про активные профили. Ищем её:
# Смотрим логи приложения и проверяем, что активировались postgres и cache
docker compose logs -f app
В логах Spring Boot вы должны увидеть что-то, что явно говорит: активны postgres и cache. Если там только postgres, значит профиль cache не включён. Если там cache, но нет postgres — вы случайно переключили приложение в режим без базы, и дальше начнутся очень странные вопросы «почему JPA ругается».
И последняя быстрая проверка (без кэш-магии): HTTP эндпоинт и health. Мы не доказываем ускорение, мы доказываем, что сервис жив.
# Проверяем, что приложение отвечает с хоста (порт проброшен наружу)
curl http://localhost:8080/actuator/health
Если вы видите UP, значит базовый wiring собран: контейнеры живы, профили включены, приложение видит Redis. Это ещё не доказательство miss -> hit — здесь мы проверили именно readiness и связность стека.
7. Типичные ошибки при работе с Redis в Compose
Ошибка №1: Redis host указывается как localhost.
Это самая частая проблема, и она почти всегда выглядит одинаково: приложение стартует, но при попытке обратиться к Redis вы получаете ошибки подключения. Внутри Compose localhost — это сам контейнер приложения. Redis живёт в другом контейнере, и правильный host — это имя сервиса redis. Лечится это не “дополнительным пробросом портов”, а правильной настройкой spring.data.redis.host.
Ошибка №2: профиль cache включили, а @EnableCaching живёт без профиля.
Иногда кэширование включено всегда, а Redis — только иногда. Тогда приложение в режимах без Redis может начать вести себя странно: оно вроде бы живёт, но при обращениях к кэшу внезапно пытается ходить в localhost:6379 или просто падает ошибками, которые новичок воспринимает как «сломался Spring». Самое спокойное решение для учебного проекта — держать @EnableCaching под @Profile("cache"), чтобы механизм включался только когда вы этого хотите.
Ошибка №3: Redis добавили как сервис, но забыли про readiness-модель.
Redis поднимается быстро, и поэтому кажется, что healthcheck — “перестраховка”. Но именно “перестраховка” и делает ваш стенд воспроизводимым. Без healthcheck вы однажды поймаете ситуацию, где приложение стартануло раньше Redis и первый же запрос дал ошибку. Самое неприятное, что это будет «через раз». В учебной среде флаки — враг: лучше один раз настроить healthcheck и больше не думать.
Ошибка №4: Смешали режимы и получили непонятную комбинацию профилей.
Профили — это не украшение, а контракт. Если вы случайно включили cache, но забыли postgres, вы получите поведение не того режима, который ожидаете. Если включили postgres, но забыли cache, Redis будет поднят, но приложение будет жить как будто его нет. Поэтому для дня 19 полезно фиксировать в Compose явную строку SPRING_PROFILES_ACTIVE: postgres,cache и не надеяться, что “где-то по умолчанию всё само”.
Ошибка №5: Попытка “настроить Redis в Dockerfile”.
Иногда хочется «быстро» и «в одном месте»: прописать host/port Redis в Dockerfile через ENV. Это ломает главный принцип курса: один и тот же image должен запускаться в разных режимах. Redis может быть, может не быть, он может называться по-разному, порт может меняться. Dockerfile — не место для таких “живых” настроек. Для этого существуют профили, env vars и Compose.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ