There is a significant difference between having a database backup and knowing that the database can be recovered.
A backup command can finish successfully. An archive can exist in the expected directory. Its checksum can match after transferring it to another machine.
Those are useful checks, but none of them proves that the database can actually be restored.
I recently had to address this distinction while decommissioning a Laravel-based SaaS platform. The infrastructure was being retired, but I wanted to preserve the option of recovering the application and its data in the future.
That meant treating the recovery process as something to test before deleting the original infrastructure, rather than something to investigate if the backups were ever needed.
The experience eventually led me to build and publish a small open-source project: Recoverable SaaS Decommission.
The problem with backup verification
Database backups are often considered successful once the backup file has been created.
For example:
mysqldump ... > database.sql
Or, with compression:
mysqldump ... | gzip > database.sql.gz
The resulting file may be present, nonempty and correctly transferred to another machine.
But several questions remain unanswered.
- Will the SQL import into a compatible database engine?
- Does the restored database contain the expected tables?
- Are important records accessible?
- Can relationships between records be verified?
- Is the restore procedure independent of the original hosting environment?
File integrity and database recoverability are related, but they are not the same thing.
A SHA-256 comparison can establish that two files contain identical bytes. It cannot establish that those bytes represent a complete or usable database backup.
That distinction becomes particularly important during infrastructure decommissioning.
Once the original database server has been deleted, discovering that the backup is unusable becomes a much more serious problem.
The original decommissioning exercise
The application involved Laravel, PHP, Nginx, Ubuntu and a managed MySQL database hosted on DigitalOcean.
It also used a multi-tenant architecture.
The decommissioning process involved preserving several categories of assets:
- A database export.
- Application files and configuration.
- Persistent application storage.
- Server configuration needed to understand the original deployment.
- Recovery credentials and encryption material, stored separately from the public project.
The archives were encrypted, transferred away from the original server and checked using SHA-256 digests.
The database backup then underwent an independent restore test using Docker on a Windows machine with Ubuntu WSL.
That exercise involved importing the database into a separate MySQL environment, inspecting the restored structure and querying representative records.
The database restoration succeeded.
Importantly, this was a database recovery test, not a full clean-room reconstruction of the SaaS application. I did not use that result to claim that every component of the original application stack had been independently recovered.
With the necessary recovery evidence and archives in place, the original infrastructure could then be retired.
Turning the process into a reusable tool
After completing the decommissioning, I wanted a reusable way to verify future database backups without manually repeating the Docker setup, import and cleanup procedure.
I deliberately kept the first implementation small.
Rather than building a complete disaster-recovery framework, I focused on one specific question:
Can this MySQL backup be imported into a disposable database, and do the supplied verification checks pass?
The resulting Bash verifier is available in the repository:
It uses Docker to create a temporary MySQL environment, restore the supplied SQL backup, perform verification and remove the container afterwards.
How the verifier works
The workflow is straightforward:
SQL backup
|
v
Validate input
|
v
Start isolated MySQL container
|
v
Wait for authenticated readiness
|
v
Import SQL backup
|
v
Check restored table count
|
v
Run optional SQL assertions
|
v
Report PASS / FAIL
|
v
Remove temporary container
The tool supports ordinary .sql backups and gzip-compressed .sql.gz files.
For isolation, the container is started without published ports and with Docker networking disabled:
docker run -d \
--name "$CONTAINER" \
--network none \
...
The temporary MySQL password is generated using OpenSSL and supplied through a restricted file mounted into the container.
The verifier also uses an exit trap to remove the container and its associated temporary resources when execution ends.
A key design decision was not to connect to the production database at all.
The verifier works entirely with a supplied backup file.
Running a restore test
The repository contains a synthetic SQL fixture and a corresponding set of verification queries.
This means the example can be tested without production data.
On Linux, with Bash and Docker available:
bash mysql/verify-restore.sh \
fixtures/sample-schema.sql \
mysql/verification-queries.sql
On Windows, I tested the Bash verifier using Ubuntu WSL with Docker Desktop:
wsl -d Ubuntu -- bash mysql/verify-restore.sh fixtures/sample-schema.sql mysql/verification-queries.sql
The fixture contains fictional organisations, users and activity records.
The verifier imports the database and executes the provided assertions.
A successful run produces output similar to:
[PASS] MySQL authenticated and ready.
[PASS] SQL import completed.
[PASS] Restored base tables: 3
[PASS] Custom verification assertions passed.
RESTORE VERIFICATION: PASS
MySQL image: mysql:8.0
Restored base tables: 3
Custom assertions: PASS
[INFO] Removed disposable MySQL container.
The same process can be used with gzip-compressed SQL backups.
When adapting this to another application, the important part is supplying verification queries that reflect that application's actual schema and recovery requirements.
Why an import alone is insufficient
An SQL import can finish without an error while still leaving important questions unanswered.
For example, a database may contain the expected tables but lack records that should have been included in the backup.
Or a backup may be internally consistent but represent an older recovery point than intended.
For that reason, the verifier supports application-specific SQL assertions.
These queries can check expected tables, record counts, relationships or other recovery invariants.
The current verifier expects the assertion file to produce exactly one scalar value:
SELECT 1;
A result of 1 indicates that the supplied assertions passed.
Any other result is treated as a failure.
Of course, SELECT 1 alone proves nothing meaningful about the restored data. It is simply the interface contract. Real verification queries should encode useful expectations.
The repository includes an example assertion file using synthetic data.
There is also an important limitation: an assertion is only as strong as the evidence behind it.
Without a trusted source baseline, the tool cannot prove that every record from the original database has been recovered.
Testing failure conditions
I didn't want a tool that only demonstrated a successful restoration.
The failure paths mattered just as much.
For the initial release, I tested five scenarios:
| Scenario | Observed result |
|---|---|
Valid .sql backup |
Restoration and assertions passed |
Valid .sql.gz backup |
Restoration and assertions passed |
| Missing backup file | Input rejected |
| Deliberately failing SQL assertion | Verification rejected |
| Malformed SQL backup | MySQL import failed and the verifier reported failure |
The failed-import and failed-assertion scenarios also demonstrated that the disposable container was removed afterwards.
These tests are useful evidence for the core workflow, but they are not a comprehensive security audit or a claim of compatibility with every MySQL dump format.
Security considerations
A disposable database is not an excuse to ignore security.
The current implementation follows several basic principles:
- It does not require access to a production database.
- MySQL is not exposed through published network ports.
- Docker networking is disabled for the restore container.
- Temporary credentials are generated rather than hardcoded.
- The script attempts to remove only the container it created.
- SQL errors and failed assertions cause verification to fail.
However, SQL backups and assertion files should still be treated as trusted inputs.
The verifier executes SQL with administrative privileges inside the temporary database. It is not intended to safely process arbitrary malicious SQL or MySQL client commands.
There are also operational limitations. Database imports and Docker image downloads are not individually time-limited, and the initial release uses the mysql:8.0 image tag rather than an immutable digest.
These are considerations to understand before using the tool in a more demanding operational environment.
What the tool does not verify
It is important not to overstate what a database restore test proves.
For a Laravel SaaS application, complete recovery may also require:
- Application source code and Composer dependencies.
- Environment variables and encryption keys.
- Uploaded files and persistent storage.
- Web server and PHP configuration.
- Queue workers and scheduled tasks.
- DNS, TLS certificates and external services.
- Tenant-specific configuration and application-level testing.
A successful database import cannot establish that all of those components are recoverable.
The verifier answers a narrower question: whether the supplied MySQL backup can be restored and whether the selected checks pass.
That is still a useful piece of recovery evidence.
Why I published the project
The work started as an operational requirement: retire an application without losing the ability to recover its preserved data.
The resulting verification procedure was useful enough to make reusable.
I have published the first release, v0.1.0, under the MIT License, with synthetic fixtures and documentation so others can examine and adapt the approach without needing access to the original system.
The repository is available here:
Recoverable SaaS Decommission on GitHub
The initial version is intentionally limited to MySQL restore verification. It is not a finished, general-purpose disaster-recovery platform.
For me, the main lesson from the exercise is that creating a backup and demonstrating recovery should be treated as separate operational activities.
Both matter, but only one involves actually attempting the restore.
Don't test whether you can create a backup. Test whether you can restore from it.
Top comments (0)