Миграция данных с docker-compose

Инструкция по переносу данных со старого сервера, развёрнутого из репозитория citeck-community (docker-compose), на новый сервер, развёрнутый через Citeck Launcher v2 в серверном режиме.

Рассчитана на администратора с root-доступом к обоим серверам. Все шаги выполняются вручную, отдельными командами в терминале.

Предупреждение

Если установка кастомизирована, команды и логины/пароли нужно будет подкорректировать под Ваше окружение.

Соглашения

  • На source команды используют синтаксис docker compose (Compose v2, plugin). Если у вас установлен legacy docker-compose (v1) – заменяйте docker compose на docker-compose в каждой команде.

  • Команды этапа 2 (выгрузка) и предусловий выполняются из каталога citeck-community (там, где лежит docker-compose.yaml), иначе compose не найдёт сервисы.

  • На target launcher2 управляет контейнерами напрямую через Docker SDK (без compose) – там используется docker exec с реальными именами контейнеров (citeck_postgres_default, citeck_mongo_default, citeck_zookeeper_default).

Что мигрируется и что нет

Мигрируется:

  • PostgreSQL – пользовательские БД (схема + данные), точечно по mapping-таблице.

  • MongoDB – одна БД ecos-processciteck_eproc.

  • Zookeeper – содержимое каталога version-2/ (snapshots + transaction log).

НЕ мигрируется (то есть выходит за рамки этой инструкции – технически данные ниже тоже можно перенести, если у кого-то такая задача возникнет, но пошаговую процедуру для них мы здесь не описываем):

  • БД keycloak – на target создаётся самим launcher2. На source соответствующая БД называется ecos_identity (сервис ecos-identity-app в community – это Keycloak старой версии).

  • Пользователи, роли, клиенты Keycloak источника (включая demo-аккаунт admin/admin). Если они заводились вручную – пересоздать после миграции. Admin на target использует пароль, сгенерированный launcher’ом при первом старте (показывается в визарде один раз; перевыпустить через citeck setup admin-password).

  • RabbitMQ – очереди и in-flight сообщения.

  • Volumes proxy/nginx – логи, кеш сертификатов Let’s Encrypt.

  • Секреты (JWT, OIDC client secret, admin password) – launcher2 генерирует свои при первой установке.

Матрица совместимости

Компонент

Source

Target

Тип миграции

PostgreSQL

12.7

17.5

логический dump (pg_dump -F c)

MongoDB

4.0

4.0.2

mongodump --archive

Zookeeper

3.8.2 (Bitnami)

3.9.4 (official)

копия version-2/ из dataDir

RabbitMQ

пропускаем

Keycloak БД

пропускаем

PostgreSQL переезжает с мажорным скачком версии (12 → 17). Физическое копирование data-каталога (volume) не сработает – нужен только логический dump.

Mapping БД и пользователей

В launcher2 действует соглашение: имя_БД == имя_пользователя == пароль. На target пароли пользовательских БД совпадают с именами этих БД.

PostgreSQL

Source DB

Source owner

Target DB

Target owner

ecos_apps

apps

citeck_eapps

citeck_eapps

ecos_uiserv

uiserv

citeck_uiserv

citeck_uiserv

ecos_integrations

integrations

citeck_integrations

citeck_integrations

ecos_model

model

citeck_emodel

citeck_emodel

ecos_notifications

notifications

citeck_notifications

citeck_notifications

ecos_history

history

citeck_history

citeck_history

ecos_process

process

citeck_eproc

citeck_eproc

ecos_camunda

camunda

citeck_camunda

citeck_camunda

ecos_edi

edi

citeck_edi

citeck_edi

Не мигрируются (на source игнорируем):

  • ecos_gateway – в launcher2 у gateway нет своей БД.

  • ecos_identity – это БД сервиса ecos-identity-app, который в community-сетапе является Keycloak (просто более старой версии). Соответствует решению «БД keycloak не мигрируем» – пользователи и роли источника при миграции теряются (см. «НЕ мигрируется» во вступлении).

  • keycloak – на target создаёт сам launcher2.

