# Database migrations on Cloud Run: run them as a job, before the deploy

> Cloud Run has no release phase. How to run migrations as a Cloud Run job with the new image before the deploy, write migrations the old revision survives, and pay about a tenth of a cent a run.

Published 2026-10-12 · Updated 2026-10-13 · [By Exodus team](https://exodus.elim.io/about) · Patterns, Data
<https://exodus.elim.io/blog/database-migrations-as-cloud-run-jobs>

Database migrations on Cloud Run go in a Cloud Run job that runs the new image once, with `--wait`, before `gcloud run deploy`, because Cloud Run has no release phase. A migration in the start command runs on every instance that starts, in parallel, and must finish inside a 240-second startup probe. A job run costs $0.00114 at list prices, usually $0.00 after the free tier.

## In short

- A migration in the start command runs on each new instance at once, and one still running after 240 seconds fails the revision.
- The job runs the same image with `--max-retries 0`, since the default is 3 retries, and a task timeout above the 10-minute default, up to 168 hours.
- A 20-second run on 1 vCPU and 512 MiB bills as 60 seconds, $0.00114; the jobs free tier covers about 4,000 such runs a month.
- Each migration must work with the revision still serving: a column rename takes 4 deploys, and `lock_timeout = '2s'` turns a lock queue into a failed migration.

Prices are europe-west1 list prices of October 12, 2026.

## What goes wrong when the container migrates at start

On [Heroku](https://exodus.elim.io/blog/heroku-to-cloud-run) the `release:` line of a Procfile runs your migrations before the new version takes traffic; Fly has `release_command`, Render a pre-deploy command. Each runs the migration once, with the new code, and stops the deploy if it fails. Cloud Run has no such step, so the migration usually lands in the start command, as `migrate && start`. It works on the first deploy with one instance and breaks later.

A Cloud Run service runs as many copies of your container as traffic needs, and each copy runs the start command from the beginning. A migration in that command runs as many times as there are instance starts, and four things follow from that.

**Several instances run it at once.** Since April 2023 Cloud Run "starts enough instances of the new revision before directing traffic to it", so a busy service starts several new instances together on every deploy, and each of them reaches the migration at about the same time. Whether that is safe depends on your tool. Rails and Prisma take a database advisory lock (a named lock the tool asks Postgres for: whoever holds it migrates, the others wait or fail) and so run it only once. Django's `migrate` takes no lock of its own that its documentation mentions. Laravel's `--isolated` needs a cache server shared by all instances, and Cloud Run has none unless you add one.

**A slow migration fails the deploy.** Cloud Run decides that a new instance has started when it accepts a TCP connection on its port. This default startup probe, described in the [health checks guide](https://exodus.elim.io/blog/health-checks-and-startup-probes), waits 240 seconds, once. A migration that rebuilds a big index runs before the server listens, and after 240 seconds the revision fails with "The user provided container failed to start and listen on the port defined by PORT=8080 environment variable within the allocated timeout". The failed instance is shut down, whether or not its migration had finished.

**A failure crashes instances instead of stopping the deploy.** If the migration fails, `migrate && start` never starts the server. The new revision never becomes ready, and the old one keeps serving, which sounds safe. But the error is now a crash-looping container rather than a red step in your pipeline, and you find it by reading container logs.

**Every cold start pays for it.** After the deploy, each new instance still runs the migration tool to find out that nothing is pending. In our sample that no-op run took 0.4 seconds on a laptop, and a framework that boots the whole app first takes longer. It is added to every scale-up, and the service account that serves your web traffic needs the right to change the schema, all the time. Google's Django tutorial recommends the opposite, limiting the database user the service runs as.

## See it before you deploy

The sample in our research folder runs this on a laptop: a throwaway `postgres:17` in Docker, a migration that creates a table and takes two seconds, and node-pg-migrate 9.0.0 as the tool. Three processes start together, as three new instances would, and each runs the migration first. Then the same migration runs as a job would: once, then a second time, then with a migration that fails.

![A terminal running the sample: with the tool's lock on, two of three instances exit 1 with "Another migration is already running"; with --no-lock, two exit 1 with a duplicate key error; one job run applies 1 migration and a second run finds none; a failing migration exits 1 and leaves no column behind; a SELECT waits 5.1 s behind ALTER TABLE without lock_timeout and 1.6 s with lock_timeout 2s](https://exodus.elim.io/media/v2026-09-16/t/exodus/assets/image-ef658d7fc16da10ee1dce2db5ba8ca46e7d78df20c2c865366332214eb08f943-680x440-svg/bytes "The sample's real output. Run 1 and 2 are the start command on three instances, run 3 and 4 are the job, run 5 is the lock queue explained under Step 5.")

With the tool's default lock, one instance applies the migration and the other two exit with code 1, because node-pg-migrate's default `--advisory-lock-mode` is `fail`. On Cloud Run those are two containers that failed to start. With `--no-lock`, all three try, and the two losers fail inside Postgres on a duplicate key in the catalog: they tried to create the same table. A tool that waits for the lock instead (Prisma, or node-pg-migrate with `--advisory-lock-mode wait`) avoids the crash but holds every new instance until the migration ends, so a long migration still runs into the 240-second probe.

As a job, the same migration runs once and exits 0. A second run finds nothing to do and exits 0. A migration that adds a `NOT NULL` column without a default to a table with rows fails, exits 1, and Postgres rolls the whole transaction back: the column does not exist afterwards. That exit code is what a job turns into a failed deploy.

## What a job is

A Cloud Run job runs a command until it exits, with no URL and no port, so no startup probe applies. The [services, jobs and worker pools guide](https://exodus.elim.io/blog/cloud-run-services-jobs-and-worker-pools) explains jobs, executions and tasks; three facts from it matter here. A job takes the same settings as a service (image, secrets, a Cloud SQL connection, a service account, a VPC network), so it can run the image you just built and only the migration command. A migration is one task, which may run for 10 minutes by default and up to 168 hours with `--task-timeout`. And a failed task is retried 3 times by default, which a migration usually should not be (Step 3). The container must exit with code 0 on success and anything else on failure, as migration tools do.

Two flags make a job usable in a deploy. `--execute-now` runs it right after `gcloud run jobs deploy` creates or updates it. `--wait` makes gcloud wait until the execution ends and fail if the execution failed; without it, gcloud "exits successfully when the execution has started". Google's own tutorials differ here: the Rails tutorial runs its migration job with `--execute-now --wait`, the Django tutorial with `--execute-now` alone, so its build step succeeds as soon as the migration starts.

![A diagram in three lanes. Your deploy script builds the image once, runs the migrate job with that image and --wait, then deploys the service with the same image. The database changes during the job. The old revision serves traffic during the build and the job, and the new revision takes traffic only after the job exits 0. A failed job stops the script before the deploy.](https://exodus.elim.io/media/v2026-09-16/t/exodus/assets/image-974eeef6f3e37e43b48d2a6403ad3816ce3e3e54e6903bdb70b32958ed470516-680x384-svg/bytes "One image, three steps. The old revision serves the new schema while the job runs, which is why the migration has to be one it can live with.")

## What it costs

A job bills for the whole life of its container with a minimum of one minute, at $0.000018 per vCPU-second and $0.000002 per GiB-second in europe-west1 and no request fee (the [jobs guide](https://exodus.elim.io/blog/cloud-run-services-jobs-and-worker-pools) has the details). A migration that runs 20 seconds on 1 vCPU and 512 MiB is billed as 60 seconds, $0.00114.

- 30 deploys a month: $0.03.
- 300 deploys a month: $0.34.
- 3,000 deploys a month: $3.42.
- A 20-minute data backfill on 2 vCPU and 2 GiB, run once: $0.05.

These are list prices before the free tier. Jobs have a free tier of their own, 240,000 vCPU-seconds and 450,000 GiB-seconds a month per billing account, which is 4,000 such one-minute runs, so all four lines cost $0.00 unless something else already uses it.

![A bar chart of what a migration job costs a month at list prices, before the free tier: $0.03 at 30 deploys, $0.34 at 300, $3.42 at 3,000, and $0.05 for one 20-minute backfill on 2 vCPU and 2 GiB](https://exodus.elim.io/media/v2026-09-16/t/exodus/assets/image-f1c4763d52a562091cfc69c88fd6e6333a17f1e73f5ab53e0c3fe10379bb37c9-680x340-svg/bytes "Cloud Run job prices in europe-west1 on 2026-10-12, a 1-minute minimum per run, 1 vCPU and 512 MiB. The jobs free tier covers every bar.")

The build costs more. Cloud Build gives 2,500 build-minutes a month, then charges $0.006 a minute, so 3,000 builds of 3 minutes cost $39.00, about eleven times the migration job.

## Step 1. Find where migrations run today

Look in every place a platform or a container could start a command:

```shell
grep -rnE 'release:|release_command|preDeployCommand|PRE_DEPLOY' \
  Procfile fly.toml render.yaml .do/ app.json 2>/dev/null
grep -rnE 'migrate|db:prepare|alembic upgrade|flyway' \
  Dockerfile* docker-compose*.yml entrypoint*.sh bin/ package.json 2>/dev/null
```

The first line finds the platform's release step. The second finds a migration in the image's start path: a `CMD ["sh", "-c", "npx prisma migrate deploy && node server.js"]`, an `entrypoint.sh` that runs `python manage.py migrate` before `gunicorn`, a `"start": "prisma migrate deploy && next start"` in `package.json`, or the `bin/docker-entrypoint` that Rails generates with a new app, which runs `./bin/rails db:prepare` before `rails server`. Note each command and its arguments, because the job will run exactly that.

## Step 2. Take the migration out of the start command

The service's start command becomes only the server. The migration command stays in the same image and runs from the job. Typical commands:

- Rails: `bin/rails db:migrate`, or `bin/rails db:prepare`, which also creates the database on the first run.
- Django: `python manage.py migrate --noinput`.
- Laravel: `php artisan migrate --force`. Without `--force` it asks for confirmation in production and a job has nobody to answer.
- Prisma 7 and earlier: `npx prisma migrate deploy`. The newest Prisma replaces it with `npx prisma db migrate`.
- Alembic: `alembic upgrade head`.
- Flyway: `flyway migrate`.
- node-pg-migrate: `npx node-pg-migrate up`.

With Google's buildpacks and a Procfile, add a line such as `migrate: python manage.py migrate --noinput` and run it with `--command migrate`. With a Dockerfile, pass the command and its arguments to the job, as below.

## Step 3. Create the job

Build the image once and tag it with something unique, such as the commit, so that the job and the service run the same code:

```shell
IMAGE=europe-west1-docker.pkg.dev/PROJECT_ID/apps/web:$(git rev-parse --short HEAD)
gcloud builds submit --tag "$IMAGE"

gcloud run jobs deploy migrate \
  --image "$IMAGE" \
  --region europe-west1 \
  --service-account web@PROJECT_ID.iam.gserviceaccount.com \
  --set-cloudsql-instances PROJECT_ID:europe-west1:main \
  --set-secrets DATABASE_URL=DATABASE_URL:latest \
  --command bin/rails --args db:migrate \
  --cpu 1 --memory 512Mi \
  --max-retries 0 \
  --task-timeout 30m \
  --wait
```

`jobs deploy` creates the job the first time and updates it later, so the same command works on every deploy. `--wait` implies `--execute-now`. `--set-cloudsql-instances` gives the job the same Unix socket at `/cloudsql/PROJECT_ID:europe-west1:main` that the service uses ([connecting Cloud Run to Cloud SQL](https://exodus.elim.io/blog/connecting-cloud-run-to-cloud-sql)); a database on a private IP needs `--network`, `--subnet` and `--vpc-egress` instead, as for the service.

Two settings differ from the defaults on purpose. `--max-retries 0`: on Postgres a failed migration rolls back and a retry fails the same way, while on MySQL, which has no transactions around schema changes, a retry starts again on a half-changed schema. Neither is worth three more attempts. `--task-timeout 30m`: the default 10 minutes is enough for most migrations, but an index on a large table can take longer, and a killed migration is worse than a slow one.

The job can run as a service account of its own whose database user may change the schema, while the service's user only reads and writes rows.

## Step 4. Put the three steps in one script

The order is build, migrate, deploy, and the deploy runs only if the job exits 0:

```shell
#!/usr/bin/env bash
set -euo pipefail
IMAGE=europe-west1-docker.pkg.dev/PROJECT_ID/apps/web:$(git rev-parse --short HEAD)

gcloud builds submit --tag "$IMAGE"
gcloud run jobs deploy migrate --image "$IMAGE" --region europe-west1 --wait
gcloud run deploy web --image "$IMAGE" --region europe-west1
```

After the first full `jobs deploy` from Step 3, a later deploy changes only the image, and the job keeps its command, secrets and Cloud SQL connection. `set -e` stops the script at the first failed command, so a failed migration leaves the old revision serving and the new one never created.

The same three steps fit Cloud Build, GitHub Actions or any CI service (the one that builds and deploys on each push) that can run `gcloud`; in a `cloudbuild.yaml` the middle step uses the `gcr.io/google.com/cloudsdktool/cloud-sdk` image. Teams on Cloud Deploy can use its `predeploy` hooks, which run as a job of the rollout before the deploy.

To run the job by hand, for example after fixing the data that made it fail:

```shell
gcloud run jobs execute migrate --region europe-west1 --wait
```

`execute` also takes `--args` for one run, so the same job can roll back one step (`--args db:rollback`) or run a one-off data fix without a new job.

## Step 5. Write migrations the old revision survives

The job changes the database while the old revision serves traffic, and the new revision takes traffic only afterwards. If you deploy with `--no-traffic` and move traffic gradually, or [roll back to an earlier revision](https://exodus.elim.io/blog/revisions-traffic-splitting-and-rollbacks) with `gcloud run services update-traffic`, old and new code run against the same schema for hours. Rolling back traffic does not roll back the schema. So every migration has to work with the code before it and the code after it.

The rule is called expand and contract. A change that breaks old code is split over several deploys, and each deploy's job only does what the code still serving can live with:

1. **Expand.** Add what the new code needs, remove nothing: a nullable column, a new table, a column with a constant default. Deploy code that writes both the old and the new place.
1. **Move the data.** Backfill the new place in batches, as a job of its own if it is large. Deploy code that reads the new place.
1. **Stop using the old place.** Deploy code that neither reads nor writes it.
1. **Contract.** Drop the old place in the next deploy's job.

Renaming a column is the common case. `ALTER TABLE orders RENAME COLUMN total TO total_cents` breaks every request the old revision serves during and after the job. Four deploys break nothing.

![A grid of four deploys renaming the column total to total_cents. For each deploy: what its migrate job does, what the code deployed after it does, and which code still serves while the job runs. Deploy 1 adds total_cents and lets total be NULL while the old code uses total only. Deploy 2 backfills. Deploy 3 changes no schema and stops touching total. Deploy 4 drops total while only code that never touches it is serving.](https://exodus.elim.io/media/v2026-09-16/t/exodus/assets/image-82592dde30a0e4300b3b6931d7b6b51cfd4ea78fdf864b19bd5cd3853b7af045-680x340-svg/bytes "Renaming a column in four deploys. In every row, the code still serving finds every column it reads and writes.")

Locks are the other half. Most forms of `ALTER TABLE` in Postgres take an `ACCESS EXCLUSIVE` lock, the only lock mode that blocks a plain `SELECT`. The statement itself is often quick, but it has to wait for every transaction that already touched the table, and while it waits, every new query on that table queues behind it. In the sample's last run, a transaction that keeps the table open for 6 seconds made an `ALTER TABLE ... ADD COLUMN` wait, and a plain `SELECT` that arrived next waited 5.1 seconds behind the `ALTER`. On a live service that is every request touching the table.

`lock_timeout` turns that into a failed migration instead of an outage. Set it at the start of the migration:

```
SET lock_timeout = '2s';
ALTER TABLE orders ADD COLUMN note text;
```

With it, the sample's `ALTER` gave up after 2 seconds, and the `SELECT`, which arrived half a second after it, waited 1.6. The job exits 1, the deploy stops, and you run it again at a quieter moment. A few more Postgres rules:

- `ADD COLUMN` with a constant default or no default does not rewrite the table, even on a large one. Changing a column's type usually does.
- Adding `NOT NULL` or a `CHECK` constraint scans the whole table under the lock.
- `CREATE INDEX` blocks writes until it ends. `CREATE INDEX CONCURRENTLY` does not, but it cannot run inside a transaction, so the migration must turn its transaction off: `disable_ddl_transaction!` in Rails, `atomic = False` with `AddIndexConcurrently` in Django.

## Step 6. When the job fails

`gcloud run jobs deploy ... --wait` returns non-zero when the execution fails. To list recent runs and read the failed one's logs:

```shell
gcloud run jobs executions list --job migrate --region europe-west1 --limit 5
gcloud beta run jobs executions logs read EXECUTION_NAME --region europe-west1
```

Or search all of the job's errors of the last week:

```shell
gcloud logging read \
  'resource.type="cloud_run_job" AND resource.labels.job_name="migrate" AND severity>=ERROR' \
  --freshness 7d --limit 20 --format 'value(timestamp,textPayload)'
```

On Postgres, Django, Rails and node-pg-migrate run migrations in a transaction, so a failure leaves the schema as it was and you fix the migration and deploy again. On MySQL, Django's documentation is plain: a failed migration has to be unpicked by hand. golang-migrate marks the database "dirty" after a failure and refuses to run until you check the schema and force the version. In both cases, look at the schema before you run anything again.

## What breaks

- **The job runs an older image.** A job created once with `:latest` runs whatever `:latest` was when the job was last updated, which can be older than the service. Update the job's image on every deploy, as Step 4 does.
- **The job is not connected to the database.** A job does not inherit the service's Cloud SQL connection, secrets or network. Copy them, or the job fails with a connection error the service never had.
- **Rails behind PgBouncer.** Rails takes an advisory lock for migrations, and the Rails guide says you might need to turn it off with `advisory_locks: false` behind an external pooler such as [PgBouncer](https://exodus.elim.io/blog/connection-pooling-on-cloud-run). The job runs once, so it does not need the lock; you can also connect the job to the database directly, not through the pooler.
- **Laravel's isolated mode in a job.** `migrate --isolated` needs a cache shared by all servers and exits 0 when another run holds the lock. One job run needs neither.
- **Prisma's lock timeout.** Prisma's advisory lock waits 10 seconds, not configurable, so of two deploys at once the second fails. That is the right outcome; run it again.
- **Seeds in the migration.** A seed that inserts rows every time it runs fails or duplicates them on the second deploy. Make it idempotent (safe to run twice), or make it a separate job you run once.

## Checklist

1. The service's start command starts only the server; nothing in the Dockerfile, entrypoint or `package.json` start script migrates.
1. A job named `migrate` runs the migration command with the same image, secrets, Cloud SQL connection and network as the service.
1. The job has `--max-retries 0` and a task timeout longer than your slowest migration.
1. The deploy script builds once, runs `gcloud run jobs deploy migrate --image "$IMAGE" --wait`, then deploys the service with the same image, and stops on the first failure.
1. Every migration works with the code before it: no renames or drops in the same deploy as the code change.
1. Migrations on large tables set `lock_timeout` and build indexes concurrently.
1. The service's database user cannot change the schema; the job's can.
1. You have run a failing migration on a staging database and seen the deploy stop.

## Where Exodus comes in

Exodus's CLI reads your repository with your coding agent and finds where migrations run: a release line in the Procfile, `release_command` in `fly.toml`, a pre-deploy command, or a migrate step inside the Dockerfile's `CMD` or an entrypoint script. The report lists each one with file and line and proposes the job, with this guide as the explanation. The move plan puts the job between the build and the deploy and prices it next to the rest of your Cloud Run bill, where it usually shows as $0.00.

## FAQ

### Does Cloud Run have a release phase like Heroku?

No. Heroku runs the Procfile's release line before the new version takes traffic, Fly has release_command and Render a pre-deploy command, but Cloud Run's deploy only starts the container. A Cloud Run job run with --wait before gcloud run deploy takes that place and stops the deploy when it fails.

### Why does my Cloud Run deploy fail with "failed to start and listen on the port"?

A migration in the start command may be the cause. Cloud Run's default startup probe waits 240 seconds for the container to accept a connection on its port, and a migration that rebuilds a big index runs before the server listens. Move the migration into a job, which has no port and no probe.

### How do I run Django migrations on Cloud Run?

Run python manage.py migrate --noinput as a Cloud Run job with the same image, secrets and Cloud SQL connection as the service, before the deploy. Use --wait: Google's Django tutorial runs the job with --execute-now alone, so its build step succeeds as soon as the migration starts.

### Should a Cloud Run migration job retry on failure?

No, set --max-retries 0. A job retries a failed task 3 times by default. On Postgres a failed migration rolls back and a retry fails the same way; on MySQL, which has no transactions around schema changes, a retry starts again on a half-changed schema.

### How much does a migration job cost on Cloud Run?

A 20-second migration on 1 vCPU and 512 MiB is billed as one minute, $0.00114 at europe-west1 list prices, so 300 deploys a month cost $0.34. The jobs free tier, 240,000 vCPU-seconds and 450,000 GiB-seconds a month, covers about 4,000 such runs.

## Sources

1. [Running database migrations with Cloud Run Jobs, Google Cloud blog (a job has the same configuration as a service: secrets, Cloud SQL, service account; Procfile entries such as migrate: python3 manage.py migrate, bundle exec rails db:migrate, php artisan migrate; --wait confirms the run; to migrate first, build the container, run the job, then deploy; read Oct 12, 2026)](https://cloud.google.com/blog/topics/developers-practitioners/running-database-migrations-cloud-run-jobs)
2. [Execute jobs, Cloud Run (create or update with --execute-now for CI/CD deployments and schema migrations; overrides of args, environment variables, tasks and task timeout for one execution; read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/execute/jobs)
3. [Create jobs, Cloud Run (a job runs its tasks and exits; task timeout 10 minutes by default, up to 168 hours; 3 retries per failed task by default, 0 to 10; an execution with a failed task is failed; read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/create-jobs)
4. [Set maximum retries for jobs, Cloud Run (per task, not per job; 0 means a task runs once; read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/configuring/max-retries)
5. [Set task timeout for jobs, Cloud Run (no timeout on the execution itself; the timeout applies to each attempt; read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/configuring/task-timeout)
6. [Container runtime contract, Cloud Run (a job's container exits 0 on success and non-zero on failure; read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/container-contract)
7. [Cloud Run pricing, Google (jobs bill at the instance-based rate for the whole life of each instance, with a minimum of 1 minute; jobs free tier 240,000 vCPU-seconds and 450,000 GiB-seconds a month per billing account; $0.000018 per vCPU-second and $0.000002 per GiB-second; read Oct 12, 2026, rates also read from the Cloud Billing Catalog API for europe-west1 the same day)](https://cloud.google.com/run/pricing)
8. [Configure container health checks for services, Cloud Run (the default TCP startup probe: timeoutSeconds 240, periodSeconds 240, failureThreshold 1; read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/configuring/healthchecks)
9. [Troubleshoot Cloud Run issues (the container failed to start and listen on the port within the allocated timeout; read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/troubleshooting)
10. [Cloud Run release notes, April 10, 2023 (Cloud Run starts enough instances of a new revision before directing traffic to it; read Oct 12, 2026)](https://cloud.google.com/run/docs/release-notes)
11. [Rollbacks, gradual rollouts, and traffic migration, Cloud Run (deploy with --no-traffic --tag, move traffic by percentage, roll back to an earlier revision; read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/rollouts-rollbacks-traffic-migration)
12. [Connect from Cloud Run, Cloud SQL for PostgreSQL (the Unix socket at /cloudsql/INSTANCE_CONNECTION_NAME, with the .s.PGSQL.5432 suffix for some libraries; read Oct 12, 2026)](https://docs.cloud.google.com/sql/docs/postgres/connect-run)
13. [Running Django on the Cloud Run environment, Google (cloudmigrate.yaml creates a job with --command migrate and --execute-now, without --wait; limit the database user's privileges; read Oct 12, 2026)](https://docs.cloud.google.com/python/django/run)
14. [Running Rails on the Cloud Run environment, Google (cloudbuild.yaml creates migrate-job with bundle exec rails db:migrate, --execute-now --wait, then deletes it; read Oct 12, 2026)](https://docs.cloud.google.com/ruby/rails/run)
15. [Logging and viewing logs in Cloud Run (resource type cloud_run_job; labels job_name, execution_name, task_index; read Oct 12, 2026)](https://docs.cloud.google.com/run/docs/logging)
16. [Cloud Build pricing, Google (2,500 free build-minutes a month per billing account on e2-standard-2, then $0.006 a minute; read Oct 12, 2026, also from the Catalog API)](https://cloud.google.com/build/pricing)
17. [Run hooks before and after deploying, Cloud Deploy (predeploy and postdeploy hooks run as jobs on the rollout, for Cloud Run targets too; read Oct 12, 2026)](https://docs.cloud.google.com/deploy/docs/hooks)
18. [Configuring Rails Applications, Rails guides (Active Record uses advisory locks for migrations by default; turn them off behind PgBouncer with advisory_locks: false; read Oct 12, 2026)](https://guides.rubyonrails.org/configuring.html)
19. [Active Record Migrations, Rails guides (disable_ddl_transaction!; bin/rails db:prepare; read Oct 12, 2026)](https://guides.rubyonrails.org/active_record_migrations.html)
20. [Migrations, Django 6.1 docs (on PostgreSQL and SQLite a migration runs in one transaction; MySQL has no transactions around schema changes, so a failed migration must be unpicked by hand; atomic = False; read Oct 12, 2026)](https://docs.djangoproject.com/en/6.1/topics/migrations/)
21. [Database migration operations, django.contrib.postgres (AddIndexConcurrently; CONCURRENTLY is not supported inside a transaction; read Oct 12, 2026)](https://docs.djangoproject.com/en/6.1/ref/contrib/postgres/operations/)
22. [Database: Migrations, Laravel (migrate --isolated takes an atomic lock in the cache, needs a shared cache server, and exits successfully when it does not run; --force in production; read Oct 12, 2026)](https://laravel.com/docs/migrations)
23. [Development and production, Prisma 6 docs (migrate deploy uses advisory locking with a 10-second timeout that cannot be changed; read Oct 12, 2026)](https://www.prisma.io/docs/v6/orm/prisma-migrate/workflows/development-and-production)
24. [Applying a migration, Prisma docs (db migrate replaces Prisma ORM 7's migrate deploy; on PostgreSQL a second concurrent run waits for the first; read Oct 12, 2026)](https://www.prisma.io/docs/orm/migrations/applying-a-migration)
25. [CLI usage, node-pg-migrate (an advisory lock by default; --advisory-lock-mode fail by default, or wait; --no-lock; read Oct 12, 2026)](https://salsita.github.io/node-pg-migrate/cli)
26. [Frequently asked questions, Flyway (several nodes can migrate at once; Flyway coordinates them with the database's locks; read Oct 12, 2026)](https://documentation.red-gate.com/fd/frequently-asked-questions-277579363.html)
27. [migrate.go, golang-migrate (takes a lock before migrating; a dirty database stops it with "Dirty database version N. Fix and force version."; read Oct 12, 2026)](https://github.com/golang-migrate/migrate/blob/master/migrate.go)
28. [docker-entrypoint template, Rails (a new app's entrypoint runs ./bin/rails db:prepare before rails server; read Oct 12, 2026)](https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/app/templates/docker-entrypoint.tt)
29. [Explicit locking, PostgreSQL docs (ACCESS EXCLUSIVE conflicts with every lock mode and is the only one that blocks a plain SELECT; many ALTER TABLE forms take it; read Oct 12, 2026)](https://www.postgresql.org/docs/current/explicit-locking.html)
30. [ALTER TABLE, PostgreSQL docs (ACCESS EXCLUSIVE unless noted; ADD COLUMN with a non-volatile default needs no table rewrite; NOT NULL and CHECK scan the table; read Oct 12, 2026)](https://www.postgresql.org/docs/current/sql-altertable.html)
31. [CREATE INDEX, PostgreSQL docs (a standard build locks out writes until done; CONCURRENTLY does not, and cannot run in a transaction block; read Oct 12, 2026)](https://www.postgresql.org/docs/current/sql-createindex.html)
32. [Client connection defaults, PostgreSQL docs (lock_timeout aborts a statement that waits longer than this for a lock; not recommended in postgresql.conf; read Oct 12, 2026)](https://www.postgresql.org/docs/current/runtime-config-client.html)
33. Exodus team: a local sample with postgres:17 and node-pg-migrate 9.0.0 (three instances migrating at start, one job run twice with the no-op run timed, a failing migration, ALTER TABLE behind a long transaction), the Catalog API price read, gcloud 587.0.0 help and read-only job commands in our own project, Oct 12, 2026
