Skip to main content

One PostgreSQL database per Git worktree with Podman and Codex


        How I give every Featlet worktree its own PostgreSQL container, port, volume, and connection string—and let Codex set it up automatically.

I use Git worktrees extensively when developing Featlet. They let me work on several branches at once without constantly stashing changes or switching the checkout in my main working directory. They are especially useful with Codex, because separate tasks can run in separate worktrees without trampling over each other’s source files.

There is a catch, though: isolating the source code does not automatically isolate everything the code depends on.

If every worktree connects to the same local PostgreSQL instance, two otherwise independent tasks can still interfere with each other. A migration on one branch can change the schema underneath another branch. Seed data added for one feature can affect tests for another. Resetting the database in one worktree can wipe out useful local data in all the others.

I wanted the database to behave like the source code: each worktree should have its own, and removing one worktree should not disturb any of the others.

Make the worktree the environment boundary

The important design decision was to treat the worktree, rather than the repository, as the boundary for the local development environment.

Featlet has a private PowerShell script called Manage-FeatletDatabase.ps1. I have shared the reusable parts as a product-neutral Manage-WorktreePostgres.ps1, together with an example Codex environment hook. The public version includes configurable migration and seed commands but leaves application-specific commands to you. When the Featlet script configures a worktree for the first time, it derives a stable identity from the normalised absolute path to that worktree. The identity is the first eight characters of a SHA-256 hash.

That identity drives the other values which must be unique:

Worktree propertyExample derived value
Stable identitya1b2c3d4
Compose projectfeatlet-wt-a1b2c3d4
PostgreSQL portA free port from 55432 to 55999
Named volumefeatlet-wt-a1b2c3d4_featlet-postgres-data
Connection stringPoints to that port on 127.0.0.1

Using the absolute path means the identity is deterministic for a particular checkout but different for another worktree. The script persists the result in that worktree’s ignored .env file, so subsequent commands keep using the same database.

A simplified result looks like this:

FEATLET_WORKTREE_ID=a1b2c3d4
COMPOSE_PROJECT_NAME=featlet-wt-a1b2c3d4
FEATLET_DB_PORT=55678
ConnectionStrings__FeatletDatabase=Host=127.0.0.1;Port=55678;Database=featlet;Username=featlet;Password=<redacted>

Another worktree receives another identity, project, port, and volume. A fresh configuration also gets a generated password; when Codex starts from an existing local .env, it preserves the copied database credentials while replacing the worktree-specific values. Either way, both worktrees can run at the same time from the same Compose definition, but a migration or data edit in one has no effect on the other. The application in each checkout reads its own ignored configuration, so I do not need to remember which database belongs to which branch.

The explicit Compose project is particularly useful. Featlet invokes Podman Compose with -p featlet-wt-<identity> on every operation. Compose uses the project name to scope resources, so the same compose.yaml can create separate containers and named volumes for several worktrees.

The PostgreSQL part of the Compose file is deliberately ordinary:

services:
  postgres:
    image: postgres:18
    ports:
      - "127.0.0.1:${FEATLET_DB_PORT:-5432}:5432"
    volumes:
      - featlet-postgres-data:/var/lib/postgresql

volumes:
  featlet-postgres-data:

The project name supplies the namespace, while the worktree-specific port provides a distinct host connection. PostgreSQL is bound to loopback rather than exposed on every network interface.

I could have run one shared PostgreSQL server and created a separate logical database inside it for every worktree. That would reduce the number of containers, but it would also require a second naming and cleanup system inside the shared server. It would be easier for one command to target the wrong database, and branches which change container-level configuration would still share the same PostgreSQL process.

For Featlet, a small local container per active worktree is a worthwhile trade. The relationship is visible in Podman, the volume follows the Compose project, and removing the project removes the complete database environment without reaching inside a shared server.

One command to get a useful database

From a new Featlet worktree, the normal setup is:

./Manage-FeatletDatabase.ps1 setup

