Where the migration runs
Never at import time and never lazily inside a request. Run it as an explicit command, once, before the app starts, so a failure is loud and the app never sees a half-migrated state.
The two-release window
For one release, support both the old and the new data shape. That window is what lets an older box still read a database that a newer box wrote. Close the window in the release after.