Appearance
Apache Fineract Liquibase Migration Guide
Upgrading Apache Fineract across the 1.6 boundary is one of the most failure-prone operations in the Fineract ecosystem. The root cause is the migration tool switch: Fineract used Flyway for database schema management up through version 1.5, and switched to Liquibase starting in version 1.6. A direct upgrade from 1.4 or 1.5 to a later version does not work out of the box - the schema history tables are incompatible, and Liquibase cannot detect the prior migration state.
This guide covers the complete upgrade path from Fineract 1.x to the latest release (currently 1.14.x), the most common Liquibase migration errors seen in production and in the community, and how to resolve each one.
Running on Finecko?
If your Fineract instance is hosted on Finecko, database migrations are handled automatically during upgrades. You can skip this guide entirely - see the Upgrades FAQ instead.
Background: Why Fineract Switched from Flyway to Liquibase
Apache Fineract used Flyway for schema versioning from its early versions through 1.5.x. The switch to Liquibase was introduced in Fineract 1.6 as part of a broader effort to support more complex multi-database environments (PostgreSQL and MariaDB/MySQL) and enable more flexible migration management.
The changeover created a persistent problem for the community: anyone running a pre-1.6 instance and attempting to upgrade directly to 1.6 or later finds that the new Liquibase bootstrap process does not recognise the existing Flyway schema history. The result is either a startup failure or a partial schema re-run, both of which can corrupt the database state if not handled correctly.
The JIRA backlog and mailing list threads reflect this - errors around SchemaUpgradeNeededException, locked DATABASECHANGELOGLOCK tables, and checksum mismatches were among the most active threads in the Fineract community between 2022 and 2024, with no single official resolution documented in the upstream README or wiki.
How the Flyway → Liquibase handoff actually works
When Fineract 1.6+ starts against a pre-existing database, the TenantDatabaseUpgradeService runs automatically on startup and:
- Checks whether a
DATABASECHANGELOGtable exists (Liquibase has never run) - If not, checks whether a Flyway
schema_versiontable is present - If Flyway is present, verifies the database is at the last required Flyway version (V392 for the tenant DB, V6 for the tenant store)
- If at V392, calls
changeLogSync()— this marks all existing Liquibase changesets as already applied without re-running them, then continues normally - If the Flyway
schema_versiontable is present but the database is not at V392, it throwsSchemaUpgradeNeededExceptionand refuses to start
This means the Flyway → Liquibase conversion is automatic when you are on the correct Flyway boundary version. No manual migration mode or special startup flag is required.
Supported Upgrade Paths
Not all version jumps are safe. Use this matrix as a guide:
| From version | To version | Strategy |
|---|---|---|
| 1.4.x or earlier | 1.6+ | Must reach Flyway V392 first - upgrade to 1.5.x latest patch |
| 1.5.x (at Flyway V392) | 1.6–1.14.x | Direct upgrade supported; Liquibase switchover is automatic |
| 1.6.x | 1.7.x–1.14.x | Direct upgrade supported |
| 1.7.x–1.13.x | 1.14.x | Direct upgrade supported |
WARNING
Skipping the 1.6 intermediate step when upgrading from 1.4 or an older 1.5.x patch is the single most common cause of SchemaUpgradeNeededException. Fineract checks that your Flyway schema is at exactly V392 (V392__interest_recovery_conf_for_rescedule.sql) before performing the switchover. If it is not, it will refuse to start.
Before You Start
Prerequisites
- Java 17 or later (required for Fineract 1.8+)
- PostgreSQL 14+ or MariaDB 10.6+
- Database access with schema-level
ALTERprivileges - The Fineract JAR or Docker image for each version you will pass through
Understanding the two databases
Fineract uses two separate databases:
fineract_tenants- the tenant store (configured viaFINERACT_HIKARI_JDBC_URL). This holds tenant metadata and is the database the HikariCP connection pool connects to directly.fineract_default(and any additional tenant databases) - the actual tenant data databases, referenced by rows infineract_tenants. These are configured viaFINERACT_DEFAULT_TENANTDB_*environment variables.
Both databases must be migrated successfully. Fineract 1.6+ handles both automatically on startup, but both need to be backed up before any upgrade.
Back up your databases first
Always take a full backup before running any migration.
PostgreSQL:
bash
pg_dump -h localhost -U postgres -d fineract_tenants > fineract_tenants_backup.sql
pg_dump -h localhost -U postgres -d fineract_default > fineract_default_backup.sqlMariaDB / MySQL:
bash
mysqldump -u root -p fineract_tenants > fineract_tenants_backup.sql
mysqldump -u root -p fineract_default > fineract_default_backup.sqlBack up all tenant databases - not just the default one. In a multi-tenant setup, each tenant has its own database and will be migrated independently.
Step 1: Reach Flyway V392 (Pre-1.6 instances only)
This step applies only if you are upgrading from 1.4.x or an older 1.5.x patch that has not yet reached Flyway version 392. If you are already on Fineract 1.6 or later, skip to Step 2.
First, identify your current Flyway version:
sql
SELECT version, script, success
FROM schema_version
ORDER BY installed_rank DESC
LIMIT 5;The most recent successful row must show version = 392 and script = V392__interest_recovery_conf_for_rescedule.sql before you proceed. If it does not, upgrade to the latest 1.5.x patch release first and start it once to let Flyway run to completion.
Step 2: Upgrade to 1.6
Once your database is at Flyway V392, start Fineract 1.6 normally. The TenantDatabaseUpgradeService will automatically detect the Flyway schema, call changeLogSync() to mark all existing changesets as applied, and hand off to Liquibase. No special startup flags are needed.
Docker example:
bash
docker run -d \
--name fineract \
-p 8443:8443 \
-e FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME=org.postgresql.Driver \
-e FINERACT_HIKARI_JDBC_URL=jdbc:postgresql://db:5432/fineract_tenants \
-e FINERACT_HIKARI_USERNAME=postgres \
-e FINERACT_HIKARI_PASSWORD=yourpassword \
-e FINERACT_DEFAULT_TENANTDB_HOSTNAME=db \
-e FINERACT_DEFAULT_TENANTDB_PORT=5432 \
-e FINERACT_DEFAULT_TENANTDB_UID=postgres \
-e FINERACT_DEFAULT_TENANTDB_PWD=yourpassword \
apache/fineract:1.6.0Watch the startup logs. A successful Flyway → Liquibase switchover produces output similar to:
INFO TenantDatabaseUpgradeService - Upgrading tenant store DB at db:5432
INFO TenantDatabaseUpgradeService - This is the first Liquibase migration for tenant store.
We'll sync the changelog for you and then apply everything else
INFO TenantDatabaseUpgradeService - Liquibase changelog sync is complete
INFO TenantDatabaseUpgradeService - Tenant store upgrade finished
INFO TenantDatabaseUpgradeService - Upgrade for tenant default has finishedVerify the migration tables
Connect to the tenant store database and confirm the Liquibase tables exist:
PostgreSQL (table names are lowercase):
sql
SELECT COUNT(*) FROM databasechangelog;
SELECT locked FROM databasechangeloglock WHERE id = 1;MariaDB/MySQL (table names are uppercase):
sql
SELECT COUNT(*) FROM DATABASECHANGELOG;
SELECT LOCKED FROM DATABASECHANGELOGLOCK WHERE ID = 1;databasechangelog should contain rows. locked should be false / 0.
Step 3: Upgrade to Latest (1.7 → 1.14.x)
Once you are on Fineract 1.6, subsequent upgrades follow the same pattern: stop the running instance, replace the image or JAR, and start the new version. Liquibase will detect pending changesets and apply them automatically.
bash
# Example: upgrading to 1.14 via Docker
docker stop fineract
docker run -d \
--name fineract \
-p 8443:8443 \
-e FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME=org.postgresql.Driver \
-e FINERACT_HIKARI_JDBC_URL=jdbc:postgresql://db:5432/fineract_tenants \
-e FINERACT_HIKARI_USERNAME=postgres \
-e FINERACT_HIKARI_PASSWORD=yourpassword \
apache/fineract:1.14.0Monitor startup logs. You should see Liquibase applying new changesets, followed by a clean lock release for both the tenant store and each tenant database.
Common Errors and Fixes
SchemaUpgradeNeededException
Symptom: Fineract fails to start with a log entry containing:
Cannot proceed with upgrading database default
It seems the database doesn't have the latest schema changes applied until the 1.6 release
SchemaUpgradeNeededException: Make sure to upgrade to Fineract 1.6 first and then to a newer versionCause: Fineract detected a Flyway schema_version table but the database is not at Flyway V392. This means you skipped the required 1.5.x → 1.6 intermediate step, or the 1.5.x instance never ran to completion.
Fix:
- Restore from your backup
- Start the latest 1.5.x release against your database and let it complete its Flyway migrations
- Verify the database is at V392:sql
SELECT version, script FROM schema_version ORDER BY installed_rank DESC LIMIT 1; -- Must return: 392 | V392__interest_recovery_conf_for_rescedule.sql - Only then proceed with the 1.6 upgrade
Liquibase checksum mismatch
Symptom:
Validation Failed:
1 change sets check sum
db/changelog/parts/NNNN_some_change.xml::NNNN::author was: 8:abc123...
but is now: 8:def456...Cause: The checksum stored in databasechangelog no longer matches the changelog file on disk. This typically happens when a migration file is modified after it has already been applied — common in development, or when a custom patch was applied without version discipline.
Fix (development / staging):
On PostgreSQL:
sql
UPDATE databasechangelog
SET md5sum = NULL
WHERE id = 'NNNN' AND author = 'author';On MariaDB/MySQL:
sql
UPDATE DATABASECHANGELOG
SET MD5SUM = NULL
WHERE ID = 'NNNN' AND AUTHOR = 'author';After clearing the checksum, restart Fineract. Liquibase will recalculate and store the new value.
Fix (production):
Do not clear checksums in production without understanding why they changed. A checksum mismatch in production usually means a changelog file was modified on the filesystem (e.g., a custom build or patched JAR). Verify the JAR or Docker image was built from a clean, unmodified source before proceeding. If the mismatch is in a Fineract-provided changeset, restoring the original file from the release tag is the correct approach.
DATABASECHANGELOGLOCK table is locked
Symptom: Fineract hangs on startup or fails immediately with:
liquibase.exception.LockException: Could not acquire change log lock.
Currently locked by [hostname] since [timestamp]Cause: A previous Liquibase run exited uncleanly (server crash, OOM kill, forced shutdown mid-migration) and left the lock row in an acquired state.
Fix:
On PostgreSQL:
sql
UPDATE databasechangeloglock
SET locked = false, lockgranted = NULL, lockedby = NULL
WHERE id = 1;On MariaDB/MySQL:
sql
UPDATE DATABASECHANGELOGLOCK
SET LOCKED = FALSE, LOCKGRANTED = NULL, LOCKEDBY = NULL
WHERE ID = 1;Then restart Fineract.
WARNING
Only release the lock manually when you are certain no other Fineract instance is actively running a migration against the same database. Releasing the lock while a migration is in progress causes data corruption.
PostgreSQL: timezone-related data errors during migration
Symptom: Certain date/time columns receive unexpected values after migration, or loan schedule dates are off by hours.
Cause: Fineract's default tenant timezone is Asia/Kolkata (FINERACT_DEFAULT_TENANTDB_TIMEZONE). If this does not match your business timezone, business date calculations will be incorrect. Separately, PostgreSQL JDBC requires the server to store timestamps in UTC for the driver to interpret them correctly.
Fix: Set the PostgreSQL server timezone to UTC in postgresql.conf:
timezone = 'UTC'And separately, configure the Fineract tenant timezone explicitly via the environment variable:
bash
FINERACT_DEFAULT_TENANTDB_TIMEZONE=UTC # or your actual business timezoneNote: Unlike MySQL/MariaDB, PostgreSQL does not use ?serverTimezone=UTC as a JDBC URL parameter. Timezone handling for PostgreSQL is set at the server level or connection level via SET TIME ZONE 'UTC'.
MariaDB: Specified key was too long during migration
Symptom:
Caused by: java.sql.SQLException: Specified key was too long; max key length is 767 bytesCause: Some Liquibase changesets create indexes on VARCHAR columns whose byte length exceeds the 767-byte InnoDB index prefix limit when using the utf8mb4 charset with the older COMPACT row format.
Fix: Ensure MariaDB is using the DYNAMIC row format (supported from MariaDB 10.2+), which extends the prefix limit to 3072 bytes. Set this in my.cnf before running the Fineract upgrade:
ini
[mysqld]
innodb_default_row_format = DYNAMICNote: innodb_file_format = Barracuda was deprecated in MariaDB 10.3 and removed in later versions - do not use it.
Tenant database migration fails while tenant store succeeds
Symptom: Fineract starts and the default tenant appears healthy, but requests to a specific tenant return schema or table-not-found errors.
Cause: In a multi-tenant setup, Liquibase runs migrations against each tenant database independently via the upgradeIndividualTenants() method. If a tenant database was at a different schema version, its migration may fail while others succeed.
Fix:
- Check the logs for the failing tenant identifier
- Connect to that tenant's database and inspect the Liquibase tables for failed entries:sql
-- PostgreSQL SELECT id, author, exectype, dateexecuted FROM databasechangelog WHERE exectype = 'FAILED' ORDER BY dateexecuted DESC; - If needed, you can run the migration in isolation using the Liquibase CLI. Use the changelog path appropriate to your Fineract version from the JAR (typically
db/changelog/db.changelog-master.xml).
Verifying a Successful Migration
After startup, confirm each of the following:
1. No failed changesets:
On PostgreSQL:
sql
SELECT COUNT(*) FROM databasechangelog WHERE exectype = 'FAILED';
-- Must return 02. Lock is released:
sql
-- PostgreSQL
SELECT locked FROM databasechangeloglock WHERE id = 1;
-- Must return false3. Health endpoint is up (Fineract runs on HTTPS port 8443 by default):
bash
curl -k https://localhost:8443/fineract-provider/actuator/healthA {"status":"UP"} response confirms the application started cleanly.
4. Tenant API is accessible:
bash
curl -k -X GET \
https://localhost:8443/fineract-provider/api/v1/loanproducts \
-H "Fineract-Platform-TenantId: default" \
-H "Authorization: Basic $(echo -n 'mifos:password' | base64)"A valid JSON response confirms the tenant schema is accessible and migrations completed successfully.
Rollback Procedure
Liquibase supports rollback for changesets that include a <rollback> block. However, most Apache Fineract changesets do not define rollback instructions, which means automated rollback is not available for the majority of migrations.
The only reliable rollback strategy is a database restore from the backup taken before the upgrade. This is why the pre-migration backup is mandatory.
bash
# PostgreSQL restore
psql -h localhost -U postgres -d fineract_tenants < fineract_tenants_backup.sql
psql -h localhost -U postgres -d fineract_default < fineract_default_backup.sqlSkip the Upgrade Complexity with Finecko
Managing Liquibase migrations across Fineract versions is one of the most error-prone parts of self-hosting. Finecko handles all database migrations automatically as part of the upgrade process - including the Flyway-to-Liquibase boundary - with rollback capability built in.