That command does more than start a container. It creates or validates the worktree’s configuration, starts its PostgreSQL service, waits for the container health check, applies the current Entity Framework Core migrations, adds representative development data, and finally displays a redacted status summary.

The result is a database which is ready to use, not merely a running PostgreSQL process which still needs several manual steps before the application can start.

The rest of the command surface follows the lifecycle I expect while working:

./Manage-FeatletDatabase.ps1 status
./Manage-FeatletDatabase.ps1 stop
./Manage-FeatletDatabase.ps1 reset
./Manage-FeatletDatabase.ps1 destroy

status shows the worktree identity, Compose project, redacted connection details, expected volume, and current container state. stop stops PostgreSQL but preserves its named volume. reset destroys only the current worktree’s database and then recreates, migrates, and seeds it. destroy permanently removes that worktree’s containers and volumes.

That distinction matters. I can stop work for the day without losing data, deliberately reset a branch to a known state, or remove its resources completely before deleting the worktree.

Letting Codex do the setup

The next step was to make this work naturally with Codex. Featlet has a checked-in Codex environment definition which runs from the root of a newly created worktree. I’ve published a product-neutral environment.toml.example alongside the database management script so you can see the complete setup and cleanup hooks.

If the main checkout already has an .env file, the setup copies it so that useful local settings do not need to be entered again. It removes only the values which belong to the source worktree—its identity, Compose project, and database port—before publishing the new file. The database manager then derives the correct replacements from the new path and performs the normal setup.

This is repository-owned automation rather than special database behaviour built into Codex. Codex provides the place to run the setup; Featlet defines what a valid local environment means.

That separation is useful because the same database manager works outside Codex. I can run it directly from a terminal, while Codex can use exactly the same entry point when it prepares a worktree. There is one lifecycle to understand and debug rather than an interactive workflow for humans and a different hidden workflow for agents.

Cleanup uses the same principle. The environment hook calls the manager’s destroy action, which targets the current worktree’s explicit Compose project and named volumes. Removing one task’s environment does not mean running a broad Podman cleanup or guessing which container belongs to it.

The guard rails are part of the feature

Automation around databases becomes dangerous when it is too eager. Much of the value in Featlet’s script comes from the things it refuses to do.

Port allocation checks both the ports reserved in other Git worktrees’ .env files and the TCP ports already in use. Allocation is protected by a repository-scoped lock so two worktrees being configured at the same time cannot both select the same port. A persisted port is never silently moved: if it becomes unavailable, setup stops and tells me to request reallocation explicitly after deciding what should happen to the existing data.

The .env handling is similarly conservative. Existing comments, ordering, authentication settings, secrets, and unrelated variables are preserved. Managed values are validated before the file changes, and updates are written to a temporary sibling file before being moved into place. Status output redacts the password, and the Codex setup does not print the connection string.

Destructive actions are also clearly different from everyday ones. stop is safe and preserves data. reset and destroy are confirmation-aware and precisely name the worktree project they will affect. I run destroy before removing a worktree because deleting a checkout does not automatically delete its Podman resources.

These checks make the workflow slightly more elaborate than generating a random port and calling podman compose up. They also make it something I can trust while several branches and agents are active at once.

The pattern is portable

Featlet currently implements this on Windows with PowerShell, Podman, PostgreSQL, and Entity Framework Core, but none of those choices is the essential idea.

The reusable pattern is:

  1. Derive a stable identity from the worktree.
  2. Use it to namespace container resources.
  3. Allocate and persist a conflict-free host port.
  4. Synchronise the application’s local connection settings.
  5. Provide explicit setup, inspect, stop, reset, and destroy operations.
  6. Make both developers and coding agents use those same operations.

A Bash or Python script could implement the same contract. Docker Compose could replace Podman Compose. Another relational database could replace PostgreSQL. The important part is that every worktree owns a complete, identifiable environment with a safe lifecycle.

Once I started treating the database as part of the worktree rather than a shared machine-level service, parallel development became much less surprising. Each branch can migrate, seed, reset, and evolve independently—and Codex can work in one checkout without quietly changing the ground beneath another.