Skip to content

feat: publish standalone Docker Compose deployments#227

Merged
ian-pascoe merged 2 commits into
mainfrom
worktree/calm-harbor-3b61
Jul 19, 2026
Merged

feat: publish standalone Docker Compose deployments#227
ian-pascoe merged 2 commits into
mainfrom
worktree/calm-harbor-3b61

Conversation

@ian-pascoe

@ian-pascoe ian-pascoe commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • publish independent SQLite, convenience PostgreSQL, and hardened PostgreSQL Compose descriptors that run without a repository checkout
  • package PostgreSQL provisioning, migration, configuration, and grant helpers in the Caplets image
  • add hardened role, secret, network, container, logging, migration-gating, and upgrade/rotation contracts
  • upload version-synchronized Compose assets with CLI releases
  • add CI container smoke coverage, operational documentation, ADR, specification, and changeset

Verification

  • pnpm verify
  • pnpm compose:smoke
  • independent Standards and Spec reviews completed with no remaining findings

Summary by CodeRabbit

  • New Features

    • Added standalone Docker Compose deployments for SQLite, convenience PostgreSQL, and hardened PostgreSQL setups.
    • Added checkout-free deployment support using published container images and matching release assets.
    • Added PostgreSQL secret-file configuration and automatic migration, role, and permission provisioning.
    • Hardened deployments now support separated database roles, restricted networking, read-only containers, and improved secret handling.
  • Bug Fixes

    • Improved startup health checks and migration gating to prevent runtime services from starting prematurely.
  • Documentation

    • Added deployment guidance, architecture updates, and operational instructions for all supported Compose variants.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ian-pascoe, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 58 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 66dae8a5-2084-4d3e-b4ba-0be236680a6f

📥 Commits

Reviewing files that changed from the base of the PR and between 6d459e7 and dae0012.

📒 Files selected for processing (4)
  • Dockerfile
  • deploy/postgres/provision-roles.mjs
  • deploy/postgres/render-config.mjs
  • docker-compose.postgres-hardened.yml
📝 Walkthrough

Walkthrough

This change adds three standalone Docker Compose deployment variants, packages PostgreSQL deployment helpers in the image, updates release automation to publish matching Compose assets, and adds configuration tests plus Docker-based smoke coverage.

Changes

Standalone Compose deployments

