> ## Documentation Index
> Fetch the complete documentation index at: https://docs.microsandbox.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Rehearse a database migration

> Snapshot PostgreSQL, apply a risky migration, and restore the baseline

<Tooltip tip="This workflow uses local snapshot verification and a published client-host PostgreSQL port."><span className="msb-badge-local">Local-only <Icon icon="circle-info" size={11} /></span></Tooltip>

Use a snapshot to rehearse a schema migration against a realistic PostgreSQL data directory, inspect the result, and return to the exact pre-migration disk state. The disposable rehearsal does not require a down migration.

<Warning>
  Keep `PGDATA` in the sandbox root filesystem for this guide. This keeps the database self-contained in the captured root disk. Do not rely on external named-directory or bind-mounted storage being copied into the snapshot.
</Warning>

## Rehearse a migration

<Steps>
  <Step title="Create an initialized baseline">
    Set a temporary password in the host shell:

    <CodeGroup>
      ```sh macOS & Linux theme={null}
      export POSTGRES_PASSWORD="$(openssl rand -hex 24)"
      ```

      ```powershell Windows theme={null}
      $bytes = New-Object byte[] 24
      $rng = [Security.Cryptography.RandomNumberGenerator]::Create()
      $rng.GetBytes($bytes)
      $rng.Dispose()
      $env:POSTGRES_PASSWORD = -join ($bytes | ForEach-Object { $_.ToString('x2') })
      ```
    </CodeGroup>

    Start the baseline database. The shell keeps the VM available after PostgreSQL stops, until the explicit `msb stop` below:

    <CodeGroup>
      ```sh macOS & Linux theme={null}
      msb run -d --name migration-base --replace \
        --cpus 1 --memory 1G --root-disk 4G \
        --entrypoint /bin/sh \
        -e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \
        -e POSTGRES_DB=examples \
        -e DOCKER_PG_LLVM_DEPS= \
        postgres:17-alpine -- \
        -ec 'docker-entrypoint.sh postgres & wait "$!"; exec sleep infinity'
      ```

      ```powershell Windows theme={null}
      msb run -d --name migration-base --replace `
        --cpus 1 --memory 1G --root-disk 4G `
        --entrypoint /bin/sh `
        -e "POSTGRES_PASSWORD=$env:POSTGRES_PASSWORD" `
        -e POSTGRES_DB=examples `
        -e DOCKER_PG_LLVM_DEPS= `
        postgres:17-alpine -- `
        -ec 'docker-entrypoint.sh postgres & wait "$!"; exec sleep infinity'
      ```
    </CodeGroup>

    Wait for initialization, then stop the database cleanly:

    ```sh theme={null}
    msb exec migration-base -- sh -lc '
      until pg_isready -h 127.0.0.1 -d examples -U postgres; do sleep 1; done
    '
    ```

    Shut PostgreSQL down with its own control command before stopping the VM and taking the snapshot. VM shutdown alone does not guarantee a clean database shutdown:

    ```sh theme={null}
    msb exec --no-tty --user postgres migration-base -- /usr/local/bin/pg_ctl -D /var/lib/postgresql/data -m fast -w stop
    msb stop migration-base
    ```
  </Step>

  <Step title="Snapshot the baseline">
    <CodeGroup>
      ```sh macOS & Linux theme={null}
      msb snap create postgres-before-migration \
        --sandbox migration-base \
        --integrity
      ```

      ```powershell Windows theme={null}
      msb snap create postgres-before-migration `
        --sandbox migration-base `
        --integrity
      ```
    </CodeGroup>

    Verify the captured snapshot:

    ```sh theme={null}
    msb snap verify migration-base:postgres-before-migration
    ```
  </Step>

  <Step title="Apply the migration">
    Restore into an unused sandbox name, then start PostgreSQL explicitly. Dedicated restore boots an idle detached sandbox instead of running the image entrypoint. The database, role password, and initialization settings already live in the captured data directory; the `POSTGRES_*` initialization variables do not need to be replayed.

    [`pg_ctl`](https://www.postgresql.org/docs/17/app-pg-ctl.html) starts PostgreSQL in its own process group with output redirected to the log, so it survives the startup exec completing. The separate status check below reconnects after that exec has exited.

    <CodeGroup>
      ```sh macOS & Linux theme={null}
      msb snap restore migration-base:postgres-before-migration --name migration-test \
        --memory 1G -p 127.0.0.1:55432:5432
      msb exec --no-tty --user postgres migration-test -- \
        /usr/local/bin/pg_ctl -D /var/lib/postgresql/data -l /var/lib/postgresql/data/rehearsal.log -w start
      ```

      ```powershell Windows theme={null}
      msb snap restore migration-base:postgres-before-migration --name migration-test `
        --memory 1G -p 127.0.0.1:55432:5432
      msb exec --no-tty --user postgres migration-test -- `
        /usr/local/bin/pg_ctl -D /var/lib/postgresql/data -l /var/lib/postgresql/data/rehearsal.log -w start
      ```
    </CodeGroup>

    Wait for the restored database to become ready:

    ```sh theme={null}
    msb exec --no-tty --user postgres migration-test -- /usr/local/bin/pg_ctl -D /var/lib/postgresql/data status
    msb exec migration-test -- sh -lc '
      until pg_isready -h 127.0.0.1 -d examples -U postgres; do sleep 1; done
    '
    ```

    Apply a sample destructive change and verify it:

    <CodeGroup>
      ```sh macOS & Linux theme={null}
      msb exec -e PGPASSWORD="$POSTGRES_PASSWORD" migration-test -- \
        psql -h 127.0.0.1 -U postgres -d examples -v ON_ERROR_STOP=1 \
        -c 'create table dangerous_migration(id integer);' \
        -c 'insert into dangerous_migration values (42);' \
        -c 'select * from dangerous_migration;'
      ```

      ```powershell Windows theme={null}
      msb exec -e "PGPASSWORD=$env:POSTGRES_PASSWORD" migration-test -- `
        psql -h 127.0.0.1 -U postgres -d examples -v ON_ERROR_STOP=1 `
        -c 'create table dangerous_migration(id integer);' `
        -c 'insert into dangerous_migration values (42);' `
        -c 'select * from dangerous_migration;'
      ```
    </CodeGroup>

    Replace those statements with your real migration command and validation suite.
  </Step>

  <Step title="Roll back by replacing the sandbox">
    Stop the mutated database and boot another clean copy of the baseline under the same name:

    ```sh theme={null}
    msb exec --no-tty --user postgres migration-test -- /usr/local/bin/pg_ctl -D /var/lib/postgresql/data -m fast -w stop
    msb stop migration-test
    msb rm migration-test
    ```

    After removing the stopped sandbox, restore a fresh copy under the same name and start PostgreSQL:

    <CodeGroup>
      ```sh macOS & Linux theme={null}
      msb snap restore migration-base:postgres-before-migration --name migration-test \
        --memory 1G -p 127.0.0.1:55432:5432
      msb exec --no-tty --user postgres migration-test -- \
        /usr/local/bin/pg_ctl -D /var/lib/postgresql/data -l /var/lib/postgresql/data/rehearsal.log -w start
      ```

      ```powershell Windows theme={null}
      msb snap restore migration-base:postgres-before-migration --name migration-test `
        --memory 1G -p 127.0.0.1:55432:5432
      msb exec --no-tty --user postgres migration-test -- `
        /usr/local/bin/pg_ctl -D /var/lib/postgresql/data -l /var/lib/postgresql/data/rehearsal.log -w start
      ```
    </CodeGroup>

    Wait for the clean database to become ready:

    ```sh theme={null}
    msb exec --no-tty --user postgres migration-test -- /usr/local/bin/pg_ctl -D /var/lib/postgresql/data status
    msb exec migration-test -- sh -lc '
      until pg_isready -h 127.0.0.1 -d examples -U postgres; do sleep 1; done
    '
    ```

    Confirm the sample table is absent:

    <CodeGroup>
      ```sh macOS & Linux theme={null}
      msb exec -e PGPASSWORD="$POSTGRES_PASSWORD" migration-test -- \
        psql -h 127.0.0.1 -U postgres -d examples -Atc \
        "select coalesce(to_regclass('public.dangerous_migration')::text, 'rolled-back');"
      ```

      ```powershell Windows theme={null}
      msb exec -e "PGPASSWORD=$env:POSTGRES_PASSWORD" migration-test -- `
        psql -h 127.0.0.1 -U postgres -d examples -Atc `
        "select coalesce(to_regclass('public.dangerous_migration')::text, 'rolled-back');"
      ```
    </CodeGroup>

    The result should be `rolled-back`.
  </Step>

  <Step title="Clean up">
    Stop the restored database cleanly, then remove the stopped sandboxes:

    ```sh theme={null}
    msb exec --no-tty --user postgres migration-test -- /usr/local/bin/pg_ctl -D /var/lib/postgresql/data -m fast -w stop
    msb stop migration-test
    msb rm migration-base migration-test
    ```

    Remove the snapshot:

    ```sh theme={null}
    msb snap rm migration-base:postgres-before-migration
    ```

    Clear the password from the host shell:

    <CodeGroup>
      ```sh macOS & Linux theme={null}
      unset POSTGRES_PASSWORD
      ```

      ```powershell Windows theme={null}
      Remove-Item Env:POSTGRES_PASSWORD
      ```
    </CodeGroup>
  </Step>
</Steps>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.