Databases in pull request previews

Run PostgreSQL as a Compose service in each stagelet. See where its data lives, when the migration command runs, and how passwords reach it.

Stagely does not create a managed database for a pull request. The database runs as one more service of your Compose project, inside the stagelet: a private, running copy of the app on its own machine in your cloud account. So each stagelet has its own database. This guide uses PostgreSQL. Another database with a public image follows the same steps.

Add the database to the Compose project

Use a public image, a named volume for the data, and a health check:

services:
  web:
    build: .
    environment:
      DATABASE_URL: postgres://shop:shop@db:5432/shop
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:17
    environment:
      POSTGRES_USER: shop
      POSTGRES_PASSWORD: shop
      POSTGRES_DB: shop
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "--username=shop", "--dbname=shop"]
      interval: 5s
      timeout: 5s
      retries: 10

volumes:
  pgdata:

Then name the public service and the migration command in .stagely.yml:

version: 1
compose:
  - compose.yml
public:
  service: web
  port: 8080
health:
  path: /healthz
migration:
  command: /app/migrate up

Replace /app/migrate up with the command of your migration tool, such as bin/rails db:migrate. The gateway serves only the public service, so people reach the app and not the database. For PostgreSQL 18 and later images, mount the volume at /var/lib/postgresql instead, as the postgres image docs say.

A development Compose file often bind-mounts files, such as ./init.sql. The Compose rules refuse bind mounts. Keep that file for development, and name a second Compose file in .stagely.yml, as Pull request previews for Docker Compose apps shows.

Where the data lives

The named volume pgdata is on the disk of the stagelet’s machine. Stagely allows only the local volume driver.

Event The data
A new deploy of the same pull request Stays. The agent starts the project again on the same machine.
Reset Stays. The agent recreates the containers, not the volumes.
The pull request closes Gone. The agent deletes the machine with its disk.
The TTL ends: 72 hours after the last deploy, by default Gone
Delete in the console Gone

When the pull request deploys again after its stagelet ended, such as after it reopens, Stagely creates a new stagelet: a new machine, an empty volume and a new URL.

When the migration command runs

After the build, each deploy runs these steps on the stagelet’s machine, in this order:

  1. Start services: the customer agent runs docker compose up --wait. Compose starts db, waits until its health check passes, then starts web (startup order). The agent waits up to 10 minutes for every service to run, and for every service with a health check to be healthy.
  2. Migrate: the agent runs the command with sh -c in the running web container, through docker compose exec (the code).
  3. Health check: the agent requests health.path until the app answers with a 2xx status, for up to two minutes.

This order has consequences for your app:

  • The app starts before the migration runs. On the first deploy, the tables do not exist yet. The app must start, and keep running, without them.
  • A Compose healthcheck on web runs before the migration, so it must not need the tables. The health.path check runs after the migration, so it can need them.
  • The command runs in the app’s container. It gets the same environment variables as the app, with every config value and secret. The image of the public service must contain sh.
  • Every deploy runs the command again, on the same data. Write migrations that change nothing on a second run.
  • The output goes to the log of the Migrate step in the console.
  • A command that exits with a status other than 0 fails the deploy. The containers of the new commit already run at that point. Reset brings back the images and config of the last successful deploy. A reset runs no migration, and it does not undo what a migration changed.

Seed data

Stagely has no seed step. To seed, add your seed command to the migration command:

migration:
  command: /app/migrate up && /app/seed

The seed runs on every deploy, after the migrations. Make it insert only the rows that are missing.

The postgres image also runs the scripts in /docker-entrypoint-initdb.d, but only when its data directory is empty. In a stagelet, that is the first deploy, in Start services, before the migration command. A bind mount of such a script is refused. Build the script into an image instead. Put a Dockerfile and the script in db/, and replace the image of the db service with build: ./db:

FROM postgres:17
COPY init.sql /docker-entrypoint-initdb.d/

Use these scripts only for setup that does not need your tables, such as a second database.

Passwords and connection URLs

The example above puts the password in the Compose file. Stagely stores the resolved Compose model in the deploy bundle that it keeps for resets, so Stagely sees that password. A plain config value is also visible to Stagely. To keep the password in your cloud, use secrets. The values in the Compose file then serve only for local development.

  1. In the console, under Secrets, add the secret POSTGRES_PASSWORD with the agent secret key db_password.
  2. Add the secret DATABASE_URL with the agent secret key database_url. Click Save config.
  3. Store both values on the agent host, as Config values and secrets shows. The URL holds the same password.

Every service gets every secret as an environment variable, and the secret overrides the value in the Compose file. The db service reads POSTGRES_PASSWORD, and web reads DATABASE_URL. A Compose file can’t use a secret as a variable, so ${POSTGRES_PASSWORD} inside DATABASE_URL does not work. That is why the URL is a secret of its own.

The postgres image reads POSTGRES_PASSWORD only when it starts with an empty data directory. After you change the secret, the database of an existing stagelet keeps the old password. To start again with an empty volume, click Delete in the console. After the delete finishes, deploy again.

What Stagely does not do

  • It creates no managed database in your cloud account. It creates machines, and your Compose project runs the database on the stagelet’s machine.
  • It copies no data from production or from another stagelet. Each new stagelet starts with an empty volume.
  • It makes no backup of the volume, and a reset does not restore data.

Start

  1. Set up Stagely once with the Quickstart.
  2. Add the database service and its named volume, and check them against the Compose rules.
  3. Add migration.command to .stagely.yml, and open a pull request.