pg-ephemeral - Ephemeral PostgreSQL for Testing
Status: Pre-1.0 - exists to serve mbj/mrs monorepo, expect breaking changes without notice.
Spin up throwaway PostgreSQL containers for development and testing. Supports Docker and Podman with automatic backend detection.
Quick Start
# Launch psql against an ephemeral database (default command)
# Run a command with PG* environment variables set
# Run an interactive shell on the container
Without a config file pg-ephemeral creates a single main instance using the latest
supported PostgreSQL image on the auto-detected container backend.
Configuration
Place a database.toml in the working directory (or pass --config-file <path>).
File paths in the config are resolved relative to the config file's location, not the
process working directory. This means tests can be run from any subdirectory without
changing the paths in database.toml.
= "17.1"
[]
= "sql-file"
= "schema.sql"
[]
= "script"
= "psql -c \"INSERT INTO users (name) VALUES ('alice'), ('bob')\""
[]
= "sql-file"
= "indexes.sql"
[]
= "command"
= "sh"
= ["-c", "psql -c \"INSERT INTO users (name) VALUES ('dynamic-$RANDOM')\""]
= { = "none" }
Top-level fields
| Field | Description |
|---|---|
image |
PostgreSQL version / image tag (e.g. "17.1") |
backend |
"docker", "podman", or omit for auto-detection |
ssl_config |
SSL configuration with hostname field |
wait_available_timeout |
How long to wait for PostgreSQL to accept connections (e.g. "30s") |
Seed types
Seeds run in declaration order inside the container. Each seed has a type:
| Type | Fields | Description |
|---|---|---|
sql-file |
path, optional git_revision |
Apply a SQL file. With git_revision, reads the file from that git commit. path is resolved relative to the config file's directory. |
script |
script |
Run a shell script with sh -e -c. |
command |
command, arguments, cache |
Run an arbitrary command. If command is a relative path (contains /), it is resolved relative to the config file's directory; bare names like psql are looked up via PATH. |
Multiple instances
Define multiple named instances under [instances.<name>]. Top-level fields serve as
defaults for all instances. Use --instance <name> on the CLI to target a specific one.
Seed Caching
pg-ephemeral caches seed results as container images so repeated runs skip already-applied seeds. Each seed's cache key is a SHA-256 chain of:
- pg-ephemeral version
- base image
- SSL configuration
- all preceding seeds' content
When the cache key matches an existing image the seed is a hit and the container boots
from that image directly. Seeds are cached in order; an uncacheable seed (e.g.
cache = { type = "none" }) breaks the chain and all subsequent seeds run without caching.
Cache commands
# Show cache status for all seeds
# JSON output with full details (references, etc.)
# Pre-populate the cache without running an interactive session
# Remove cached images
# Force-remove cached images (even if referenced by stopped containers)
Command cache strategies
For command type seeds, the cache field controls how the cache key is computed:
| Strategy | Description |
|---|---|
{ type = "command-hash" } |
Hash the command and arguments (default). |
{ type = "key-command", command = "...", arguments = [...] } |
Run a separate command whose stdout is hashed as the cache key. |
{ type = "key-script", script = "..." } |
Run a script whose stdout is hashed as the cache key. |
{ type = "none" } |
Disable caching. Breaks the cache chain for this and all subsequent seeds. |
Rust Library
pg-ephemeral can be used as a Rust library for integration tests or any code that needs a throwaway PostgreSQL instance.
Basic usage
async
with_container handles the full lifecycle: populate the seed cache, boot a container
(from the latest cache hit if available), apply any remaining uncached seeds, run the
closure, and stop the container.
Seed types
Seeds are added to a Definition via builder methods:
# async
Configuration
The Definition builder supports additional options:
# async
Accessing connection details
Inside with_container, the Container provides several ways to connect:
# async
Language Integrations
Ruby
The pg-ephemeral Ruby gem bundles the binary and provides a native API:
# Yields a PG::Connection to an ephemeral database
PgEphemeral.with_connection do
conn.exec()
end
# Or get the server URL for manual connection management
PgEphemeral.with_server do
puts server.url # => "postgres://postgres:...@127.0.0.1:54321/postgres"
end
The gem is available for x86_64-linux, aarch64-linux, and arm64-darwin.
See integrations/ruby for details.
Other Languages
Any language can integrate via run-env or the integration server protocol:
Environment variables — run a command with PG* and DATABASE_URL set:
Integration server — for programmatic control over the container lifecycle:
Boots a container, writes a JSON line with connection details to the result pipe FD, then waits for EOF on the control pipe FD before shutting down. The parent process creates the pipes and passes the inherited file descriptors. Close the control pipe write end to stop the server.
CLI Reference
pg-ephemeral [OPTIONS] [COMMAND]
Commands:
psql Run interactive psql on the host (default)
run-env Run a command with PG* and DATABASE_URL environment variables
container-psql Run interactive psql inside the container
container-shell Run interactive shell inside the container
container-schema-dump Dump schema from the container
cache Cache management (status, populate, reset)
integration-server Run integration server (pipe-based control protocol)
list List defined instances
platform Platform support checks
Options:
--config-file <PATH> Config file path (default: database.toml)
--no-config-file Use defaults, ignore any config file
--backend <BACKEND> Override backend (docker, podman)
--image <IMAGE> Override PostgreSQL image
--ssl-hostname <HOST> Enable SSL with the specified hostname
--instance <NAME> Target instance (default: main)
Requirements
- Docker Engine 20.10+ / Docker Desktop 4.34+, or Podman 5.3+
- PostgreSQL client tools (
psql) for host-side commands
Release Build Configuration
Release builds use split-debuginfo = "packed" to separate debug information from the binary:
- Linux: Debug info stored in
.dwpfile alongside the binary - macOS: Debug info stored in
.dSYMbundle alongside the binary
This provides smaller binaries while preserving full backtraces with file paths and line numbers.