Why Does a Self-Hosted App Run the Same Database Migration Twice After Redeployment?

Eva Wong is the Technical Writer and resident tinkerer at ZimaSpace. A lifelong geek with a passion for homelabs and open-source software, she specializes in translating complex technical concepts into accessible, hands-on guides. Eva believes that self-hosting should be fun, not intimidating. Through her tutorials, she empowers the community to demystify hardware setups, from building their first NAS to mastering Docker containers.

A database migration can run twice when more than one startup path, container, or scheduler believes it owns the same upgrade step.

Self-hosted applications often launch migrations from an entrypoint, web process, worker, sidecar, systemd unit, or deployment hook. After a redeployment, an old container may overlap with a new one, a restart policy may relaunch a failed migrator, or two replicas may reach the database before one records completion. The diagnosis should identify every potential runner and prove whether the migration framework uses a durable lock or schema-history record before any data repair begins.

Identify Every Process That Can Launch the Migration

Search the image entrypoint, Compose command, worker command, systemd unit, cron job, deployment script, and application startup log for the migration command. Record process IDs and container names at the two execution times.

Docker Compose can start services according to declared dependencies, but startup order alone does not make an application-level migration single-owner. The official startup-order guidance shows why a database being available and a migration being uniquely owned are separate conditions.

If the same command appears in both the web entrypoint and a dedicated migration service, remove one owner. If only one command exists, continue by checking replicas, restart loops, and migration-state records.

Check for Overlapping Old and New Containers

List running, restarting, exited, and orphaned containers during deployment. Compare project names, service names, container IDs, and creation timestamps.

Systemd documents that service restart policy can relaunch a failed command according to unit settings. Its service restart model helps explain why a host launcher can run the migration again after the containerized attempt exits nonzero.

Remove proven orphaned runners only after preserving their logs. A second migration timestamp shortly after the first often reflects a retry rather than a separately scheduled job.

Use a Database Lock Before Applying Schema Changes

Determine whether the application acquires a database-level lock before reading migration state and applying changes. Test two simultaneous startup attempts in a disposable environment.

PostgreSQL provides advisory locks for application-defined coordination, allowing one migration runner to exclude another even when both start at nearly the same time.

A lock must cover the full decision and execution window. Checking the current schema version before acquiring the lock still permits two runners to choose the same pending migration.

Verify Lock Semantics on MySQL or MariaDB

For MySQL-compatible applications, inspect whether the migration tool uses a named lock, transaction, or lock table and whether the connection remains alive for the entire migration.

MySQL documents connection-scoped named locks, which are released when the owning session ends and therefore must be reacquired safely after a crash or restart.

A failed connection can release the lock before the migration framework records completion. Compare database logs with container restart timestamps to identify this sequence.

Inspect the Migration Frameworkโ€™s History Table

List migration identifiers, execution order, success flags, checksums, and timestamps. Compare the two logged runs with the records actually committed to the database.

Flyway uses a schema-history table to track applied migrations and their states.

If the first run changed the schema but failed before recording success, the second run may retry a migration that was not written to be idempotent. Repair the history only after comparing the real schema with the migrationโ€™s expected outcome.

Check Changelog Identity and Checksum Changes

Compare migration filenames, IDs, authors, paths, and checksums before and after the image update. Determine whether the image contains duplicated or renamed changelog entries.

Liquibase records executed changes in the DATABASECHANGELOG table, where change identity depends on its ID, author, and file path.

Moving a changelog file or regenerating identifiers can make old work appear new even when the SQL is similar. Restore stable migration identity rather than manually deleting broad history ranges.

Redeploy With One Migration Owner and Verify Idempotence

Choose one migration owner, add durable locking, keep the web and worker services waiting for successful completion, and redeploy in a test copy of the database.

The ZimaSpace article on container scheduling boundaries provides the adjacent rule: one maintenance task should have one proven runtime owner.

The issue is resolved when concurrent or repeated starts produce one applied migration, one durable history record, and no second schema mutation after restart.

Frequently Asked Questions

Does running a migration twice always damage the database?

No. Idempotent migrations may safely detect existing objects, but non-idempotent data transforms, index creation, or column changes can fail or duplicate data.

Should every web replica be allowed to run migrations?

Only when the framework provides reliable database-level coordination. A dedicated one-shot migration owner is easier to audit in a home-server deployment.

Can I mark the migration complete manually?

Only after proving the live schema and data match the migrationโ€™s expected result. Editing history first can hide a partially applied change.

Support & Tips

More to Read

Get More Builds Like This

Stay in the Loop

Get updates from Zima - new products, exclusive deals, and real builds from the community.

Stay in the Loop preferences

We respect your inbox. Unsubscribe anytime.