What makes a schema migration safe to run against a live production database?
Four properties, and all four have to hold.
First, every intermediate state is a state the running application can work with. The old code is still running while you migrate, so the schema must never be in a state the old code cannot handle. That is the constraint that drives everything else, and it is why the safe pattern is expand and contract rather than change in place.
Second, the migration is idempotent. Running it twice has the same effect as running it once, and running it after a partial failure converges on the intended state rather than erroring out or making things worse. That is what lets you retry a failed migration without a human unpicking it.
Third, it is reversible, or you know it is not. Some migrations genuinely cannot be undone — a column type narrowing that drops information, for instance — and that has to be a conscious decision rather than a surprise. For everything else, write the down migration, and test it.
Fourth, it is observable. You want to know how far it got, how long it is taking, and whether it is still making progress. A migration that updates a million rows should be batched so that each batch commits, and so that the progress is visible rather than one enormous transaction that is either fully applied or fully rolled back after an hour of blocking.
The mechanics follow from those four. Use a versioned migration table so the system knows what has been applied. Take a lock so two deploys cannot run the same migration at once. Run inside a transaction where the engine supports transactional DDL — SQLite and PostgreSQL do, MySQL does not for most DDL. And test the migration against a copy of production data before you run it, because a migration that assumes the data is clean and is not is the most common failure of all.