MongoDB

Source DB

Target DB

ecos-process

citeck_eproc

Zookeeper

Source путь (хост)

Target путь (хост)

services/ecos-community-demo-data/zookeeper-app/data/version-2/

/opt/citeck/data/runtime/default/volumes/zookeeper2/data/version-2/ (snapshots) + /opt/citeck/data/runtime/default/volumes/zookeeper2/datalog/version-2/ (logs)

Особенности:

  • Source (Bitnami) хранит и снапшоты, и transaction log в одном каталоге version-2/.

  • Target (official zookeeper) разделяет: data/version-2/ для снапшотов и служебных файлов (acceptedEpoch, currentEpoch), datalog/version-2/ для transaction log. Если их не разделить, target падает с SnapDirContentCheckException.

  • Хранилище в launcher2 – это bind-mount каталог на хосте (не named docker volume). Точный путь можно подтвердить через docker inspect citeck_zookeeper_default --format '{{(index .Mounts 0).Source}}'.

Предусловия

0. Резервный backup source ДО любых действий

Это страховка на случай, если апгрейд до 2026.1 (предусловие 1) сломает старый сервер. Backup НЕ используется для миграции – только для отката source.

Простейший вариант (с остановкой сервера):

docker compose stop
tar -czf citeck-community-backup-$(date +%F).tar.gz \
  services/ecos-community-demo-data/ \
  services/backups/
docker compose start

Если останавливать сервер нельзя – pg_dumpall + mongodump + копия каталога zookeeper в горячем режиме на работающих контейнерах.

1. Source обновлён до релиза 2026.1

Это снимает риск рассинхронизации schema/changelog при заливке в launcher2 (тоже 2026.1). В каталоге citeck-community:

git pull
docker compose up -d

Дождаться, пока все микросервисы дойдут до RUNNING / healthy – Liquibase должен догнать changelog. Проверить можно так:

docker compose ps

2. Target подготовлен

Установлен citeck-launcher версии ≥ 2.x, bundle community:2026.1 или enterprise:2026.1 (в зависимости от лицензии). Подробности первого запуска – в этапе 1.

3. Свободное место

  • На source: ≥ объём данных × 1.5 (для дампов).

  • На target: то же.

4. Доступ к docker

docker ps без sudo либо через sudo на обоих серверах.

Этап 1. Подготовка target (launcher2)

Цель этапа: получить запущенный один раз namespace, чтобы launcher создал инфраструктуру (контейнеры + docker volumes), завёл пустые БД с пользователями и сгенерировал секреты.

1.1. Установить launcher

Если ещё не установлен:

curl -fsSL https://github.com/Citeck/citeck-launcher/releases/latest/download/install.sh | bash

В TUI-визарде выбрать bundle community:2026.1 или enterprise:2026.1 (под лицензию). Записать сгенерированный admin-пароль – он показывается один раз. Если пароль потерян – после миграции его можно перевыпустить через citeck setup admin-password.

1.2. Дождаться полного запуска

citeck status -w

Все приложения должны быть в статусе RUNNING. На сервере с 16 GB RAM это занимает 5–15 минут (для enterprise – дольше). Этот шаг критичен: если webapp’ы не успеют отработать Liquibase на пустых БД, на этапе 4 импорт пойдёт в БД с неполной схемой.

1.3. Остановить все webapp’ы, оставив инфраструктуру

Состав webapp в community и enterprise отличается, единого списка не приводим.

  1. Посмотреть текущий список:

    citeck status
    
  2. Остановить всё, что не относится к инфраструктуре. Инфраструктурные контейнеры, которые остаются запущенными для импорта: postgres и mongo (zookeeper тоже инфраструктура, но его остановим непосредственно перед заливкой данных в шаге 4.3).

    Перечислить остальные приложения списком (gateway, eapps, emodel, uiserv, history, notifications, integrations, eproc, transformations, proxy, и так далее – что вернёт citeck status):

    citeck stop <app1> <app2> <app3> ...
    

    Команда citeck stop <app> помечает приложение как detached: после очередного citeck start без аргументов оно автоматически не поднимется. Это нужно, чтобы пока идёт restore, никакой webapp не писал в БД.

