What is the best way to manage database schema migrations in 2026?
Since this sort of thing is getting easier with AI tooling, I spent some time doing a survey across a bunch of recognizable multi-contributor open source projects to see how they do database schema change management.

Biggest takeaway: the framework provided by your programming language is the most common pattern. After that seems to be custom project-specific code. Even while Pramod Sadalage and Martin Fowler’s twenty-year-old general evolutionary pattern is followed, I was surprised to see very few occurrences of the specific tools they listed in their 2016 article about Evolutionary Database Design. Those tools might be used behind some corporate firewalls, but they aren’t showing up in collaborative open source projects.
Second takeaway: it should be obvious that we still have schema migrations with document databases and distributed NoSQL databases; but lots of interesting illustrations here of what it looks like in practice to deal with document models and NoSQL schemas as they change over time. My recent comment on an Adam Jacob LinkedIn post:“life is great as long as changing your schema can remain avoidable (ie. requiring some kind of migration).”
What about the method of triggering the schema migrations? The most common pattern is that the application process itself triggers schema migration. After that we have kubernetes jobs.
The rest of this blog post is the supporting data I generated with some AI tooling. I made sure to include links to source code, for verifying accuracy. I spot checked a few and they were all accurate – but I didn’t go through every single project.
If you spot errors, please let me know!! I’ll update the blog.
A survey of how major open-source projects handle database schema migrations. Each project includes a real code example and how migrations are triggered during upgrades.
Projects with no official Helm chart or k8s support (Mastodon, Discourse, Sentry, Zulip, NetBox, Metabase, Lemmy, MediaWiki, Matrix Synapse†, CHT Core, Signal Server, Firefox, Chromium, Signal Desktop, FDB Record Layer, RxDB) are omitted.
| Trigger Method | Projects |
|---|---|
| Dedicated k8s Job (Helm hook) | GitLab (post-deploy), Airflow (post-install/upgrade), Superset (post-install/upgrade), Temporal (pre-deploy), Kong (pre-install), Jaeger (pre-deploy), ThingsBoard (install only; upgrades require a separate manual pod) |
| Init container in pod spec | Gitea (official chart runs gitea migrate in init container before main container starts) |
| App process migrates on pod startup | Ghost, Backstage, Keycloak, Grafana, Mattermost, Odoo, Parse Server, Appsmith, Rocket.Chat, Graylog |
| Triggered by action against running process | WordPress (first admin HTTP request), Kubernetes (StorageVersionMigration CRD triggers in-cluster controller), Dgraph (POST /admin API call; async index rebuild) |
| Manual operator action | Calico (calico-upgrade CLI), Neo4j-Migrations (neo4j-migrations migrate CLI), Nextcloud (occ upgrade via exec or Job), Zipkin (SQL DDL applied before deploy), APISIX (no tooling; manual etcd data transformation) |
| No migration needed | Cortex (schema versioned in YAML config; new period appended and deployed, old data untouched) |
† Matrix Synapse has no official Helm chart from Element; the widely-used community chart (ananace/matrix-synapse) relies on in-process startup migration.
| Projects | Language | Framework | Trigger |
|---|---|---|---|
| GitLab, Mastodon, Discourse | Ruby | Rails ActiveRecord | GitLab: dedicated k8s Job (Helm). Mastodon: manual two-phase CLI; no official Helm chart. Discourse: launcher script runs rake db:migrate during rebuild; no official Helm chart. |
| Sentry, Zulip, NetBox | Python | Django Migrations | Sentry: sentry upgrade CLI (acquires distributed lock; post-deployment migrations must be run separately); official self-hosted is docker-compose only, no official Helm chart. Zulip: scripts/upgrade-zulip script; no official Helm chart, typically deployed on VMs. NetBox: container entrypoint script runs manage.py migrate on container start (netbox-docker); no official Helm chart. |
| Airflow, Superset | Python | Alembic | Both: dedicated k8s Job as Helm post-install/post-upgrade hook. |
| Ghost, Backstage | JavaScript, TypeScript | Knex.js | Both: app code calls migration runner on startup. Both have official Helm charts (Bitnami for Ghost, backstage/charts for Backstage); migrations run in-process at pod startup, no separate job. |
| Keycloak, Metabase | Java, Clojure | Liquibase | Both: app code calls Liquibase on startup. Keycloak: DefaultJpaConnectionProviderFactory; official Helm chart (Bitnami) and k8s Operator exist, auto-migrates at pod startup. Metabase: setup-db! (custom Clojure macros wrap Liquibase changesets); no official Helm chart. |
| Lemmy | Rust | Diesel | App code calls run_pending_migrations() on startup (before pool is returned). No official Helm chart; typically deployed via docker-compose. |
| Gitea | Go | XORM | Official Helm chart exists; init container explicitly runs gitea migrate before the main container starts (not relying on auto-migration). AUTO_MIGRATION=false can disable the in-process fallback. |
| Nextcloud | PHP | Doctrine DBAL | occ upgrade CLI or web-based updater; not automatic. Official Helm chart exists (nextcloud/helm); init containers only wait for DB readiness. occ upgrade must be run manually (e.g., exec into pod). |
Recent Comments