Skip to content

Legacy Migration Toolkit

By Romi Fadli, S.Kom. 11 min read Backup & Recovery
On this page
An old layout with a grey banner and a new structure with a green banner, each holding a blue database, a purple document store, and an orange telemetry store; three coloured paths pass through one export package marked with a green check

Legacy Migration Toolkit is a set of four commands that move a running stack from its old layout into a new structure, across major versions of its database and its search engine. One exports everything from the old server; one imports it into the new structure, each store in the way its version allows; one verifies what actually arrived; and one renames module folders without losing the data that hangs off their names.

A migration like this cannot be a folder copy. PostgreSQL refuses to start on a data directory written by an older major version, a search-engine upgrade in place cannot be undone, and the records being moved include signing logs that must survive their full retention period.

At a glance

Toolsexport-legacy-data, import-legacy-data, verify-migration, rename-modules
FormCommand-line scripts
LanguageBash, driving Docker, pg_dump and pg_restore, the Elasticsearch snapshot API, rsync, and openssl
StatusStable, used for a completed migration
Source codePrivate
What the toolkit is, what it is built with, and its state.

What it does

A. Exports without changing the source's data

The export dumps each database, copies the object storage, takes a snapshot of the telemetry indices through the search engine's own API, and collects the runtime state that version control never held, such as a certificate authority's revocation list. It writes a manifest with a checksum and table count for every dump, the sizes, and the image versions of the source. It checks the free disk space first and warns when the object storage would not fit, and it can write to another disk or leave the object storage out. It locks the export folder to its owner. It checks that the source looks like the old production folder, and keeps the old layout's container names on purpose, with a note not to unify them, because the source does not change just because the target did. Databases are dumped in the custom format without owners or privileges, which tolerates a newer server and roles that differ there. The telemetry snapshot covers only the telemetry indices, without the cluster's global state; when the old search engine has no repository path, the export prints the exact compose lines and commands to add one, and the repository is handed to the engine's user and copied out through short-lived containers, because its files belong to that user rather than to mine. The revocation list is copied with a checksum, must parse as a valid DER file, is checked for expiry, and is explained when it has wrongly become a directory. Environment files and configuration are carried as reference only, not to be copied over the new structure, whose keys differ; the application's logs come along too. Copy progress is shown only on a terminal, so a log file does not fill with carriage returns.

B. Imports each store in the way its version allows

Databases are restored from their dumps into the new major version. The search engine restores the snapshot, after removing the data streams that would collide with it. The object storage is copied as files, because its format on disk carries forward. Cache and queue stores are left behind, since they are not a source of truth. The revocation list is put in place, after any directory left where it should be is removed, and its validity dates are printed. Databases are restored with clean, if-exists, no-owner, and no-privileges options, so a restore can be repeated safely. Bucket policies are applied again as a separate step; the run copies through a container when root-owned folders refuse a plain copy, and can be limited to databases, object storage, or telemetry for a migration in stages.

C. Refuses before it damages anything

The import validates every checksum before it touches a single database, so a damaged dump stops the run while the target is still intact. When the export recorded search indices too old for the new engine, it skips the telemetry restore and asks for a reindex first, while the other stages continue. After each restore it compares the number of tables with the number the export recorded and flags a difference for checking. A failure inside a stage — a database that never becomes ready within a minute, errors from the restore tool, a failed snapshot restore — is reported and the remaining stages continue; the verification command gives the final verdict.

D. Verifies what is really there

The verification command inspects the target rather than trusting that a script finished: data present in each store, files readable by the user each container runs as, folders owned by the user the search engine needs, and the telemetry user in place. One failed check does not stop the others, and a container that is down is reported as skipped rather than failed, so the command stays useful while a migration is done in stages. The container prefix is read from the environment file, not guessed from the folder name, after a guessed prefix that never existed turned every check into "skipped" and hid real damage. It confirms the database server is at least version 18, proof the major upgrade really happened, and that the revocation list is a valid, unexpired file rather than a missing one or a directory. File readability is checked for the two certificate authorities' configuration files: the user each image runs as is translated into its numeric id by asking inside the image, read from the compose file when the container does not exist yet, and checked before anything starts, so a wrong permission is found without waiting through thirty health-check retries. A module created after the old export has no entry in its manifest and only needs tables to exist. It ends with counts of passes, warnings, and failures, and exits with 1 when anything failed.

E. Renames modules without losing their data

Module folders once used underscores and now use hyphens. Since each module's data path contains its name, renaming a folder also moves where Docker looks for its data, so the rename command moves the data folder in the same step, and keeps the folder's history when version control tracks it. It covers development, production, and every staging version. Root access is checked before anything changes, after a password prompt that needed a terminal once left the project folder renamed and its data not. The data folder's size is compared before and after the move, and an existing destination is never overwritten. Each renamed module's compose project name is rewritten, so old and new are not treated as two projects. The project name falls back to the folder name when it is unset, after its absence once let a rename go ahead while development's containers were running. The table of names to rename carries a boxed warning against any global search-and-replace of underscores, after one such sweep made both columns equal and the rename quietly did nothing until production needed it. A key rewrite that finds nothing no longer ends the run after the first module, and paths are computed when used, so a data prefix set later is honoured.

How I use it