1.4. Учётные данные для импорта

Искать вручную не нужно:

  • PostgreSQL: postgres / postgres (захардкожено в launcher2).

  • MongoDB: пароль root сгенерирован launcher’ом, читается через docker exec citeck_mongo_default printenv MONGO_INITDB_ROOT_PASSWORD. Команды импорта в этапе 4 берут его именно так.

Этап 2. Выгрузка с source

Все команды этого этапа выполняются на старом сервере, из каталога citeck-community. Используются service-имена из docker-compose.yaml:

  • PostgreSQL – ecos-microservices-postgresql-app

  • MongoDB – mongodb-app

  • Zookeeper – zookeeper-app

Подход: запускаем pg_dump / mongodump через docker compose exec -T, перенаправляем stdout прямо в файл на хосте. Никаких промежуточных docker cp и временных файлов внутри контейнера.

Рабочий каталог для дампов в инструкции – ~/citeck-migration/. Создаём подкаталоги заранее:

mkdir -p \
  ~/citeck-migration/postgres \
  ~/citeck-migration/mongo \
  ~/citeck-migration/zookeeper

2.1. PostgreSQL

Параметры pg_dump:

  • -F c – формат custom (компактный, поддерживает параллельный restore).

  • --no-owner --no-acl – выкидываем OWNER TO / GRANT (на target пользователи называются иначе, ownership проставит pg_restore --role).

  • --clean --if-exists НЕ используем – целевую БД чистим отдельно командой DROP SCHEMA.

  • Без -f /tmp/...pg_dump пишет в stdout, мы перенаправляем > в файл на хосте.

Примечание

Если какая-то из БД отсутствует на вашем source (например, в community-only deployment нет ecos_edi) – pg_dump упадёт с database "<name>" does not exist. Просто пропустите соответствующий блок: соответствующая target-БД на launcher2 либо отсутствует, либо останется пустой и Liquibase webapp’а наполнит её при первом старте.

Команды для каждой БД из mapping-таблицы (выполнять блоками, по одному):

ecos_apps:

docker compose exec -T \
  -e PGPASSWORD=postgresstorngpassword \
  ecos-microservices-postgresql-app \
  pg_dump -U postgres -F c --no-owner --no-acl ecos_apps \
  > ~/citeck-migration/postgres/ecos_apps.dump

ecos_uiserv:

docker compose exec -T \
  -e PGPASSWORD=postgresstorngpassword \
  ecos-microservices-postgresql-app \
  pg_dump -U postgres -F c --no-owner --no-acl ecos_uiserv \
  > ~/citeck-migration/postgres/ecos_uiserv.dump

ecos_integrations:

docker compose exec -T \
  -e PGPASSWORD=postgresstorngpassword \
  ecos-microservices-postgresql-app \
  pg_dump -U postgres -F c --no-owner --no-acl ecos_integrations \
  > ~/citeck-migration/postgres/ecos_integrations.dump

ecos_model:

docker compose exec -T \
  -e PGPASSWORD=postgresstorngpassword \
  ecos-microservices-postgresql-app \
  pg_dump -U postgres -F c --no-owner --no-acl ecos_model \
  > ~/citeck-migration/postgres/ecos_model.dump

ecos_notifications:

docker compose exec -T \
  -e PGPASSWORD=postgresstorngpassword \
  ecos-microservices-postgresql-app \
  pg_dump -U postgres -F c --no-owner --no-acl ecos_notifications \
  > ~/citeck-migration/postgres/ecos_notifications.dump

ecos_history:

docker compose exec -T \
  -e PGPASSWORD=postgresstorngpassword \
  ecos-microservices-postgresql-app \
  pg_dump -U postgres -F c --no-owner --no-acl ecos_history \
  > ~/citeck-migration/postgres/ecos_history.dump

