CLI tool
fergusstrange/embedded-postgres avatar
fergusstrange/embedded-postgres

embedded-postgres v2: a real PostgreSQL per test, with a supervisor behind it

Run a real Postgres database locally on Linux, OSX or Windows as part of another Go application or test

1,259 stars105 forksGoMIT

At a glance

What is it?
The second version of embedded-postgres gives each Go test its own PostgreSQL instance with an available port and automatic cleanup, ships a CLI for other test runners, and moves the supervision into a separate process watching an owner pipe. It is an alpha release requiring Go 1.26, with zero third-party dependencies in the production packages.
Who is it for?
embedded-postgres v2 fits a Go project whose tests need real PostgreSQL semantics, with SQL that has to be right rather than approximately right, and whose team is willing to run a separate database process per test. It is the wrong choice for a project that only needs a driver and a schema, and it is the wrong choice for a service that has to keep its data, because the default is a private disposable instance and the data directory is never removed for you.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 12 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • fergusstrange/embedded-postgres1,259 ★ · +9
Star history
Repository2026-10-192026-11-07
fergusstrange/embedded-postgres1.3k1.3k

9 Stars in the last 19 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

v2 moves supervision out of process

The second version of embedded-postgres is a rewrite with a different centre of gravity, and the summary line in the README says it plainly: run real PostgreSQL in your tests, each test gets its own database, an available port and automatic cleanup, a CLI provides the same lifecycle for other languages, and the production Go packages have zero third-party dependencies.

The interesting change is the supervision. In v1 the library started PostgreSQL in the same process and relied on the process lifetime to decide when it should die. In v2 an independent supervisor watches a dedicated owner pipe, and the pipe is what ties the two processes together: an unrecovered panic, an `os.Exit`, or a forced termination of the owner closes that pipe and triggers a PostgreSQL shutdown.

That design is what makes the library usable inside a test suite. A test that panics, or a test binary that calls `os.Exit` after a failure, would otherwise leave a PostgreSQL process behind holding a port and a data directory, and the next test run would fail on a port that nothing owns. With the owner pipe, the server is cleaned up because the owner disappeared, not because something remembered to call a stop function.

The trade-off is stated as honestly as the feature. The library installs no process-wide signal handlers, and userspace cannot guarantee a graceful shutdown after a whole-machine failure or after the supervisor and PostgreSQL are terminated simultaneously. Persistent data still requires the same care as any PostgreSQL cluster.

The package module is `github.com/fergusstrange/embedded-postgres/v2` with Go 1.26.0 declared and no requirements block, and v2 is available as an alpha release. There is a migration guide for people coming from v1, and the v1 lineage is credited to the embedded postgres binaries project that powered it.

eptest.Start gives each test its own database

The Go entry point for tests is a small package called `eptest`, and the example in the README is the whole pattern.

go
package app_test

import (
    "testing"

    "github.com/fergusstrange/embedded-postgres/v2/eptest"
)

func TestRepository(t *testing.T) {
    pg := eptest.Start(t)
    connectionURL := pg.ConnectionURL()
    _ = connectionURL // Open your chosen Go driver/pool here.
    // Register pool cleanup after eptest.Start, so the pool closes first.
}

The ordering comment is the part worth internalising. Pool cleanup is registered after `eptest.Start`, so that the connection pool closes before the database does, and the same rule is stated in the text: choose your own SQL driver and close connection pools before test cleanup.

The dependency is added to the module like any other:

bash
go get github.com/fergusstrange/embedded-postgres/[email protected]

Configuration goes through the `postgres` package. `postgres.DefaultConfig()` gives the defaults, `.Port(0)` asks for an available port rather than the default 5432, and `.Database("app_test")` names the database. Passing that config to `eptest.Start` is the documented way to change them, and `eptest.Run` is the variant for sharing one instance across a `TestMain`.

For the cases where a test needs a real server rather than a helper, the direct lifecycle is available:

go
pg := postgres.NewDatabase(postgres.DefaultConfig().Port(0))
if err := pg.StartContext(ctx); err != nil {
    return err
}
defer pg.Close()
// pg.ConnectionURL() and pg.GetPort() now contain the actual endpoint.

The context is the lifetime: cancelling the lifetime context also closes the server. `Close` is idempotent, and `Start` and `Stop` keep the familiar v1 calling pattern for code being ported. `StopContext` can stop waiting when its context expires while cleanup continues, `Done` closes when cleanup finishes, `Err` reports a lifecycle failure, and `Logs` keeps a bounded, password-redacted tail of the server output for diagnostics.

One v2 gotcha is called out in the README: source checkouts, local replacements and vendored tests need an explicitly selected supervisor, with the details in the migration guide.

The CLI gives other test runners the same lifecycle

