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-afor one schema change rehearsalmigration-bfor a competing migration plancustomer-incidentfor 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:
- prepare one parent box
- put
mongodstart and stop logic in/etc/run9_on_start.shand/etc/run9_on_stop.sh - insert seed data
- stop the box to run its database shutdown hook
snap fork --from-box ...- 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.