feat: publish standalone Docker Compose deployments#227
Conversation
|
Warning Review limit reached
Next review available in: 58 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThis 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. ChangesStandalone Compose deployments
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
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Preview DeployedLanding: https://pr-227.preview.caplets.dev Built from commit 0133685 |
|
| 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)
%%{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)
Reviews (2): Last reviewed commit: "fix: address standalone compose review" | Re-trigger Greptile
There was a problem hiding this comment.
🧹 Nitpick comments (3)
deploy/postgres/provision-roles.mjs (1)
46-150: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winRole identifiers interpolated unquoted; inconsistent with
quoteIdentifierusage for database/schema.Throughout
provisionConvenienceRole,provisionHardenedRoles, andreconcileRole, role names (role,migratorRole,runtimeRole) are interpolated directly into SQL (e.g. Line 58, Line 61, Line 64-65, Line 115-138, Line 144), whiledatabase/schemaare always wrapped inquoteIdentifier. 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 howrender-config.mjsalready supports aCAPLETS_POSTGRES_USERoverride). Line 144'sCREATE ROLE ${role}isn't even routed through theformat(%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 inprovisionConvenienceRoleandprovisionHardenedRoles.🤖 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 winHardcoded 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 valueUse the container’s node UID/GID instead of hardcoding
1000:1000The secret prep step atdocker-compose.postgres-hardened.yml:61-63is 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
📒 Files selected for processing (19)
.changeset/standalone-compose-deployments.md.github/workflows/ci.yml.github/workflows/release.ymlDockerfiledeploy/postgres/init-caplets-roles.shdeploy/postgres/postgres-environment.mjsdeploy/postgres/provision-roles.mjsdeploy/postgres/render-config.mjsdocker-compose.postgres-hardened.ymldocker-compose.postgres.ymldocker-compose.ymldocs/adr/0006-single-role-postgres-convenience-compose.mddocs/architecture.mddocs/operations/sql-authoritative-host-state.mddocs/specs/2026-07-18-standalone-compose-deployments.mdpackage.jsonpackages/core/test/postgres-deployment-config.test.tsscripts/smoke-compose-deployments.mjsscripts/sync-compose-image-version.mjs
💤 Files with no reviewable changes (1)
- deploy/postgres/init-caplets-roles.sh
* feat: publish standalone compose deployments * fix: address standalone compose review
Summary
Verification
pnpm verifypnpm compose:smokeSummary by CodeRabbit
New Features
Bug Fixes
Documentation