Skip to content

feat(container): Add PostgreSQL image - #241

Merged
robinnsc merged 1 commit into
mainfrom
feat/postgres-container-image-main
Aug 7, 2026
Merged

feat(container): Add PostgreSQL image#241
robinnsc merged 1 commit into
mainfrom
feat/postgres-container-image-main

Conversation

@robinnsc

@robinnsc robinnsc commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

What

Add a production-shaped, registry-neutral container image for ExtendDB using the PostgreSQL storage backend.

Container image

  • Build only the PostgreSQL backend using the locked Cargo dependency graph.
  • Use canonical, digest-pinned Rust and approved Debian runtime images.
  • Pin the Dockerfile frontend and installed Debian package versions.
  • Require explicit version, source revision, and build-time metadata.
  • Run as fixed UID/GID 10001:10001.
  • Remove setuid/setgid privilege paths.
  • Support a read-only root filesystem.
  • Include ExtendDB’s LICENSE, project NOTICE, and generated software license
    notices.
  • Preserve Debian’s dpkg metadata and /usr/share/doc/*/copyright files.
  • Start ExtendDB directly through tini.
  • Use extenddb healthcheck for container liveness.
  • Expose port 18443.
  • Keep PostgreSQL external; the image contains no PostgreSQL server or data
    directory.

The normal image startup does not initialize or migrate a database.

Local Compose environment

Add a local-development-only Compose stack with four separate roles:

  1. A digest-pinned PostgreSQL 16 service.
  2. A capability-limited state-volume ownership initializer.
  3. A one-time ExtendDB init or migrate bootstrap container.
  4. A hardened, long-running ExtendDB serving container.

The serving container:

  • Runs as 10001:10001.
  • Uses a read-only root filesystem.
  • Drops all Linux capabilities.
  • Enables no-new-privileges.
  • Uses a writable state volume and temporary /tmp filesystem.
  • Binds the host port only to 127.0.0.1.
  • Receives no PostgreSQL bootstrap or ExtendDB administrative credentials.

The volume initializer runs as root with only CAP_CHOWN. The bootstrap container receives the elevated credentials only for the one-time lifecycle operation.

Non-argv bootstrap secrets

Add environment-based alternatives for bootstrap database passwords:

EXTENDDB_PG_PASSWORD
EXTENDDB_APP_PASSWORD

These are supported by:

  • extenddb init
  • extenddb migrate
  • extenddb destroy where applicable

Existing --pg-pass and --extenddb-pass arguments remain supported. Explicit CLI arguments take precedence.

Environment-sourced values are appended only to the in-process argument vector used by the storage bootstrapper. They do not appear in the operating system’s process command line.

Build metadata

Allow container builds to inject deterministic metadata through:

EXTENDDB_GIT_HASH
EXTENDDB_BUILD_TIME

Local non-container builds retain the existing Git and current-time fallbacks.

Software license notices

Add reproducible license-notice generation using pinned cargo-about 0.9.0:

devtools/generate-software-license-notices
devtools/generate-software-license-notices --check

The generated SOFTWARE-LICENSE-NOTICES.html represents the locked PostgreSQL build graph for both supported GNU/Linux architectures:

  • x86_64-unknown-linux-gnu
  • aarch64-unknown-linux-gnu

Build dependencies are included, development dependencies are excluded, and generation fails on unresolved license metadata. The artifact currently covers 293 selected Rust packages and 28 license identifiers.

Debian package attribution remains in /usr/share/doc/*/copyright with the dpkg database intact.

Smoke testing

Add ci/smoke-test-container.sh, which supports two modes:

  • Build a uniquely tagged image from the current source.
  • Test a supplied prebuilt image without rebuilding or retagging it.

The smoke test:

  • Uses a unique Compose project, image tag, and Docker-allocated loopback port.

  • Confirms volume-init, bootstrap, and serving roles use the same image ID.

  • Verifies UID/GID, read-only root, capabilities, and

    no-new-privileges.

  • Confirms no PostgreSQL server exists in the image.

  • Confirms no setuid/setgid executable remains.

  • Verifies configuration and TLS private-key modes.

  • Compares OCI version, revision, and creation labels with embedded binary

    metadata.

  • Runs extenddb verify.

  • Creates an account, user, policy, and access key.

  • Executes CreateTable, PutItem, and GetItem through the AWS CLI.

  • Restarts the serving container and verifies persistence.

  • Sends SIGTERM and requires a clean exit.

  • Recreates the Compose stack without deleting volumes.

  • Confirms the existing-config migration path runs successfully.

  • Verifies data remains available after migration.

  • Removes disposable containers, networks, volumes, images, and credentials.

Documentation

Document:

  • The external PostgreSQL 14+ requirement.
  • Local Compose usage.
  • The explicit production init, migrate, and serve lifecycle.
  • Non-root/read-only deployment expectations.
  • Bootstrap credential separation.
  • Liveness versus PostgreSQL readiness.
  • Self-signed TLS limitations for local development.
  • Software license notice generation and maintenance.