ecos_process:

docker compose exec -T \
  -e PGPASSWORD=postgresstorngpassword \
  ecos-microservices-postgresql-app \
  pg_dump -U postgres -F c --no-owner --no-acl ecos_process \
  > ~/citeck-migration/postgres/ecos_process.dump

ecos_camunda:

docker compose exec -T \
  -e PGPASSWORD=postgresstorngpassword \
  ecos-microservices-postgresql-app \
  pg_dump -U postgres -F c --no-owner --no-acl ecos_camunda \
  > ~/citeck-migration/postgres/ecos_camunda.dump

ecos_edi:

docker compose exec -T \
  -e PGPASSWORD=postgresstorngpassword \
  ecos-microservices-postgresql-app \
  pg_dump -U postgres -F c --no-owner --no-acl ecos_edi \
  > ~/citeck-migration/postgres/ecos_edi.dump

После выполнения в ~/citeck-migration/postgres/ должно быть 9 файлов:

ls ~/citeck-migration/postgres/
# ecos_apps.dump  ecos_camunda.dump  ecos_edi.dump  ecos_history.dump
# ecos_integrations.dump  ecos_model.dump  ecos_notifications.dump
# ecos_process.dump  ecos_uiserv.dump

Примечание

Если пароль postgres был изменён (вместо postgresstorngpassword из services/environments/ecos-microservices-postgresql-app.env) – заменить значение PGPASSWORD=... во всех командах.

2.2. MongoDB

Дамп одной БД ecos-process. Логин/пароль root – из services/environments/mongodb-app.env. Если они менялись – заменить в команде. --archive без значения = вывод в stdout.

docker compose exec -T mongodb-app mongodump \
  --username root_user --password root_user_password --authenticationDatabase admin \
  --db ecos-process --archive --gzip \
  > ~/citeck-migration/mongo/ecos-process.archive

После выполнения в ~/citeck-migration/mongo/ должен быть один файл ecos-process.archive.

2.3. Zookeeper

Делаем на остановленном контейнере, чтобы не получить частично записанный snapshot. Bind-mount каталог принадлежит uid Bitnami zookeeper (1001), а не вашему пользователю – поэтому tar запускаем во временном контейнере, чтобы не зависеть от sudo:

docker compose stop zookeeper-app
docker run --rm \
  -v "$(pwd)/services/ecos-community-demo-data/zookeeper-app/data:/src:ro" \
  -v "$HOME/citeck-migration/zookeeper:/dst" \
  alpine sh -c "
    tar -czf /dst/version-2.tar.gz -C /src version-2 && \
    chown $(id -u):$(id -g) /dst/version-2.tar.gz
  "
docker compose start zookeeper-app

Путь services/ecos-community-demo-data/zookeeper-app/data – это host-путь bind-mount из services/zookeeper-app.yaml. Compose разрешает относительные пути относительно файла сервиса (т. е. services/...), а не корня репо. Если в вашем развёртывании layout другой – указать свой путь.

2.4. Итоговая структура каталога

~/citeck-migration/
├── postgres/   # 9 .dump файлов
├── mongo/      # 1 .archive
└── zookeeper/  # version-2.tar.gz

Этап 3. Передача файлов

Передайте каталог ~/citeck-migration/ (PostgreSQL-дампы + Mongo-архив + Zookeeper-tarball) на целевой сервер любым удобным способом: rsync, scp, S3, переносной носитель – на ваше усмотрение.

На целевом сервере положите его по тому же пути – ~/citeck-migration/. Важно сохранить структуру с тремя подкаталогами (postgres/, mongo/, zookeeper/) – команды этапа 4 ссылаются на пути вида ~/citeck-migration/postgres/<имя>.dump. Если файлы скопированы плоско в одну папку, импорт сломается.

Этап 4. Загрузка на target

Все команды – на новом сервере. Имена контейнеров launcher2 в серверном режиме (namespace всегда default):

  • PostgreSQL – citeck_postgres_default

  • MongoDB – citeck_mongo_default

  • Zookeeper – citeck_zookeeper_default

