Database Migration Readiness for Self-Hosted Applications: Can You Roll Back Safely?
Use this database migration rollback checklist before updating a self-hosted application. Separate code rollback from database recovery, assess compatibility and locks, verify backups, and define recovery decisions before deployment.

Why application rollback and database rollback are different
Replacing a new application container with the previous image can be quick. That is a code rollback. It does not automatically return the database to the structure and contents expected by the earlier release.
A database migration may add tables, rename columns, transform stored values, create indexes, tighten constraints, delete data, or launch work that continues after the deployment. Once a migration has committed, a transaction-level ROLLBACK is no longer available for that committed work. A safe recovery may instead require restoring data, repairing data, or completing a forward fix.
Treat the application release and the database change as two related but separate deployment objects. Approval should depend on whether the old application can safely operate against the post-migration database state, not simply on whether an older container image is available.
- Code rollback question: can the former application release be started and routed back into service?
- Database recovery question: can the prior schema and the required business data be restored or reconstructed without unacceptable loss?
- Compatibility question: can old code, new code, and the changed database coexist during the planned transition?
- Decision rule: do not describe a release as reversible until both the code and database paths have been reviewed.

Map every database-affecting change before approval
Build a change map from the application’s release notes, migration files, deployment scripts, and database commands. Do not rely on a label such as “automatic migration”; identify what that automation actually changes and when it runs.
Classify each operation by its effect on schema, data, availability, and reversibility. The goal is not to predict every implementation detail. It is to expose operations that change the recovery strategy or require operational controls.
For containerized deployments, also identify where persistent database data lives. Docker Compose volumes are persistent stores managed by the container engine, so the database volume, any external database service, and the backup source must be unambiguous. Recreating an application workload is not evidence that the database has been preserved.
- Schema: new or dropped tables, columns, types, indexes, foreign keys, defaults, and constraints.
- Data transformation: backfills, value conversions, deduplication, encryption changes, identifier rewrites, and deletions.
- Performance and locking: table rewrites, index builds, long-running queries, and operations that can wait on other transactions.
- Background work: queued jobs, workers, scheduled tasks, or application startup tasks that continue changing data after the schema migration.
- Dependencies: reporting tools, integrations, exports, views, API consumers, and custom scripts that may rely on existing fields or values.
- Migration metadata: the migration identifier, execution order, tool or framework, and whether each step has a documented reverse operation.

