rails db:migrate, prisma migrate deploy, alembic upgrade head, php artisan migrate. The question is where in the deploy that command runs. Two things matter:
- It must run once per release, not once per task or once per service.
- It must run against the target database, from inside the VPC, with the same credentials as the app.
Option 1: At the end of the build (recommended when Ravion builds your image)
If Ravion builds your image with Railpack or a Dockerfile, run the migration as the last build command. The build runner is already running, so the migration adds only its own execution time to the deploy — no container to schedule, pull, and tear down. Builds run on EC2 in your AWS account, inside the module’s builder execution environment (seervn-ecs-web; by default, the same network placement as the ECS cluster). That is what lets the runner reach a private RDS instance. You need to give it two things it does not get by default:
- The connection string. Add
DATABASE_URLtobuild_environment_variables, referenced from Secrets Manager rather than pasted in. Build variables are separate from runtime variables, so the value has to be declared in both places. - Network access to the database. Allow the build runner’s security group or subnet CIDR in the RDS module’s
allowed_security_group_idsorallowed_cidr_blocks. If the runner is in the same VPC, the VPC CIDR is the simplest rule.
ravion.yaml
dockerfile_environment_variable_injection_enabled so build variables arrive as build arguments, declare ARG DATABASE_URL and run the migration in a RUN instruction that uses it directly. Do not copy it into an ENV, or the connection string is baked into the image.
Trade-offs to be aware of:
- The build environment has production database access. A compromised build step can reach your database. Keep the build variables scoped to the one service that owns the schema, and use a database user that can alter schema but nothing more than the app needs.
- The migration runs before the image is deployed. If the deploy itself later fails (health checks, capacity), the schema has already moved. This is the same window a pre-deploy hook has, just earlier; backward-compatible migrations (below) make it safe.
- Builds are no longer reproducible on their own. Rebuilding an old commit reruns its migrations, which is a no-op for frameworks that track applied migrations but worth knowing.
- One service per environment. If a web and a worker both build from the same repo, put the migration in only one of the two build configs — otherwise both runners race for the migration lock.
Option 2: Pre-deploy hook (recommended for prebuilt images)
When you deploy an image Ravion did not build —build_source: ecr or image_registry — there is no build runner to piggyback on, so use the pre-deploy hook instead. rvn-ecs-web and rvn-ecs-worker can start a one-off ECS task before the service rolls out. The task uses the image that is about to be deployed, the same task role, subnets, security groups, environment variables, and secrets as the service, so it reaches the database exactly the way your app does without any extra configuration. If the command exits non-zero, the deploy is aborted and the running tasks are left untouched.
ravion.yaml
/bin/sh -lc when you need shell features such as && or environment variable expansion.
Hooks accept their own pre_deploy_environment_variables, pre_deploy_cpu, pre_deploy_memory, pre_deploy_ephemeral_storage_size_gib, and pre_deploy_timeout, so a heavy backfill can get more resources than the steady-state web task without changing the service.
The cost is time: every deploy waits for ECS to place the task, pull the image, run the command, and stop, typically a minute or more on top of the migration itself. If Ravion builds your image and deploy speed matters, prefer option 1. Pre-deploy is still the right choice when you do not want the build environment to have database access.
Option 3: A pipeline step
When migrations need to run before several services deploy, need to run from a different image than the app, or should be gated by anapproval, use a custom step in your pipeline. Steps in a list run in order, so place it between the build and the deploys:
pipeline.yaml
Where not to run migrations
Dockerfile image build instructions on their own. Runningdb:migrate in an arbitrary RUN without the setup in option 1 fails for lack of credentials or network access, and leaking the connection string into an ENV bakes it into the image.
Procfile release phase. Ravion does not run Heroku-style release processes. If you are migrating from Heroku, move the command from release: to the end of the build or to a pre-deploy hook.
Application start. Running migrations in the container entrypoint runs them on every task — including autoscaling events and task replacements — and usually in parallel. It also means a broken migration takes the whole service down rather than failing the deploy.
Rollbacks
A rollback redeploys a previous image; it does not reverse the schema. Write migrations that are backward compatible with the previous release — add columns before reading them, drop columns one release after you stop writing them — so a rollback is always safe.Related pages
- Build — build sources and build environment variables
- Deploy — deployment types, hooks, and rollback
- Pipeline step types
- Managing secrets
- Migrate Heroku Postgres to RDS