bash
export-legacy-data --src old/production --dry-run
export-legacy-data --src old/production
import-legacy-data --from migration-<ts> --target-dir production --dry-run
import-legacy-data --from migration-<ts> --target-dir production
verify-migration --target-dir production --from migration-<ts>
rename-modules --dry-run
StoreOldNewHow it moves
PostgreSQL1418dump and restore; a data directory is tied to its major version
Elasticsearch89snapshot and restore; an in-place upgrade is one way only
Object storageunpinnedpinned releasecopy the folder; the format carries forward
Redisunpinnedpinned releasenot migrated; cache and queue only
Why each store needs its own way across.
StoreIts container must beWhy
Object storagestoppedcopying into a live storage folder can corrupt its backend
PostgreSQLrunninga restore needs a server that accepts connections
Elasticsearchrunninga snapshot is restored through its API
The state each store needs during the import.

Design decisions

Export and import are separate, and the source's data is never changed. The data usually has to be secured before the new structure is ready to receive it, so the two halves run on different days and meet through a folder. The export dumps, copies, and snapshots, and the only thing it adds beside the old data is the snapshot repository the search engine needs to write its snapshot. The manifest is what lets the import trust that folder: checksums are checked before any target database is dropped, table counts are compared after each restore, and the source's image versions are on record. The folder is readable by its owner alone, and the export warns before doubling tens of gigabytes of object storage on a disk that is too small.

Each store crosses the version gap in its own way. PostgreSQL 18 will not start on a directory written by 14, so the databases travel as dumps. Elasticsearch 9 reads only indices created by 8, and once it has touched a data folder version 8 can no longer read it, so the telemetry travels as a snapshot, which is portable across versions and leaves a point to return to. Indices created by version 7 are caught at export and stop the restore unless it is forced. The object storage only needs its files, and Redis, which holds a cache and a queue, is left behind.

It never starts or stops a stack itself. The stores need opposite states — object storage stopped, PostgreSQL and the search engine running — and the import checks each one. When a state is wrong, it stops and prints the command that would fix it, and it waits up to a minute for a database to accept connections before reporting that one and moving to the next. Control over a production stack stays with the person running the migration. The rename command follows the same rule, and refuses while any related container is still running.

Verification looks for the failures that look like success. A configuration file written readable only by its owner worked on my laptop, where my user id happened to be 1000, the same as the user inside the certificate authority's image; on a server with a different id the authority restarted endlessly, and two services that waited for it showed as unhealthy, pointing everywhere but the cause. A missing telemetry user is quieter still: every service stays healthy, the dashboards open, and every log, trace, and metric is dropped — which happened for two days in production. So the verification checks file readability against each container's user id, folder ownership, and the telemetry user, before anyone declares the migration done. The last check is mine: one document signed from start to finish.

Specifications

InputsAn old production folder to export; an export folder and a target environment to import into
OutputsAn export folder with dumps, object files, a search snapshot, runtime state, and a manifest; restored stores in the target; a verification report
SafetyDry run for export, import, and rename; database-dump checksums validated before any target is dropped; table counts compared; never starts or stops a stack; data moved with its folder on rename, with sizes compared and no overwrite; failures inside export and import stages are reported, and verification gives the final verdict
RequirementsDocker; the target structure in place; disk space for the export, or a second disk; network access to pull small helper images at run time
Inputs, outputs, and guarantees of the toolkit.

The story of moving databases and a log archive across major versions.

The toolkit that takes over once the data has moved; its snapshot repository is one of the places the verification checks.

The toolkit that gives the target environment its network before anything is imported into it.

Each script follows the shape I give every small tool, in its Bash variant.

The toolkit that creates production's telemetry user — the step whose result the verification here checks.

Why a migration is treated like a recovery: a point to return to first, then the move.

Why telemetry that has lost its user is dropped without a word.

Terms

Major version
A release that may change on-disk formats, so data written by one major version cannot always be read by the next.
Dump and restore
Exporting a database as a portable file and loading it into another server, of the same or a newer version.
Snapshot repository
The location where Elasticsearch writes its snapshots, which another node can restore from.
Manifest
A file listing what an export contains, with a checksum for each database dump so damage can be detected before use.
User id
The number the kernel uses to decide who may read a file; names do not matter, only the number does.
Data stream
An append-only series of indices in Elasticsearch, used here for telemetry.

Technologies covered

  • Bash
  • Elasticsearch
  • PostgreSQL
  • Docker
  • MinIO

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Referenced by

A purple root circle linked to a blue and an orange intermediate circle, which lead to a signed document with a green seal and to a clock.

Aurograf

DevOps

The digital signature platform of the City Government of South Tangerang: signers prove who they are, sign PDFs to PAdES B-LTA with certificates from the platform's own authorities, and anyone can verify the result — version 2, which I designed, built and operate.

10 min read Read more →

Related Articles

A stack of Markdown documents and an orange arrow leading to a browser page with a side menu, a heading, paragraphs, a dark code panel, and a table of contents with its active entry highlighted

A single script that turns a project's Markdown documentation into a static site that opens from a folder with no server or internet: a menu, a table of contents that follows the reader, tables that never truncate, code with a copy button, and links that keep working.

7 min read Read more →
Three image layer stacks, each with a version tag of its own colour, blue, orange, and green, each leading to an archive file banded in the same colour

Commands that version every module of a stack on its own, build its image under a tag that means exactly one build, refuse to reuse a version or swallow a failed compile, and archive images by name and tag so versions never overwrite each other.

7 min read Read more →
Four identical grey keys on the left each become a key of its own colour on the right, every new key marked with a green check; below, a purple key stays behind a red bar

One command that rotates an environment's internal credentials key by key: a dry run by default, a backup first, a unique value for every key, a real login to prove each credential that can log in, and rollback for any that fail.

8 min read Read more →