Layer / File(s) Summary
PostgreSQL helpers and provisioning
deploy/postgres/*, Dockerfile
Adds shared credential, schema, connection, identifier, role-provisioning, and rendered-config helpers, with PostgreSQL modules packaged in the runtime image.
Standalone Compose topologies
docker-compose*.yml
Defines SQLite, convenience PostgreSQL, and hardened PostgreSQL deployment flows with migration gating, persistence, health checks, secrets, and runtime restrictions.
Image versioning and release assets
scripts/sync-compose-image-version.mjs, package.json, .github/workflows/*, .changeset/*
Synchronizes hardened image tags, runs Compose smoke checks in CI, detects CLI releases, uploads Compose assets, and records a patch release.
Deployment tests and smoke validation
packages/core/test/*, scripts/smoke-compose-deployments.mjs
Tests file-backed PostgreSQL credentials and exercises SQLite, convenience PostgreSQL, hardened PostgreSQL, migration gates, and legacy compatibility.
Deployment specifications and operational documentation
docs/adr/*, docs/architecture.md, docs/operations/*, docs/specs/*
Documents deployment contracts, role boundaries, hardened operations, migration cutover, and release distribution.

Estimated code review effort: 5 (Critical) | ~90+ minutes

Sequence Diagram(s)

sequenceDiagram
  participant DockerCompose
  participant caplets-postgres
  participant caplets-postgres-migrate
  participant caplets
  DockerCompose->>caplets-postgres: start PostgreSQL
  caplets-postgres-->>caplets-postgres-migrate: pass health check
  caplets-postgres-migrate->>caplets-postgres: provision roles and migrate schema
  caplets-postgres-migrate-->>caplets: report successful completion
  caplets->>caplets: render config and serve
Loading

Possibly related PRs

Poem

I’m a rabbit with Compose files neat,
PostgreSQL hops on sturdy feet.
Secrets tucked away, migrations run,
Smoke tests dance beneath the sun.
Release assets leap in line—
Three little deployments, all ready to shine!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: publishing standalone Docker Compose deployments.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree/calm-harbor-3b61

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR publishes three standalone Docker Compose deployments (SQLite, convenience PostgreSQL, hardened PostgreSQL) that run without a repository checkout, packages PostgreSQL provisioning helpers into the Caplets image, and uploads the Compose files as GitHub Release assets synchronized to each CLI release.

  • Standalone Compose files: All three descriptors switch from local build: to the published image, removing checkout dependencies; the convenience file simplifies to a single caplets owner role with one password, while the hardened file retains separate administrator/migrator/runtime roles, file-backed secrets, an internal database network, read-only containers, and dropped capabilities.
  • Packaged helpers: provision-roles.mjs, render-config.mjs, and finalize-runtime-grants.mjs are copied into the image at /usr/local/lib/caplets/postgres/; all credential handling is done via env/file-backed readCredential() and PostgreSQL's format() with %I/%L specifiers.
  • Release pipeline: The Compose files are uploaded to the matching caplets@VERSION GitHub release after image publication; image-version synchronization is enforced in verify via sync-compose-image-version.mjs.

Confidence Score: 4/5

Safe to merge with one fix: the release workflow step that detects whether to publish Docker artifacts can fail the entire step rather than gracefully skip if gh release view exits non-zero, leaving the Docker image and Compose files unpublished.

The provisioning, secret-handling, and Compose topology changes are well-tested and carefully designed. The release workflow step that detects whether to publish Docker artifacts can fail the entire step rather than gracefully skip if gh release view exits non-zero, which would leave the Docker image and Compose files unpublished for that release run.

.github/workflows/release.yml — the Detect CLI release for this commit step needs the gh release view call guarded so it sets published=false rather than failing the step when the release is not found.

Important Files Changed

Filename Overview
.github/workflows/release.yml Release detection refactored from changesets output to git-tag + gh CLI check; the gh release view call can fail the step if the API is briefly unavailable, silently skipping Docker image and Compose asset publication.
deploy/postgres/provision-roles.mjs New idempotent role provisioner for convenience and hardened modes; uses parameterized PostgreSQL format() for all role/password interpolation; bootstrap-error is swallowed in connectConvenience().
deploy/postgres/postgres-environment.mjs Clean shared utility for credential reading (env or file), schema validation, connection-string construction, and identifier quoting.
docker-compose.postgres-hardened.yml New hardened reference with file-backed secrets, secret-prep sidecar, internal database network, read-only containers, dropped capabilities, and bounded logging.
docker-compose.postgres.yml Rebuilt as standalone convenience deployment; single caplets owner role, published image, no checkout bind mounts.
scripts/smoke-compose-deployments.mjs Comprehensive smoke harness covering SQLite, convenience Postgres, hardened Postgres, migration gates, and legacy three-role compatibility; cleans up all projects in finally.
scripts/sync-compose-image-version.mjs Keeps the hardened compose default image version in sync with the CLI package version; enforces exactly three Caplets image references.
Dockerfile Adds COPY of all deploy/postgres/*.mjs helpers into the image at /usr/local/lib/caplets/postgres/; VOLUME ["/data"] remains.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Op as Operator
    participant Secrets as caplets-postgres-secrets
    participant PG as caplets-postgres
    participant Migrate as caplets-postgres-migrate
    participant Runtime as caplets (runtime)

    Op->>Secrets: docker compose up (run once)
    Secrets->>Secrets: Copy secret files into role-scoped volumes (mode 0400, owned by node)
    Secrets-->>Op: exited 0

    Op->>PG: docker compose up (healthcheck)
    PG-->>Op: healthy (pg_isready)

    Op->>Migrate: docker compose up (depends on PG healthy + secrets completed)
    Migrate->>PG: provision-roles.mjs hardened (admin creds)
    PG-->>Migrate: roles/schema/grants reconciled
    Migrate->>Migrate: render-config.mjs migrator
    Migrate->>PG: storage schema-migrate (migrator role)
    PG-->>Migrate: schema up to date
    Migrate->>PG: finalize-runtime-grants.mjs (migrator role)
    PG-->>Migrate: runtime grants applied
    Migrate-->>Op: exited 0

    Op->>Runtime: docker compose up (depends on migrate completed)
    Runtime->>Runtime: render-config.mjs runtime
    Runtime->>PG: serve (runtime role, DML only)
    Runtime-->>Op: healthy (HTTP /v1/healthz)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Op as Operator
    participant Secrets as caplets-postgres-secrets
    participant PG as caplets-postgres
    participant Migrate as caplets-postgres-migrate
    participant Runtime as caplets (runtime)

    Op->>Secrets: docker compose up (run once)
    Secrets->>Secrets: Copy secret files into role-scoped volumes (mode 0400, owned by node)
    Secrets-->>Op: exited 0

    Op->>PG: docker compose up (healthcheck)
    PG-->>Op: healthy (pg_isready)

    Op->>Migrate: docker compose up (depends on PG healthy + secrets completed)
    Migrate->>PG: provision-roles.mjs hardened (admin creds)
    PG-->>Migrate: roles/schema/grants reconciled
    Migrate->>Migrate: render-config.mjs migrator
    Migrate->>PG: storage schema-migrate (migrator role)
    PG-->>Migrate: schema up to date
    Migrate->>PG: finalize-runtime-grants.mjs (migrator role)
    PG-->>Migrate: runtime grants applied
    Migrate-->>Op: exited 0

    Op->>Runtime: docker compose up (depends on migrate completed)
    Runtime->>Runtime: render-config.mjs runtime
    Runtime->>PG: serve (runtime role, DML only)
    Runtime-->>Op: healthy (HTTP /v1/healthz)
Loading

Reviews (2): Last reviewed commit: "fix: address standalone compose review" | Re-trigger Greptile

Comment thread deploy/postgres/render-config.mjs Outdated
Comment thread Dockerfile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
deploy/postgres/provision-roles.mjs (1)

46-150: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Role identifiers interpolated unquoted; inconsistent with quoteIdentifier usage for database/schema.

Throughout provisionConvenienceRole, provisionHardenedRoles, and reconcileRole, role names (role, migratorRole, runtimeRole) are interpolated directly into SQL (e.g. Line 58, Line 61, Line 64-65, Line 115-138, Line 144), while database/schema are always wrapped in quoteIdentifier. Currently these role names are hardcoded literals so there's no live injection risk, but the pattern is inconsistent and would become exploitable the moment role naming is parameterized (similar to how render-config.mjs already supports a CAPLETS_POSTGRES_USER override). Line 144's CREATE ROLE ${role} isn't even routed through the format(%I) pattern used elsewhere in the same function.

🔒 Proposed fix
 async function reconcileRole(role, password) {
   const existing = await client.query("SELECT 1 FROM pg_roles WHERE rolname = $1", [role]);
-  if (existing.rowCount === 0) await client.query(`CREATE ROLE ${role}`);
+  if (existing.rowCount === 0) await client.query(`CREATE ROLE ${quoteIdentifier(role)}`);

Apply the same quoteIdentifier(role) wrapping to the other unquoted role interpolations in provisionConvenienceRole and provisionHardenedRoles.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/postgres/provision-roles.mjs` around lines 46 - 150, Quote every
interpolated role identifier before embedding it in SQL, matching the existing
quoteIdentifier usage for database and schema. Update provisionConvenienceRole,
provisionHardenedRoles, and reconcileRole, including CREATE ROLE, ALTER ROLE,
GRANT, and ALTER DEFAULT PRIVILEGES statements, while preserving the existing
SQL behavior.
docker-compose.postgres-hardened.yml (2)

61-63: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Hardcoded UID/GID 1000 couples secret ownership to the base image's "node" user.

install --owner=1000 --group=1000 ... assumes the "node" user/group in the app image is exactly uid/gid 1000. This holds for the current official Node.js base images but would silently break secret readability if the base image ever changes its UID/GID scheme.

🔧 Proposed fix
     command:
       - |
-        install --owner=1000 --group=1000 --mode=0400 /run/secrets/caplets_postgres_admin_password /prepared/admin/password
-        install --owner=1000 --group=1000 --mode=0400 /run/secrets/caplets_postgres_migrator_password /prepared/migrator/password
-        install --owner=1000 --group=1000 --mode=0400 /run/secrets/caplets_postgres_runtime_password /prepared/runtime/password
+        install --owner="$(id -u node)" --group="$(id -g node)" --mode=0400 /run/secrets/caplets_postgres_admin_password /prepared/admin/password
+        install --owner="$(id -u node)" --group="$(id -g node)" --mode=0400 /run/secrets/caplets_postgres_migrator_password /prepared/migrator/password
+        install --owner="$(id -u node)" --group="$(id -g node)" --mode=0400 /run/secrets/caplets_postgres_runtime_password /prepared/runtime/password
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.postgres-hardened.yml` around lines 61 - 63, Replace the
hardcoded 1000 UID/GID ownership in the secret preparation commands with
ownership resolved from the image’s actual node user/group, while preserving
mode 0400 and the existing admin, migrator, and runtime secret destinations.

100-106: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Use the container’s node UID/GID instead of hardcoding 1000:1000 The secret prep step at docker-compose.postgres-hardened.yml:61-63 is coupled to the current base image layout; deriving ownership dynamically would avoid a future image change breaking access.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.postgres-hardened.yml` around lines 100 - 106, Update the
secret preparation step near the postgres migration command to derive ownership
from the container’s node user and group instead of hardcoding 1000:1000. Reuse
the container’s node UID/GID when applying ownership so the generated secrets
remain accessible across base-image changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@deploy/postgres/provision-roles.mjs`:
- Around line 46-150: Quote every interpolated role identifier before embedding
it in SQL, matching the existing quoteIdentifier usage for database and schema.
Update provisionConvenienceRole, provisionHardenedRoles, and reconcileRole,
including CREATE ROLE, ALTER ROLE, GRANT, and ALTER DEFAULT PRIVILEGES
statements, while preserving the existing SQL behavior.

In `@docker-compose.postgres-hardened.yml`:
- Around line 61-63: Replace the hardcoded 1000 UID/GID ownership in the secret
preparation commands with ownership resolved from the image’s actual node
user/group, while preserving mode 0400 and the existing admin, migrator, and
runtime secret destinations.
- Around line 100-106: Update the secret preparation step near the postgres
migration command to derive ownership from the container’s node user and group
instead of hardcoding 1000:1000. Reuse the container’s node UID/GID when
applying ownership so the generated secrets remain accessible across base-image
changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a7194a6a-4b95-4dc7-bd31-a21d68edc030

📥 Commits

Reviewing files that changed from the base of the PR and between 8f93b49 and 6d459e7.

📒 Files selected for processing (19)
  • .changeset/standalone-compose-deployments.md
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • Dockerfile
  • deploy/postgres/init-caplets-roles.sh
  • deploy/postgres/postgres-environment.mjs
  • deploy/postgres/provision-roles.mjs
  • deploy/postgres/render-config.mjs
  • docker-compose.postgres-hardened.yml
  • docker-compose.postgres.yml
  • docker-compose.yml
  • docs/adr/0006-single-role-postgres-convenience-compose.md
  • docs/architecture.md
  • docs/operations/sql-authoritative-host-state.md
  • docs/specs/2026-07-18-standalone-compose-deployments.md
  • package.json
  • packages/core/test/postgres-deployment-config.test.ts
  • scripts/smoke-compose-deployments.mjs
  • scripts/sync-compose-image-version.mjs
💤 Files with no reviewable changes (1)
  • deploy/postgres/init-caplets-roles.sh

@ian-pascoe ian-pascoe changed the title Publish standalone Docker Compose deployments feat: publish standalone Docker Compose deployments Jul 19, 2026
@ian-pascoe
ian-pascoe merged commit 9b076d0 into main Jul 19, 2026
9 checks passed
@ian-pascoe
ian-pascoe deleted the worktree/calm-harbor-3b61 branch July 19, 2026 10:55
ian-pascoe added a commit that referenced this pull request Jul 19, 2026
* feat: publish standalone compose deployments

* fix: address standalone compose review
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.

1 participant