Data migration from docker-compose
Instructions for migrating data from an old server deployed from the citeck-community repository (docker-compose) to a new server deployed via Citeck Launcher v2 in server mode.
Intended for an administrator with root access to both servers. All steps are performed manually, with individual commands in the terminal.
Warning
If the installation is customized, commands and login/password values will need to be adjusted for your environment.
Conventions
On the source, commands use the
docker composesyntax (Compose v2, plugin). If you have the legacydocker-compose(v1) installed, replacedocker composewithdocker-composein every command.Stage 2 commands (export) and prerequisites are executed from the
citeck-communitydirectory (wheredocker-compose.yamlresides), otherwise compose will not find the services.On the target, launcher2 manages containers directly via Docker SDK (without compose) — it uses
docker execwith real container names (citeck_postgres_default,citeck_mongo_default,citeck_zookeeper_default).
What is migrated and what is not
Migrated:
PostgreSQL — user databases (schema + data), selectively per the mapping table.
MongoDB — one database
ecos-process→citeck_eproc.Zookeeper — contents of the
version-2/directory (snapshots + transaction log).
NOT migrated (i.e., out of scope for this guide — technically the data below can also be transferred if needed, but the step-by-step procedure for doing so is not described here):
The
keycloakdatabase — created on the target by launcher2 itself. On the source, the corresponding database is namedecos_identity(theecos-identity-appservice in community is an older version of Keycloak).Users, roles, and Keycloak clients from the source (including the demo account
admin/admin). If they were created manually — recreate them after migration. The admin on the target uses the password generated by the launcher on first startup (shown in the wizard once; re-issue viaciteck setup admin-password).RabbitMQ — queues and in-flight messages.
Proxy/nginx volumes — logs, Let’s Encrypt certificate cache.
Secrets (JWT, OIDC client secret, admin password) — launcher2 generates its own on first installation.
Compatibility matrix
Component |
Source |
Target |
Migration type |
|---|---|---|---|
PostgreSQL |
12.7 |
17.5 |
logical dump ( |
MongoDB |
4.0 |
4.0.2 |
|
Zookeeper |
3.8.2 (Bitnami) |
3.9.4 (official) |
copy of |
RabbitMQ |
– |
– |
skip |
Keycloak DB |
– |
– |
skip |
PostgreSQL undergoes a major version jump (12 → 17). Physical copying of the data directory (volume) will not work — only a logical dump is required.
Database and user mapping
In launcher2, the convention is: db_name == username == password. On the target, user database passwords match the names of those databases.
PostgreSQL
Source DB |
Source owner |
Target DB |
Target owner |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Not migrated (ignore on source):
ecos_gateway— in launcher2, gateway does not have its own database.ecos_identity— this is the database of theecos-identity-appservice, which in the community setup is Keycloak (just an older version). This aligns with the decision «do not migrate thekeycloakdatabase» — source users and roles are lost during migration (see «NOT migrated» in the introduction).keycloak— created on the target by launcher2 itself.
MongoDB
Source DB |
Target DB |
|---|---|
|
|
Zookeeper
Source path (host) |
Target path (host) |
|---|---|
|
|
Notes:
Source (Bitnami) stores both snapshots and the transaction log in a single
version-2/directory.Target (official zookeeper) separates them:
data/version-2/for snapshots and auxiliary files (acceptedEpoch,currentEpoch),datalog/version-2/for the transaction log. If they are not separated, the target fails withSnapDirContentCheckException.Storage in launcher2 is a bind-mount directory on the host (not a named docker volume). The exact path can be confirmed via
docker inspect citeck_zookeeper_default --format '{{(index .Mounts 0).Source}}'.
Prerequisites
0. Back up source BEFORE any actions
This is a safety net in case the upgrade to 2026.1 (prerequisite 1) breaks the old server. The backup is NOT used for migration — only for rolling back the source.
Simplest option (with server shutdown):
docker compose stop
tar -czf citeck-community-backup-$(date +%F).tar.gz \
services/ecos-community-demo-data/ \
services/backups/
docker compose start
If the server cannot be stopped — pg_dumpall + mongodump + a hot copy of the zookeeper directory on running containers.
1. Source updated to release 2026.1
This eliminates the risk of schema/changelog desynchronization when loading into launcher2 (also 2026.1). In the citeck-community directory:
git pull
docker compose up -d
Wait until all microservices reach RUNNING / healthy — Liquibase must catch up with the changelog. You can verify with:
docker compose ps
2. Target prepared
citeck-launcher version ≥ 2.x is installed, bundle community:2026.1 or enterprise:2026.1 (depending on your license). First-run details are in Stage 1.
3. Free disk space
On source: ≥ data volume × 1.5 (for dumps).
On target: same.
4. Docker access
docker ps without sudo or via sudo on both servers.
Stage 1. Preparing the target (launcher2)
Goal of this stage: run the namespace once so that launcher creates the infrastructure (containers + docker volumes), initializes empty databases with users, and generates secrets.
1.1. Install launcher
If not yet installed:
curl -fsSL https://github.com/Citeck/citeck-launcher/releases/latest/download/install.sh | bash
In the TUI wizard, select bundle community:2026.1 or enterprise:2026.1 (matching your license). Write down the generated admin password — it is shown only once. If the password is lost, it can be re-issued after migration via citeck setup admin-password.
1.2. Wait for full startup
citeck status -w
All applications must be in RUNNING status. On a server with 16 GB RAM this takes 5–15 minutes (longer for enterprise). This step is critical: if webapps do not complete Liquibase on the empty databases in time, the import in Stage 4 will load into databases with an incomplete schema.
1.3. Stop all webapps, leaving infrastructure running
The webapp set in community and enterprise differs; no single list is provided.
View the current list:
citeck statusStop everything that does not belong to infrastructure. Infrastructure containers that remain running for the import:
postgresandmongo(zookeeper is also infrastructure, but it will be stopped immediately before loading data in step 4.3).List the remaining applications (
gateway,eapps,emodel,uiserv,history,notifications,integrations,eproc,transformations,proxy, etc. — as returned byciteck status):citeck stop <app1> <app2> <app3> ...
The
citeck stop <app>command marks the application as detached: after the nextciteck startwithout arguments it will not start automatically. This ensures that no webapp writes to the database while the restore is in progress.
1.4. Credentials for import
No manual lookup required:
PostgreSQL:
postgres / postgres(hardcoded in launcher2).MongoDB: the root password is generated by the launcher and read via
docker exec citeck_mongo_default printenv MONGO_INITDB_ROOT_PASSWORD. Import commands in Stage 4 use this approach.
Stage 2. Exporting from source
All commands in this stage are executed on the old server, from the citeck-community directory. Service names from docker-compose.yaml are used:
PostgreSQL —
ecos-microservices-postgresql-appMongoDB —
mongodb-appZookeeper —
zookeeper-app
Approach: run pg_dump / mongodump via docker compose exec -T, redirecting stdout directly to a file on the host. No intermediate docker cp and no temporary files inside the container.
The working directory for dumps in this guide is ~/citeck-migration/. Create subdirectories in advance:
mkdir -p \
~/citeck-migration/postgres \
~/citeck-migration/mongo \
~/citeck-migration/zookeeper
2.1. PostgreSQL
pg_dump parameters:
-F c— custom format (compact, supports parallel restore).--no-owner --no-acl— stripsOWNER TO/GRANTstatements (target users have different names; ownership will be set bypg_restore --role).--clean --if-existsis NOT used — the target database is cleaned separately with aDROP SCHEMAcommand.Without
-f /tmp/...—pg_dumpwrites to stdout, which we redirect>to a file on the host.
Note
If any of the databases is missing on your source (for example, ecos_edi does not exist in a community-only deployment) — pg_dump will fail with database "<name>" does not exist. Simply skip the corresponding block: the corresponding target database on launcher2 either does not exist or will remain empty and be populated by the webapp’s Liquibase on first startup.
Commands for each database from the mapping table (execute in blocks, one at a time):
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
After execution, ~/citeck-migration/postgres/ should contain 9 files:
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
Note
If the postgres password was changed (from the default postgresstorngpassword in services/environments/ecos-microservices-postgresql-app.env) — replace the PGPASSWORD=... value in all commands.
2.2. MongoDB
Dump of a single database ecos-process. Root login/password — from services/environments/mongodb-app.env. If they were changed — update them in the command. --archive without a value = output to 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
After execution, ~/citeck-migration/mongo/ should contain one file ecos-process.archive.
2.3. Zookeeper
Performed on a stopped container to avoid getting a partially written snapshot. The bind-mount directory belongs to the Bitnami zookeeper uid (1001), not your user — so tar is run inside a temporary container to avoid depending on 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
The path services/ecos-community-demo-data/zookeeper-app/data is the host bind-mount path from services/zookeeper-app.yaml. Compose resolves relative paths relative to the service file (i.e., services/...), not the repo root. If your deployment has a different layout — specify your own path.
2.4. Final directory structure
~/citeck-migration/
├── postgres/ # 9 .dump файлов
├── mongo/ # 1 .archive
└── zookeeper/ # version-2.tar.gz
Stage 3. Transferring files
Transfer the ~/citeck-migration/ directory (PostgreSQL dumps + Mongo archive + Zookeeper tarball) to the target server using any convenient method: rsync, scp, S3, portable media — your choice.
Place it on the target server at the same path — ~/citeck-migration/. It is important to preserve the three-subdirectory structure (postgres/, mongo/, zookeeper/) — Stage 4 commands reference paths like ~/citeck-migration/postgres/<name>.dump. If files are copied flat into one folder, the import will break.
Stage 4. Loading onto target
All commands — on the new server. Container names for launcher2 in server mode (namespace is always default):
PostgreSQL —
citeck_postgres_defaultMongoDB —
citeck_mongo_defaultZookeeper —
citeck_zookeeper_default
4.1. PostgreSQL
For each database pair from the mapping table — three actions:
Copy the dump into the container.
Clear all user schemas in the target database (Liquibase created service objects on first startup; they must be removed, otherwise the restore will have naming conflicts).
Run
pg_restorewith--role=<target_user>so that object ownership is assigned correctly.
Extensions (pg_trgm, uuid-ossp, etc.) are restored under the postgres user — --role does not affect CREATE EXTENSION.
Command block for each pair (repeat 9 times):
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
Options:
--nsFrom 'ecos-process.*' --nsTo 'citeck_eproc.*'— namespace renaming during restore.--drop— drops collections left over from the first startup of theeprocwebapp.The root username and password are read from the container ENV via
printenv, so as not to depend on what the launcher generated.
4.3. Zookeeper
Stop zookeeper via citeck stop, not docker stop. This is critical: launcher2 has a reconciler that will restart a container stopped «from outside» in ~1 minute, potentially while data is being written. citeck stop marks the application as detached and the reconciler leaves it alone.
Zookeeper storage in launcher2 is a bind-mount directory on the host (not a named docker volume): /opt/citeck/data/runtime/default/volumes/zookeeper2/. Inside it — the data/ subdirectory where zookeeper holds snapshots, and datalog/ where it holds the transaction log. Bitnami on the source stored both snapshots and the log in a single data/version-2/ directory. During import, files must be split: log.* go into datalog/version-2/, and everything else (snapshot.*, acceptedEpoch, currentEpoch, and any other auxiliary files) — into 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
Notes:
Verify the exact bind-mount path via
docker inspect citeck_zookeeper_default --format '{{(index .Mounts 0).Source}}'.chown 1001:1001— the uid/gid of thezookeeperuser in the official image. Without it, startup fails withpermission denied.If files are not split — the official zookeeper fails with
SnapDirContentCheckException: Snapshot directory has log files.citeck start zookeeperre-attaches the application, after which the reconciler manages it again.Verify startup:
docker logs --tail 100 citeck_zookeeper_default
The logs should show
Snapshot loadedandSnapshotting:without errors.
4.4. Bring webapps back up
The same list of applications that was stopped in step 1.3. Important: unlike citeck stop (which accepts any number of arguments), citeck start accepts only one app at a time, so we run it in a loop — without --detach, so that each startup waits for RUNNING before starting the next:
for app in <app1> <app2> <app3> ...; do
citeck start "$app"
done
If started with --detach, a race condition occurs: proxy starts before onlyoffice (or another dependency) becomes DNS-resolvable, and fails with host not found in upstream "onlyoffice". Sequential startup (without --detach) uses launcher2’s waitForDeps and brings applications up in the correct order.
The loop takes 10–20 minutes (each Java webapp starts in 1–3 minutes). Progress can be monitored in parallel via citeck status -w in another session.
Each webapp’s Liquibase will see the current changelog on startup (source was updated to 2026.1 before migration) — there will be no schema changes.
Stage 5. Verification
5.1. What should work
Log in to the Web UI with
admin / <admin password from step 1.1>. Notadmin / adminfrom the source — the password is now generated by the launcher.Record lists in
eapps,eproc,uiservshow source data.BPMN processes from
eprocstart (mongo + postgres are in sync).Keycloak contains only the
adminuser and theciteckservice account.
5.2. Verification commands
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 should show all applications in RUNNING status. citeck health — exit code 0.
5.3. What was reset
Source Keycloak users and roles.
In-flight RabbitMQ messages.
nginx/proxy logs.
Let’s Encrypt certificate cache — will be re-issued on the first request.
Rollback
Failure on target before data delivery
Reinstall from scratch:
citeck uninstall --delete-data
Then repeat the target installation and preparation (Stage 1).
Failure after import
Start over from scratch:
citeck uninstall --delete-data
Then installation → target preparation → import. Source is already on 2026.1, no changes needed there — dumps from ~/citeck-migration/ are still valid.
Failure to upgrade source to 2026.1 (prerequisite 1)
Restore from the backup made in step 0:
If the backup was made via
tar—docker compose down, extract thetar,docker compose up -d.If it was made via
pg_dumpall+mongodump— follow the standard PostgreSQL/MongoDB restore procedure.
Keep dumps from ~/citeck-migration/ for at least 1–2 weeks after migration — in case problems are discovered late.