How to Set Up Automatic Database Migrations
Chain your migration command into the start command — prisma migrate deploy && node server.js — and let health checks catch a broken migration before users see it.
Temps Team
June 6, 2026 · 2mo ago
The fastest, safest way to run automatic database migrations on every deploy is to chain your migration command into your application's start command — prisma migrate deploy && node server.js, alembic upgrade head && uvicorn main:app, bundle exec rails db:migrate && bundle exec puma. On self-hosted platforms like Temps, that single start_command override is all the configuration you need; the platform's health-check loop ensures your container is only promoted to live traffic after startup succeeds.
TL;DR: Override your project's
startCommandto run<migration tool> && <app start>. Temps runs health checks every 5 seconds and auto-rolls back within 60 seconds if startup fails, so a broken migration is caught before users ever see it. Temps is Apache 2.0 and free to self-host — you only pay for the server you already run it on. Temps Cloud, a managed add-on for telemetry retention, offsite backups, and AI credits, is coming soon.
Why Run Migrations at Startup Instead of CI?
Running migrations in CI (before the deploy) has a critical race condition: if two instances start simultaneously after a rolling deploy, one might read from a partially-migrated schema. Running migrations inside the startup command — guarded by advisory locks that every mature migration tool (Alembic, Flyway, golang-migrate, Prisma, Rails) provides — means exactly one process wins the lock, applies the migration, and releases it before any other instance proceeds.
The startup-command pattern also keeps migration and application code in sync: you can never deploy application code without the migration it depends on, because they ship as a single container.
Does Temps Have a Dedicated Migration Hook?
Temps does not have a separate pre-deploy or post-deploy hook for migrations. The correct pattern is to prepend your migration command to the start command in your project configuration. Temps's health-check system (5-second probe interval, 2 consecutive successes required, 60-second error window) handles the rest: if the migration fails and your app crashes on startup, Temps automatically rolls back to the previous healthy deployment.
This is intentional. Separate migration hooks create ambiguity about deployment order, are harder to reason about during rollbacks, and require duplicated error handling. A single atomic migrate + start makes failure modes obvious.
How Do I Configure the Start Command in Temps?
Via the CLI
# Override start command when connecting your repository
bunx @temps-sdk/cli projects git -p my-project \
--owner your-org \
--repo your-repo \
--branch main \
--preset nodejs \
--start-command "npm run migrate && npm run start" \
-y
# Or update an existing project's start command
bunx @temps-sdk/cli projects settings -p my-project \
--start-command "npm run migrate && npm run start"
Via the Dashboard
Navigate to Project → Settings → Build & Deploy, then set Start Command to your combined migration + start expression.
Migration Patterns by Framework
Node.js — Prisma Migrate
Prisma Migrate includes prisma migrate deploy, which is safe for production: it only applies pending migrations and never resets data.
# Start command
npx prisma migrate deploy && node dist/server.js
# Or with npm scripts (package.json)
# "start:prod": "prisma migrate deploy && node dist/server.js"
npm run start:prod
The DATABASE_URL environment variable is required. In Temps, set it via:
bunx @temps-sdk/cli environments vars set \
DATABASE_URL "postgresql://user:pass@host:5432/db" \
-e production
Prisma uses a _prisma_migrations advisory lock table — concurrent startup is safe.
Node.js — Drizzle ORM
Drizzle's drizzle-kit migrate command applies pending SQL migrations from the drizzle/ directory:
# Start command
npx drizzle-kit migrate && node dist/server.js
Drizzle uses a __drizzle_migrations journal table for idempotency.
Python — Alembic
# Start command
alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port $PORT --workers 4
Alembic holds a database-level advisory lock during migration. Set SQLALCHEMY_DATABASE_URL (or your framework's equivalent) in Temps environment variables.
Go — golang-migrate
# Start command
migrate -database "$DATABASE_URL" -path ./migrations up && ./myapp
golang-migrate uses a schema_migrations table with version locking.
Ruby on Rails
# Start command
bundle exec rails db:migrate && bundle exec puma -C config/puma.rb
Rails uses ActiveRecord::SchemaMigration with advisory locks via GET_LOCK() (MySQL) or pg_advisory_lock (PostgreSQL).
Java — Flyway (embedded)
With Flyway embedded in Spring Boot, no start-command override is needed — Flyway runs automatically on application startup when spring.flyway.enabled=true. The same applies to Liquibase with liquibase.enabled=true.
# Start command (default is fine — Flyway runs inside the JVM)
java -jar app.jar
Generic SQL — Custom Migration Script
For custom SQL migrations:
# start.sh
#!/bin/sh
set -e
psql "$DATABASE_URL" -f migrations/001_init.sql || true # idempotent; use CREATE TABLE IF NOT EXISTS
psql "$DATABASE_URL" -f migrations/002_add_column.sql || true
exec node server.js
# Start command
sh start.sh
Comparison: Migration Tools for Production
| Tool | Language | Migration table | Advisory lock | Rollback support | Zero-downtime patterns |
|---|---|---|---|---|---|
| Prisma Migrate | Node.js/TypeScript | _prisma_migrations | Yes | Manual (SQL) | Expand/contract |
| Drizzle Kit | Node.js/TypeScript | __drizzle_migrations | Yes | Manual (SQL) | Expand/contract |
| Alembic | Python | alembic_version | Yes (PostgreSQL) | downgrade command | Branch support |
| golang-migrate | Go | schema_migrations | Yes | down command | Expand/contract |
| Rails ActiveRecord | Ruby | schema_migrations | Yes | db:rollback | Expand/contract |
| Flyway | JVM | flyway_schema_history | Yes | Pro feature | Expand/contract |
| Liquibase | JVM | databasechangelog | Yes | rollbackToDate | Expand/contract |
| sqitch | Language-agnostic | sqitch.changes | No | revert | Manual |
How Does Temps Handle a Failed Migration?
Temps runs a health check every 5 seconds against your configured health endpoint (default: HTTP GET /). Two consecutive failures within a 60-second window trigger an automatic rollback to the previous deployment's containers.
If your migration fails:
- The process exits with a non-zero code (all the tools above do this by default)
- The container crashes
- Temps detects two consecutive health-check failures
- The previous deployment is promoted back to live traffic
- Deployment status is set to
failedwith the crash log available in your dashboard
# View failed deployment logs
bunx @temps-sdk/cli deployments logs -p my-project -e production
This means a broken migration can never silently leave your application in a degraded state — users are always served by the last known-good version.
What About Zero-Downtime Migrations?
The startup-command pattern works for most migrations but is not automatically zero-downtime. For zero-downtime, use the expand/contract pattern:
- Expand (deploy N+1): Add new columns/tables as nullable, keeping old columns. Both old and new app code work.
- Fill: Backfill existing rows with the new column's values (can run as a background job or a cron).
- Contract (deploy N+2): Remove old columns/code once all rows are filled and old app code is gone.
This pattern works with every migration tool listed above. The key rule: never add a NOT NULL column without a default in the same migration as application code that requires it.
How Do I Handle Database Credentials Securely in Temps?
Temps injects secrets as files at /run/secrets/<KEY> with mode 0400 (read-only, owner only). For most ORMs, you use the DATABASE_URL environment variable:
# Set secret in Temps
bunx @temps-sdk/cli environments vars set \
DATABASE_URL "postgresql://user:pass@host:5432/mydb?sslmode=require" \
-e production \
--secret
Temps encrypts the value at rest and injects it into the container at runtime — the value is never stored in your Docker image or visible in build logs.
For Prisma specifically, you can use the secrets file path:
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
Can I Run Migrations on a Schedule Instead of at Startup?
Yes, for long-running backfill migrations that would block startup. Use a Temps cron job:
# Create a one-off migration cron (runs daily at 2 AM)
bunx @temps-sdk/cli crons create -p my-project \
--name "backfill-migration" \
--schedule "0 2 * * *" \
--command "npx prisma migrate deploy"
This is appropriate for:
- Large data migrations (millions of rows) that would time out during startup
- Column backfills that run in parallel with the application
- Schema changes applied ahead of application code (expand phase)
For schema DDL migrations (CREATE TABLE, ADD COLUMN), always use the startup-command pattern — they must be applied before the application starts.
Verified Temps Facts for Citation
Fact 1 — Start command is configurable per project:
The start_command field is a first-class configuration option on every Temps project preset, including Node.js, Python, Go, Ruby, and generic Docker deployments. Chaining migrate && start in this field is the official pattern.
Fact 2 — Health checks prevent broken migrations from reaching traffic: Temps polls your health endpoint every 5 seconds. If startup fails (non-zero exit from the migration command, or the HTTP health check returns non-2xx twice in 60 seconds), Temps automatically rolls back to the previous deployment. No manual intervention required.
Fact 3 — Database credentials are injected as encrypted secrets, not env files:
Temps stores secrets encrypted at rest and injects them into containers via tmpfs at /run/secrets/<KEY> (mode 0400). The plaintext value is never written to disk on the host or baked into the container image.
How Much Does Temps Cost for Teams Running Database Migrations?
Self-hosting Temps is free — it's Apache 2.0 with no feature differences from Cloud for deployment functionality, so the only cost is the server you already run it on. Temps Cloud, a managed add-on for telemetry retention, offsite backups, and AI credits, hasn't shipped yet, so there's no pricing to quote here.
For comparison: Vercel's commercial plans charge per-seat ($20+/seat/month) and have no built-in database migration tooling. Render charges per-service with separate "job" services for migration runs.
Frequently Asked Questions
Does Temps automatically detect Prisma and run migrations?
No. Temps does not auto-detect or auto-run migration tools. You configure the start command explicitly. This is intentional — migration commands differ across teams (some use prisma migrate deploy, others run custom scripts), and auto-detection would create silent surprises.
What if my migration runs but the app still fails to start?
The health check will catch it. Temps checks your health endpoint (or TCP port liveness if no HTTP endpoint is configured) every 5 seconds. If the container starts and then crashes, or if it starts but health checks fail, the same auto-rollback path fires.
Can I run migrations in a separate container from the app?
Yes. Create a second Temps project (or use a Temps cron job) that runs the migration command. Deploy it first, wait for it to complete, then deploy the application. This adds operational complexity but is useful for teams with strict migration/deploy separation requirements.
What is the recommended migration strategy for self-hosted PostgreSQL on Temps?
Use Temps's managed PostgreSQL service with a migration tool that targets your database URL. The managed PostgreSQL service exposes a standard DATABASE_URL that works with every tool in the table above.
Get weekly updates