4.1. PostgreSQL

На каждую пару БД из mapping-таблицы – три действия:

  1. Скопировать дамп в контейнер.

  2. Очистить все пользовательские схемы целевой БД (Liquibase на первом старте создал в ней служебные объекты; их надо снести, иначе restore конфликтует по именам).

  3. Выполнить pg_restore с указанием --role=<target_user>, чтобы ownership объектов навешивался корректно.

Расширения (pg_trgm, uuid-ossp и др.) восстанавливаются от имени postgres--role на CREATE EXTENSION не влияет.

Блок команд для каждой пары (повторить 9 раз):

ecos_apps → citeck_eapps:

docker cp ~/citeck-migration/postgres/ecos_apps.dump citeck_postgres_default:/tmp/

docker exec -i \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  psql -U postgres -d citeck_eapps <<'SQL'
DO $$ DECLARE r RECORD; BEGIN
  FOR r IN SELECT schema_name FROM information_schema.schemata
           WHERE schema_name NOT IN ('pg_catalog','pg_toast','information_schema')
  LOOP EXECUTE 'DROP SCHEMA IF EXISTS '||quote_ident(r.schema_name)||' CASCADE'; END LOOP;
END $$;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO citeck_eapps;
SQL

docker exec \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  pg_restore -U postgres \
    -d citeck_eapps \
    --no-owner --no-acl --role=citeck_eapps \
    -j 4 /tmp/ecos_apps.dump

docker exec citeck_postgres_default rm /tmp/ecos_apps.dump

ecos_uiserv → citeck_uiserv:

docker cp ~/citeck-migration/postgres/ecos_uiserv.dump citeck_postgres_default:/tmp/

docker exec -i \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  psql -U postgres -d citeck_uiserv <<'SQL'
DO $$ DECLARE r RECORD; BEGIN
  FOR r IN SELECT schema_name FROM information_schema.schemata
           WHERE schema_name NOT IN ('pg_catalog','pg_toast','information_schema')
  LOOP EXECUTE 'DROP SCHEMA IF EXISTS '||quote_ident(r.schema_name)||' CASCADE'; END LOOP;
END $$;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO citeck_uiserv;
SQL

docker exec \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  pg_restore -U postgres \
    -d citeck_uiserv \
    --no-owner --no-acl --role=citeck_uiserv \
    -j 4 /tmp/ecos_uiserv.dump

docker exec citeck_postgres_default rm /tmp/ecos_uiserv.dump

ecos_integrations → citeck_integrations:

docker cp ~/citeck-migration/postgres/ecos_integrations.dump citeck_postgres_default:/tmp/

docker exec -i \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  psql -U postgres -d citeck_integrations <<'SQL'
DO $$ DECLARE r RECORD; BEGIN
  FOR r IN SELECT schema_name FROM information_schema.schemata
           WHERE schema_name NOT IN ('pg_catalog','pg_toast','information_schema')
  LOOP EXECUTE 'DROP SCHEMA IF EXISTS '||quote_ident(r.schema_name)||' CASCADE'; END LOOP;
END $$;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO citeck_integrations;
SQL

docker exec \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  pg_restore -U postgres \
    -d citeck_integrations \
    --no-owner --no-acl --role=citeck_integrations \
    -j 4 /tmp/ecos_integrations.dump

docker exec citeck_postgres_default rm /tmp/ecos_integrations.dump

ecos_model → citeck_emodel:

docker cp ~/citeck-migration/postgres/ecos_model.dump citeck_postgres_default:/tmp/

docker exec -i \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  psql -U postgres -d citeck_emodel <<'SQL'
DO $$ DECLARE r RECORD; BEGIN
  FOR r IN SELECT schema_name FROM information_schema.schemata
           WHERE schema_name NOT IN ('pg_catalog','pg_toast','information_schema')
  LOOP EXECUTE 'DROP SCHEMA IF EXISTS '||quote_ident(r.schema_name)||' CASCADE'; END LOOP;