Not every test suite is written in Go, and the CLI is the answer for the rest. It provides the same lifecycle, a database that starts, runs your command and stops again.

The install is a piped script, and the binary lands in a directory you add to your path:

bash
curl -fsSL https://raw.githubusercontent.com/fergusstrange/embedded-postgres/master/install/install.sh | bash
export PATH="$HOME/.local/bin:$PATH"
embedded-postgres exec -- npm test

The `exec` subcommand is the whole contract in one line: it starts PostgreSQL, supplies `DATABASE_URL` and the usual `PG*` variables, runs your command, then stops the database and returns the command's exit code. Replace `npm test` with your test command and the exit code propagates, so a CI step that runs the wrapper fails exactly when the test fails.

The CLI chooses an available port and a random password for each run, which is what makes it safe to run in parallel with anything else on the machine. The CLI guide covers foreground and background instances, Windows installation, configuration files and wrapper integration for build systems that want the database managed rather than the command wrapped.

There is one behaviour to be aware of, and it is a good one. The first start downloads PostgreSQL and the matching supervisor, and later starts reuse the cache. So the first test run on a fresh machine is a network operation and every run after that is a local one.

Cache, work directory and data directory have different contracts

The storage configuration is where a test helper can do damage, so v2 gives the three directories three separate and explicit contracts.

go
config := postgres.DefaultConfig().Port(0).Storage(postgres.Storage{
    CacheDir: "/path/to/shared-cache",
    WorkDir:  "/path/to/temporary-parent",
    DataDir:  "/path/to/persistent-cluster", // Omit for disposable test data.
})

`CacheDir` holds verified, immutable installations shared under process leases. Stopping an instance retains the reusable binaries, and `PruneCache` removes unused recognized entries while preserving active installations and unknown files, so a cache directory that is also used for something else is not a problem.

`WorkDir` is a parent for private, uniquely named temporary children. The caller's parent directory is never erased, and each normal lifecycle removes its own child. That is the difference between a helper that cleans up after itself and one that can delete a directory you pointed it at.

`DataDir` is persistent and never automatically removed. The protection against pointing it at the wrong place is explicit: a nonempty unrelated directory, or a cluster from a different PostgreSQL major, is rejected.

The default is a private disposable instance, with the cache under `os.UserCacheDir()/embedded-postgres/v2`. A test that wants to keep its data across runs sets a data directory and takes on the same responsibility a real cluster has.

Providers, hooks and extensions that stay out of core

The acquisition of a PostgreSQL binary is a policy, and v2 makes it replaceable.

`BinaryProvider` replaces the whole acquisition policy. `LocalProvider` uses a preinstalled PostgreSQL, for a machine that already has the right version, and `DownloadProvider` can be customised for mirrors, HTTP transports and offline caches, which is what an organisation behind a proxy or an air-gapped build needs. The contracts are in `docs/providers-and-hooks.md`, and there is a compiled reference implementation in `examples/custom/provider.go`.

Lifecycle hooks cover preparation and migrations with reverse-order cleanup, so a hook that runs a migration at startup has its undo run in the opposite order at shutdown.

The extension story is deliberate. Native extensions such as pgvector and PostGIS can live in caller-owned distributions without becoming core dependencies. That is a design decision about what the library guarantees: the core ships a PostgreSQL without vector and geography support, and a project that needs them supplies its own build, which also means the core dependency list stays empty.

On the operating system side there is a related constraint. Root callers must select an existing non-root Unix identity with `RunAs(postgres.User{UID: 10001, GID: 10001})`; the caller keeps its own identity and PostgreSQL and its utilities run as the selected account. Windows uses a restricted token and process Job Objects. The non-root reference guide covers container setup, directory ownership and the native library requirements, and it is linked from the note that PostgreSQL needs the native libraries for your operating system even though the Go packages have none.

Defaults, version pins and the platform matrix

The defaults table in the README is short enough to reproduce, and it is the first thing to check against your expectations.

| Setting | Library default | |---|---| | PostgreSQL | `V18` / 18.6.0 | | Role, password, database | `postgres` | | Port | 5432; use `Port(0)` for parallel tests | | Locale / encoding | `C` / `UTF8` | | Startup / shutdown | 2 minutes / 10 seconds | | Data / workspace | Private disposable instance | | Cache | `os.UserCacheDir()/embedded-postgres/v2` |

The version support is narrow and deliberate. `V15`, `V16`, `V17` and `V18` pin 15.19.0, 16.15.0, 17.11.0 and 18.6.0, and PostgreSQL 14 and older are excluded. The reasoning given is the upstream five-year major-version support policy, which is not a separate LTS edition, so a version that upstream no longer supports is not supported here either.