Check backward and forward compatibility explicitly
A safe deployment frequently depends on a compatibility window: a period when both the previous and new application releases can use the same database state. Without that window, a code rollback after migration may fail even if the old image starts normally.
Review read and write behavior separately. Old code may tolerate a new nullable column but fail if a column it writes has been removed, renamed, made mandatory, or changed in meaning. New code may start before a backfill completes only if it can correctly handle old and new representations.
Make compatibility a recorded decision rather than an assumption. If old code is not compatible with the migrated database, the recovery plan must prioritize a restore, data repair, or forward fix over a simple code rollback.
- Old code with expanded schema: can it ignore additive tables, columns, and indexes?
- New code before data migration completes: can it read both old and new value formats?
- Old code after a backfill: will it overwrite transformed data or create records in an obsolete format?
- Constraint timing: will a new NOT NULL, uniqueness, or foreign-key constraint reject writes made by an older release?
- External consumers: do integrations depend on a column name, output format, identifier, or API behavior affected by the change?
- Mixed-version operation: if multiple application workers are restarted gradually, is concurrent old/new operation supported?
Flag destructive operations and lock-sensitive changes
Some changes deserve an explicit recovery plan because they remove information, make older assumptions invalid, or affect availability. Dropping a PostgreSQL column can also remove indexes and table constraints involving that column; dependencies outside the table, such as foreign keys or views, can require further action. A destructive change should never be approved solely because it appears late in a migration sequence.
Lock effects are equally important. In PostgreSQL, ALTER TABLE lock requirements differ by subcommand, and ACCESS EXCLUSIVE is the default unless documentation says otherwise. Review the exact statements rather than treating every ALTER TABLE operation as equivalent.
Index creation also requires a deployment choice. A standard PostgreSQL index build blocks writes while it runs. CREATE INDEX CONCURRENTLY avoids blocking concurrent inserts, updates, and deletes, but it cannot run inside a transaction block, performs additional table scans, and waits for relevant transactions to finish. That changes both timing and failure handling.
Table-rewriting ALTER TABLE forms and TRUNCATE need heightened review when concurrent access exists. PostgreSQL documents MVCC caveats for these operations, including cases where concurrent snapshots can see an empty or inconsistent view after commit.
- Destructive: DROP COLUMN, DROP TABLE, TRUNCATE, deletion backfills, irreversible value conversion, and replacement of identifiers.
- Compatibility-breaking: rename or removal of a field used by old code, tightening a constraint, and changing the meaning of stored values.
- Availability-sensitive: table rewrites, lock-heavy ALTER TABLE operations, and normal index builds on actively written tables.
- Non-single-transaction steps: concurrent index creation and background work that occur outside one transaction boundary.
- Required response: name the exact recovery method for each flagged operation before deployment.
Prefer expand, migrate, contract when the application supports it
For material changes, use an expand–migrate–contract pattern where the application and its vendor guidance support it. This separates compatibility work from destructive cleanup, creating room to validate and roll back code before irreversible changes are introduced.
Expand means adding new structures without removing the old ones: for example, a new nullable field, table, or index. Migrate means backfilling data and teaching the new application version to read and write the compatible representation. Contract means removing obsolete structures only after the compatibility window has ended and validation is complete.
Do not force this pattern onto an application whose supplied migration path does not support staged versions. In that case, document the vendor-required sequence, test it faithfully, and choose an appropriate maintenance and recovery plan. The useful principle is separation of risk, not an artificial rewrite of third-party migrations.
- Expand: add the new structure and confirm old code continues to work.
- Deploy compatible code: ensure new code handles both representations when necessary.
- Migrate: run backfills in observable batches where the application supports that approach.
- Validate: compare counts, required records, permissions, integrations, and key workflows.
- Contract: remove legacy fields or formats only after the rollback window has intentionally closed.
- Record the point of no return: state exactly when simple code rollback is no longer safe.
Collect deployment evidence, not just a backup status
A backup is useful only when its method, coverage, location, and restoration process are understood. PostgreSQL distinguishes SQL dumps, file-system backups, and continuous archiving; each has different strengths and limitations. Record which method protects this change rather than using the generic statement “backup completed.”
A logical PostgreSQL dump is an internally consistent snapshot from when pg_dump begins, but operations requiring an exclusive lock, including most ALTER TABLE forms, are exceptions to its otherwise non-blocking behavior. Confirm that the backup timing and method are compatible with the migration’s locking and recovery requirements.
Point-in-time recovery is not a generic promise. For PostgreSQL, it requires a suitable prior physical backup and archived write-ahead logs covering the target time. If those prerequisites are absent, do not list point-in-time recovery as an available option.
Airbip provides configurable daily, weekly, and monthly backups for application deployments. Teams should still verify the scope, retention configuration, database coverage, and restore procedure applicable to their own instance before depending on those backups for a migration decision.
- Backup identity: method, completion time, scope, storage location, and the person who verified it.
- Restore confidence: a recent restore test, estimated steps, required credentials, target environment, and known limitations.
- Migration evidence: exact release version or image reference, migration identifiers, start and finish times, logs, and errors.
- Baseline checks: record important counts, representative records, critical workflows, and integration status before the change.
- Post-change checks: record the same checks after migration and define acceptable differences.
- Recovery threshold: decide the maximum acceptable outage and data-loss exposure before beginning.
Decide whether a maintenance window or temporary write restriction is required
A maintenance window is warranted when the migration can block writes, create incompatible mixed states, take an uncertain amount of time, or require a restore that cannot safely merge later user changes. A temporary write restriction may be sufficient when reads remain safe but writes would conflict with a backfill, schema change, or potential rollback.
Base the decision on actual operations and business impact. For example, a normal PostgreSQL index build blocks writes, while a concurrent index build avoids that specific write block but has longer-running operational characteristics and cannot share a normal transaction boundary. Neither choice is automatically safer without considering workload, timing, and recovery.
Define what users will see and what operators will do. A write restriction can mean pausing background workers, disabling scheduled imports, putting an application into a vendor-supported maintenance mode, or temporarily rejecting write requests. Ensure integrations and administrators receive the same instruction; otherwise they may create data that complicates recovery.
- Use a full maintenance window when schema changes or restoration make concurrent writes unsafe.
- Use a targeted write restriction when read access can continue safely and the application supports that mode.
- Pause or account for background workers, imports, webhooks, and scheduled jobs.
- Set a start time, expected duration, extension decision point, and user communication path.
- Confirm how queued work will be resumed, deduplicated, or reconciled after deployment or recovery.
- Stop if locks, duration, or error rates exceed the approved threshold.
Choose the recovery path before you deploy
Recovery is a decision tree, not a single rollback button. Pre-approve the conditions under which the team will roll back application code, restore data, repair a limited set of records, or continue with a forward fix. Name who can authorize each action, especially a restore that may discard legitimate writes made after the selected backup point.
A code rollback is appropriate only when compatibility has been confirmed and the migration has not created an unsafe database state for the older release. A restore is appropriate when the data state itself must return to a known point, but it requires careful treatment of writes that occurred after the backup. Data repair may work for a small, fully understood, auditable error. A forward fix is often safer when restoration would lose more valid business activity than correcting the defect.
Framework behavior also matters. For example, Django identifies RunPython steps without reverse_code and RunSQL steps without reverse_sql as irreversible. Django migration transaction behavior also varies by database engine: its default handling differs between engines with DDL transactions, such as PostgreSQL and SQLite, and engines such as MySQL and Oracle. Read the application’s migration framework and database-specific guidance before assuming reversal is available.
- Code rollback: specify the compatible prior release and the checks required before routing traffic back.
- Restore: specify the recovery point, method, expected downtime, data-loss implications, and reconciliation owner.
- Data repair: specify the affected records, repair script, audit trail, validation query, and reversal method for the repair itself.
- Forward fix: specify the safe interim state, owner, escalation path, and user-impact controls.
- Authority: name the technical operator, business data owner, and final decision maker for each recovery option.
- Communications: prepare internal and user-facing messages for extended maintenance, data reconciliation, or service restoration.
Frequently asked questions
Can I roll back a database migration by rolling back the application container?
Not necessarily. Returning to an older application image changes code, not the committed database schema or data. Use code rollback only after confirming that the older release is compatible with the post-migration database state.
What should a database migration rollback checklist include?
Include the exact schema and data changes, migration reversibility, old/new code compatibility, lock and downtime risks, backup method and scope, restore-test evidence, pre- and post-change validation, a write-control plan, recovery options, and named decision makers.
When is a backup not enough for a safe rollback?
A backup alone is insufficient when the team does not know whether it includes the database, whether it can be restored, what point in time it represents, or how post-backup user changes will be handled. A restore plan needs both evidence and a business decision about acceptable data loss.
Is CREATE INDEX CONCURRENTLY always the right option in PostgreSQL?
No. It avoids blocking concurrent inserts, updates, and deletes, but it cannot run inside a transaction block, uses additional table scans, and waits for relevant transactions. Choose it based on workload, deployment tooling, duration, and failure handling.
When should we use a maintenance window for a database migration?
Use one when operations may block writes, when old and new application versions cannot coexist safely, when background activity would complicate recovery, or when a restore would be the likely response to failure. A temporary write restriction may be enough for lower-impact changes when reads can safely continue.
How does Airbip fit into database migration readiness?
Airbip manages Docker-based application deployments and offers configurable daily, weekly, and monthly backups. Migration readiness remains a shared operational responsibility: the team must verify what is backed up, test restoration where appropriate, understand the application’s migration behavior, and approve data and recovery decisions.
Sources and further reading
- PostgreSQL transactions — PostgreSQL Global Development Group
- PostgreSQL ALTER TABLE — PostgreSQL Global Development Group
- PostgreSQL CREATE INDEX — PostgreSQL Global Development Group
- PostgreSQL backup and restore — PostgreSQL Global Development Group
- PostgreSQL SQL dump — PostgreSQL Global Development Group
- PostgreSQL write-ahead logging — PostgreSQL Global Development Group
- PostgreSQL MVCC caveats — PostgreSQL Global Development Group
- Django migration operations — Django Software Foundation
- Django migrations — Django Software Foundation
- Docker Compose volume reference — Docker