END $$;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO citeck_emodel;
SQL

docker exec \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  pg_restore -U postgres \
    -d citeck_emodel \
    --no-owner --no-acl --role=citeck_emodel \
    -j 4 /tmp/ecos_model.dump

docker exec citeck_postgres_default rm /tmp/ecos_model.dump

ecos_notifications → citeck_notifications:

docker cp ~/citeck-migration/postgres/ecos_notifications.dump citeck_postgres_default:/tmp/

docker exec -i \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  psql -U postgres -d citeck_notifications <<'SQL'
DO $$ DECLARE r RECORD; BEGIN
  FOR r IN SELECT schema_name FROM information_schema.schemata
           WHERE schema_name NOT IN ('pg_catalog','pg_toast','information_schema')
  LOOP EXECUTE 'DROP SCHEMA IF EXISTS '||quote_ident(r.schema_name)||' CASCADE'; END LOOP;
END $$;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO citeck_notifications;
SQL

docker exec \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  pg_restore -U postgres \
    -d citeck_notifications \
    --no-owner --no-acl --role=citeck_notifications \
    -j 4 /tmp/ecos_notifications.dump

docker exec citeck_postgres_default rm /tmp/ecos_notifications.dump

ecos_history → citeck_history:

docker cp ~/citeck-migration/postgres/ecos_history.dump citeck_postgres_default:/tmp/

docker exec -i \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  psql -U postgres -d citeck_history <<'SQL'
DO $$ DECLARE r RECORD; BEGIN
  FOR r IN SELECT schema_name FROM information_schema.schemata
           WHERE schema_name NOT IN ('pg_catalog','pg_toast','information_schema')
  LOOP EXECUTE 'DROP SCHEMA IF EXISTS '||quote_ident(r.schema_name)||' CASCADE'; END LOOP;
END $$;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO citeck_history;
SQL

docker exec \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  pg_restore -U postgres \
    -d citeck_history \
    --no-owner --no-acl --role=citeck_history \
    -j 4 /tmp/ecos_history.dump

docker exec citeck_postgres_default rm /tmp/ecos_history.dump

ecos_process → citeck_eproc:

docker cp ~/citeck-migration/postgres/ecos_process.dump citeck_postgres_default:/tmp/

docker exec -i \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  psql -U postgres -d citeck_eproc <<'SQL'
DO $$ DECLARE r RECORD; BEGIN
  FOR r IN SELECT schema_name FROM information_schema.schemata
           WHERE schema_name NOT IN ('pg_catalog','pg_toast','information_schema')
  LOOP EXECUTE 'DROP SCHEMA IF EXISTS '||quote_ident(r.schema_name)||' CASCADE'; END LOOP;
END $$;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO citeck_eproc;
SQL

docker exec \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  pg_restore -U postgres \
    -d citeck_eproc \
    --no-owner --no-acl --role=citeck_eproc \
    -j 4 /tmp/ecos_process.dump

docker exec citeck_postgres_default rm /tmp/ecos_process.dump

ecos_camunda → citeck_camunda:

docker cp ~/citeck-migration/postgres/ecos_camunda.dump citeck_postgres_default:/tmp/

docker exec -i \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  psql -U postgres -d citeck_camunda <<'SQL'
DO $$ DECLARE r RECORD; BEGIN
  FOR r IN SELECT schema_name FROM information_schema.schemata
           WHERE schema_name NOT IN ('pg_catalog','pg_toast','information_schema')
  LOOP EXECUTE 'DROP SCHEMA IF EXISTS '||quote_ident(r.schema_name)||' CASCADE'; END LOOP;
END $$;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO citeck_camunda;
SQL

docker exec \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  pg_restore -U postgres \
    -d citeck_camunda \
    --no-owner --no-acl --role=citeck_camunda \
    -j 4 /tmp/ecos_camunda.dump

docker exec citeck_postgres_default rm /tmp/ecos_camunda.dump

ecos_edi → citeck_edi:

docker cp ~/citeck-migration/postgres/ecos_edi.dump citeck_postgres_default:/tmp/

docker exec -i \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  psql -U postgres -d citeck_edi <<'SQL'
DO $$ DECLARE r RECORD; BEGIN
  FOR r IN SELECT schema_name FROM information_schema.schemata
           WHERE schema_name NOT IN ('pg_catalog','pg_toast','information_schema')
  LOOP EXECUTE 'DROP SCHEMA IF EXISTS '||quote_ident(r.schema_name)||' CASCADE'; END LOOP;
END $$;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO citeck_edi;
SQL

docker exec \
  -e PGPASSWORD=postgres \
  citeck_postgres_default \
  pg_restore -U postgres \
    -d citeck_edi \
    --no-owner --no-acl --role=citeck_edi \
    -j 4 /tmp/ecos_edi.dump

docker exec citeck_postgres_default rm /tmp/ecos_edi.dump

4.2. MongoDB

docker cp ~/citeck-migration/mongo/ecos-process.archive citeck_mongo_default:/tmp/

docker exec citeck_mongo_default mongorestore \
  --username "$(docker exec citeck_mongo_default printenv MONGO_INITDB_ROOT_USERNAME)" \
  --password "$(docker exec citeck_mongo_default printenv MONGO_INITDB_ROOT_PASSWORD)" \
  --authenticationDatabase admin \
  --nsFrom 'ecos-process.*' --nsTo 'citeck_eproc.*' \
  --archive=/tmp/ecos-process.archive --gzip --drop

docker exec citeck_mongo_default rm /tmp/ecos-process.archive

Опции:

  • --nsFrom 'ecos-process.*' --nsTo 'citeck_eproc.*' – переименование namespace при restore.

  • --drop – удаляет коллекции, оставшиеся от первого старта webapp eproc.

  • Username и пароль root читаются из ENV контейнера через printenv, чтобы не зависеть от того, что launcher сгенерировал.

4.3. Zookeeper

Останавливаем zookeeper через citeck stop, а не docker stop. Это критично: launcher2 имеет reconciler, который через ~1 минуту перезапустит контейнер, остановленный «снаружи», прямо во время того, как мы пишем в данные. citeck stop помечает приложение как detached и reconciler оставляет его в покое.

Хранилище zookeeper в launcher2 – это bind-mount каталог на хосте (не named docker volume): /opt/citeck/data/runtime/default/volumes/zookeeper2/. Внутри него – подкаталог data/, в котором zookeeper держит snapshots, и datalog/, в котором держит transaction log. Bitnami же на source складывал и snapshots, и log в один каталог data/version-2/. При импорте нужно разделить файлы: log.* идут в datalog/version-2/, а всё остальное (snapshot.*, acceptedEpoch, currentEpoch и любые другие служебные файлы) – в data/version-2/.

ZK_DIR=/opt/citeck/data/runtime/default/volumes/zookeeper2

citeck stop zookeeper

# Очистить старые данные (они созданы launcher'ом при первом старте)
rm -rf $ZK_DIR/data/version-2 $ZK_DIR/datalog/version-2
mkdir -p $ZK_DIR/data/version-2 $ZK_DIR/datalog/version-2

# Распаковать дамп во временный каталог
TMP=$(mktemp -d)
tar -xzf ~/citeck-migration/zookeeper/version-2.tar.gz -C $TMP