Registry provisioning, release-version selection, SBOM/scanning/signing, and publishing automation are outside this PR.

Why

ExtendDB needs a tested container artifact before publishing an image to a public registry.

The image intentionally contains only ExtendDB. PostgreSQL remains separate so its backup, patching, scaling, upgrade, and availability lifecycle can be managed independently.

The container must also support production orchestrator expectations:

  • A fixed non-root identity.
  • A read-only root filesystem.
  • Foreground process execution.
  • Graceful SIGTERM handling.
  • Explicit one-time initialization and migration jobs.
  • No elevated database credentials in the serving container.
  • Immutable source and build metadata.
  • Complete software attribution.

This PR is based directly on main and does not include PR235. The container implementation can be reviewed independently because Compose and the documented production lifecycle run exactly one initialization or migration operation. PR235 remains a prerequisite for the first official public release because it adds migration serialization as defense in depth.

Testing done

Rust validation

cargo fmt --all -- --check

cargo clippy --all-targets -- -D warnings

cargo test --workspace

cargo build --locked --release
-p extenddb
--no-default-features
--features postgres

Script and configuration validation

bash -n
ci/smoke-test-container.sh
devtools/generate-software-license-notices

shellcheck
ci/smoke-test-container.sh
devtools/generate-software-license-notices

devtools/generate-software-license-notices --check

docker compose -f docker-compose.yml config --quiet

hadolint Dockerfile

git diff --check

Container smoke testing

The complete build-mode and prebuilt-image smoke tests passed, including:

  • One shared image artifact for init/bootstrap/serve.
  • Prebuilt mode with no rebuild or retag.
  • Non-root and read-only-root execution.
  • Capability and no-new-privileges assertions.
  • No PostgreSQL executable or data directory.
  • No setuid/setgid executables.
  • OCI and embedded metadata consistency.
  • CreateTable, PutItem, and GetItem.
  • Restart persistence.
  • Graceful SIGTERM exit.
  • Existing-config migration bootstrap.
  • Data persistence across Compose recreation.
  • Complete cleanup.

Multi-architecture validation

A local OCI build completed successfully for:

  • linux/amd64
  • linux/arm64

The resulting index contained both image manifests and per-platform provenance
attestations.

The renamed runtime notice artifact was also validated in a built image:

/usr/share/doc/extenddb/SOFTWARE-LICENSE-NOTICES.html

It is installed read-only at mode 0444; the previous filename is absent.

Checklist

  • I have read CONTRIBUTING.md (../CONTRIBUTING.md)
  • All tests pass (cargo test --workspace)
  • Code is formatted (cargo fmt --all -- --check)
  • Clippy is clean (cargo clippy --all-targets -- -D warnings)
  • I have added or updated tests for new functionality
  • I have updated documentation if behavior changed
  • Breaking changes are noted below
  • This does not change the wire protocol, Storage trait, auth model, or on-disk format

ADR / RFC: n/a — this adds packaging, local deployment, and release-support
artifacts without changing the service wire protocol or storage architecture.

Breaking changes

None.

The existing password CLI arguments remain supported. The new environment
variables provide a safer alternative for one-time bootstrap jobs.

By submitting this pull request, I confirm that my contribution is made under
the terms of the Apache License 2.0 and I agree to the Developer Certificate of
Origin (DCO). See CONTRIBUTING.md (../CONTRIBUTING.md) for details.

Add a digest-pinned, non-root ExtendDB image built only with the
PostgreSQL backend while keeping the PostgreSQL server external.

Provide a hardened local Compose stack with separate volume-init,
bootstrap, and serving roles. Exercise build and prebuilt artifacts,
API persistence, graceful shutdown, and migration bootstrap behavior.

Generate reproducible Rust dependency notices and embed immutable source
metadata. Accept bootstrap secrets from environment variables so they do
not appear in process arguments.
@robinnsc
robinnsc marked this pull request as ready for review August 7, 2026 08:52
pg_user: String,

/// PostgreSQL admin password
/// PostgreSQL admin password.

@jcshepherd jcshepherd Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should pause here and retire the "PG"-isms: they're just going to get more confusing. Would it be easier to do that now, maybe in a separate PR, and then revise this one? Like EXTENDDB_DB_PASSWORD or some such. I think the PR/RFC for TiDB had some work on this too.

Comment thread crates/app/src/util.rs
"EXTENDDB_PG_PASSWORD",
)
.unwrap();
assert_eq!(args.len(), 3);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: More convincing if this also validated the actual args.

@jcshepherd jcshepherd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple comments below, but on reflection I think addressing the "PG" smell should be done separately, as it'll touch more than just one env variable.

@robinnsc
robinnsc added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 9acb679 Aug 7, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants