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 one file system at a time, so a database whose files live together 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:

sys9 run box create pg-base --image docker.io/library/ubuntu:24.04
sys9 run 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

This tutorial uses a clean database shutdown: Stop runs on_stop before the root file system is forked. Online Fork is also available, but does not run on_stop or guarantee application-level transaction consistency. Keeping the explicit Stop here gives the new database branches a clean starting point:

sys9 run box stop pg-base
sys9 run snap fork --from-box pg-base
sys9 run box create migration-a --snap <forked-snap-id>
sys9 run box create migration-b --snap <forked-snap-id>
sys9 run 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

The database files and startup hooks are inherited; the new boxes start their own database processes. Keep all required database files on the selected Snap. Root forks exclude Volumes, and independently forking several disks does not create an atomic database snapshot.

Rehearse migration A

sys9 run 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

sys9 run 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

sys9 run 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, use the same clean-shutdown flow: stop migration-a and run sys9 run 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 to run its database shutdown hook
  5. snap fork --from-box ...
  6. create branch boxes from the forked snap

Example setup:

sys9 run box create mongo-base --image mongodb/mongodb-community-server:7.0-ubuntu2204
sys9 run 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.