# Сначала переносим log'и в datalog/, потом всё остальное в data/
mv $TMP/version-2/log.* $ZK_DIR/datalog/version-2/ 2>/dev/null || true
mv $TMP/version-2/*     $ZK_DIR/data/version-2/
rm -rf $TMP

# Выставить владельца -- uid/gid пользователя zookeeper в official образе
chown -R 1001:1001 $ZK_DIR/data/version-2 $ZK_DIR/datalog/version-2

citeck start zookeeper

Замечания:

  • Точный путь bind-mount проверить через docker inspect citeck_zookeeper_default --format '{{(index .Mounts 0).Source}}'.

  • chown 1001:1001 – uid/gid пользователя zookeeper в официальном образе. Без него запуск падает с permission denied.

  • Если файлы не разделены – official zookeeper падает с SnapDirContentCheckException: Snapshot directory has log files.

  • citeck start zookeeper re-attaches приложение, после чего reconciler снова им управляет.

  • Проверить старт:

    docker logs --tail 100 citeck_zookeeper_default
    

    В логах должно быть Snapshot loaded и Snapshotting: без ошибок.

4.4. Поднять webapp’ы обратно

Тот же список приложений, что был остановлен в шаге 1.3. Важно: в отличие от citeck stop (принимает любое число аргументов), citeck start принимает только один app за раз, поэтому запускаем циклом – без --detach, чтобы каждый старт дожидался RUNNING перед стартом следующего:

for app in <app1> <app2> <app3> ...; do
  citeck start "$app"
done

Если запускать с --detach, возникает race condition: proxy запускается раньше, чем onlyoffice (или другая зависимость) успевает стать DNS-резолвимым, и падает с host not found in upstream "onlyoffice". Sequential-старт (без --detach) использует waitForDeps launcher2 и поднимает приложения в правильном порядке.

Цикл занимает 10–20 минут (каждый Java-webapp стартует 1–3 минуты). Прогресс параллельно можно смотреть через citeck status -w в другой сессии.

Liquibase каждого webapp при старте увидит актуальный changelog (source перед миграцией обновлён до 2026.1) – изменений schema не будет.

Этап 5. Проверка

5.1. Что должно работать

  • Логин в Web UI под admin / <admin-пароль из шага 1.1>. Не admin / admin источника – пароль теперь сгенерирован launcher’ом.

  • Списки записей в eapps, eproc, uiserv показывают данные источника.

  • BPMN-процессы из eproc запускаются (mongo + postgres согласованы).

  • В Keycloak присутствуют только пользователь admin и сервисный аккаунт citeck.

5.2. Проверочные команды

citeck status
citeck health
docker exec -e PGPASSWORD=postgres citeck_postgres_default \
  psql -U postgres -d citeck_eapps -c "\dn"
docker exec -e PGPASSWORD=postgres citeck_postgres_default \
  psql -U postgres -d citeck_eapps -c "\dt+ *.*" | head
docker exec citeck_mongo_default mongo \
  -u "$(docker exec citeck_mongo_default printenv MONGO_INITDB_ROOT_USERNAME)" \
  -p "$(docker exec citeck_mongo_default printenv MONGO_INITDB_ROOT_PASSWORD)" \
  --authenticationDatabase admin \
  citeck_eproc --eval "db.getCollectionNames()"
docker exec citeck_zookeeper_default \
  bash -c 'echo "ls /" | zkCli.sh -server localhost:2181'

citeck status должна показать все приложения в статусе RUNNING. citeck health – exit-код 0.

5.3. Что заведомо обнулилось

  • Пользователи и роли Keycloak источника.

  • In-flight сообщения RabbitMQ.

  • Логи nginx/proxy.

  • Кеш сертификатов Let’s Encrypt – будет перевыпущен при первом запросе.

Откат

Сбой на target до доставки данных

Переустановить с нуля:

citeck uninstall --delete-data

Затем пройти установку и подготовку target заново (этап 1).

Сбой после импорта

Повторить с нуля:

citeck uninstall --delete-data

Затем установка → подготовка target → импорт. Source уже на 2026.1, его трогать не нужно – дампы из ~/citeck-migration/ ещё валидны.

Сбой апгрейда source до 2026.1 (предусловие 1)

Восстановить из backup’а из шага 0:

  • Если backup делался через tardocker compose down, распаковать tar, docker compose up -d.

  • Если делался через pg_dumpall + mongodump – следовать стандартной процедуре restore PostgreSQL/MongoDB.

Дампы из ~/citeck-migration/ сохранять минимум 1–2 недели после миграции – на случай поздно обнаруженных проблем.