cpflow 5.1.1 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.agents/agent-workflow.yml +26 -0
- data/.agents/bin/README.md +20 -0
- data/.agents/bin/docs +5 -0
- data/.agents/bin/lint +5 -0
- data/.agents/bin/setup +5 -0
- data/.agents/bin/test +5 -0
- data/.agents/bin/validate +5 -0
- data/.agents/trusted-github-actors.yml +32 -0
- data/.agents/workflows/ai-rollout-e2e-test.md +166 -0
- data/.github/actions/cpflow-setup-environment/action.yml +1 -1
- data/.github/actions/cpflow-wait-for-health/action.yml +87 -15
- data/.github/pull_request_template.md +18 -0
- data/.github/workflows/claude-code-review.yml +2 -0
- data/.github/workflows/claude.yml +94 -1
- data/.github/workflows/cpflow-delete-review-app.yml +621 -33
- data/.github/workflows/cpflow-deploy-review-app.yml +656 -21
- data/.github/workflows/cpflow-review-app-help.yml +5 -13
- data/.github/workflows/rspec-shared.yml +10 -3
- data/.github/workflows/rspec-specific.yml +1 -0
- data/.github/workflows/rspec.yml +58 -1
- data/AGENTS.md +14 -0
- data/CHANGELOG.md +54 -1
- data/CLAUDE.md +3 -0
- data/CONTRIBUTING.md +15 -3
- data/Gemfile.lock +1 -1
- data/README.md +21 -7
- data/docs/ai-github-flow-prompt.md +18 -16
- data/docs/ci-automation.md +239 -27
- data/docs/commands.md +30 -2
- data/docs/grafana-opentelemetry.md +699 -0
- data/docs/secrets-and-env-values.md +37 -2
- data/docs/sidebars.ts +70 -0
- data/docs/telemetry/application-instrumentation.md +161 -0
- data/docs/telemetry/collector.md +297 -0
- data/docs/telemetry/index.md +152 -0
- data/docs/telemetry/pipelines.md +98 -0
- data/docs/telemetry/review-apps.md +55 -0
- data/docs/telemetry/troubleshooting.md +92 -0
- data/docs/terraform/example/.controlplane/controlplane.yml +0 -1
- data/docs/terraform/overview.md +11 -0
- data/docs/tips.md +475 -28
- data/examples/controlplane.yml +2 -0
- data/lib/command/ai_github_flow_prompt.rb +2 -2
- data/lib/command/apply_template.rb +104 -2
- data/lib/command/base.rb +69 -5
- data/lib/command/deploy_image.rb +93 -7
- data/lib/command/promote_app_from_upstream.rb +1 -0
- data/lib/command/ps_wait.rb +2 -10
- data/lib/command/run.rb +133 -10
- data/lib/command/setup_app.rb +10 -5
- data/lib/core/config.rb +94 -0
- data/lib/core/controlplane.rb +38 -5
- data/lib/core/controlplane_api.rb +8 -0
- data/lib/core/controlplane_api_direct.rb +257 -63
- data/lib/core/doctor_service.rb +44 -3
- data/lib/core/shell.rb +9 -2
- data/lib/core/template_parser.rb +43 -9
- data/lib/cpflow/version.rb +1 -1
- data/lib/generator_templates/controlplane.yml +1 -2
- data/lib/github_flow_templates/.github/cpflow-help.md +34 -10
- data/lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml +10 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +9 -0
- metadata +22 -2
data/docs/tips.md
CHANGED
|
@@ -6,18 +6,29 @@
|
|
|
6
6
|
4. [CPU](#cpu)
|
|
7
7
|
5. [Remote IP](#remote-ip)
|
|
8
8
|
6. [Secrets and ENV Values](/docs/secrets-and-env-values.md)
|
|
9
|
-
7. [
|
|
10
|
-
8. [
|
|
11
|
-
9. [
|
|
12
|
-
10. [
|
|
9
|
+
7. [Telemetry](#telemetry)
|
|
10
|
+
8. [CI](#ci)
|
|
11
|
+
9. [Logs](#logs)
|
|
12
|
+
10. [Grafana and OpenTelemetry](#grafana-and-opentelemetry)
|
|
13
|
+
11. [Memcached](#memcached)
|
|
14
|
+
12. [Sidekiq](#sidekiq)
|
|
13
15
|
- [Quieting Non-Critical Workers During Deployments](#quieting-non-critical-workers-during-deployments)
|
|
14
16
|
- [Setting Up a Pre Stop Hook](#setting-up-a-pre-stop-hook)
|
|
15
17
|
- [Setting Up a Liveness Probe](#setting-up-a-liveness-probe)
|
|
16
|
-
|
|
17
|
-
- [
|
|
18
|
+
13. [Minimizing Non-Production App Costs](#minimizing-non-production-app-costs)
|
|
19
|
+
- [Share One Control Plane Postgres for Staging and Review Apps](#share-one-control-plane-postgres-for-staging-and-review-apps)
|
|
20
|
+
- [Enable Capacity AI for Demo and Starter Staging Apps](#enable-capacity-ai-for-demo-and-starter-staging-apps)
|
|
21
|
+
- [Use an Always-Available Landing Page for a Serverless App](#use-an-always-available-landing-page-for-a-serverless-app)
|
|
18
22
|
- [Delete or Pause Abandoned Apps with `cleanup-stale-apps`](#delete-or-pause-abandoned-apps-with-cleanup-stale-apps)
|
|
19
23
|
- [Pause and Resume with `ps:stop` / `ps:start`](#pause-and-resume-with-psstop--psstart)
|
|
20
|
-
|
|
24
|
+
14. [Right-Sizing Non-Production Workloads](#right-sizing-non-production-workloads)
|
|
25
|
+
- [Enable Capacity AI on Idle Workloads](#enable-capacity-ai-on-idle-workloads)
|
|
26
|
+
- [Don't Autoscale Idle Workloads on CPU](#dont-autoscale-idle-workloads-on-cpu)
|
|
27
|
+
- [Right-Size Reserved CPU and Memory](#right-size-reserved-cpu-and-memory)
|
|
28
|
+
- [Drop Workloads You Don't Use](#drop-workloads-you-dont-use)
|
|
29
|
+
- [Share One Postgres Across Non-Production Apps](#share-one-postgres-across-non-production-apps)
|
|
30
|
+
- [Keep Templates as the Source of Truth](#keep-templates-as-the-source-of-truth)
|
|
31
|
+
15. [Useful Links](#useful-links)
|
|
21
32
|
|
|
22
33
|
## GVCs vs. Orgs
|
|
23
34
|
|
|
@@ -114,6 +125,14 @@ So `REMOTE_ADDR` should not be used directly, only `request.remote_ip`.
|
|
|
114
125
|
> **Warning:** Do not use `REMOTE_ADDR` for authentication, rate limiting, auditing, or IP allowlists. Always use
|
|
115
126
|
> framework-specific mechanisms that understand proxy headers (such as Rails' `request.remote_ip`).
|
|
116
127
|
|
|
128
|
+
## Telemetry
|
|
129
|
+
|
|
130
|
+
If your app emits OpenTelemetry, StatsD, or structured log signals, run an
|
|
131
|
+
OpenTelemetry Collector as a Control Plane workload in the same GVC and point
|
|
132
|
+
application env vars at the collector's internal service name. See the
|
|
133
|
+
[telemetry guide](https://www.shakacode.com/control-plane-flow/docs/telemetry/) for the template shape, recommended ports,
|
|
134
|
+
review-app guardrails, and troubleshooting commands.
|
|
135
|
+
|
|
117
136
|
## CI
|
|
118
137
|
|
|
119
138
|
**Note:** Docker builds much slower on Apple Silicon, so try configuring CI to build the images when using Apple
|
|
@@ -202,6 +221,15 @@ To check for truncation, compare line count to the limit: `wc -l < incident.log`
|
|
|
202
221
|
likely cut off. Prefer narrowing the time window (and concatenating the sub-ranges) over raising `--limit`, since the
|
|
203
222
|
server-side cap may be lower than the flag value.
|
|
204
223
|
|
|
224
|
+
## Grafana and OpenTelemetry
|
|
225
|
+
|
|
226
|
+
Control Plane's built-in Grafana gives useful workload metrics such as CPU, memory, restarts, and request rate. For
|
|
227
|
+
Rails applications that need app-level request latency, database spans, Redis spans, Sidekiq job metrics, or
|
|
228
|
+
trace-to-log correlation, add OpenTelemetry and an internal collector workload that exposes generated Prometheus
|
|
229
|
+
metrics.
|
|
230
|
+
|
|
231
|
+
See [Grafana and OpenTelemetry on Control Plane](/docs/grafana-opentelemetry.md) for the full setup guide.
|
|
232
|
+
|
|
205
233
|
## Memcached
|
|
206
234
|
|
|
207
235
|
On the workload container for Memcached (using the `memcached:alpine` image), configure the command with the args
|
|
@@ -266,28 +294,311 @@ To do this:
|
|
|
266
294
|
|
|
267
295
|
To set up a liveness probe on port 7433, see: https://github.com/arturictus/sidekiq_alive
|
|
268
296
|
|
|
269
|
-
## Minimizing
|
|
297
|
+
## Minimizing Non-Production App Costs
|
|
270
298
|
|
|
271
299
|
Long-tail review apps — PRs that linger for days or weeks with little traffic — can drive up Control Plane spend if every
|
|
272
300
|
workload runs full-time. `cpflow` already provides several knobs to manage this without custom orchestration.
|
|
301
|
+
The same cost-control pass applies to public demos, starter staging apps, and long-lived review apps: start with
|
|
302
|
+
Capacity AI for app workloads, then reserve true scale-to-zero for apps where cold starts and planned migrations are
|
|
303
|
+
acceptable.
|
|
273
304
|
|
|
274
305
|
> **Note:** Scaling workloads to zero or stopping review apps does not reduce costs from external databases, managed
|
|
275
306
|
> Redis instances, object storage, or other third-party services. Those continue to bill independently of Control Plane
|
|
276
307
|
> workload state.
|
|
277
308
|
|
|
278
|
-
###
|
|
309
|
+
### Share One Control Plane Postgres for Staging and Review Apps
|
|
310
|
+
|
|
311
|
+
For non-production Rails apps, a per-GVC Postgres workload is often the largest avoidable review-app cost. Each app can
|
|
312
|
+
end up with its own always-on Postgres replica and its own volume. If staging/review data can be reset, create one
|
|
313
|
+
shared Postgres GVC in the staging org, then point staging and review app GVCs at separate logical databases inside that
|
|
314
|
+
single Postgres instance.
|
|
315
|
+
|
|
316
|
+
Use separate logical databases per app or review app. Do not point multiple Rails apps at the same database/schema unless
|
|
317
|
+
they intentionally share migrations and data. For example:
|
|
318
|
+
|
|
319
|
+
```text
|
|
320
|
+
Shared GVC (in the staging org):
|
|
321
|
+
staging-shared-postgres
|
|
322
|
+
postgres workload
|
|
323
|
+
shared-postgres-vs volume
|
|
324
|
+
|
|
325
|
+
Client GVCs (each points to a separate logical database):
|
|
326
|
+
react-webpack-rails-tutorial-staging
|
|
327
|
+
react-webpack-rails-tutorial-review-pr-123
|
|
328
|
+
react-on-rails-starter-staging
|
|
329
|
+
react-on-rails-starter-review-pr-123
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The shared Postgres workload must accept internal traffic from other GVCs. `same-gvc` is not enough when the database
|
|
333
|
+
lives in a separate GVC; use `same-org`, or `workload-list` if you can keep an explicit allowlist current. `same-org` is
|
|
334
|
+
convenient for trusted staging orgs, but every workload in the org can reach the database port, including production
|
|
335
|
+
workloads if production GVCs share the same org. Use `workload-list` for a tighter blast radius if you can automate
|
|
336
|
+
entries as review apps appear and disappear.
|
|
337
|
+
|
|
338
|
+
```yaml
|
|
339
|
+
kind: volumeset
|
|
340
|
+
name: shared-postgres-vs
|
|
341
|
+
spec:
|
|
342
|
+
fileSystemType: ext4
|
|
343
|
+
initialCapacity: 10
|
|
344
|
+
performanceClass: general-purpose-ssd
|
|
345
|
+
snapshots:
|
|
346
|
+
createFinalSnapshot: true
|
|
347
|
+
retentionDuration: 7d
|
|
348
|
+
# Periodic snapshots need a schedule; without one, only a final snapshot is taken when the volumeset is deleted.
|
|
349
|
+
schedule: "0 2 * * *" # daily at 02:00 UTC; adjust to your retention needs
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
kind: workload
|
|
353
|
+
name: postgres
|
|
354
|
+
spec:
|
|
355
|
+
type: stateful
|
|
356
|
+
containers:
|
|
357
|
+
- name: postgres
|
|
358
|
+
image: postgres:17 # pin a specific patch (e.g. postgres:17.x) for reproducible stateful deploys
|
|
359
|
+
cpu: 250m
|
|
360
|
+
memory: 512Mi
|
|
361
|
+
env:
|
|
362
|
+
- name: PGDATA
|
|
363
|
+
value: /var/lib/postgresql/data/pg_data
|
|
364
|
+
- name: POSTGRES_DB
|
|
365
|
+
value: postgres
|
|
366
|
+
- name: POSTGRES_USER
|
|
367
|
+
value: postgres
|
|
368
|
+
- name: POSTGRES_PASSWORD
|
|
369
|
+
# Recommended after adding the workload identity/policy binding:
|
|
370
|
+
# value: cpln://secret/shared-postgres-password.password
|
|
371
|
+
# Plain-value fallback for disposable non-production experiments only.
|
|
372
|
+
# Do not commit this file with a real password in place.
|
|
373
|
+
value: REPLACE_WITH_NON_PRODUCTION_PASSWORD
|
|
374
|
+
ports:
|
|
375
|
+
- number: 5432
|
|
376
|
+
protocol: tcp
|
|
377
|
+
volumes:
|
|
378
|
+
- uri: cpln://volumeset/shared-postgres-vs
|
|
379
|
+
path: /var/lib/postgresql/data
|
|
380
|
+
recoveryPolicy: retain # keep the volume if the workload is deleted; clean up manually when no longer needed
|
|
381
|
+
defaultOptions:
|
|
382
|
+
autoscaling:
|
|
383
|
+
metric: disabled
|
|
384
|
+
minScale: 1
|
|
385
|
+
maxScale: 1
|
|
386
|
+
capacityAI: false
|
|
387
|
+
firewallConfig:
|
|
388
|
+
internal:
|
|
389
|
+
inboundAllowType: same-org
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
`POSTGRES_DB: postgres` initializes the administrative database for the server. Apps should use their own logical
|
|
393
|
+
databases, not the administrative `postgres` database.
|
|
394
|
+
|
|
395
|
+
Field note: `100m` CPU and `256Mi` memory were enough for tiny Rails migrations, but a real staging seed that inserted
|
|
396
|
+
hundreds of thousands of rows caused Postgres to log `server process ... terminated by signal 9: Killed`. `250m` and
|
|
397
|
+
`512Mi` handled the same seed while still replacing multiple always-on per-app Postgres workloads.
|
|
398
|
+
|
|
399
|
+
For review apps, keep the logical database name unique per app. `cpflow`'s default app template uses `{{APP_NAME}}` in
|
|
400
|
+
both the Postgres host and database name:
|
|
401
|
+
|
|
402
|
+
```yaml
|
|
403
|
+
- name: DATABASE_URL
|
|
404
|
+
value: postgres://the_user:the_password@postgres.{{APP_NAME}}.cpln.local:5432/{{APP_NAME}}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Create a review-only app template by copying `.controlplane/templates/app.yml` to
|
|
408
|
+
`.controlplane/templates/app-review.yml`. In that copy, point `DATABASE_URL` at a per-review-app Control Plane secret.
|
|
409
|
+
Trusted automation should create the secret with a full URL whose database name is still that review app's
|
|
410
|
+
`{{APP_NAME}}`:
|
|
411
|
+
|
|
412
|
+
```yaml
|
|
413
|
+
spec:
|
|
414
|
+
env:
|
|
415
|
+
- name: DATABASE_URL
|
|
416
|
+
value: cpln://secret/{{APP_NAME}}-database.DATABASE_URL
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Control Plane's `cpln://secret/...` syntax replaces the entire env value; it is not substring interpolation, so avoid
|
|
420
|
+
forms such as `postgres://cpln://secret/...@...`. A full per-app URL secret also avoids depending on any runtime
|
|
421
|
+
ordering between secret resolution and `$(VAR)` env-var expansion. Because `{{APP_NAME}}-database` is a separate secret
|
|
422
|
+
from the app dictionary secret, trusted automation must create that secret and add it to the app identity's reveal policy
|
|
423
|
+
before the workload starts; otherwise workloads cannot resolve the `cpln://secret/...` value. For trusted staging/review
|
|
424
|
+
apps where a single shared database role is acceptable, you can still create one URL secret per app that reuses the same
|
|
425
|
+
database user/password while keeping the database name unique.
|
|
426
|
+
|
|
427
|
+
The `cpln://secret/NAME.FIELD` field syntax resolves only against **dictionary** secrets; an `opaque` or `tls` secret
|
|
428
|
+
leaves the workload with an empty or literal `cpln://...` string rather than a clear error. Define the database secret as
|
|
429
|
+
a dictionary, apply it with `cpln apply -f secret.yaml`, and confirm the app identity's policy grants `reveal` on it
|
|
430
|
+
before the workload starts:
|
|
431
|
+
|
|
432
|
+
```yaml
|
|
433
|
+
kind: secret
|
|
434
|
+
name: my-app-review-pr-123-database
|
|
435
|
+
type: dictionary
|
|
436
|
+
data:
|
|
437
|
+
DATABASE_URL: postgres://the_user:the_password@postgres.staging-shared-postgres.cpln.local:5432/my-app-review-pr-123
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
`cpflow` can automate this secret-and-policy wiring. Declare a `shared_secret_grants` entry on the review app and
|
|
441
|
+
reference the generated `{{SHARED_SECRET_DATABASE}}` placeholder in your templates instead of hardcoding the secret name;
|
|
442
|
+
`cpflow setup-app`, `deploy-image`, `delete`, and `cleanup-stale-apps` then keep the policy binding and cleanup automatic.
|
|
443
|
+
See [Shared Secrets for Review Apps](secrets-and-env-values.md#shared-secrets-for-review-apps) for the full setup.
|
|
444
|
+
|
|
445
|
+
`{{APP_NAME}}` keeps databases separate by convention, not by itself as a security boundary. If review apps can run
|
|
446
|
+
untrusted PR code, do not give every review app the same database role with `CREATEDB` or ownership of every review
|
|
447
|
+
database. Prefer one of these safer models:
|
|
448
|
+
|
|
449
|
+
1. Create a database and role per review app, store that app's URL/credentials in its DB secret, and grant the role only to
|
|
450
|
+
its own database.
|
|
451
|
+
2. Keep review app database roles low-privilege and run create/drop cleanup from trusted admin automation against the
|
|
452
|
+
shared Postgres workload.
|
|
453
|
+
|
|
454
|
+
A single shared role/password is acceptable only for trusted staging apps or review apps where database separation is a
|
|
455
|
+
cost-control convenience rather than a security boundary. The hook example below assumes the review app role is allowed
|
|
456
|
+
to create its own logical database; if you choose admin-owned cleanup, run create/drop steps from trusted automation
|
|
457
|
+
instead of from the review app workload.
|
|
458
|
+
|
|
459
|
+
Then point the review-app entry at the review-only template and remove the per-PR Postgres workload:
|
|
460
|
+
|
|
461
|
+
```yaml
|
|
462
|
+
my-app-review:
|
|
463
|
+
match_if_app_name_starts_with: true
|
|
464
|
+
setup_app_templates:
|
|
465
|
+
# postgres removed, so no per-PR database workload is created
|
|
466
|
+
- app-review # was: app
|
|
467
|
+
- redis
|
|
468
|
+
- rails
|
|
469
|
+
additional_workloads:
|
|
470
|
+
- redis # postgres removed
|
|
471
|
+
hooks:
|
|
472
|
+
post_creation: bundle exec rails db:prepare
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
The `post_creation` hook creates only that review app's logical database because the database name is still
|
|
476
|
+
`{{APP_NAME}}`. Do not rely on a generic `pre_deletion: rails db:drop` hook for shared databases: `cpflow delete` runs
|
|
477
|
+
the pre-deletion hook before it removes or suspends the app workloads, so live Rails/worker processes can still hold
|
|
478
|
+
connections and make PostgreSQL reject the drop. Stop the review app workloads first, or run trusted admin cleanup
|
|
479
|
+
against the shared Postgres workload with `DROP DATABASE ... WITH (FORCE)`.
|
|
480
|
+
|
|
481
|
+
Rails apps with multiple production databases need each connection isolated. Either set connection-specific URLs such as
|
|
482
|
+
`CACHE_DATABASE_URL`, `QUEUE_DATABASE_URL`, and `CABLE_DATABASE_URL`, or make the database names in `config/database.yml`
|
|
483
|
+
derive from an app-specific environment variable. If the database names are hard-coded, every review app for that repo
|
|
484
|
+
will collide inside the shared Postgres instance.
|
|
485
|
+
|
|
486
|
+
Suggested cutover order:
|
|
487
|
+
|
|
488
|
+
1. Create the shared Postgres GVC, workload, and volume.
|
|
489
|
+
2. Create the app roles and logical databases, or make sure the app role has `CREATEDB` and let `rails db:prepare`
|
|
490
|
+
create them. For admin-created databases, generate the password in trusted automation, store it in the matching app DB
|
|
491
|
+
secret, then run the setup from an interactive `psql` session so the password is not written into shell history or
|
|
492
|
+
process arguments:
|
|
493
|
+
|
|
494
|
+
```sh
|
|
495
|
+
cpln workload exec postgres --org ORG --gvc staging-shared-postgres --stdin --tty -- psql -U postgres
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Then enter the SQL in `psql`:
|
|
499
|
+
|
|
500
|
+
```sql
|
|
501
|
+
CREATE ROLE "my-app-staging" LOGIN;
|
|
502
|
+
\password "my-app-staging"
|
|
503
|
+
CREATE DATABASE "my-app-staging" OWNER "my-app-staging";
|
|
504
|
+
\connect "my-app-staging"
|
|
505
|
+
GRANT ALL ON SCHEMA public TO "my-app-staging";
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
In CI, run equivalent SQL through a secret-aware step that does not echo the password, add it to process arguments, or
|
|
509
|
+
persist it in logs.
|
|
510
|
+
|
|
511
|
+
3. Update staging/review GVC environment values to the shared host.
|
|
512
|
+
4. Run `cpflow run -a APP -- bin/rails db:prepare` for each app.
|
|
513
|
+
5. Force redeploy app workloads so live replicas pick up the new GVC env.
|
|
514
|
+
6. Stop the old per-app Postgres workloads and smoke test the apps.
|
|
515
|
+
7. Delete the old Postgres workloads and volumes only after smoke tests pass.
|
|
516
|
+
8. When a review app is deleted, drop its logical database from the shared instance so orphaned review databases do not
|
|
517
|
+
accumulate. For the most reliable cleanup, stop the app workloads first, then run the drop from trusted admin
|
|
518
|
+
automation or directly against the shared Postgres workload. Use `WITH (FORCE)` on PostgreSQL 13+ to terminate
|
|
519
|
+
remaining sessions:
|
|
520
|
+
|
|
521
|
+
```sh
|
|
522
|
+
cpln workload exec postgres --org ORG --gvc staging-shared-postgres -- \
|
|
523
|
+
psql -U postgres -c 'DROP DATABASE IF EXISTS "my-app-review-pr-123" WITH (FORCE);'
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
`cpln workload exec` runs `psql` inside the container over its local Unix socket, which the official Postgres image
|
|
527
|
+
grants the `postgres` superuser `trust` auth — so no `PGPASSWORD` or `-W` flag is required here.
|
|
528
|
+
|
|
529
|
+
When updating URL-like env values, prefer applying a full GVC YAML update with `cpln apply`, then re-read the GVC env to
|
|
530
|
+
confirm the new reference took effect:
|
|
531
|
+
|
|
532
|
+
```sh
|
|
533
|
+
cpln apply -f my-app-review-pr-123-gvc.yaml
|
|
534
|
+
cpln gvc get my-app-review-pr-123 -o yaml | grep DATABASE_URL
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
`cpln gvc update --set` also works, but treat it as a known-fragile shortcut. Quote the entire `path=value` expression,
|
|
538
|
+
or the CLI can leave the old value in place while the command appears superficially successful. The `spec.env.NAME.value`
|
|
539
|
+
path relies on env-array lookup by name, so verify against your installed CLI version before relying on it:
|
|
540
|
+
|
|
541
|
+
```sh
|
|
542
|
+
cpln gvc update my-app-review-pr-123 \
|
|
543
|
+
--set 'spec.env.DATABASE_URL.value=cpln://secret/my-app-review-pr-123-database.DATABASE_URL'
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
A few tradeoffs remain even after the cost savings:
|
|
547
|
+
|
|
548
|
+
- **Noisy neighbor risk.** All staging/review apps share one server's CPU, RAM, disk, and connection pool. A runaway
|
|
549
|
+
query or connection leak in one app can affect the others; per-app connection caps or PgBouncer can help. Mind
|
|
550
|
+
Postgres's own `max_connections` (default 100): a staging seed running alongside several review apps, each at Rails'
|
|
551
|
+
default `pool: 5`, can exhaust it before any query runs. The official image ignores a `POSTGRES_MAX_CONNECTIONS` env
|
|
552
|
+
var; raise the server limit with a `-c max_connections=N` server argument or a custom `postgresql.conf`, and lower
|
|
553
|
+
`pool:` in `config/database.yml` for review apps as the simplest app-side lever.
|
|
554
|
+
- **Operational ownership.** Backups, restores, password rotation, sizing, and access control move to the shared server.
|
|
555
|
+
- **Other trusted services can use the same pattern.** Redis and Memcached can also be shared for trusted apps, but a
|
|
556
|
+
per-app key prefix or logical database index is only conventional separation when apps share credentials. If review
|
|
557
|
+
app code is not trusted, use enforced isolation such as per-app ACL users/credentials or separate instances.
|
|
558
|
+
|
|
559
|
+
### Enable Capacity AI for Demo and Starter Staging Apps
|
|
560
|
+
|
|
561
|
+
`templates/rails.yml` ships with CPU autoscaling pinned to one replica (`minScale: 1`, `maxScale: 1`) and
|
|
562
|
+
`capacityAI: false`. That's a conservative production-safe default, but for public demos, starter staging apps, and
|
|
563
|
+
long-lived review apps, Capacity AI can right-size CPU and memory allocation while keeping the same warm replica count.
|
|
564
|
+
For these non-production apps, keep the Rails workload as `type: standard`, disable the explicit autoscaling metric,
|
|
565
|
+
and enable Capacity AI. Apply the snippet below to your project's `.controlplane/templates/rails.yml`, or create an
|
|
566
|
+
environment-specific template (for example `rails-review.yml` or `rails-demo-staging.yml`) and list it under
|
|
567
|
+
`setup_app_templates` for the matching app entry in `.controlplane/controlplane.yml`.
|
|
568
|
+
|
|
569
|
+
```yaml
|
|
570
|
+
# Only `autoscaling.metric` and `capacityAI` change from templates/rails.yml.
|
|
571
|
+
# `type: standard` is shown here to confirm this is not a serverless migration.
|
|
572
|
+
# Keep containers, firewallConfig, identityLink, and everything else from the template intact.
|
|
573
|
+
kind: workload
|
|
574
|
+
name: rails
|
|
575
|
+
spec:
|
|
576
|
+
type: standard
|
|
577
|
+
defaultOptions:
|
|
578
|
+
autoscaling:
|
|
579
|
+
minScale: 1
|
|
580
|
+
maxScale: 1
|
|
581
|
+
metric: disabled
|
|
582
|
+
capacityAI: true
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
See [`templates/rails.yml`](https://github.com/shakacode/control-plane-flow/blob/main/templates/rails.yml) for the full default — `containers`, `firewallConfig`,
|
|
586
|
+
`identityLink`, and the other required fields must be preserved when you copy the snippet above.
|
|
587
|
+
|
|
588
|
+
This is not the same as scale-to-zero. Capacity AI can reduce over-allocation for mostly idle demos, but it will not
|
|
589
|
+
make costs approach zero when a workload has steady RAM usage or background load. Expect it to settle over several
|
|
590
|
+
hours, and treat memory sizing as a separate cost lever.
|
|
591
|
+
|
|
592
|
+
Shared Postgres is the usual exception: keep shared databases manually sized rather than enabling Capacity AI
|
|
593
|
+
indiscriminately. Apply this guidance to stateless app/service workloads first (Rails, renderers, workers, and similar
|
|
594
|
+
staging-only services). Stateful workloads are not supported by Capacity AI, so keep stateful Redis, Elasticsearch,
|
|
595
|
+
Mongo, and similar support services manually sized unless you intentionally deploy them as supported stateless
|
|
596
|
+
workloads.
|
|
279
597
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
scales to zero replicas when idle. Apply the snippet below to your project's `.controlplane/templates/rails.yml`, or
|
|
283
|
-
create a review-app-specific template (for example `rails-review.yml`) and list it under `setup_app_templates` for the
|
|
284
|
-
review-app entry in `.controlplane/controlplane.yml`.
|
|
598
|
+
If you intentionally need true idle scale-to-zero, use a separate `type: serverless` workload with `minScale: 0` and
|
|
599
|
+
an HTTP wake-up autoscaling metric such as `rps` or `concurrency`:
|
|
285
600
|
|
|
286
601
|
```yaml
|
|
287
|
-
# Only `type` and `minScale` change from templates/rails.yml; `maxScale`, `capacityAI` and `timeoutSeconds`
|
|
288
|
-
# are shown for context so the full `defaultOptions` block reaches the destination intact.
|
|
289
|
-
# Update the relevant fields in your full templates/rails.yml (or a review-app-specific template); keep
|
|
290
|
-
# containers, firewallConfig, identityLink, and everything else from that file intact.
|
|
291
602
|
kind: workload
|
|
292
603
|
name: rails
|
|
293
604
|
spec:
|
|
@@ -296,19 +607,32 @@ spec:
|
|
|
296
607
|
autoscaling:
|
|
297
608
|
minScale: 0
|
|
298
609
|
maxScale: 1
|
|
299
|
-
|
|
300
|
-
|
|
610
|
+
metric: rps
|
|
611
|
+
target: 1
|
|
301
612
|
```
|
|
302
613
|
|
|
303
|
-
|
|
304
|
-
|
|
614
|
+
Existing `type: standard` workloads cannot change to `serverless` in place; that requires a planned delete/recreate
|
|
615
|
+
migration and can interrupt traffic.
|
|
616
|
+
|
|
617
|
+
> **Warning:** Treat a `standard` to `serverless` conversion as an operational migration because deleting a running
|
|
618
|
+
> workload can interrupt traffic.
|
|
305
619
|
|
|
306
|
-
|
|
307
|
-
`type: standard` always keeps at least one replica running.
|
|
620
|
+
### Use an Always-Available Landing Page for a Serverless App
|
|
308
621
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
622
|
+
For a public demo, review app, or staging app where the first request's cold start would be a poor first impression,
|
|
623
|
+
serve a lightweight landing page independently of the app that scales to zero. Route visitors to that landing page
|
|
624
|
+
first, then have a button on that page request the separate serverless app. The page can immediately explain that the
|
|
625
|
+
app is starting while the serverless workload wakes; it does not remove the cold start, but it keeps that wait out of
|
|
626
|
+
the initial page render.
|
|
627
|
+
|
|
628
|
+
```text
|
|
629
|
+
always-available landing page -> Open app request -> serverless app (minScale: 0) -> cold start -> app response
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
The landing page, its hosting, and any DNS, proxy, rewrite, or redirect rules are application infrastructure you
|
|
633
|
+
choose and operate. `cpflow` does not create that routing infrastructure. Keep the app as a separate serverless
|
|
634
|
+
workload from its first deployment (or perform the planned delete/recreate migration above); it cannot convert an
|
|
635
|
+
existing standard workload in place. The wake-up path also requires the HTTP autoscaling configuration shown above.
|
|
312
636
|
|
|
313
637
|
> **Note:** if you later suspend the app with `cpflow ps:stop`, Control Plane will not auto-wake it on the next
|
|
314
638
|
> request. Run `cpflow ps:start` explicitly first. See
|
|
@@ -390,7 +714,130 @@ No re-deploy is needed; the workloads come back with the same images they had be
|
|
|
390
714
|
> keep running while only the web tier sleeps. `cpflow ps:stop -a $APP_NAME` suspends every configured workload, web
|
|
391
715
|
> included, and `cleanup-stale-apps --mode=stop` applies the same pause behavior to stale review apps.
|
|
392
716
|
|
|
717
|
+
## Right-Sizing Non-Production Workloads
|
|
718
|
+
|
|
719
|
+
[Minimizing Non-Production App Costs](#minimizing-non-production-app-costs) above focuses on review-app lifecycle
|
|
720
|
+
controls: scale-to-zero, explicit pauses, and stale app cleanup. Long-lived staging and demo apps are the other common
|
|
721
|
+
source of avoidable Control Plane spend: they tend to keep generously-sized workloads running full-time. The levers
|
|
722
|
+
below apply to any non-production environment (staging, demos, and review apps alike).
|
|
723
|
+
|
|
724
|
+
### Enable Capacity AI on Idle Workloads
|
|
725
|
+
|
|
726
|
+
Control Plane bills the CPU and memory a running replica *reserves*. With `minScale: 1` and
|
|
727
|
+
Capacity AI off, a workload reserves its full `cpu`/`memory` around the clock, even when the
|
|
728
|
+
app is idle. **Capacity AI** lets Control Plane right-size that reservation toward actual
|
|
729
|
+
usage, so an idle non-production workload costs a fraction of its ceiling.
|
|
730
|
+
|
|
731
|
+
Set it in `defaultOptions`:
|
|
732
|
+
|
|
733
|
+
```yaml
|
|
734
|
+
kind: workload
|
|
735
|
+
name: rails
|
|
736
|
+
spec:
|
|
737
|
+
defaultOptions:
|
|
738
|
+
capacityAI: true
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
Also disable CPU-utilization autoscaling for idle non-production workloads; the
|
|
742
|
+
next section shows the complete `capacityAI` and autoscaling shape together.
|
|
743
|
+
|
|
744
|
+
Tradeoff: Control Plane reprovisions the replica when it adjusts the reservation. For
|
|
745
|
+
stateless web/renderer workloads that's negligible. For stateful workloads, see the
|
|
746
|
+
[guidance above](#enable-capacity-ai-for-demo-and-starter-staging-apps) — Postgres,
|
|
747
|
+
Redis, Elasticsearch, Mongo, and similar services should remain manually sized.
|
|
748
|
+
|
|
749
|
+
### Don't Autoscale Idle Workloads on CPU
|
|
750
|
+
|
|
751
|
+
CPU-utilization autoscaling adds nothing for an idle non-production app and works against
|
|
752
|
+
Capacity AI. Disable it and let Capacity AI handle right-sizing:
|
|
753
|
+
|
|
754
|
+
```yaml
|
|
755
|
+
kind: workload
|
|
756
|
+
name: rails
|
|
757
|
+
spec:
|
|
758
|
+
defaultOptions:
|
|
759
|
+
capacityAI: true
|
|
760
|
+
autoscaling:
|
|
761
|
+
metric: disabled
|
|
762
|
+
minScale: 1
|
|
763
|
+
maxScale: 1
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
(For the web tier you can go further and scale to zero — see
|
|
767
|
+
[Enable Capacity AI for Demo and Starter Staging Apps](#enable-capacity-ai-for-demo-and-starter-staging-apps).)
|
|
768
|
+
|
|
769
|
+
### Right-Size Reserved CPU and Memory
|
|
770
|
+
|
|
771
|
+
The shipped templates use production-leaning defaults. Check each workload's reserved
|
|
772
|
+
`cpu`/`memory` against its real usage — the workload's **Metrics** tab in Control Plane
|
|
773
|
+
shows Grafana CPU/memory graphs — because non-production workloads are routinely
|
|
774
|
+
over-provisioned.
|
|
775
|
+
|
|
776
|
+
Postgres is the usual offender: a demo or staging database does **not** need a full core.
|
|
777
|
+
Pinning `cpu: 1000m` keeps a whole reserved CPU running 24/7, while an idle Postgres
|
|
778
|
+
typically sits at single-digit millicores. Something like `cpu: 250m` / `memory: 512Mi`
|
|
779
|
+
is a field-tested non-production starting point; raise memory toward `1Gi` if the
|
|
780
|
+
workload's Metrics tab shows pressure during seeds, imports, or larger staging datasets.
|
|
781
|
+
|
|
782
|
+
```yaml
|
|
783
|
+
kind: workload
|
|
784
|
+
name: postgres
|
|
785
|
+
spec:
|
|
786
|
+
containers:
|
|
787
|
+
- name: postgres
|
|
788
|
+
cpu: 250m
|
|
789
|
+
memory: 512Mi
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
### Drop Workloads You Don't Use
|
|
793
|
+
|
|
794
|
+
Every workload listed under `app_workloads` / `additional_workloads` is another full-time
|
|
795
|
+
container. Remove the ones a non-production app doesn't actually need.
|
|
796
|
+
|
|
797
|
+
A common one is a separate background-job worker when the app has no jobs to run. On Rails
|
|
798
|
+
8, [Solid Queue](https://github.com/rails/solid_queue) can run inside Puma instead of as its
|
|
799
|
+
own workload — set `SOLID_QUEUE_IN_PUMA=true` when the app uses the Rails 8 default
|
|
800
|
+
`config/puma.rb`, or add `plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]` manually for
|
|
801
|
+
apps upgraded from Rails 7. Then drop the `worker` workload from `app_workloads` and
|
|
802
|
+
`setup_app_templates` in `.controlplane/controlplane.yml`, and delete its template.
|
|
803
|
+
Solid Queue is database-backed, so job processing needs no Redis; if your app uses Redis for caching or Action Cable,
|
|
804
|
+
keep that workload.
|
|
805
|
+
|
|
806
|
+
### Share One Postgres Across Non-Production Apps
|
|
807
|
+
|
|
808
|
+
Running a dedicated Postgres workload — and its SSD volume — for every staging and review
|
|
809
|
+
app multiplies standing cost. For non-production, several apps can share a single Postgres
|
|
810
|
+
server, each using its own database:
|
|
811
|
+
|
|
812
|
+
- Point each app's `DATABASE_URL` environment variable (in `.controlplane/templates/`) at the shared instance — for
|
|
813
|
+
example `postgres://user:pass@postgres.staging-shared-postgres.cpln.local:5432/my_app_staging` — and give each app a
|
|
814
|
+
distinct database name in the path.
|
|
815
|
+
- Set `inboundAllowType` to the narrowest scope that covers your use case — `workload-list` gives the tightest blast
|
|
816
|
+
radius when you can keep an explicit per-workload allowlist current, while `same-org` is the practical default when
|
|
817
|
+
client apps are too dynamic to maintain manually. `same-gvc` only works when the shared Postgres and every client app
|
|
818
|
+
live in the same GVC, which is not the cross-GVC setup described in
|
|
819
|
+
[Share One Control Plane Postgres for Staging and Review Apps](#share-one-control-plane-postgres-for-staging-and-review-apps).
|
|
820
|
+
Overly broad allow-types expand the attack surface, especially when review apps can run untrusted PR code.
|
|
821
|
+
- Store shared database credentials in Control Plane secrets for long-lived staging and demos; plaintext
|
|
822
|
+
`DATABASE_URL` values are only reasonable for disposable non-production experiments.
|
|
823
|
+
- Prefer per-app database roles over a shared superuser or broad `CREATEDB` role, especially when review apps can run
|
|
824
|
+
untrusted PR code.
|
|
825
|
+
|
|
826
|
+
A managed alternative is a single small RDS instance hosting many databases; see
|
|
827
|
+
[Hetzner RDS Postgres](https://pelle.io/posts/hetzner-rds-postgres).
|
|
828
|
+
|
|
829
|
+
### Keep Templates as the Source of Truth
|
|
830
|
+
|
|
831
|
+
It's tempting to tune `cpu`, `capacityAI`, or autoscaling directly in the Control Plane UI.
|
|
832
|
+
Don't: `cpflow apply-template` reconciles workloads from your `.controlplane/templates/`, so console edits are
|
|
833
|
+
overwritten when it runs next; non-interactive CI runs with `--yes` do that silently, while interactive runs prompt
|
|
834
|
+
before re-creating existing workloads. Make cost changes in the templates and deploy them.
|
|
835
|
+
|
|
836
|
+
If you want drift caught automatically, manage long-lived environments with Terraform via
|
|
837
|
+
[`cpflow terraform`](/docs/terraform/overview.md) — `terraform plan` reports any difference
|
|
838
|
+
between the repo and live infrastructure before you apply.
|
|
839
|
+
|
|
393
840
|
## Useful Links
|
|
394
841
|
|
|
395
842
|
- For best practices for the app's Dockerfile, see: https://lipanski.com/posts/dockerfile-ruby-best-practices
|
|
396
|
-
- For
|
|
843
|
+
- For Hetzner RDS Postgres, see: https://pelle.io/posts/hetzner-rds-postgres
|
data/examples/controlplane.yml
CHANGED
|
@@ -135,6 +135,8 @@ apps:
|
|
|
135
135
|
post_creation: bundle exec rake db:prepare
|
|
136
136
|
|
|
137
137
|
# Used by the command `cpflow delete` to run a hook before deleting the app.
|
|
138
|
+
# For a shared database, prefer admin-side cleanup instead: `cpflow delete` runs this hook before removing the
|
|
139
|
+
# workloads, so live connections can block the drop. See docs/tips.md ("Share One Control Plane Postgres").
|
|
138
140
|
pre_deletion: bundle exec rake db:drop
|
|
139
141
|
|
|
140
142
|
my-app-production:
|
|
@@ -30,9 +30,9 @@ module Command
|
|
|
30
30
|
|
|
31
31
|
def prompt
|
|
32
32
|
<<~PROMPT
|
|
33
|
-
Set up Control Plane GitHub Flow for this repo. Start with `cpflow github-flow-readiness` and stop on any reported blockers. The repo must be deployable from a clean clone: published package versions, complete runtime scaffold, and a production Dockerfile that can build the app. If any package version is unpublished, inaccessible from CI, or requires credentials that are not already modeled in the repo or GitHub settings, stop and report the blocker instead of generating workflow files. If the repo is a legacy sample pinned to an obsolete Ruby or Bundler toolchain, if it does not even have a production Dockerfile yet, or if it is a monorepo without an already-decided single app boundary for this flow, stop and report that as a prerequisite instead of forcing the rollout.
|
|
33
|
+
Set up Control Plane GitHub Flow for this repo. First make sure the `cpflow` CLI is available: use the repo's existing `bundle exec cpflow` if present, otherwise install the published `cpflow` Ruby gem with `gem install cpflow`; if neither is possible, stop and report that blocker. Use the same `cpflow` invocation for the rest of the rollout. Start with `cpflow github-flow-readiness` and stop on any reported blockers. The repo must be deployable from a clean clone: published package versions, complete runtime scaffold, and a production Dockerfile that can build the app. If any package version is unpublished, inaccessible from CI, or requires credentials that are not already modeled in the repo or GitHub settings, stop and report the blocker instead of generating workflow files. If the repo is a legacy sample pinned to an obsolete Ruby or Bundler toolchain, if it does not even have a production Dockerfile yet, or if it is a monorepo without an already-decided single app boundary for this flow, stop and report that as a prerequisite instead of forcing the rollout.
|
|
34
34
|
|
|
35
|
-
If `.controlplane/` is missing, run `cpflow generate`. Treat the generated app names as the repo-name default (`#{inferred_app_prefix}`) and rename them only if the project needs a different prefix. Then run `cpflow generate-github-actions` (or `cpflow generate-github-actions --staging-branch BRANCH` when staging should deploy from a branch other than `main`/`master`), keep review apps opt-in via `+review-app-deploy`, make sure any `STAGING_APP_BRANCH` repository variable is also present in the generated staging workflow's `on.push.branches` filter, and list the GitHub secrets and variables that must be configured. Do not hand-edit duplicated upstream refs into the generated wrappers: the only downstream Control Plane Flow pin should be the reusable workflow `uses: ...@vX.Y.Z` value generated from the installed `cpflow` gem version, and upstream workflows load their matching shared actions automatically. When bumping the `cpflow` gem in a downstream repo, run `cpflow update-github-actions` (or `bundle exec cpflow update-github-actions`) and validate with `bin/test-cpflow-github-flow` in the same PR so the checked-in wrappers move to the matching release tag. Keep the
|
|
35
|
+
If `.controlplane/` is missing, run `cpflow generate`. Treat the generated app names as the repo-name default (`#{inferred_app_prefix}`) and rename them only if the project needs a different prefix. Then run `cpflow generate-github-actions` (or `cpflow generate-github-actions --staging-branch BRANCH` when staging should deploy from a branch other than `main`/`master`), keep review apps opt-in via `+review-app-deploy`, make sure any `STAGING_APP_BRANCH` repository variable is also present in the generated staging workflow's `on.push.branches` filter, and list the GitHub secrets and variables that must be configured. Do not hand-edit duplicated upstream refs into the generated wrappers: the only downstream Control Plane Flow pin should be the reusable workflow `uses: ...@vX.Y.Z` value generated from the installed `cpflow` gem version, and upstream workflows load their matching shared actions automatically. When bumping the `cpflow` gem in a downstream repo, run `cpflow update-github-actions` (or `bundle exec cpflow update-github-actions`) and validate with `bin/test-cpflow-github-flow` in the same PR so the checked-in wrappers move to the matching release tag. Keep the normal generated review-app setup simple: review apps require only `CPLN_TOKEN_STAGING` when the generated review app config can be inferred. For public demos, starter staging apps, and long-lived review apps, keep the app workload `type: standard` with one warm replica, set its autoscaling metric to `disabled`, and enable `capacityAI: true` so Control Plane can right-size CPU and memory allocation at that fixed replica count. Shared Postgres and other stateful workloads are the usual exceptions and should stay manually sized; Capacity AI is for supported stateless app/service workloads. If true idle scale-to-zero is explicitly required, create a separate `serverless` workload before first deploy or plan a delete/recreate migration because Control Plane will not change an existing `standard` workload to `serverless` in place. For shared review-app resources such as one staging database, use `shared_secret_grants` and `{{SHARED_SECRET_DATABASE}}` placeholders instead of hardcoding the base app secret name; this keeps review-app policy binding and cleanup automatic while avoiding per-PR database cost. Document the one-time Control Plane bootstrap command for persistent staging and production apps with `cpflow setup-app --skip-post-creation-hook`; for existing apps or later template updates, document `cpflow apply-template` and the need for the app identity to have `reveal` on the app secret policy. Do not imply the staging deploy or promotion workflows create those persistent GVCs. For production promotion, document a protected `production` GitHub Environment with required reviewers, prevent self-review, and `CPLN_TOKEN_PRODUCTION` stored as an environment secret, not as a repository or organization secret.
|
|
36
36
|
|
|
37
37
|
Keep Node available in the final image if asset compilation or SSR depends on ExecJS, Yarn, `pnpm`, or npm after the main install layer. Make sure the generated Dockerfile uses a Ruby base image compatible with the app's declared Ruby requirement. Preserve repo-defined frontend build hooks: if `config/shakapacker.yml` defines a `precompile_hook`, or React on Rails enables `config.auto_load_bundle = true`, confirm the generated Dockerfile runs that codegen step before `rails assets:precompile`. If `config/database.yml` shows SQLite in production, confirm that the generated scaffold uses persistent `db` and `storage` volumes plus a release script that runs `rails db:prepare`; otherwise keep the default Postgres workload. If the public workload is not named `rails`, set `PRIMARY_WORKLOAD` or adjust the generated workflows. Inspect the Dockerfile and package sources for private GitHub dependencies or `RUN --mount=type=ssh`; if present, wire `DOCKER_BUILD_SSH_KEY`, optionally set `DOCKER_BUILD_SSH_KNOWN_HOSTS` for non-GitHub SSH hosts, and keep `DOCKER_BUILD_EXTRA_ARGS` to newline-delimited single tokens such as `--build-arg=FOO=bar`.
|
|
38
38
|
|