The binaries are pinned too. Binary checksums live in a reviewed manifest, and automatic library releases do not silently change those pins, which is what lets a build be reproducible.

The native CI matrix covers Linux and macOS on AMD64 and ARM64, Windows on AMD64, and Windows on ARM64 with x64 PostgreSQL emulation. Alpine 3.21 on AMD64 has a separate musl integration job, because the upstream bundle requires ICU 74, and other Linux architectures can use a source-built companion when a pinned bundle exists. FreeBSD needs a custom distribution and companion and is not in the supported release matrix.

The development policy is written down in `docs/releases.md`: a native GitHub Actions matrix, a 90 percent production coverage gate, security checks and automatic releases from master. The root of the repository carries the tests that match those claims by name, among them `leak_test.go`, `lifecycle_failure_test.go`, `ownership_regression_test.go` and `review_regression_test.go`.

Where embedded-postgres is the wrong tool

The cost of this library is that a test suite now starts real servers.

Each test gets its own PostgreSQL, which is the feature and the bill. A suite of two hundred tests can start two hundred servers over a run, each with a startup budget of two minutes and a shutdown of ten seconds, and the first run on a clean machine also downloads a PostgreSQL bundle. For a small suite that is invisible. For a large parallel suite it is a wall of disk, ports and processes, and the 90 percent coverage gate and the leak test in the repository show that the project is paying attention to exactly this.

The second limit is the platform list. FreeBSD is out of the matrix, some Linux architectures need a source-built companion, and Alpine needs the musl job because of the ICU 74 requirement in the upstream bundle. A project whose CI runs on a distribution outside that list is not going to be covered by the published artefacts.

The third is that this is an alpha. Versions v2.0.0-alpha.1, alpha.2 and alpha.3 were all published on 2026-10-06, the API changed under the v2 major, and the migration guide exists because of it.

The alternative for many teams is a database in CI rather than in the test: a PostgreSQL service in the CI configuration, or a container that the suite talks to, with the schema created and dropped per test. The difference in approach is that the embedded approach gives every test an isolated, disposable server with no shared state and no cleanup to remember, and it costs a download, a process and a startup per test. The service approach costs one server for the whole run and gives the tests a shared database that has to be cleaned up carefully, which is why parallel test isolation has to be designed rather than assumed. A team that writes few tests and wants them independent takes the embedded path. A team that writes many and already has a CI database is usually better served by the service.

Whichever path is taken, the licence is the easy part: MIT, with the text in the `LICENSE` file at the root.

Editorial conclusion

embedded-postgres v2 fits a Go project whose tests need real PostgreSQL semantics, with SQL that has to be right rather than approximately right, and whose team is willing to run a separate database process per test. It is the wrong choice for a project that only needs a driver and a schema, and it is the wrong choice for a service that has to keep its data, because the default is a private disposable instance and the data directory is never removed for you. Before adopting it, read `docs/migration-v2.md`, because the API changed and a supervisor has to be selected explicitly for source checkouts and vendored builds, and check the native library requirements for your operating system, since PostgreSQL needs real system libraries even though the Go packages have no third-party dependencies. Note also that this is an alpha: v2.0.0-alpha.1 to alpha.3 were all published on 2026-10-06.

Frequently asked questions

How do I run PostgreSQL inside a Go test with embedded-postgres?

Call `eptest.Start(t)` and take the connection URL from `pg.ConnectionURL()`. Register your pool cleanup after `eptest.Start`, so that the pool closes before the database, and use `postgres.DefaultConfig().Port(0).Database("app_test")` to pick an available port and name the database.

What does embedded-postgres download on the first run?

PostgreSQL and the matching supervisor. Later starts reuse the cache, which lives under `os.UserCacheDir()/embedded-postgres/v2`, and PostgreSQL itself needs the native libraries for your operating system even though the Go packages have no third-party dependencies.

Can I use embedded-postgres with a test runner that is not Go?

Yes, the CLI provides the same lifecycle. `embedded-postgres exec -- npm test` starts PostgreSQL, supplies DATABASE_URL and the usual PG* variables, runs your command, stops the database and returns the command's exit code, choosing an available port and a random password each time.

Which PostgreSQL versions does embedded-postgres v2 support?

V15, V16, V17 and V18, pinned to 15.19.0, 16.15.0, 17.11.0 and 18.6.0, with the default at V18. PostgreSQL 14 and older are excluded, and the project follows the upstream five-year major-version support policy rather than shipping a separate LTS edition.

Official sources

  1. fergusstrange/embedded-postgres on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/fergusstrange-embedded-postgres.svg)](https://hysenlabs.com/projects/fergusstrange-embedded-postgres)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/fergusstrange-embedded-postgres.svg)](https://hysenlabs.com/projects/fergusstrange-embedded-postgres)

Was this useful?