Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

PostgresContainer

A single-node PostgreSQL container. Defaults to a test/test/test user/password/database trio so connection_string() is usable with zero configuration.

Default image: floats to postgres:latest — this module previously pinned postgres:18-alpine; Docker Hub publishes postgres:latest as a Debian-based image rather than Alpine, functionally equivalent for this module’s own use, just a larger pull. Guest port: 5432 Expected repository: postgres

MethodOnEffect
PostgresContainer::new()builderFloating default image, test/test/test.
PostgresContainer::with_image(image)builderCaller-chosen image, kept verbatim.
.with_username(u) / .with_password(p) / .with_database(d)builderOverride any of the trio before start().
.start()builder → Result<PostgresGuard>Checks the image’s repository, then boots the container.
.username() / .password() / .database_name()guardThe configured trio.
.connection_string()guardpostgres://user:pass@host:port/db.
.stop()guardStops and removes the container, releases its port.

Compatibility checking

with_image takes impl Into<ImageName> and keeps the image verbatim. start() then checks that image’s repository (registry host, tag, and digest stripped) against postgres before any backend is resolved or any sandbox is created, which keeps the constructors infallible like every other module’s. A mismatch returns RightsizeError::IncompatibleImage; ImageName::parse(image) .as_compatible_substitute_for("postgres") is the escape hatch for a verified drop-in replacement from another registry. new() goes through this same check against its own floating reference, so it can never fail in practice.

Readiness: why times = 2

The postgres entrypoint starts the server twice: once to run initdb scripts against it, then shuts it down and starts it again for real — printing “database system is ready to accept connections” both times. Waiting for the first occurrence races the restart: a client can connect to the init-time server just before it’s torn down. This module’s wait strategy is Wait::for_log_message(".*database system is ready to accept connections.*", 2) — it waits for the second occurrence, the durable listen. This is the canonical showcase for Wait::for_log_message’s times parameter — see Wait Strategies.

The control-character env fix (DOCKER_PG_LLVM_DEPS)

The official postgres:*-alpine image bakes DOCKER_PG_LLVM_DEPS into its manifest with a literal tab character in the value (a package list built with \t\t continuation). msb 0.6.2’s krun VMM builder panics with InvalidAscii on that boot-env value before the guest ever starts — reproduced with zero rightsize-set env vars, so this is the image’s own baked value, not anything this module or your test adds. Docker is unaffected. The module works around it by overriding the variable to an empty string (.with_env("DOCKER_PG_LLVM_DEPS", "")) — a no-op for the build the image already baked, and harmless on Docker.

This override was measured against the Alpine-tagged manifest specifically; it stays in place unconditionally now that new() floats to the Debian-based postgres:latest — it is a documented no-op on any manifest that doesn’t carry the tab-bearing value, so keeping it costs nothing and protects a caller who supplies an *-alpine tag via with_image.

If you hit InvalidAscii on a different image under RIGHTSIZE_BACKEND=microsandbox, suspect a baked env var with a control character the same way — see Troubleshooting.

Complete example

use rightsize_modules::PostgresContainer;

#[tokio::test]
async fn postgres_round_trips_a_row() -> Result<(), Box<dyn std::error::Error>> {
    let guard = PostgresContainer::new().start().await?;

    let conn_str = guard.connection_string().replacen("postgres://", "postgresql://", 1);
    let (client, connection) = tokio_postgres::connect(&conn_str, tokio_postgres::NoTls).await?;
    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("postgres connection task ended: {e}");
        }
    });

    client
        .execute("CREATE TABLE smoke (id INT PRIMARY KEY, note TEXT)", &[])
        .await?;
    client
        .execute("INSERT INTO smoke (id, note) VALUES ($1, $2)", &[&1i32, &"hello-rightsize"])
        .await?;
    let rows = client.query("SELECT note FROM smoke WHERE id = $1", &[&1i32]).await?;
    let note: &str = rows[0].get(0);
    assert_eq!(note, "hello-rightsize");

    guard.stop().await?;
    Ok(())
}

Note the postgres://postgresql:// rewrite: tokio-postgres requires the postgresql:// scheme; connection_string() returns postgres:// to match the conventional URI form used elsewhere in this crate’s docs and other clients.

Backend notes

No memory-limit override needed — Postgres’s default footprint fits microsandbox’s ~450 MB microVM default. The only backend-relevant quirk is the DOCKER_PG_LLVM_DEPS fix above, which is microsandbox-only in cause but applied unconditionally (harmless on Docker) so the module behaves identically on both.