Новое имя. Пустая база?
Новое имя. Пустая база?

Снаружи проект уже называется по-новому. Домены, логотип, тексты, почта — всё переехало. Но на сервере до сих пор живёт строка:

name: legacy-prod

И контейнер базы по-прежнему называется примерно так:

legacy-prod-db-1

Это не забытый пункт ребрендинга. Я оставил старое имя специально.

Когда начал готовить переезд, первой мыслью было заменить старое название по всему репозиторию. Обычный поиск находил его в Compose, названиях баз, OAuth-клиентах, переменных окружения, миграциях и путях. Выглядело как механическая работа на вечер.

Но строка в заголовке Compose — не подпись под логотипом. Это namespace для инфраструктуры. Если «красиво» переименовать её вместе с сайтом, Docker не перенесёт данные. Он создаст новый набор ресурсов и запустит на них полностью исправное приложение.

Именно полностью исправное. С зелёными healthcheck и пустой базой.

Почему name: влияет на данные

В production Compose-файле база подключена к логическому тому postgres_data:

name: silvercode-prod

services:
  db:
    image: postgres:17-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Но в Docker Engine том получает не просто имя postgres_data. Compose добавляет namespace проекта:

silvercode-prod_postgres_data

Если поменять одну строку:

- name: silvercode-prod
+ name: iconcode-prod

следующий docker compose up -d запросит уже другой том:

iconcode-prod_postgres_data

Такого тома нет, поэтому Compose создаст его. Postgres увидит пустой каталог, штатно выполнит инициализацию, создаст базы из init-скрипта и станет healthy.

Смена project name создаёт другой набор volumes
Смена project name создаёт другой набор volumes

Старый том никуда не исчезает. Данные физически остаются на диске. Но приложение больше к ним не подключено, поэтому снаружи картина почти не отличается от потери данных:

  • список пользователей пуст;

  • контент исчез;

  • заказы исчезли;

  • миграции стартуют на чистой схеме;

  • администратор снова видит первоначальную установку.

На этом месте легко сделать ситуацию хуже. Увидеть пустую базу, решить, что старый stack больше не нужен, и запустить docker compose down -v. Вот тогда временная ошибка маршрутизации тома превращается в настоящее удаление.

Я стараюсь вообще не использовать -v в процедурах обновления. Удаление volumes не должно быть побочным эффектом обычного деплоя.

Исчезает не только PostgreSQL

В моём Compose постоянное состояние разбито на несколько томов. При смене project name каждый получает нового двойника:

Логическое имя

Что покажется потерянным

postgres_data

пользователи, контент, платежи и настройки

auth_keys

RSA-ключи подписи токенов

code_uploads

изображения портфолио и загруженные файлы

code_bot_data

сохранённые сессии административного бота

call_media

ещё живые вложения звонков

prometheus_data, grafana_data

история метрик и настройки мониторинга

caddy_data

ACME-состояние и сертификаты Caddy

Самый неприятный том после базы — auth_keys. При первом старте сервис авторизации создаёт пару RSA-ключей. Приватным ключом подписываются access-токены, публичный раздаётся через JWKS.

Новый пустой том означает новую пару ключей. База при этом может быть восстановлена идеально, но все ранее выданные токены перестанут проходить проверку подписи. Для пользователя это выглядит как массовый выход из аккаунтов после «обычного» изменения бренда.

Поэтому резервная копия базы без копии ключевого тома в такой системе неполна.

Как увидеть ловушку до деплоя

Compose позволяет проверить resolved-конфигурацию без запуска контейнеров:

docker compose \
  -f docker-compose.silvercode-prod.yml \
  config | sed -n '1,25p'

Текущие проекты и принадлежащие им тома можно посмотреть отдельно:

docker compose ls

docker volume ls \
  --filter label=com.docker.compose.project=silvercode-prod

А у конкретного тома полезно проверить метки:

docker volume inspect silvercode-prod_postgres_data \
  --format '{{json .Labels}}'

Я делаю эту проверку до up, а не после. Если в новом конфиге поменялся project name, список ожидаемых физических имён тоже поменялся — даже когда секция volumes: выглядит совершенно одинаково.

У project name есть несколько источников. По приоритету Compose учитывает флаг -p, переменную COMPOSE_PROJECT_NAME, верхнеуровневый name: и только потом имя каталога. Поэтому проверить один YAML недостаточно: CI или deploy-скрипт может переопределить его снаружи.

Самое простое решение: не переименовывать

Я выбрал скучный вариант:

name: silvercode-prod

остался неизменным.

Внутреннее имя stack не показывается клиентам, не участвует в SEO и не мешает новому бренду. Его переименование не добавляет продукту ни одной возможности. Зато требует миграции состояния и отдельного rollback-плана.

Старое техническое имя в данном случае — не долг. Долгом было бы отсутствие документации о том, почему оно зафиксировано.

Если название всё же нужно изменить, я бы сначала отвязал физические тома от namespace проекта:

name: iconcode-prod

volumes:
  postgres_data:
    name: silvercode-prod_postgres_data
    external: true

  silver_id_keys:
    name: silvercode-prod_silver_id_keys
    external: true

  code_uploads:
    name: silvercode-prod_code_uploads
    external: true

name: внутри описания volume задаёт физическое имя как есть, без префикса нового stack. А external: true заставляет Compose завершиться ошибкой, если том не найден, вместо молчаливого создания пустого.

Это позволяет переименовать проект, продолжая использовать старые хранилища. Позже каждый том можно перенести отдельно, с резервной копией и проверкой. Для PostgreSQL я предпочту логический dump/restore, а не копирование живого каталога /var/lib/postgresql/data.

Например, перед любыми изменениями:

docker compose exec -T db \
  pg_dumpall -U silver > dump.sql

Ключи и загруженные файлы копируются отдельно. На время такой операции запись должна быть остановлена либо переведена в режим обслуживания. Архивировать изменяемый том и надеяться на консистентность — плохая миграционная стратегия.

client_id — тоже не название приложения

Второй кандидат на красивое переименование выглядел так:

SSO_CLIENT_ID: silvercode

Во фронтенде передаётся то же значение. Сервис авторизации хранит его в таблице OAuth-клиентов, записывает в authorization codes и refresh tokens, а access-токен получает claim:

{
  "aud": "silvercode"
}

Бэкенд проверяет aud. Refresh endpoint сверяет, какому client_id принадлежит токен. Поэтому замена legacy-web → newbrand-web только на фронтенде ломает вход сразу. Замена во всех сервисах требует зарегистрировать нового OAuth-клиента, перенести разрешённые redirect URI и решить судьбу уже выпущенных refresh tokens.

Это возможно. Но это миграция протокольной идентичности, а не правка текста. Пользовательское название клиента можно сменить отдельно, оставив стабильный client_id.

По той же причине я не переименовывал базу legacy_app. Имя базы видно только строкам подключения и администраторам. Чтобы поменять его без практической пользы, пришлось бы координировать подключения, backup-скрипты, monitoring и процедуры восстановления.

Публичные и внутренние имена меняются по-разному
Публичные и внутренние имена меняются по-разному

Что действительно нужно менять при переезде домена

Стабильные внутренние идентификаторы не означают, что достаточно заменить логотип.

Новый домен затрагивает:

  • публичный issuer сервиса авторизации;

  • точные OAuth redirect_uri и post-logout URI;

  • Domain у SSO-cookie;

  • CORS allowlist;

  • canonical, Open Graph, sitemap и robots.txt;

  • адреса webhook и callback у внешних систем;

  • Caddy и выпуск новых сертификатов;

  • постоянные редиректы со старых адресов.

OAuth redirect URI я добавлял до переключения трафика. Authorization server сравнивает его целиком, поэтому новый callback, не зарегистрированный заранее, заканчивается ошибкой входа.

Cookie перенести между двумя независимыми доменами нельзя. Cookie для .old.example браузер не отправит на .new.example. Даже при сохранении базы, refresh tokens и RSA-ключей пользователю может понадобиться один раз войти на новом домене. 301 эту границу не отменяет.

С issuer похожая история. Старый access-токен содержит старое значение iss; сервис, уже настроенный только на новый issuer, должен его отклонить, даже если подпись сделана тем же ключом. В моём случае access-токен живёт недолго, поэтому проще учитывать короткое переходное окно, чем учить все сервисы принимать две идентичности бессрочно.

Старые домены после переключения остаются в Caddy только как редиректы:

{$LEGACY_CODE_DOMAIN}, www.{$LEGACY_CODE_DOMAIN} {
    redir https://{$CODE\\_DOMAIN}{uri} permanent
}

{$LEGACY_ID_DOMAIN} {
    redir https://{$ID\\_DOMAIN}{uri} permanent
}

Путь сохраняется: старая ссылка на конкретную страницу ведёт на ту же страницу нового домена, а не на главную.

Почему мой rebrand-скрипт почти ничего не заменяет

Скрипт запускается в dry-run по умолчанию и меняет только две группы строк:

старый-домен.example → новый-домен.example
СтароеНазвание       → НовоеНазвание

Строчное старое имя проекта он намеренно не трогает. По одной строке невозможно понять, перед нами подпись в интерфейсе или идентификатор базы, Compose-проекта либо OAuth-клиента.

Скрипт также пропускает:

  • боевой .env — домен Caddy переключается только после DNS;

  • уже применённые миграции Alembic;

  • бинарные файлы и зависимости;

  • сам скрипт ребрендинга;

  • каталоги и внутренние пути.

Старую миграцию нельзя «исправить» заменой домена: она описывает историю, которая уже произошла на production-базе. Если URL хранится в строках таблицы, для него нужна новая миграция данных.

После dry-run я просматриваю список файлов вручную. Автоматизация здесь нужна не для того, чтобы убрать человека из процесса, а чтобы не пропустить сотню публичных ссылок и при этом не тронуть стабильные идентификаторы.

Порядок, который оставляет путь назад

У меня получился такой порядок переезда:

  1. Сделать dump баз, архив ключей подписи и копию загруженных файлов.

  2. Зафиксировать текущий Compose project name и реальные имена volumes.

  3. Добавить новые DNS-записи и OAuth redirect URI, не отключая старые.

  4. Выполнить dry-run замены и проверить каждое изменение .env отдельно.

  5. Развернуть новый код со старым Compose project name и старыми protocol ID.

  6. Проверить количество пользователей, fingerprint публичного ключа JWKS и наличие файлов.

  7. Переключить публичные домены и включить 301 со старых.

  8. Не удалять старую конфигурацию до окончания rollback-окна.

Главное правило здесь одно: не смешивать ребрендинг и миграцию хранилищ в одну операцию. Если после выкладки что-то пошло не так, должно быть понятно, какой слой откатывать.

Я не получил красивый newbrand-prod_postgres_data. Зато получил переезд, после которого данные продолжили лежать там же, где лежали до него.

И считаю это правильным обменом. Инфраструктурный идентификатор не обязан соответствовать названию на вывеске. Если старое имя никому не видно и его изменение несёт только риск, последовательность иногда важнее аккуратности.

Механика project name и порядок его переопределения описаны в документации Docker Compose. Поведение явного name и external для томов — в Compose volume reference. Требование точного совпадения redirect URI закреплено в RFC 10017, а правила области cookie — в RFC 6265.