TUTORIAL

Fork a Database

Keep database state in a forkable box, wake it with hooks, and branch the whole environment into isolated database copies.

run9 forks the whole box environment, so a database fits naturally into the same model. If the database files live in the box and Hooks start and stop the daemon at the right moments, every fork becomes a new database branch without any database-specific product mode.

This is a natural fit when one PostgreSQL baseline should split into several realistic lines of work:

  • migration-a for one schema change rehearsal
  • migration-b for a competing migration plan
  • customer-incident for replaying one production issue without touching the other branches

What you are building

docker.io/library/ubuntu:24.04
              |
              v
         box: pg-base
 (PostgreSQL + hooks + seed data)
              |
              v
      snap: <forked-snap-id>
       /          |               \
      v           v                v
migration-a  migration-b  customer-incident

The database is not special here. It is just one more part of the environment that the fork carries forward.

PostgreSQL: prepare one parent box

Create the box and open one interactive shell:

run9 box create pg-base --image docker.io/library/ubuntu:24.04
run9 box exec pg-base -it bash

Inside that shell:

# inside the shell:
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y postgresql

cat > /etc/run9_on_start.sh <<'EOF'
#!/bin/sh
set -eu
pg_ctlcluster 16 main start
EOF

cat > /etc/run9_on_stop.sh <<'EOF'
#!/bin/sh
set -eu
pg_ctlcluster 16 main stop --mode fast
EOF

chmod +x /etc/run9_on_start.sh /etc/run9_on_stop.sh

# start PostgreSQL once for the current shell session
pg_ctlcluster 16 main start

su - postgres
createdb app
psql app

Inside the psql prompt:

create table notes(branch text, body text);
insert into notes values ('base', 'seed row from the parent box');
select * from notes order by branch, body;
\q

Then leave the PostgreSQL user shell and the box shell:

exit
exit

Those hook files become part of the box state, so later forks inherit both the database files and the wake/stop behavior. The manual pg_ctlcluster ... start above is only for this first live session.

Fork the whole environment

Stop the parent first so on_stop can flush the database and the fork captures a quiet file system:

run9 box stop pg-base
run9 snap fork --from-box pg-base
run9 box create migration-a --snap <forked-snap-id>
run9 box create migration-b --snap <forked-snap-id>
run9 box create customer-incident --snap <forked-snap-id>

At this point:

  • all three boxes start from the same PostgreSQL data directory
  • all three branches inherited the same hook scripts
  • waking any branch starts PostgreSQL automatically

That is the whole idea behind database forking in run9: fork the environment, and the database comes with it.

Rehearse migration A

run9 box exec migration-a -it bash

Inside the shell:

# inside the shell:
su - postgres
psql app

Inside the psql prompt:

insert into notes values ('migration-a', 'only on migration a');
select * from notes order by branch, body;
\q

Exit the shells:

exit
exit

Rehearse migration B

run9 box exec migration-b -it bash

Inside the shell:

# inside the shell:
su - postgres
psql app

Inside the psql prompt:

insert into notes values ('migration-b', 'only on migration b');
select * from notes order by branch, body;
\q

Exit the shells:

exit
exit

Replay one customer incident

run9 box exec customer-incident -it bash

Inside the shell:

# inside the shell:
su - postgres
psql app

Inside the psql prompt:

insert into notes values ('customer-incident', 'only on the customer incident branch');
select * from notes order by branch, body;
\q

Exit the shells:

exit
exit

What the branches now contain

migration-a shows:

 branch |            body
--------+-----------------------------
 base   | seed row from the parent box
 migration-a | only on migration a

migration-b shows:

 branch |            body
--------+-----------------------------
 base   | seed row from the parent box
 migration-b | only on migration b

customer-incident shows:

 branch |            body
--------+------------------------------------------
 base   | seed row from the parent box
 customer-incident | only on the customer incident branch

If you later want a new branch that already includes changes from migration-a, stop migration-a and run run9 snap fork --from-box migration-a. Fork from the box state you want future branches to inherit.

MongoDB follows the same pattern

MongoDB uses the same environment-fork pattern:

  1. prepare one parent box
  2. put mongod start and stop logic in /etc/run9_on_start.sh and /etc/run9_on_stop.sh
  3. insert seed data
  4. stop the box
  5. snap fork --from-box ...
  6. create branch boxes from the forked snap

Example setup:

run9 box create mongo-base --image mongodb/mongodb-community-server:7.0-ubuntu2204
run9 box exec mongo-base -it bash

Inside that shell:

# inside the shell:
mkdir -p /data/db /var/log/mongodb

cat > /etc/run9_on_start.sh <<'EOF'
#!/bin/sh
set -eu
mongod --dbpath /data/db --bind_ip 127.0.0.1 --logpath /var/log/mongodb/mongod.log --fork
EOF

cat > /etc/run9_on_stop.sh <<'EOF'
#!/bin/sh
set -eu
mongosh --quiet --eval 'db.getSiblingDB("admin").shutdownServer()'
EOF

chmod +x /etc/run9_on_start.sh /etc/run9_on_stop.sh

mongod --dbpath /data/db --bind_ip 127.0.0.1 --logpath /var/log/mongodb/mongod.log --fork
mongosh

Inside mongosh:

db.notes.insertOne({ branch: "base", body: "seed row from the parent box" })
db.notes.find().toArray()
exit

Then exit the shell, stop the box, fork the snap, and create branch boxes exactly as in the PostgreSQL example above.