---
name: coolify-architect
description: Design, adapt, audit, troubleshoot, and production-harden Docker Compose stacks for deployment on Coolify, from minimal few-service applications to complex multi-service systems with databases, queues, workers, reverse proxies, callbacks, persistent storage, generated secrets, health checks, and multiple public domains. Use when converting an upstream self-hosted project to a Coolify service/template or reviewing an existing Coolify Compose. Do not use for generic Docker tutorials unrelated to Coolify.
license: MIT
---

# coolify-architect

Build Coolify Compose templates by preserving the upstream application's real architecture, then making the smallest justified Coolify-specific changes. Never generate a plausible-looking stack from memory when upstream deployment material exists.

## Use when

Use this skill for any of these modes:

- **ADAPT** — convert an upstream Docker/Compose/self-hosted deployment to Coolify.
- **CREATE** — build a Coolify Compose when upstream has no Compose, using Dockerfiles, entrypoints, docs, environment examples, Helm manifests, or other authoritative deployment sources.
- **AUDIT** — review an existing Coolify Compose for correctness, portability, persistence, security, readiness, and maintainability.
- **TROUBLESHOOT** — diagnose a deployed stack that starts but has broken routing, authentication, callbacks, previews, workers, storage, or public endpoints.
- **CLEAN** — remove historical debugging noise and normalize comments without changing executable behavior.

## Do not use when

Do not use this skill for:

- generic Docker or Docker Compose explanations with no Coolify requirement;
- Kubernetes-only deployment work unless the user explicitly wants a Compose translation;
- destructive production operations unless the user explicitly asks and the consequences are clear;
- blindly copying any bundled golden/regression fixture into an unrelated application.

## Core principle

Treat the upstream deployment as the baseline and Coolify as the hosting adaptation layer.

The order is:

```text
CURRENT UPSTREAM ARCHITECTURE
    -> CURRENT APPLICATION REQUIREMENTS
    -> CURRENT COOLIFY CONVENTIONS
    -> VALIDATED GENERAL KNOWLEDGE
    -> COMPOSE CANDIDATE
    -> STATIC + RUNTIME EVIDENCE GATES
```

Do not reverse this order.

## Source priority

Before editing or creating Compose, inspect the most authoritative available sources in this order:

1. current upstream repository and release/tag intended for deployment;
2. upstream self-hosting documentation;
3. upstream `compose.yml` / `docker-compose.yml` / Dockerfiles / entrypoints;
4. upstream `.env.example`, config templates, migrations, health endpoints, worker commands, and reverse-proxy configuration;
5. current official Coolify Docker Compose documentation;
6. current official Coolify template repository for comparable patterns;
7. issue trackers or community reports only for unresolved behavior, never as the primary source of truth.

When currentness matters, verify the latest Coolify behavior and upstream release rather than relying on remembered syntax.

Read `references/source-priority.md` before performing research-heavy adaptations. Classify evidence using `references/source-priority.md`: upstream fact, official Coolify fact, direct runtime observation, operator-confirmed runtime result, cross-benchmark pattern, application-specific adaptation/workaround, or inference/REVIEW REQUIRED. Current upstream/Coolify facts take precedence over Golden-case habits. When repository Compose mounts conflict with what the selected image already contains, inspect the Dockerfile/image and distinguish deployment-repository composition from runtime image requirements before copying artifacts.

## Official Coolify template corpus gate

Before finalizing an ADAPT or CREATE result, inspect current official templates in `coollabsio/coolify/templates/compose` and select comparable examples by **architecture**, not by product category. Read `references/official-coolify-template-corpus.md`.

Use the corpus to learn current Coolify conventions for `SERVICE_URL_*`, `SERVICE_FQDN_*`, generated credentials, health checks, persistence, path routing, workers, and readiness. Do not copy a pattern only because it is common: upstream behavior remains authoritative.

For a public service, prefer the official `SERVICE_URL_<SERVICE>_<PORT>` convention when it matches the deployment. Introduce `SERVICE_FQDN_*` only when the application requires a hostname value. **Do not assume a port-suffixed `SERVICE_FQDN_<SERVICE>_<PORT>` is host-only:** current Coolify semantics can include the routing port. When an application needs a canonical public origin without that internal port, model the canonical identity separately and verify the exact generated value before wiring it. If multiple processes intentionally share one public URL at different paths, evaluate Coolify-native path routing before creating a custom reverse proxy.

## Mandatory discovery gate

Do not write the final Compose until you can produce all five maps below.

### 1. Service map

Classify every process as one of:

- public edge / gateway;
- application/API;
- frontend;
- worker;
- scheduler/beat/cron;
- database;
- cache/broker;
- object storage;
- one-time initialization/migration;
- optional integration.

Record image/build source, command/entrypoint, internal port, dependencies, whether it must be public, and the current-upstream provenance or explicit Coolify-operational reason for the process.

### 2. URL and routing map

For every externally visible component distinguish:

- **public browser URL** — canonical HTTPS URL users and external systems see;
- **internal service URL** — Docker service-to-service address such as `http://api:8000`;
- **canonical callback URL** — public URL that another container may itself need to call;
- **Coolify proxy target** — service + internal port that Coolify routes to.

Never assume those four concepts are interchangeable.

### 3. Persistence map

For every stateful path/database record:

- path or database name;
- named volume or bind mount;
- whether data is authoritative, in-flight/durability-sensitive, queue/session, reconstructable, cache-only, or ephemeral;
- backup priority;
- migration/upgrade implications.

A normal redeploy must not destroy authoritative user data. Identify coherence groups when multiple databases/filesystems/Redis or other stores must be captured/restored as one logical application state; inspect important volume subpaths rather than assigning one durability class to the whole volume.

### 4. Secret/configuration map

Separate:

- persistent secrets/credentials;
- encryption/signing keys;
- generated users/passwords;
- ordinary configuration;
- public URLs/FQDNs;
- optional integrations.

Do not rotate database, encryption, session, or application secrets merely because a Compose file was regenerated. Classify identity, credential, role, issuer/origin, encoding, persistence, rotation **and transport/serialization path** first: platform-generated deployment secrets, application-issued credentials, operator-provided identities/secrets and external-provider-issued credentials are not interchangeable. A generator is suitable only when its output survives the actual `.env`/Compose/shell path unchanged while satisfying the application contract.

### 5. Startup/readiness graph

Document:

- what must exist before each service starts;
- what must be merely running versus actually ready;
- migrations/bootstrap behavior;
- worker dependencies;
- expected first-start duration.

Prefer real health checks and readiness conditions over arbitrary sleeps. Remember that Compose dependency conditions are **startup gates, not continuous orchestration**. For long-running workers or schedulers that must survive a slow application bootstrap, use upstream-supported application-level retry/readiness behavior when necessary instead of assuming a failed initial `depends_on: service_healthy` gate will later reconcile itself.

Use `references/architecture-discovery.md` for the detailed worksheet. When applicable it also requires an **External Dependency Map**, **Browser Origin / CORS Map**, **Secret Origin Map**, **Migration Ownership Map**, **Persistent Store Inventory**, **Build Reproducibility / Dependency Closure Map**, **Managed File Provenance Ledger**, **Interpolation / Serialization Layer Map**, **Credential Topology Map**, **Host Orchestration vs Runtime Responsibility Map**, and **Deployment Profile Capability Matrix**. Internal process topology is not automatically Compose topology; select the exact edition/profile before importing features. When upstream supports multiple legitimate deployment profiles, profile selection precedes topology normalization.

## Complexity budget / provenance gate

**Complexity must be inherited from the current upstream architecture, not from previous golden cases.**

Before finalizing an ADAPT or CREATE candidate, compare the candidate service/capability graph with the current upstream baseline. For every meaningful capability that is added, removed, split, merged, or replaced, record the cause:

- preserved directly from current upstream;
- replaced by a Coolify platform responsibility such as external TLS/routing;
- added because current upstream behavior or a documented operational requirement needs it; or
- intentionally omitted because it is optional for the selected deployment profile.

Every service, proxy, sidecar, init job, volume, custom network, workaround, or embedded script needs upstream, current Coolify, or demonstrated-runtime provenance. Missing provenance is **REVIEW REQUIRED**, not automatically ERROR. A previous Golden is never justification by itself; do not score minimality by service count. Repository config/release metadata/published artifacts can disagree: verify the actual artifact and full-stack architecture intersection rather than inventing a tag or inferring support from dependencies. Preserve image-native configuration/migration/bootstrap primitives until a proven gap requires replacement.

## Coolify adaptation rules

Read `references/coolify-rules.md` before generating the final file.

Apply these defaults unless upstream requirements justify an exception:

1. The Compose file is the source of truth for service definitions, mounts, commands, health checks, and environment wiring.
2. Prefer Coolify-managed/default networking. Do not invent a custom network unless upstream behavior requires it and proxy connectivity has been verified.
3. Use Coolify-native domain routing instead of hand-written Traefik labels when native routing is sufficient.
4. Use `SERVICE_URL_<SERVICE>[_PORT]` / `SERVICE_FQDN_<SERVICE>[_PORT]` only when their generated-domain behavior matches the application's needs.
5. Use stable custom canonical domains when the application stores, signs, shares, or calls its own public URLs and generated resource hostnames would be unsafe or unstable for that role.
6. Keep databases, Redis, MongoDB, queues, and other internal stores off public host ports unless the user explicitly requires public access.
7. Prefer `expose` or no published port for internal-only services.
8. Use only currently documented Coolify magic families when their generated **format/encoding** satisfies upstream requirements. Distinguish usernames, no-symbol passwords, symbol passwords, true Base64 (`SERVICE_REALBASE64_*`), hex, URLs and FQDNs; `SERVICE_BASE64_*` is not true Base64. Prefer explicitly sized families when exact length matters and re-check current docs before depending on base-family length.
9. One logical shared credential gets one **exact complete magic-variable identity** across every producer/consumer. Never generate a second lookalike password for the same DB/user/key.
10. Generated persistent credentials are deployment state: verify they are non-empty before first stateful bootstrap and do not rename/rotate their identifiers casually after initialization.
11. Pin production image versions or digests when upstream provides stable releases. Do not silently replace pins with `latest`.
12. Preserve upstream entrypoints, container/process boundaries and selected edition/profile unless there is a documented reason to change them; a Toolkit or internal microservice list is not permission to add services.
13. Make initialization idempotent. Re-running a deployment must not recreate users/databases destructively or corrupt existing state.
14. Do not add one-shot init sidecars merely for aesthetic separation if the upstream/native initialization path is simpler and already reliable.
15. Use `depends_on` readiness conditions where supported and meaningful, but do not treat dependency order as a substitute for application-level retry/readiness behavior.
16. Preserve proxy headers (`Host`, `X-Forwarded-*`, scheme) required by the application.
17. Coolify terminates public TLS in normal deployments; do not duplicate TLS termination inside the application unless upstream requires end-to-end TLS.
18. For Coolify-managed bind mounts using inline `content:`, distinguish **generated file content** from Compose command-string interpolation. Do not add `$$` escaping to shell variables inside the generated file merely because `$$` is needed in some `command:`/`healthcheck:` fields.
19. When an inline `content:` bind is intended to be a file, make its file semantics unambiguous (`is_directory: false` where supported/needed) and verify that the deployment host did not create a directory at the source path.
20. If an upstream entrypoint sources hook files, do not let a hook casually mutate parent-shell options (`set -u`, traps, `cd`, etc.). Inspect whether hooks are sourced or executed before adding shell prologues.
21. Treat every candidate service/capability not traceable to the selected upstream topology as requiring explicit justification. Do not add infrastructure merely to make the Compose look more production-grade. Include platform-derived `SERVICE_*`/labels/DNS from service names in effective-config review when they can reach the application. Do not normalize `localhost` and `127.0.0.1` in HTTP probes without checking Host-header/allowed-host semantics.
22. For source-built services, separate the application source pin from the **transitive build graph**. A pinned tag/commit is not proof of a hermetic build when Dockerfiles/scripts still resolve mutable Git branches, package repositories, curl targets, or unlocked dependencies.
23. For a local-only `image:` + `build:` service, verify the current Coolify pull/build sequence. If the target Coolify demonstrably pre-pulls before building, `pull_policy: never` can be a version-sensitive fix on those exact services; it is not a universal Compose rule.
24. Treat critical Compose primitives as layered support questions: Compose specification -> Coolify parser -> Coolify persistence/model -> actual deployment. Documentation-level support alone is insufficient for PASS when the selected path has not been proven.
25. For managed files, distinguish declared content, Coolify managed-resource identity, host file, container-mounted file, consumer-loaded file, and effective runtime behavior. If a change is not reflected at runtime, verify provenance before changing the application again.
26. Dollar escaping is transport-specific. Build the actual Compose -> Coolify -> shell/envsubst -> application-parser path before deciding whether source text needs `$`, `$$`, or another representation. When `envsubst` is necessary for a config language that also owns `$variables`, prefer an explicit variable whitelist.
27. Normalize reverse-proxy headers to the actual canonical external origin; an internal gateway listener port must not become the browser-visible public port unless it genuinely belongs to the public origin.

## Magic-variable pre-deploy gate

Before a first stateful deployment, read `references/coolify-rules.md` and verify current Coolify docs. Build a Magic Variable Ledger: complete variable, longest-match type, identifier, producer/consumers, purpose/format, required/persistent/public status, and parser ambiguity. Treat blank/malformed required generated credentials as a stop condition before bootstrap; prefer `${VAR:?message}` for critical generated values.

Credential/random IDs and URL/FQDN service IDs are different grammars. Frappe runtime demonstrated that `SERVICE_PASSWORD_64_FRAPPE_DB_ROOT` could remain blank while `SERVICE_PASSWORD_64_FRAPPEDBROOT` generated correctly in Docker Compose Empty, but current docs do not establish a timeless underscore ban for every `SERVICE_*`. Use conservative alphanumeric credential IDs by default, parse compound types longest-first, and bind URL/FQDN IDs to the real Compose service/port semantics.

## Public callback and hairpin rule

First classify the edge: browser→public URL, external system→public endpoint, service→Docker service, service→public URL, callback→public endpoint, or service→host proxy. Internal Docker DNS does not replace canonical public semantics when the application actually requires them.

When a container must call its canonical public HTTPS URL:

1. confirm the application truly needs the canonical public URL rather than an internal endpoint;
2. test whether that URL can return through the Coolify proxy from containers;
3. if external hairpin routing is unreliable, use a deliberate loopback solution such as Docker `host-gateway` only for the affected canonical hostnames;
4. keep TLS termination and hostname semantics intact;
5. document why the mapping exists;
6. do not generalize `host-gateway` to applications that do not need it.

Read `references/networking-and-domains.md`.

## Reverse-proxy rule

Distinguish the **platform edge proxy** from an **application-semantic gateway**. Coolify can own Internet routing/TLS while an upstream Nginx/gateway still owns assets, protected files, application routing, headers, websocket/realtime or tenant/site semantics. Do not delete it merely because Coolify has a proxy.

If upstream has different public surfaces with different route semantics, keep them separate.

Do not collapse multiple public gateways merely because they eventually reach the same backend. A root redirect, API route, OpenRosa-style endpoint, webhook surface, websocket path, or form renderer can require distinct host/path behavior.

Model public origin explicitly as scheme + host + browser-visible port. Keep that separate from Coolify's proxy target port, any internal semantic-gateway port, and the application listener port.

Do not replace explicit route allowlists with unconditional redirects unless upstream semantics prove that safe.

## Initialization and migrations

Initialization must satisfy all of these:

- safe on a fresh install;
- safe on a normal redeploy;
- does not require deleting volumes to become healthy;
- detects existing users/databases/indexes/extensions before creating them where practical;
- preserves stable identifiers and ownership when migrating existing state;
- hands control back to the official upstream entrypoint whenever possible;
- accounts for first-bootstrap-only init directories: a database engine can restart healthy while earlier application roles/databases/scripts were never completed;
- preserves existing account identity/credentials on normal redeploy; automatic bootstrap fails closed on conflicting/unknown identity state and verifies authoritative post-conditions after mutation.

Never recommend wiping a database as the first fix for a routing or health-check bug.

## Platform / product activation gate

For modular or tenant-aware platforms distinguish four layers: **image capability -> runtime/platform -> instance/site/tenant -> installed/enabled product**. Do not infer a claimed product from code bundled in an image or from instance existence alone. A process/framework health endpoint can be green while the selected product/module bundle is still installing or has failed; when the claimed profile has an authoritative activation state, make readiness activation-aware before releasing dependent workers/gateways.

If a target is a sibling product of an existing Golden, produce a **Sibling Product Delta Gate** covering base platform, shared infrastructure, product-specific state, bootstrap, migration, acceptance and recovery. Record what is inherited, revalidated, changed, and why. A prior Golden supplies causal knowledge, not permission for `copy -> rename`.

When product activation is persistent, detect authoritative state before mutation. Existing instance + product present may reconcile/migrate; existing instance + product absent is a mismatch/conversion decision; partial or unreadable state must fail closed. Do not silently convert persistent product profiles.

Treat fresh activation and later migration as separate lifecycle phases, and verify a durable activation command with an authoritative post-condition when available. Infrastructure automation must not invent organization-specific business truth merely to bypass product onboarding.

Read `references/erpnext-case-study.md` and `references/twelve-benchmark-audit.md` for the current corpus audit; the sibling-product example remains ERPNext.

## Health checks

Health checks must test the real component with syntax valid inside its image.

Check for common failure modes:

- quoting/escaping errors introduced by YAML;
- accidental leading whitespace in `python -c` or shell snippets;
- health command requiring a binary not present in the image;
- checking a public endpoint before DNS/TLS is ready when a local readiness test is sufficient;
- marking a worker healthy based only on a stale PID file;
- using a health check that mutates data;
- making a healthy application appear broken due solely to a malformed probe;
- brittle process-string matching that contradicts stronger evidence that a dedicated worker is actually running and processing jobs;
- assuming a database engine probe proves application roles, schemas, or migrations exist.

A failed health check is evidence to diagnose, not something to delete merely to make Coolify green. `localhost` and `127.0.0.1` can be TCP-equivalent yet HTTP-different when Host validation applies; preserve the hostname proven by upstream/runtime. Keep local readiness separate from external-provider availability where possible; periodic probes should not consume paid provider calls or turn provider quota/outage into local container health unless upstream explicitly defines that dependency as readiness.

## Production-readiness gate

Do not call a stack **production-ready** merely because earlier gates passed. Track the exact evidence ladder: YAML valid -> Compose valid -> containers running -> healthy -> publicly accessible -> platform workflow validated -> product workflow validated when a product is claimed -> persistence validated -> backup validated -> isolated restore validated -> production-readiness evidence. Even the last state does not prove untested HA, load, advanced security, or disaster recovery.

For any production-ready claim, require evidence for all applicable items:

- pinned application/dependency images;
- persistent authoritative data;
- no unintended public database/cache ports;
- non-empty required secrets;
- restart behavior;
- graceful stop period where needed;
- health checks/readiness;
- idempotent initialization/migrations;
- correct public/internal/callback URLs;
- proxy headers and websocket/streaming behavior where applicable;
- backup targets identified;
- restore procedure identified and preferably tested;
- SMTP/notifications explicitly classified as configured or not configured;
- logging/retention considerations;
- upgrade and rollback path;
- end-to-end acceptance tests.

Read `references/production-readiness.md`.

## Acceptance testing

Do not stop at “all containers are healthy”. Build application-level tests from upstream behavior.

At minimum test:

1. DNS and TLS for every public hostname;
2. public root/path behavior for each gateway;
3. login/authentication;
4. create/read/write of a representative object at the highest application/product layer claimed;
5. background worker execution when workers exist — enqueue a real application job, observe consumption, and prove resulting state;
6. native scheduler work when a scheduler exists — a running scheduler PID is not sufficient;
7. a representative feature that crosses service boundaries when the architecture has such a path;
8. callbacks/webhooks/form previews/websockets/realtime if present — prove the actual path/origin/auth/upgrade behavior, not only the main HTTP page; for collaborative products prefer two-session synchronized-edit evidence;
9. anonymous/public behavior in a private browser when applicable;
10. persistence across a normal redeploy;
11. backup and restore for production handover when feasible; if the product compiles/renders/executes jobs, run a real representative job/output path before adding privileged runners or Docker socket access.

When latency is reported, compare application request timings with browser/proxy behavior. If the expected request never reaches the application while server render times are fast, investigate generated URLs, redirects, DNS/TLS, and proxy routing before tuning compute resources.

For multi-domain applications, explicitly test cookies/session scope across the domains that are expected to share authentication.

## Coolify-native operator UX / Service configuration — advisory only

Treat Coolify **Service configuration** exposure as a post-correctness operator-experience layer. Current Coolify can surface selected credentials and parameters when its `Service::extraFields()` logic recognizes the image and environment-variable conventions used by the Compose. This may improve day-2 usability for recognized databases, object stores, or application credentials, but it is **not** part of the runtime contract.

The priority is always:

```text
upstream correctness
  -> runtime correctness
  -> security
  -> persistence
  -> backup / restore
  -> product acceptance
  -> Coolify-native operator UX polish
```

Formalize the separation:

```text
Runtime Credential Contract
!=
Coolify Operator UI Exposure
```

Do not rename a valid Magic Variable, weaken a secret format, change credential ownership, or alter architecture merely to make a field appear in **Service configuration**. A template without this UI section can still be fully valid and mergeable. This knowledge is **advisory** and must never become a benchmark gate.

Golden fixtures remain immutable runtime oracles. Never rename a Magic Variable inside a Golden solely for `extraFields()` compatibility. When preparing a later official Coolify contribution, start from the accepted Golden / production-ready candidate, inspect the current `Service::extraFields()` conventions, identify recognized services/variables, and only then consider a separately validated UI-friendly derivative. Any variable rename or UX-polish delta must preserve semantic role, generated-secret format, security and persistent identity, and the derivative must receive full regression/runtime retesting.

```text
Golden
= immutable runtime oracle

Official contribution candidate
= validated derivative that may receive Coolify-native UX polish
```

Read `references/coolify-rules.md`, `references/cross-benchmark-lessons.md`, and `references/production-readiness.md` for the detailed advisory rule.

## Troubleshooting order

When a deployment is unhealthy, diagnose from the outside in and from dependencies upward:

```text
DNS/TLS/domain assignment
    -> Coolify proxy target
    -> edge/gateway config
    -> application readiness
    -> database/cache/broker
    -> migrations/bootstrap
    -> workers/scheduler
    -> cross-service callbacks
    -> session/cookie/auth semantics
```

Distinguish:

- container running;
- health check passing;
- public route reachable;
- framework/runtime ready;
- selected product/profile activated;
- application feature actually working.

Those are different states.

Build a causal timeline before fixing the newest-looking log line. Classify the earliest proven fault separately from retries, worker races, serialization conflicts, crash-loop consequences, and later independent errors. If edited managed-file behavior appears unchanged, inspect the effective file chain before redesigning application routing.

## CLEAN mode: Compose hygiene

When cleaning a known-working Compose, preserve executable behavior first.

Use these comment conventions only:

- `# NOTE:` for non-obvious implementation context;
- `# REQUIRED:` for configuration that must stay synchronized or be supplied;
- `# SAFETY:` for a change that can cause data loss, authentication breakage, or exposure if altered incorrectly.

Rules:

1. Keep one human-readable template/revision marker near the top of the Compose when a revision marker is useful.
2. Do not repeat the template revision in environment variables, Bash `echo`, Nginx comments, health checks, script names, or scattered comments.
3. Do not use historical labels such as `V12 fix`, `V15 preflight`, `V19 workaround` in operational code.
4. Keep history in Git/CHANGELOG, not inside runtime scripts.
5. Remove comments that merely restate obvious YAML.
6. Preserve comments that explain a non-obvious compatibility constraint.
7. Use functional names such as `kpi-entrypoint.bash`, not revision-bound names such as `v19-kpi-entrypoint.bash`.
8. After cleanup, compare parsed configuration and embedded scripts to ensure no accidental behavior change.

## Official-template architecture matching

Before designing a non-trivial Coolify adaptation, classify the target by deployment architecture and select 2-5 current official Coolify templates as references.

Read:

- `references/official-coolify-template-corpus.md`
- `references/official-template-taxonomy.md`

Then:

1. build the target fingerprint: compute roles, databases, cache, queue, storage, public routes, non-HTTP ports, init jobs, readiness dependencies, host integrations;
2. run `scripts/select_reference_templates.py --compose <target-compose>` when a Compose exists;
3. use architectural similarity, not product category, as the dominant signal;
4. select a reference set: primary topology, routing, stateful services and any special feature;
5. fetch the current official files before applying their patterns;
6. explain why each selected template is relevant;
7. never import a dependency merely because a reference template contains it.

If a local checkout of `coollabsio/coolify/templates/compose` is available, generate a complete current index with:

```bash
python scripts/select_reference_templates.py \
  --build-index /path/to/coolify/templates/compose \
  --index-output official-template-index.json
```

This index is descriptive, not a production-readiness certification.

## Output contract

For **ADAPT** or **CREATE**, produce:

1. architecture summary;
2. service/URL/persistence/secret/readiness maps;
3. final Coolify Compose;
4. required user-supplied variables and Coolify-generated variables;
5. domain assignment instructions;
6. first-deploy procedure;
7. acceptance-test checklist;
8. backup/restore targets;
9. upgrade notes;
10. unresolved assumptions or items requiring live verification.

For **AUDIT**, produce findings ordered by severity and a corrected file when requested.

For **TROUBLESHOOT**, identify the earliest proven failure in the dependency/routing chain before proposing changes.

For **CLEAN**, provide the cleaned file plus a short statement of what was removed and validation proving behavior was preserved.

## Validation workflow

Before final delivery, run as many of these as the environment supports:

1. YAML parse;
2. `docker compose config` or equivalent Compose schema/render validation;
3. check for unresolved environment interpolation;
4. validate embedded Bash with `bash -n`;
5. validate embedded JavaScript with `node --check`;
6. validate JSON with a parser;
7. validate generated/rendered configuration with the target's reliable native validator when available (for example `nginx -t` for Nginx), ideally before the long-lived process starts;
8. run `scripts/audit_compose.py`;
9. run `scripts/validate_embedded.py`;
10. run `scripts/validate_golden_cases.py` when modifying this Skill or any bundled golden fixture;
11. compare the rendered service/capability graph with the upstream baseline; when an upstream Compose exists, `scripts/validate_golden_cases.py --candidate ... --upstream ...` can add advisory capability-delta evidence;
12. inspect the diff to ensure every divergence from upstream has a reason and provenance;
13. for Coolify inline `content:` mounts, inspect the created-file semantics and provenance: managed-resource identity, host path, container target, effective content/checksum where available, reload/restart requirement, and shell/config-dollar usage;
14. inspect canonical/public URL variables for accidental reuse of port-qualified routing values;
15. inspect magic-variable generator families, shared-credential identity, and username/password/encoding mismatches;
16. syntax-check embedded `command:` shell bodies as well as generated files where possible, and validate deterministic nested literal tools such as `sed` when practical after Compose `$$ -> $` representation;
17. when a worker healthcheck fails despite runtime job evidence, validate the probe itself before changing worker architecture;
18. run `scripts/test_magic_variables.py` when changing Magic Variable parsing/audit behavior;
19. when reviewing a candidate refactor after state exists, use `scripts/audit_compose.py --previous-compose <known-good>` to surface Magic Variable identity changes;
20. report the evidence stage actually reached: YAML -> Compose -> started -> healthy -> reachable -> platform workflow -> product workflow when applicable -> persistence -> backup -> restore.
21. run `scripts/test_embedded_validation.py` when changing embedded-shell validation.

If a validator is unavailable, state that it was not run. Never claim a validation you did not actually perform. A successful first runtime candidate can validate **prevented failures** when prior rules removed risks before deployment; record that evidence without treating fewer RC iterations as a universal quality score.

## Regression rule

When a version is known to deploy successfully, treat it as a regression baseline.

For each new change:

- state the single problem being solved;
- change the smallest possible surface;
- preserve data and secrets;
- rerun static validation;
- rerun the relevant acceptance tests;
- do not bundle unrelated cleanup, version upgrades, branding, and infrastructure changes into one untestable revision.

## Knowledge-accumulation regression rule

> **Adding new Golden knowledge must not reduce the Skill's ability to rediscover a target from current upstream evidence.**
>
> **Skill evolution is monotonic only when new knowledge improves or preserves performance on previously solvable architecture classes.** If a newer release regresses on the same target that an older release solved, retrieve the exact older accepted path, diff newly introduced rules, preserve legitimate newer knowledge, and narrow only the rule whose scope caused unnecessary divergence. Do not assume newer = better. **Golden fixtures are regression oracles, not architecture templates.**

## KoboToolbox case study

The bundled KoboToolbox material is a **case study and regression reference**, not a universal template.

Read `references/kobotoolbox-case-study.md` when handling applications with several of these traits:

- multiple public hostnames;
- a backend plus workers and scheduler;
- PostgreSQL plus MongoDB/Redis;
- form renderer or callback service;
- public canonical URLs called from containers;
- shared authentication/session scope across sibling domains;
- edge gateways with distinct route semantics.

The sanitized known-working Compose is `assets/kobotoolbox-v19.3-golden.yml`.

## CKAN case study

The bundled CKAN 2.12 material is the **second golden/regression case**, not a generic Coolify skeleton.

Read `references/ckan-case-study.md` when a target has several analogous concerns such as:

- a single public application plus private state/search/import services;
- a database bootstrap that creates application-specific roles/databases;
- a long-running worker whose readiness cannot rely only on a one-time Compose dependency gate;
- upstream hook scripts that are sourced by a parent entrypoint;
- Coolify-managed inline file content;
- a canonical public origin that must differ from a port-qualified proxy-routing variable;
- an internal callback URL that upstream explicitly permits as distinct from the public canonical site.

The runtime acceptance-tested fixture is `assets/ckan-v1.0.8-golden.yml`.

## OpenMRS case study

The bundled OpenMRS 3.7.1 material is the **third golden/regression case**, not a generic Coolify skeleton.

Read `references/openmrs-case-study.md` for the four-service upstream topology, Coolify magic-credential corrections, semantic gateway preservation, long first-bootstrap behavior, transient-503 diagnosis, persisted-admin-credential lesson, and acceptance path.

The accepted regression fixture is `assets/openmrs-3.7.1-v1.0.0-golden.yml` and carries the explicit warning **“OpenMRS regression fixture / golden case — NOT a generic Coolify skeleton.”**

## OpenEMR case study

The bundled OpenEMR 8.3.0 material is the **fourth golden/regression case**, not a generic Coolify skeleton. Read `references/openemr-case-study.md`; the fixture is `assets/openemr-8.3.0-v1.0.0-golden.yml`.

## ODK Central case study

ODK Central v2026.2.4 is the **fifth golden/regression case**, not a generic Coolify skeleton. Read `references/odk-central-case-study.md` for its upstream PostgreSQL/Enketo/Pyxform/Nginx topology, build-context and mounted-file lessons, one-shot idempotent admin bootstrap, interpolation failures, runtime acceptance, persistence and recovery.

The accepted fixture is `assets/odk-central-v2026.2.4-v1.0.0-golden.yml` and carries the warning **“ODK Central regression fixture / golden case — NOT a generic Coolify skeleton.”**

Read `references/golden-regression-cases.md`, `references/cross-benchmark-lessons.md`, and `references/twelve-benchmark-audit.md` before transferring lessons among the twelve cases or to a new target. `references/eleven-benchmark-audit.md` remains the historical pre-OpenSPP snapshot. `references/ten-benchmark-audit.md` remains the historical pre-Baserow snapshot. `references/five-benchmark-audit.md`, `references/six-benchmark-audit.md`, `references/seven-benchmark-audit.md`, and `references/eight-benchmark-audit.md` remain historical snapshots. Similar components are not contamination by themselves; lack of current-upstream justification is the problem.

## Frappe Framework case study

Frappe Framework v16.32.0 is the **sixth golden/regression case**, not a generic Coolify skeleton. Read `references/frappe-framework-case-study.md` for the RC1→RC4 timeline: credential Magic Variable grammar failure/correction, semantic Frappe Nginx preservation, site/bootstrap one-shots, Frappe-vs-ERPNext activation state, URL/FQDN/port separation, functional worker/scheduler/realtime acceptance, persistence and recovery.

The accepted fixture is `assets/frappe-framework-v16.32.0-v1.0.0-golden.yml`.

Do not transfer Frappe's Redis/workers/scheduler/websocket/Nginx topology to another target without current-upstream provenance. The Golden fixture is a regression oracle, not a skeleton.

## ERPNext case study

ERPNext v16.33.0 is **Golden / Regression Case #7** and the first sibling-product Golden built on an already-golden runtime family. Read `references/erpnext-case-study.md` for the platform/product activation layers, existing-site conversion guard, fail-closed partial/unknown state, install-vs-migrate lifecycle, post-condition verification, product-level acceptance, business-onboarding boundary and Compose/shell/`sed` regression.

The immutable fixture is `assets/erpnext-v16.33.0-v1.0.0-golden.yml`. Frappe Golden #6 must remain Frappe-only; ERPNext Golden #7 requires `frappe + erpnext`. Future Frappe CRM/Helpdesk/HRMS/LMS candidates must redo current sibling-product discovery and acceptance rather than mechanically replacing `erpnext`.

## Mem0 case study

Mem0 v2.0.19 is **Golden / Regression Case #8** and the first compact AI/provider/vector benchmark. Read `references/mem0-case-study.md` for the exact RC1-first-acceptance evidence, separate dashboard/API origins and CORS, secret-origin taxonomy, application-issued API keys, provider-independent readiness, PostgreSQL+pgvector plus SQLite state, startup Alembic, browser-first setup, immutable remote Git source build and prevented-failure learning.

The immutable fixture is `assets/mem0-v2.0.19-v1.0.0-golden.yml` and preserves the exact accepted RC1 bytes. Do not transfer Mem0's three-service topology, pgvector, OpenAI baseline, two public origins, browser-first bootstrap or remote source-build choice to another AI application without current-upstream provenance.


## Overleaf Community Edition case study

Overleaf Community Edition 6.2.2 is **Golden / Regression Case #9**. Read `references/overleaf-ce-case-study.md` for Toolkit-vs-runtime separation, internal-process-vs-Compose topology, CE-vs-Server-Pro boundary, Mongo replica-set/native init, Redis AOF/durability, filesystem/coherent recovery, real LaTeX/realtime acceptance, platform-generated `SHARELATEX` environment collision, and fail-closed admin bootstrap.

The immutable fixture is `assets/overleaf-ce-6.2.2-v1.0.0-golden.yml`, exact accepted RC4 bytes. It intentionally uses Compose service/Magic identity `overleaf`/`SERVICE_URL_OVERLEAF`; `SERVICE_URL_SHARELATEX` is syntactically valid but caused runtime rejection through Coolify-generated `SHARELATEX` environment names. Do not transfer Mongo/Redis/compile/storage/admin details or Server Pro features to another target without current-upstream provenance.

## NetBox case study

NetBox 4.6.9 / netbox-docker 5.0.2 is **Golden / Regression Case #10** and the first explicit knowledge-accumulation regression benchmark. Read `references/netbox-case-study.md` and `references/rc5-vs-rc6-netbox-regression-analysis.md` for image-baked configuration, minimal managed override, two role-distinct Valkey stores, queue security, native migration/superuser lifecycle, `localhost` vs `127.0.0.1` health semantics and secret transport safety.

The immutable fixture is `assets/netbox-4.6.9-v1.0.0-golden.yml`, exact accepted RC5-era RC2 bytes. Do not reconstruct it from later RC6 attempts.

## Anti-patterns

Read `references/anti-patterns.md`. In particular, do not:

- invent architecture from a README summary alone;
- expose internal databases for convenience;
- rotate secrets during harmless refactors;
- use `latest` silently in production;
- add custom networks by habit;
- add manual Traefik labels when Coolify native routing is enough;
- replace readiness with `sleep 30` without evidence;
- delete health checks to hide failures;
- delete volumes to fix an unrelated routing problem;
- conflate public URLs with Docker service names;
- assume root `200 OK` is the correct behavior for every public gateway;
- declare production-ready because containers are green;
- retain a diary of old revisions inside the Compose;
- assume `SERVICE_FQDN_<SERVICE>_<PORT>` omits the port;
- write `$${VAR}` inside Coolify-managed shell-file `content:` when the generated file should contain `${VAR}`;
- use `set -u` in a hook without checking whether upstream sources the hook into its parent shell;
- treat `depends_on: service_healthy` as a durable worker supervisor;
- conclude that `pg_isready` proves application roles/databases/migrations were initialized;
- inflate a simple upstream topology with workers, caches, gateways, sidecars, or stores borrowed from previous golden cases without current-upstream cause;
- delete an application-semantic gateway merely because Coolify already owns the Internet edge;
- infer site/tenant app activation from code bundled in an image;
- randomize an upstream-fixed semantic account identity merely to consume `SERVICE_USER_*`;
- treat worker/scheduler/realtime process state as functional acceptance;
- copy a sibling Golden and replace only the product/app name;
- treat instance/site existence as proof the claimed product is installed;
- mutate unknown product state after authoritative detection failed;
- silently convert a persistent platform-only instance into a different product profile;
- declare a product validated from platform ping/Desk alone;
- invent company/currency/jurisdiction/accounting facts during infrastructure bootstrap;
- treat `install-app` exit zero as stronger than an available authoritative post-condition;
- fabricate external-provider credentials with `SERVICE_PASSWORD_*`;
- replace application-issued API keys with deployment-generated strings without upstream support;
- call paid/external AI providers in periodic healthchecks by default;
- configure browser JavaScript with Docker-internal DNS;
- infer database extension activation/use from a capable image alone;
- assume a primary SQL database is the complete backup scope;
- create a migrator/admin-init sidecar merely because another Golden had one;
- describe a pinned source commit as a fully hermetic dependency build;
- copy an upstream Toolkit wholesale, explode internal image processes into services, or import features from a different edition/profile;
- assume Redis is disposable, merge role-distinct same-technology stores, primary-DB backup is a coherent recovery set, or dependency multi-arch support proves full-stack support;
- reset persisted admin credentials on redeploy, ignore platform-generated service metadata, copy a whole upstream config tree without checking image-baked defaults, normalize `localhost`/`127.0.0.1` blindly, or add Docker socket/privileged compile runners without selected-edition evidence;
- count intentional deployment-profile exploration as failed repair iterations;
- assume distributed means production or all-in-one means development;
- infer process decomposition from state externalization;
- delete an application-semantic gateway because Coolify already owns TLS;
- make a version-specific runtime workaround permanent without revalidating its cause;
- treat a local root-path 4xx as proof the service is down before checking Host/path/router semantics.

## References

Load only what is needed for the current task:

- `references/source-priority.md`
- `references/architecture-discovery.md`
- `references/coolify-rules.md`
- `references/networking-and-domains.md`
- `references/production-readiness.md`
- `references/kobotoolbox-case-study.md`
- `references/ckan-case-study.md`
- `references/openmrs-case-study.md`
- `references/openemr-case-study.md`
- `references/odk-central-case-study.md`
- `references/five-benchmark-audit.md` (historical)
- `references/six-benchmark-audit.md` (historical)
- `references/seven-benchmark-audit.md` (historical)
- `references/eight-benchmark-audit.md` (historical)
- `references/nine-benchmark-audit.md` (historical)
- `references/ten-benchmark-audit.md`
- `references/golden-regression-cases.md`
- `references/cross-benchmark-lessons.md`
- `references/anti-patterns.md`
- `references/sources.md`
- `references/evaluation-prompts.md`
- `references/frappe-framework-case-study.md`
- `references/erpnext-case-study.md`
- `references/mem0-case-study.md`
- `references/overleaf-ce-case-study.md`
- `references/netbox-case-study.md`
- `references/rc5-vs-rc6-netbox-regression-analysis.md`
- `references/baserow-case-study.md`
- `references/baserow-profile-evidence-matrix.md`
- `references/eleven-benchmark-audit.md` (historical pre-OpenSPP)
- `references/twelve-benchmark-audit.md`
- `references/openspp-case-study.md`
- `references/openspp-rc1-to-rc8-causal-ledger.md`
- `references/openspp-skill-learning-delta.md`
- `references/openspp-golden-sha256.txt`

Use the bundled scripts and assets only after understanding the upstream project.

## Multi-profile deployment reasoning

A mature deployment skill must be able to represent multiple valid architectures for the same product without collapsing them into one universal topology.

Distinguish:

```text
REPAIR ITERATION
!=
DEPLOYMENT PROFILE VARIANT
```

A repair iteration changes the same selected profile to correct a demonstrated fault. An all-in-one profile, a distributed profile, and an all-in-one application with external state are intentionally different operational choices. **Multiple intentionally evaluated deployment profiles are not failure iterations. Iteration count measures repairs within one selected profile, not exploration across legitimate upstream profiles.**

When upstream supports multiple profiles, classify each as one of:

- **Canonical Golden Profile** — the exact runtime-accepted profile used as the primary regression oracle;
- **Validated Alternative Profile** — a different architecture with sufficient runtime/operator acceptance, preserved as diversity evidence without creating a fake additional numbered Golden;
- **Reference / Candidate Profile** — useful upstream architecture or experiment without sufficient runtime evidence for a validated label.

Use a **Deployment Profile Selection Gate** before choosing one. Compare upstream support, the stated deployment target, horizontal scaling, failure isolation, observability, state isolation, backup/recovery, upgrade complexity, resource control, shared filesystem requirements, semantic gateway responsibilities and Coolify operational complexity. Do not select the fewest or most containers by principle.

> **Choose the least operationally complex current-upstream-supported profile that preserves the required product responsibilities, state boundaries and recovery contract for the stated deployment target.**

Process decomposition and state externalization are independent dimensions. `all-in-one app + external PostgreSQL` does not imply `backend + frontend + workers` must become separate Compose services. Likewise a vendor-supported multi-process image is not an anti-pattern merely because it contains several runtime roles. Decompose only when the target has a demonstrated need for independent scaling, per-process resource control, failure isolation, independent observability or a distributed shared-state topology.

State externalization, process decomposition, public routing and lifecycle ownership are independent deployment dimensions unless current upstream evidence couples them.

Compare alternative profiles by **product capability equivalence** and operational properties, not by service-by-service structural similarity. A profile capability matrix should record at least: profile, upstream status, services, public shape, authoritative state, worker model, realtime, semantic proxy, migration owner, first-user lifecycle, backup primitive, restore primitive, scaling model, operational complexity and runtime evidence.

A runtime workaround belongs to its causal version and profile, not automatically to the product forever. Preserve native lifecycle primitives unless runtime evidence proves a gap. If a version-specific race or correctness defect requires a temporary migration/init gate, scope it to the affected version/profile, keep it minimal and idempotent, verify post-conditions, fail closed on ambiguous persistent state, and re-test whether the workaround is still necessary on every upstream upgrade.

Automatic repair of duplicated singleton state is acceptable only when the tool can prove the instance is still pristine and the repair preserves one unambiguous authoritative object. Existing user/product state plus conflicting singleton state requires manual review.

For HTTP health probes, reason about:

```text
HTTP health semantics
= network target
+ Host header
+ path semantics
+ application router
+ authentication/state expectations
```

A reachable local endpoint may still enter tenant/domain/product routing and return a deliberate 4xx. Prefer a dedicated local liveness endpoint when a product route is Host/path-sensitive; do not redesign architecture from a local 404 until the Host/path/router semantics are understood.

## Baserow case study

Baserow 2.3.3 is **Golden / Regression Case #11** and the first explicit multi-profile Golden. Read `references/baserow-case-study.md`, `references/baserow-profile-evidence-matrix.md`, and `references/eleven-benchmark-audit.md`.

The Canonical Golden is the exact runtime-accepted distributed/custom RC5 fixture: `assets/baserow-2.3.3-v1.0.0-golden.yml`, SHA-256 `143c3a94952b16e85638d87bd50fa29a49fa756b7c12cba097fe32383c4312f6`. The operator also accepted the official all-in-one application with external PostgreSQL as a distinct **Validated Alternative Profile**. The tested all-in-one path affected by the Baserow 2.3.3 fresh-install auth-provider failure remains a **Reference / Candidate Profile**, not a second Golden.

Baserow confirms that Coolify's Internet/TLS proxy does not replace an application-semantic Caddy gateway when that gateway owns frontend/API/realtime/static/media/Application Builder routing. It also generalizes the NetBox Host-health lesson: `127.0.0.1` can be a valid network target yet an invalid semantic request for a product route. The Golden therefore protects its dedicated `__coolify_gateway_health` endpoint without making that path universal.

Do not import Baserow's pgvector, Redis, Celery topology, Caddy routes, migration workaround or password-provider safeguard into another product without current-upstream/runtime cause. Do not import NetBox AOF semantics into Baserow merely because both use Redis-compatible stores.

## OpenSPP case study

OpenSPP V2 2026.08 is **Golden / Regression Case #12**. Read `references/openspp-case-study.md`, `references/openspp-rc1-to-rc8-causal-ledger.md`, `references/openspp-skill-learning-delta.md`, and `references/twelve-benchmark-audit.md`.

The immutable fixture is `assets/openspp-2026.08-v1.0.0-golden.yml`, exact accepted RC8 bytes, SHA-256 `f00a8755fa2be8e8b1f50970978ae1b57c1877093c2a35108edf35a675d4587b`. Promotion is based on the final operator verdict that RC8 worked without problems, data persistence was correct, and all requested acceptance tests were completed successfully. Treat that as operator-confirmed runtime evidence; do not invent granular raw outputs that are not preserved.

Generalize only the causal lessons: source pin != dependency closure, dependency drift can break a released build, framework health != product activation, readiness protects shared initialization, local source-build pull behavior is Coolify-version sensitive, declared managed-file content != effective runtime file, critical platform primitives need end-to-end support evidence, dollar escaping is transport-specific, whitelisted substitution protects target-language `$variables`, canonical public origin != internal listener port, and intentional database-role splits require credential topology rather than password-equality assumptions.

Keep OpenSPP-specific topology, versions, SP-MIS module, XML compatibility shims, exact Nginx routes, database roles, queue command, and backup implementation local to Golden #12.

---

## Portable embedded resources

> This single-file edition embeds every structured resource from `coolify-architect/` (except `SKILL.md` itself and portable outputs). Each resource carries source and embedded SHA-256 values so synchronization can be validated mechanically.

> Embedded resource count: **68**.

<!-- BEGIN PORTABLE RESOURCE: README.md -->
<!-- SOURCE SHA256: 1fe2ea9ac98e49f4128b74892eba7039338daad1e2aa5f49b059db0fab9f1e1c -->
<!-- EMBEDDED SHA256: 2b9f19ab96d018a1a889abb3f13fa61c4d00dc1692fa174a1d9604b826ce1f68 -->

## Portable resource: `README.md`

### coolify-architect

**Current release:** `1.0.0-rc10` — OpenSPP Golden #12 + Runtime-Layer/Reproducibility Reasoning. RC10 preserves all eleven RC9 Golden fixtures byte-for-byte, freezes the exact runtime-accepted OpenSPP RC8 candidate as Golden #12, and adds dependency-closure, activation-aware readiness, managed-file provenance, transport-specific interpolation, current-Coolify source-build, effective-platform-support, canonical-origin and credential-topology reasoning. The RC9 non-blocking Coolify Service configuration / `extraFields()` advisory layer is preserved.

`coolify-architect` is a reusable Agent Skill for designing, adapting, auditing, troubleshooting, and cleaning Docker Compose stacks intended for Coolify.

The engineering method is backed by **twelve runtime golden/regression cases**. ERPNext intentionally shares the Frappe runtime family, while Mem0 adds a compact AI/provider/vector/browser-frontend class without turning the corpus into an average stack:

- KoboToolbox V19.3 — multi-public-host, Enketo/KPI, PostgreSQL/MongoDB/Redis, Celery, callback/hairpin and route-semantics lessons;
- CKAN 2.12 / Coolify V1.0.8 — single public app, PostgreSQL/DataStore, Solr, Redis, DataPusher, RQ worker/scheduler, managed-file and canonical-URL lessons;
- OpenMRS 3.7.1 / Coolify V1.0.0 — semantic nginx gateway + O3 frontend + OpenMRS backend + MariaDB, generated credential/reuse and long-bootstrap readiness lessons;
- OpenEMR 8.3.0 / Coolify V1.0.0 — deliberately minimal public OpenEMR + private MariaDB, native bootstrap, database + site/document persistence, and tested recovery;
- ODK Central v2026.2.4 / Coolify — single semantic Nginx host + Central backend + Pyxform + Enketo + PostgreSQL/Redis lifecycle, one-click admin bootstrap, persistence and tested recovery.
- Frappe Framework v16.32.0 / Coolify — MariaDB + Redis + RQ workers + scheduler + Socket.IO + semantic Nginx, one-shot site lifecycle, Magic Variable grammar failure/correction, full workflow/persistence/recovery acceptance.
- ERPNext v16.33.0 / Coolify — sibling-product benchmark on the Frappe runtime: explicit ERPNext activation, fail-closed existing-site conversion, product-level acceptance, health coverage, persistence/redeploy/recovery.
- Mem0 v2.0.19 / Coolify — compact three-service AI stack with separate dashboard/API origins, PostgreSQL+pgvector, native auth/API keys, external provider credentials, startup Alembic, PostgreSQL+SQLite state, immutable remote Git source build, and first-candidate runtime acceptance.
- Overleaf Community Edition 6.2.2 / Coolify — Toolkit/runtime responsibility separation, supervised internal microservices, CE-vs-Server-Pro boundary, Mongo replica set, Redis AOF, authoritative filesystem/coherent recovery, real LaTeX compilation/realtime acceptance, and fail-closed generated-password admin bootstrap.
- NetBox 4.6.9 / netbox-docker 5.0.2 / Coolify — five-service upstream topology, image-baked configuration + minimal override, native migrations/superuser bootstrap, RQ worker, PostgreSQL, separate Valkey tasks/cache semantics, `localhost` health Host regression, and knowledge-accumulation regression control.
- Baserow 2.3.3 / Coolify — multi-profile selection, exact distributed/custom Golden, validated all-in-one + external PostgreSQL alternative, process-topology/state-topology independence, semantic Caddy preservation, Host/path-sensitive health, and version/profile-scoped lifecycle workaround learning.
- OpenSPP V2 2026.08 / Odoo 19 / PostgreSQL 18 + PostGIS 3.6 / Coolify — production-hardened single-node SP-MIS benchmark, transitive build-graph drift, activation-aware readiness, shared-initialization gating, local-build pre-pull behavior, managed-file provenance/identity, transport-specific `$` semantics, whitelisted `envsubst`, native config validation, canonical public-origin normalization, intentional DB-role split and coherent PostgreSQL+filestore recovery.

No golden fixture is a generic skeleton. The skill generalizes **causes and engineering principles**, not application-specific dependencies. OpenEMR, Mem0, Overleaf, NetBox, Baserow and OpenSPP are explicit anti-inflation/abstraction counterexamples: successful adaptation does not require importing complexity from earlier cases, while Mem0 also proves a three-service topology can still have subtle browser/CORS/provider/auth/vector/recovery complexity.

#### What it does

The skill forces an agent to discover the real upstream architecture before generating Compose, then reason explicitly about:

> **Complexity must be inherited from the current upstream architecture, not from previous golden cases.**


- public vs internal vs canonical callback URLs and proxy targets;
- Coolify-native domains and `SERVICE_*` variables;
- databases, caches, queues, workers, schedulers, search/import services, and initialization;
- persistent volumes and disaster recovery;
- generated credentials and immutable secrets;
- managed inline files and shell interpolation contexts;
- health checks, startup readiness, and long first-boot migrations;
- reverse-proxy behavior;
- server-to-server public URL loopback/hairpin versus explicit internal callbacks;
- platform/runtime/site/product activation layers and sibling-product deltas;
- application- and product-level acceptance tests;
- production-readiness evidence;
- golden-case regression and anti-contamination discipline.

#### Structure

```text
coolify-architect/
├── SKILL.md
├── README.md
├── LICENSE
├── CHANGELOG.md
├── agents/
│   └── openai.yaml
├── references/
│   ├── anti-patterns.md
│   ├── architecture-discovery.md
│   ├── ckan-case-study.md
│   ├── coolify-rules.md
│   ├── evaluation-prompts.md
│   ├── frappe-framework-case-study.md
│   ├── erpnext-case-study.md
│   ├── mem0-case-study.md
│   ├── golden-regression-cases.md
│   ├── cross-benchmark-lessons.md
│   ├── kobotoolbox-case-study.md
│   ├── openmrs-case-study.md
│   ├── openemr-case-study.md
│   ├── odk-central-case-study.md
│   ├── five-benchmark-audit.md       # historical pre-Frappe audit
│   ├── six-benchmark-audit.md       # historical pre-ERPNext audit
│   ├── seven-benchmark-audit.md       # historical pre-Mem0 audit
│   ├── eight-benchmark-audit.md       # historical pre-Overleaf audit
│   ├── nine-benchmark-audit.md       # historical pre-NetBox audit
│   ├── ten-benchmark-audit.md
│   ├── netbox-case-study.md
│   ├── baserow-case-study.md
│   ├── baserow-profile-evidence-matrix.md
│   ├── eleven-benchmark-audit.md       # historical pre-OpenSPP audit
│   ├── openspp-case-study.md
│   ├── openspp-rc1-to-rc8-causal-ledger.md
│   ├── openspp-skill-learning-delta.md
│   ├── openspp-golden-sha256.txt
│   ├── twelve-benchmark-audit.md
│   ├── rc5-vs-rc6-netbox-regression-analysis.md
│   ├── mem0-case-study.md
│   ├── overleaf-ce-case-study.md
│   ├── networking-and-domains.md
│   ├── official-coolify-template-corpus.md
│   ├── official-template-taxonomy.md
│   ├── production-readiness.md
│   ├── source-priority.md
│   └── sources.md
├── scripts/
│   ├── audit_compose.py
│   ├── build_portable.py
│   ├── select_reference_templates.py
│   ├── validate_compose.sh
│   ├── validate_embedded.py
│   ├── validate_golden_cases.py
│   ├── test_magic_variables.py
│   ├── test_embedded_validation.py
│   ├── test_regression_learning.py
│   ├── test_profile_selection.py
│   ├── test_openspp_learning.py
│   ├── validate_portable.py
│   └── validate_skill.py
├── assets/
│   ├── ckan-v1.0.8-golden.yml
│   ├── kobotoolbox-v19.3-golden.yml
│   ├── openmrs-3.7.1-v1.0.0-golden.yml
│   ├── openemr-8.3.0-v1.0.0-golden.yml
│   ├── odk-central-v2026.2.4-v1.0.0-golden.yml
│   ├── frappe-framework-v16.32.0-v1.0.0-golden.yml
│   ├── erpnext-v16.33.0-v1.0.0-golden.yml
│   ├── mem0-v2.0.19-v1.0.0-golden.yml
│   ├── overleaf-ce-6.2.2-v1.0.0-golden.yml
│   ├── netbox-4.6.9-v1.0.0-golden.yml
│   ├── baserow-2.3.3-v1.0.0-golden.yml
│   └── openspp-2026.08-v1.0.0-golden.yml
├── coolify-architect-portable.SKILL.md   # included in release ZIP
└── coolify-architect-portable.txt        # byte-identical portable copy
```

#### Codex installation

For a repository-scoped skill, copy the folder to:

```text
<repo>/.agents/skills/coolify-architect/
```

For a user-scoped skill available across repositories, copy it to:

```text
$HOME/.agents/skills/coolify-architect/
```

Example prompts:

```text
Use coolify-architect to adapt this repository to a Coolify Compose template.
```

```text
Use coolify-architect in AUDIT mode on docker-compose.yml. Do not change behavior unless a finding requires it.
```

```text
Use coolify-architect to troubleshoot why the app is fast in server logs but browser redirects contain the container port.
```

#### ChatGPT

The same `SKILL.md` bundle follows the Agent Skills directory convention. In supported ChatGPT surfaces, import the structured ZIP/folder through the Skills interface.

The portable edition is for LLMs/agents/CLI coding tools that can consume one file but do not understand the directory convention.

#### Validation

From the parent directory:

```bash
python3 coolify-architect/scripts/validate_skill.py coolify-architect
python3 coolify-architect/scripts/validate_golden_cases.py coolify-architect
python3 coolify-architect/scripts/test_magic_variables.py
python3 coolify-architect/scripts/test_embedded_validation.py
python3 coolify-architect/scripts/test_regression_learning.py
python3 coolify-architect/scripts/test_profile_selection.py
python3 coolify-architect/scripts/test_openspp_learning.py
```

For a candidate Compose:

```bash
coolify-architect/scripts/validate_compose.sh docker-compose.yml
```

For an advisory candidate-vs-upstream architecture-provenance review when an upstream Compose exists:

```bash
python3 coolify-architect/scripts/validate_golden_cases.py coolify-architect \
  --candidate docker-compose.yml \
  --upstream upstream-compose.yml
```

This reports architecture-capability deltas for human review; it does not enforce a service-count threshold.

Also run, when available:

```bash
docker compose -f docker-compose.yml config
```

Static checks do not replace a live deployment acceptance test.

#### Design rule

The skill uses staged evidence language:

```text
YAML valid
    -> Compose valid
    -> containers running
    -> healthy
    -> publicly accessible
    -> platform workflow validated
    -> product workflow validated when a product is claimed
    -> persistence validated
    -> backup validated
    -> isolated restore validated
    -> production-readiness evidence
```

Even production-readiness evidence is scoped: it does not imply untested HA, load/performance, advanced security hardening, or multi-region disaster recovery. The twelve-Golden corpus is a release-candidate body of regression evidence, not a maturity claim; Golden status still does not imply untested HA, load, advanced hardening or every optional integration.

#### Architecture-aware official-template selection

Rank official architecture references for a target Compose:

```bash
python scripts/select_reference_templates.py --compose docker-compose.yml
```

With a local Coolify checkout:

```bash
python scripts/select_reference_templates.py \
  --build-index /path/to/coolify/templates/compose \
  --index-output official-template-index.json
```

#### Portable edition and synchronization

Build the portable single-file edition with:

```bash
python scripts/build_portable.py \
  --output coolify-architect-portable.SKILL.md
```

Validate both structure **and source synchronization** with:

```bash
python scripts/validate_portable.py \
  coolify-architect-portable.SKILL.md \
  --root .
```

The release process copies the exact same bytes to `coolify-architect-portable.txt`. The release ZIP includes both portable copies alongside the structured skill.

<!-- END PORTABLE RESOURCE: README.md -->

<!-- BEGIN PORTABLE RESOURCE: CHANGELOG.md -->
<!-- SOURCE SHA256: 16957762b0d10df668bb2ba81dcc0e17af8b39b7a2b9c5db3a6143eb6feb6414 -->
<!-- EMBEDDED SHA256: 581e1e54db3593289a9053d1aa8e7d902ce03507a0aa3091caaa3a1b2900ff31 -->

## Portable resource: `CHANGELOG.md`

### Changelog

#### 1.0.0-rc10 — OpenSPP Golden #12 + Runtime-Layer/Reproducibility Reasoning

OpenSPP V2 2026.08 is promoted to **Golden / Regression Case #12** from the exact RC8 candidate explicitly accepted by the operator after the requested runtime test suite was reported complete, with persistence confirmed. The immutable fixture is `assets/openspp-2026.08-v1.0.0-golden.yml`, SHA-256 `f00a8755fa2be8e8b1f50970978ae1b57c1877093c2a35108edf35a675d4587b`. RC10 does not claim independently captured raw evidence for every final gate beyond the operator-confirmed acceptance; Golden status is scoped to the demonstrated benchmark.

All eleven RC9 Golden fixtures remain byte-for-byte identical. OpenSPP-specific topology, versions, SP-MIS module names, XML compatibility shims, exact Nginx routes, DB names/roles, queue command and backup implementation remain local to the case.

##### New generalized reasoning

- **Source release pin != transitive build-graph pin.** Source builds now require a dependency-closure/reproducibility map and an honest `HERMETIC` / `PARTIALLY PINNED` / `FLOATING TRANSITIVE DEPENDENCIES` / `UNKNOWN` classification.
- **Dependency contract drift** is handled by proving the causal upstream change and preferring the smallest version/dependency-scoped compatibility boundary rather than copying/forking dependency trees.
- **Process/framework health != selected product activation != product workflow acceptance.** Modular platforms require activation-aware readiness when a specific module/app/profile is claimed.
- **Readiness protects shared mutable initialization.** Workers/schedulers/gateways must not race authoritative schema/registry/module initialization unless upstream explicitly supports it.
- **Current Coolify local-build pull behavior is version/path sensitive.** `image:` + `build:` may require `pull_policy: never` for local-only images when the target Coolify deployment path demonstrably pre-pulls before build; this is not a universal Compose law.
- **Declared managed-file content != effective runtime configuration.** RC10 adds a managed-file provenance ledger spanning Compose content, Coolify resource identity, host file, container mount, consumer-loaded file and effective behavior.
- **Compose specification support != effective Coolify Service support.** Critical primitives must be considered across spec, parser, persistence/model and deployment layers; incomplete proof is `REVIEW REQUIRED`.
- **Dollar escaping is transport-specific.** Compose-command `$$` and literal managed-file `$` can both be correct in different pipelines; an interpolation/serialization layer map is now required for ambiguous nested tokens.
- **Whitelist `envsubst`** when the target configuration language also owns native `$variables`, and run a reliable native configuration validator before a long-lived process where available.
- **Internal proxy target/listener ports are not canonical browser origin metadata.** Normalize scheme/host/port to the actual public origin, not the internal gateway target.
- **Credential topology is role-aware.** Different passwords for intentionally distinct DB roles are not automatically a mismatch; same logical role/account with inconsistent passwords remains an error.
- **One-Click security baseline != organization-specific production security program.** KMS/TDE/off-site backup/monitoring/RPO/RTO/PITR controls are classified by ownership rather than fabricated.
- **Error chronology matters.** The last `ERROR` is not automatically the root cause; troubleshooting builds a causal timeline and distinguishes primary fault, downstream races/retries and independent later errors.

##### OpenSPP causal ledger

`references/openspp-rc1-to-rc8-causal-ledger.md` records the benchmark as a sequence of different evidence-layer failures rather than eight architecture failures: Coolify pre-pull, mutable transitive dependency drift, product activation/readiness, proxy/session/origin semantics, stale managed-file provenance, unsupported effective platform primitive, literal managed-file dollar transport, then the accepted RC8 render/validation path.

##### Validator and regression additions

- `scripts/validate_golden_cases.py` freezes OpenSPP Golden #12 exact bytes and demonstrated invariants.
- `scripts/audit_compose.py` adds conservative `REVIEW REQUIRED` findings for build+image pull-sequence ambiguity, `configs.content` effective-Coolify support, suspicious managed-Nginx `$$` transport and unrestricted `envsubst` over native Nginx `$variables`; the intentional multi-DB-role heuristic remains review-only unless the same generated DB user is wired to conflicting passwords.
- `scripts/validate_embedded.py` rejects the high-confidence RC7 class where a Coolify managed Nginx file contains literal Compose-style `$$remote_addr`/related native Nginx variables.
- `scripts/test_embedded_validation.py` now proves Overleaf-style nested `$$set` and OpenSPP-style managed-file `$remote_addr` are contextually compatible rather than contradictory.
- `scripts/test_openspp_learning.py` adds executable regression coverage for the generalized OpenSPP lessons and exact Golden hash.
- `references/evaluation-prompts.md` adds the A–M OpenSPP reasoning regressions requested for partial pins, activation, worker races, pull order, managed files, dollar transport, envsubst, proxy ports, DB roles, platform primitives, causal chronology and version-scoped shims.

The RC9 Coolify-native operator UX / `Service::extraFields()` advisory layer, Baserow multi-profile reasoning, NetBox knowledge-accumulation regression controls and all previous Golden lessons are preserved.

#### 1.0.0-rc9 — Coolify Operator UX / Service configuration advisory layer

This release adds a **non-blocking** contribution-stage lesson for Coolify **Service configuration** exposure. Current Coolify may surface selected credentials and parameters when `Service::extraFields()` recognizes the images/services/environment-variable conventions used by a Compose, but that UI behavior is explicitly separated from runtime correctness.

New permanent distinctions:

```text
Runtime Credential Contract
!=
Coolify Operator UI Exposure
```

and:

```text
Golden
= immutable runtime oracle

Official contribution candidate
= validated derivative that may receive Coolify-native UX polish
```

Rules added:

- `extraFields()` / Service configuration compatibility is advisory operator UX, never a benchmark gate;
- upstream correctness, runtime correctness, security, persistence, backup/restore and product acceptance always outrank UI exposure;
- a template without Service configuration fields can still be valid and mergeable;
- no Golden may be renamed or rewritten solely to make credentials visible in that UI;
- official Coolify contribution polish begins from an accepted Golden/candidate, inspects current `Service::extraFields()` conventions, and may produce a separately validated UI-friendly derivative only when secret semantics/format/security/persistent identity remain compatible;
- every UI-oriented rename requires full relevant regression/runtime retesting.

No new Golden is added. **All eleven Golden fixtures remain byte-for-byte unchanged from RC8.** No existing correctness, security, persistence, recovery, acceptance, anti-contamination, profile-selection, or knowledge-accumulation rule is weakened.

#### 1.0.0-rc8 — Baserow Golden #11 + Multi-Profile Deployment Reasoning

Baserow 2.3.3 becomes **Golden / Regression Case #11** from the exact distributed/custom RC5 candidate accepted at runtime by the operator. The immutable fixture is `assets/baserow-2.3.3-v1.0.0-golden.yml`, SHA-256 `143c3a94952b16e85638d87bd50fa29a49fa756b7c12cba097fe32383c4312f6`.

This release does **not** reinterpret Baserow's deliberately explored deployment profiles as a single failure chain. It introduces `REPAIR ITERATION != DEPLOYMENT PROFILE VARIANT` and three evidence statuses: Canonical Golden Profile, Validated Alternative Profile, and Reference / Candidate Profile. The official all-in-one application + external PostgreSQL profile is retained as an operator-validated alternative, while the tested all-in-one path affected by the Baserow 2.3.3 auth-provider/fresh-install failure remains a reference candidate rather than a fake Golden.

Major generalized learning:

- deployment-profile selection precedes topology normalization;
- process decomposition and state externalization are independent dimensions;
- vendor-supported multi-process images can be legitimate production profiles;
- distributed profiles are also legitimate when target-specific scaling/isolation/observability justify them;
- Coolify edge proxy != application-semantic gateway;
- HTTP health semantics include network target + Host + path + application router + state/auth expectations;
- state externalization can change the valid backup/recovery primitive;
- same technology across products does not imply the same durability semantics;
- runtime workarounds remain version/profile scoped and must be revalidated on upgrade;
- multiple valid profiles are compared by product-capability equivalence plus operational properties, not service-by-service similarity.

All Golden fixtures #1-#10 remain byte-for-byte unchanged from RC7.

#### 1.0.0-rc7 — NetBox Golden #10 + RC5→RC6 regression correction

NetBox 4.6.9 / netbox-docker 5.0.2 is promoted to **Golden / Regression Case #10** from the exact runtime-accepted RC2 candidate produced on the RC5 benchmark path. The immutable fixture is `assets/netbox-4.6.9-v1.0.0-golden.yml`, SHA-256 `e4be06751d206704a2e9460ac2926d92833b39a71266cf1bd5a8a788da319804`. RC6 remains the release baseline; all nine prior Golden fixtures remain byte-identical.

##### NetBox runtime learning

- preserve NetBox web + RQ worker + PostgreSQL + Valkey tasks + Valkey cache from current upstream;
- keep tasks/cache separate by state semantics, with AOF on tasks only in the accepted fixture;
- treat queue write access as trusted execution infrastructure;
- verify Dockerfile/image contents before recreating repository bind-mounted configuration; NetBox already bakes `/etc/netbox/config` into the image;
- prefer image baseline + environment + smallest justified managed override;
- keep NetBox migrations and first-admin creation in the native image lifecycle;
- preserve HTTP healthcheck hostname semantics: `localhost` and `127.0.0.1` are not interchangeable when Host validation applies;
- treat application secret format and Coolify `.env`/Compose transport as separate contracts; demonstrated `$` interpolation risk is not a universal symbol ban.

##### RC5→RC6 behavioral regression correction

RC5 solved NetBox in two iterations while RC6 diverged for longer despite a larger corpus. RC7 adds `references/rc5-vs-rc6-netbox-regression-analysis.md` and the permanent rule:

> Adding new Golden knowledge must not reduce the Skill's ability to rediscover a target from current upstream evidence.

Overleaf RC6 rules remain valid but are scoped to their causes. Toolkit/runtime, edition/profile, nested-language interpolation, platform-derived metadata and fail-closed identity rules are preserved; they must not become reasons to copy config bundles, add bootstrap helpers, inflate architecture, distrust proven native lifecycle, or overconstrain unrelated secret formats.

##### Added

- `assets/netbox-4.6.9-v1.0.0-golden.yml`;
- `references/netbox-case-study.md`;
- `references/ten-benchmark-audit.md`;
- `references/rc5-vs-rc6-netbox-regression-analysis.md`;
- `scripts/test_regression_learning.py`;
- NetBox positive invariants + exact SHA lock in `validate_golden_cases.py`;
- health Host-semantics advisory in `audit_compose.py`;
- meta-regression and NetBox evaluation prompts.

##### Principle

> Skill evolution is monotonic only when new knowledge improves or preserves performance on previously solvable architecture classes. A newer Skill that regresses on a target solved by an older release must treat that divergence as a first-class regression.

Golden fixtures remain **regression oracles, not architecture templates**.

#### 1.0.0-rc6 — Overleaf Community Edition Golden Case #9 promotion

Overleaf Community Edition 6.2.2 is promoted to **Golden / Regression Case #9** after RC1→RC4 runtime learning and operator-confirmed completion of the requested acceptance suite. The immutable fixture is the exact accepted `overleaf-coolify-v1.0.0-rc4.yml` bytes at `assets/overleaf-ce-6.2.2-v1.0.0-golden.yml`, SHA-256 `b8cb9425523d38088f7069c70d762ab24fbf07232d736f607572e5b191621585`.

##### RC1→RC4 runtime evidence

- RC1 preserved the correct CE-scale runtime, Mongo replica-set semantics, Redis AOF, application filesystem persistence, no Docker socket/Server Pro and pinned images, but failed because service name `sharelatex` caused Coolify to inject `SERVICE_URL_SHARELATEX`, `SERVICE_NAME_SHARELATEX`, and `SERVICE_FQDN_SHARELATEX`; Overleaf 5+ rejected all `SHARELATEX`-named environment variables before starting.
- RC2 renamed the Compose service/public Magic identity to `overleaf`/`SERVICE_URL_OVERLEAF`, added `OVERLEAF_REDIS_PORT`, removed unnecessary CE variables, and reached successful migrations/runit startup; native `/launchpad` first-admin flow worked.
- RC3 added a fail-closed One-Click `adminbootstrap` using operator-provided email + `SERVICE_PASSWORD_64_OVERLEAFADMIN`, but Compose consumed the embedded JavaScript `$set`, producing a Node syntax error.
- RC4 changed source `$set` to `$$set` so runtime JavaScript receives `$set`. Admin creation/login succeeded and the operator confirmed the requested application, persistence, compile, realtime, redeploy and recovery tests passed.

##### Added / promoted

- `references/overleaf-ce-case-study.md`;
- `assets/overleaf-ce-6.2.2-v1.0.0-golden.yml`;
- `references/nine-benchmark-audit.md`;
- Overleaf-specific Golden invariants and exact-RC4 SHA lock;
- Magic Variable regression coverage for `SERVICE_URL_OVERLEAF`, syntactic `SERVICE_URL_SHARELATEX`, `SERVICE_REALBASE64_32_OVERLEAFINVITE`, and `SERVICE_PASSWORD_64_OVERLEAFADMIN`;
- embedded validation coverage for Compose-dollar Mongo/JavaScript operators such as `$set`;
- evaluation prompts for Toolkit/runtime, Redis durability, CE-vs-Pro, coherent backup, compute/compile execution model, admin redeploy identity, platform-derived environment and full-stack architecture support.

##### New generalizable lessons

- **Toolkit vs runtime:** upstream deployment Toolkits mix host orchestration with runtime evidence; host lifecycle scripts are not automatically Compose services.
- **Internal process topology vs Compose topology:** independently named microservices/processes inside a supervised image do not automatically become containers.
- **Edition/profile boundary:** features from another edition of the same product are not target-edition requirements.
- **Coherence groups:** database, filesystem and in-flight/durability-sensitive stores can form one logical recovery state.
- **Redis role-based durability:** Redis is not automatically cache/disposable/authoritative; classify actual upstream role.
- **Operator identity vs generated credential:** identity, credential, role, issuer, persistence and rotation are separate dimensions.
- **Publication/config discrepancy:** verify the artifact that is actually published; never invent a tag from repository configuration.
- **Full-stack architecture intersection:** dependency multi-arch support does not prove application-stack multi-arch support.
- **Encoded secret semantics:** Magic Variable family selection must match upstream encoding, not merely entropy/length.
- **Platform-derived configuration:** Compose service names can indirectly affect application environment via Coolify-generated `SERVICE_*` metadata.

##### Confirmed / strengthened

- upstream-first complexity provenance and no speculative infrastructure;
- exact Magic Variable identity and longest-family parsing;
- private internal stores and URL/proxy/Docker-DNS separation;
- native database init lifecycle and database topology semantics;
- idempotent/fail-closed one-shot bootstrap with authoritative post-condition verification;
- real product workflow acceptance including compilation/realtime rather than homepage health;
- nested YAML→Compose→shell→JavaScript dollar/interpolation validation;
- image downgrade is not data rollback after incompatible migrations.

##### Overleaf-specific boundary

Keep CE 6.2.2/image digest, service name `overleaf`, historical `sharelatex/sharelatex` image namespace, Mongo 8.0.29 replica set, Redis 7.4.11/AOF, `/var/lib/overleaf`, exact trusted-proxy configuration, AMD64 pin, exact adminbootstrap and exact Magic Variables local to Golden #9. Do not import Server Pro sandboxed compiles, sibling containers, Docker socket or internal process decomposition by analogy.

`SERVICE_URL_SHARELATEX` remains a syntactically valid parser fixture but is intentionally **not** used in the Golden; RC1 proved the generated `SHARELATEX` environment names are rejected by Overleaf 6.2.2. RC4 uses `SERVICE_URL_OVERLEAF`.

##### Release discipline

The Skill remains in the `1.0.0-rc*` family. Historical five/six/seven/eight-benchmark audits and RC1→RC3 Overleaf candidates remain historical evidence. The previous eight Golden fixtures remain byte-identical. Structured source is authoritative; portable `.SKILL.md` and `.txt` are regenerated and validated byte-identical afterward.

#### 1.0.0-rc5 — Mem0 Golden Case #8 promotion

Mem0 v2.0.19 is promoted to **Golden / Regression Case #8** after the exact first RC1 candidate passed static validation, fresh Coolify deployment, remote Git source builds, PostgreSQL/API health and operator-confirmed requested runtime acceptance. The Golden preserves the exact RC1 bytes with SHA-256 `b2f2b6442a49275f692e5bd586a20f6d35a109538df56e2f82055ccd86b1fcc7`.

##### Added / promoted

- `references/mem0-case-study.md`;
- `assets/mem0-v2.0.19-v1.0.0-golden.yml`;
- `references/eight-benchmark-audit.md`;
- Mem0-specific positive Golden invariants;
- Magic Variable regression fixtures for `SERVICE_PASSWORD_64_MEM0DB`, `SERVICE_PASSWORD_64_MEM0JWT`, `SERVICE_URL_MEM0`, `SERVICE_URL_DASHBOARD`;
- external-provider secret and provider-healthcheck REVIEW coverage;
- evaluation prompts for provider credentials, application-issued API keys, browser/Docker DNS, CORS, migration ownership, pgvector activation, auxiliary persistence, remote Git builds, first-user setup and AI anti-contamination.

##### New generalizable lessons

- **Secret origin taxonomy:** deployment-generated, application-issued, operator-provided and external-provider-issued credentials have different issuers/lifecycles and must not be conflated.
- **Provider-independent readiness:** local service health and external AI provider availability are separate evidence layers.
- **Browser Origin / CORS Map:** separate frontend/API deployments require caller-aware public/internal URL and CORS analysis.
- **Migration ownership:** preserve upstream startup/sidecar/operator migration ownership; migrations do not imply a migrator service. Automatic startup migrations affect restore ordering.
- **Mixed-state recovery:** a primary SQL DB does not prove all durable state is SQL; backup follows the complete store/path inventory.
- **Remote source builds:** an immutable remote Git build can be valid when justified and runtime-tested; source pinning is not full dependency reproducibility.
- **First-run wizard:** secure upstream browser-first setup can be the correct One-Click application contract.
- **Prevented failure:** first-candidate runtime success can validate accumulated preventive rules when the accepted runtime demonstrates those risks were avoided before deployment.

##### Cross-benchmark confirmations

- upstream-first complexity provenance and anti-contamination;
- exact shared Magic Variable identity for one logical DB credential;
- private internal databases and no custom network by habit;
- public browser URLs are distinct from Docker DNS;
- PostgreSQL major upgrades require version-aware migration;
- Golden fixtures remain regression oracles, not templates.

##### Mem0-specific boundary

Keep Mem0 v2.0.19, commit `dc82354e143c2581d505d581a00286d6ef8c3605`, exact pgvector pin/digest, service names, `mem0_app`, `history.db`, `/setup`, current API-key prefix/model defaults/retention/Alembic details local to the case. Mem0 is **not** a generic AI stack pattern. Runtime Golden status is separate from official Coolify catalogue PR readiness.

##### Release discipline

The Skill remains in the `1.0.0-rc*` family. Historical five/six/seven-benchmark audits and the pre-runtime Mem0 acceptance artifact remain historical. Structured source remains authoritative; portable `.SKILL.md` and `.txt` are regenerated and validated byte-identical afterward.

#### 1.0.0-rc4 — ERPNext Golden Case #7 promotion

ERPNext is promoted to **Golden / Regression Case #7** as the first sibling-product benchmark built on an already-golden platform runtime. The accepted executable is the operator-selected RC5 candidate, preserved byte-for-byte as `assets/erpnext-v16.33.0-v1.0.0-golden.yml` with SHA-256 `64660809aba082409a41e20006d0d24dbc913928a30f07590ba873171ee2a7cb`.

##### Added / promoted

- `references/erpnext-case-study.md`;
- `assets/erpnext-v16.33.0-v1.0.0-golden.yml`;
- `references/seven-benchmark-audit.md`;
- ERPNext positive invariants and Frappe-vs-ERPNext anti-contamination checks in `validate_golden_cases.py`;
- ERPNext Magic Variable fixtures;
- `scripts/test_embedded_validation.py`;
- conservative nested literal-`sed` validation in `validate_embedded.py`;
- sibling-product regression prompts and product/business-acceptance rules.

##### ERPNext runtime-learning integration

The case distinguishes image capability, Frappe runtime, site/tenant state and installed product state. Existing Frappe-only sites are not silently converted to ERPNext; failed `list-apps` or partial site state fails closed. Fresh `install-app`/`--install-app` activation and later migrations are distinct lifecycle phases, and final `list-apps` post-condition verification is stronger than command exit alone.

The benchmark also records the real embedded-shell failure where malformed `sed` syntax passed `bash -n` but failed at runtime before site creation, plus the later healthcheck quoting error that sent literal `${FRAPPE_SITE_NAME}`. The validator now checks deterministic literal `sed` programs where practical and the documentation adds a Compose/shell/nested-parser dollar-context matrix.

##### Sibling-product and acceptance rules

- a proven platform Golden is causal knowledge, not permission for mechanical sibling substitution;
- a platform health endpoint does not validate an installed product;
- instance existence and product activation are independent state dimensions;
- persistent product-profile conversion requires explicit policy;
- infrastructure bootstrap must not invent company/currency/jurisdiction/accounting truth;
- product profiles install only the applications they actually claim.

##### Golden relationship

Frappe Golden #6 remains **Frappe-only**. ERPNext Golden #7 requires `frappe + erpnext`. The new validator locks both identities and rejects unrelated Frappe sibling apps in the ERPNext fixture.

##### Validation / release discipline

The release remains in the `1.0.0-rc*` family; successful ERPNext runtime acceptance does not by itself promote the Skill to `1.0.0`. Historical five- and six-benchmark audits remain unchanged snapshots. Structured source is authoritative; portable `.SKILL.md` and `.txt` are regenerated afterward and validated for source/hash synchronization.

#### 1.0.0-rc3 — Frappe Golden Case #6 promotion

The operator confirmed that the Frappe Framework benchmark completed the full required runtime acceptance path. The accepted RC3 executable behavior is promoted to **Golden / Regression Case #6**. RC4's optional Administrator login alias remains outside the immutable RC3 fixture until separately regression-tested.

##### Added / promoted

- `assets/frappe-framework-v16.32.0-v1.0.0-golden.yml`;
- `references/six-benchmark-audit.md`;
- Frappe positive invariants in `validate_golden_cases.py`;
- six-golden structural checks in `validate_skill.py`;
- six-golden anti-contamination evaluation coverage.

##### Frappe acceptance status

Golden promotion records operator-confirmed PASS for fresh deployment, automatic bootstrap, HTTPS, Administrator authentication, representative DB workflow, file persistence, worker execution, scheduler work, realtime/WebSocket behavior, normal restart, Coolify redeploy persistence, backup and isolated restore. The Skill does not fabricate missing log lines; the case study records the evidence class and preserves the exact accepted RC3 Compose as the regression oracle.

##### Cross-benchmark strengthening

- semantic gateway retention is now independently supported by OpenMRS, ODK and Frappe while OpenEMR remains the counterexample against mandatory gateways;
- worker/scheduler/realtime functional acceptance is now represented by a completed golden case;
- Frappe + OpenEMR explicitly reject both complexity inflation and service-count minimality;
- image capability vs site/tenant activation becomes a permanent regression principle;
- Magic Variable grammar/identity, one-shot lifecycle and URL/FQDN/proxy-target distinctions remain cause-based and version-scoped.

##### Bias controls

The new six-benchmark audit explicitly guards against overuse of Redis, workers, schedulers, WebSockets, semantic gateways, site-bootstrap helpers, randomized semantic usernames and bundled-app activation assumptions. **Golden fixtures remain regression oracles, not architecture templates.**

##### Release discipline

The historical five-benchmark audit and all five earlier Golden fixtures are preserved. No existing fixture is rewritten to resemble Frappe.

#### 1.0.0-rc2 — Frappe runtime-learning integration

Frappe Framework is added as a **partial runtime case study, not Golden Case #6**. RC1→RC3 supplied new failure/correction evidence for Coolify Magic Variable parsing, one-shot lifecycle, semantic gateways and URL/FQDN/port taxonomy. RC4's optional Administrator login alias remains statically validated only.

##### Added

- `references/frappe-framework-case-study.md` with the RC1→RC4 evidence timeline and explicit incomplete-gate ledger;
- `assets/frappe-framework-v1.0.0-rc3-runtime-case.yml` as a non-golden runtime evidence snapshot;
- longest-type-first Magic Variable parser coverage and a Magic Variable Ledger in `audit_compose.py`;
- `scripts/test_magic_variables.py` regression suite covering Frappe identifiers, compound types, service-bound URL/FQDN variables, shared-credential mismatch and identity rename review;
- optional `--previous-compose` audit path for detecting generated credential identity changes on the same binding.

##### Strengthened general rules

- **image capability is not application activation**: code present in an image does not prove an app/plugin is installed or enabled for a site/tenant;
- **platform edge proxy != application-semantic gateway**: Coolify can replace Internet TLS/ACME while an application gateway remains required for assets/files/realtime/routing semantics;
- credential Magic Variable IDs use a conservative alphanumeric default when current docs do not guarantee separator parsing; Frappe runtime demonstrated underscore-containing credential IDs can remain blank in Docker Compose Empty;
- Magic Variable types are parsed longest-match-first (`PASSWORD_64`, `REALBASE64_64`, `BASE64_128`, `HEX_64`, etc.);
- required generated values that initialize durable state should fail before bootstrap (`${VAR:?message}`) rather than reach a database/application as empty strings;
- Magic Variable names that initialized persistent state are deployment state, and changing a generated value does not prove the persisted credential rotated;
- browser canonical URL, public FQDN, proxy-target declaration, Docker hostname, internal application URL and realtime origin are separate concepts;
- one-shot `Exited 0` is valid when successful completion is the declared lifecycle;
- worker/scheduler/realtime acceptance requires real functional evidence, not only a running process or main-page HTTP 200;
- fixed upstream semantic accounts must not be randomized merely to consume `SERVICE_USER_*`.

##### Provenance / non-generalization

- Current Coolify docs enumerate generated families and bind URL/FQDN IDs to Compose service names. They do **not** establish a universal ban on underscores in every credential ID.
- The Frappe credential-ID separator behavior is recorded as **Frappe runtime demonstrated + Coolify issue evidence**, therefore the linter emits `REVIEW REQUIRED`, not a timeless Docker/Compose `ERROR`.
- Frappe's MariaDB/Redis/workers/scheduler/websocket/Nginx topology remains Frappe-specific. OpenEMR remains the explicit counterexample against complexity inflation.
- Frappe is not promoted to Golden #6 because Administrator authentication, real DB workflow, file persistence, worker/scheduler/realtime functional gates, restart/redeploy persistence, backup and isolated restore are not all proven.

##### Validation

- existing five golden invariants remain unchanged;
- new Magic Variable tests cover valid `FRAPPEDBROOT` / `FRAPPEADMIN`, problematic `FRAPPE_DB_ROOT`, `SERVICE_URL_FRONTEND`, `SERVICE_FQDN_FRONTEND`, `SERVICE_URL_FRONTEND_8080`, and compound type parsing;
- portable edition and `.txt` copy are regenerated from the structured skill and validated for source/hash synchronization.

#### 1.0.0-rc1 — five-benchmark V1 candidate consolidation

ODK Central v2026.2.4 becomes golden case #5 and triggers the first transversal audit across KoboToolbox, CKAN, OpenMRS, OpenEMR and ODK Central. This release is a **V1 candidate, not a maturity claim**.

##### Added

- ODK Central runtime case study and accepted regression fixture;
- five-benchmark architecture/bias audit;
- knowledge provenance levels (upstream, Coolify, runtime, cross-benchmark, application-specific);
- explicit complexity budget/provenance gate;
- flow-first network/callback decision model;
- ODK regression invariants and portable synchronization coverage.

##### Confirmed / strengthened

- current upstream + current Coolify facts outrank golden-case habits;
- shared magic credentials reuse one exact generated identity and remain stable after bootstrap;
- URL/FQDN/proxy-target/Docker-hostname roles must be separated before wiring;
- semantic application gateways are conditional, while `host-gateway` is never automatic;
- health, workflow, persistence, backup and isolated restore are separate evidence gates;
- official images may still depend on deployment files mounted externally by upstream Compose.

##### Refactored / demoted

- port-qualified service-ID underscore behavior moved from hard error to version-sensitive `REVIEW REQUIRED` because official Coolify documentation channels have evolved;
- one-shot stateful helpers are no longer warned merely for lacking a healthcheck when successful completion is the lifecycle signal;
- Kobo/CKAN/OpenMRS/OpenEMR/ODK mechanisms remain in their case studies; cross-benchmark guidance keeps only causal principles;
- production-readiness wording no longer implies untested HA, load, advanced security or disaster recovery.

##### Validator changes

- conservative review for empty entrypoint overrides in Coolify;
- conservative extraction/syntax-checking of obvious embedded JavaScript/Python heredocs inside shell commands;
- fifth golden fixture and stale four-benchmark wording checks.

#### 0.5.0 — OpenEMR minimality benchmark integration

OpenEMR 8.3.0 becomes the fourth runtime golden/regression case. The benchmark succeeded with a deliberately minimal two-service topology and completed application acceptance, redeploy persistence, backup, and restore.

##### New general rules

- **No wholly new platform rule was added.** OpenEMR did not contradict the existing upstream-first/minimal-adaptation method.
- The existing principle was made explicit as a **complexity provenance gate**: complexity must come from the current upstream or a documented Coolify-operational need, never from previous golden cases; raw service count is not a pass/fail metric.
- Candidate-vs-upstream capability-delta reporting was added as advisory regression evidence, not as an automatic architecture rejection.

##### Existing rules confirmed/strengthened

- native upstream initialization should remain responsible when it already provides the required lifecycle;
- internal databases stay private; generated credential identities remain persistent deployment state;
- application/file persistence must be mapped in addition to the primary database;
- image-local healthchecks must be proven in the selected image;
- Coolify can terminate public TLS while a simple upstream app remains on internal HTTP;
- health is not acceptance, and redeploy persistence plus real backup/restore remain separate evidence gates;
- warnings during bootstrap must be correlated with later runtime evidence before changing architecture.

##### OpenEMR-specific facts retained only in the case study/golden

- two-service `openemr + mysql` topology; MariaDB/OpenEMR image pins; port 80 route; `/meta/health/readyz`; `admin`/`openemr` usernames; exact `SERVICE_PASSWORD_64_OPENEMR*` IDs; three volume names/paths; native database-creation behavior; omission of `MYSQL_DATABASE`; and the tested MariaDB + `sitevolume` recovery path.

#### 0.4.0 — OpenMRS benchmark integration

OpenMRS 3.7.1 becomes the third golden/regression case after KoboToolbox and CKAN.

##### What OpenMRS added

- added `assets/openmrs-3.7.1-v1.0.0-golden.yml` with an explicit **NOT a generic Coolify skeleton** guard;
- added a full OpenMRS retrospective, failure/correction timeline, A/B/C/D/E learning classification, and regression acceptance path;
- reworked Coolify Magic Variable guidance from generic “generated secrets” into documented families, format selection, exact shared-variable reuse, persistence/rotation semantics, and a pre-deploy generation gate;
- added static detection for username/password generator swaps, true-Base64 mistakes, likely shared-credential mismatches, malformed magic declarations, and ambiguous/manual locally-generatable secrets;
- added syntax validation for embedded shell `command:` bodies, not only managed-file `content:` scripts;
- replaced golden anti-contamination token blacklists with positive fixture invariants plus advisory architecture similarity;
- added a three-benchmark abstraction review that promotes repeated causes/principles and demotes case-specific mechanisms;
- expanded the validation-status ladder through YAML, Compose, runtime, health, reachability, workflow, persistence, backup, and isolated restore;
- retained the rule that three successful applications are **not** evidence of final maturity; future heterogeneous benchmarks remain required.

<!-- END PORTABLE RESOURCE: CHANGELOG.md -->

<!-- BEGIN PORTABLE RESOURCE: LICENSE -->
<!-- SOURCE SHA256: 114c9b28027378a0f95e4429c36b05479a4dae29eaef9334cce95719da629283 -->
<!-- EMBEDDED SHA256: 79ae7dd516bebe6fcfb643127622bd774432116a859544029dd802ad2dab991c -->

## Portable resource: `LICENSE`

````text
MIT License

Copyright (c) 2026 coolify-architect contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
````

<!-- END PORTABLE RESOURCE: LICENSE -->

<!-- BEGIN PORTABLE RESOURCE: agents/openai.yaml -->
<!-- SOURCE SHA256: 1bc422ee4d74755618727b95bba863f798b1c0cf71a49d42a5683aecf6def0fe -->
<!-- EMBEDDED SHA256: 6e52c7655c431cd8591077e59800ba5d22ef9321a4dd77bc956bafc9338b8105 -->

## Portable resource: `agents/openai.yaml`

````yaml
interface:
  display_name: "Coolify Architect"
  short_description: "Adapt simple or complex OSS stacks to Coolify without inventing architecture."
  default_prompt: "Analyze this open-source application and produce or audit a production-grade Coolify Docker Compose candidate using upstream-first architecture discovery, explicit complexity provenance, Coolify-native routing, persistence, secret origin/lifecycle, external dependencies, readiness layers, and end-to-end validation. Distinguish image capability, runtime/platform, instance/site/tenant, installed product state, and business-level acceptance; when a prior Golden shares the runtime, reuse causal knowledge only after a sibling-product delta analysis. Preserve the target's real complexity; when upstream ships a Toolkit/installer, separate host orchestration from runtime and enforce edition/profile boundaries; distinguish internal process topology from Compose topology; identify coherent multi-store recovery groups; distinguish deployment-generated, application-issued and external-provider credentials; separate browser-visible URLs from Docker-internal URLs; and use precise evidence-stage labeling before any production-ready claim. Verify image-baked configuration before recreating repository mounts; preserve role-distinct state stores and native lifecycle primitives; validate healthcheck Host semantics and secret transport separately from application secret format; and treat an older runtime-accepted path outperforming a newer Skill as a first-class knowledge-accumulation regression. When upstream offers multiple supported deployment profiles, distinguish repair iterations from profile variants, select by target requirements, keep process decomposition independent from state externalization, and preserve validated alternatives without creating fake extra Goldens. For source builds, separate application revision pinning from transitive dependency closure; for modular products, separate framework health from product activation; for managed configuration, trace declared content through Coolify resource/host/mount/effective-file and interpolation layers; and keep internal proxy target ports separate from the canonical public origin. Golden fixtures are regression oracles, not architecture templates."
policy:
  allow_implicit_invocation: true
````

<!-- END PORTABLE RESOURCE: agents/openai.yaml -->

<!-- BEGIN PORTABLE RESOURCE: references/anti-patterns.md -->
<!-- SOURCE SHA256: d3f8afd4fc32760e432e4a7ad0302ee0e967b4607851f560640855fa90867f0d -->
<!-- EMBEDDED SHA256: d7b6a385efc444583e43a40005434cdbb3dbef3b11dd78719b3564c9675143ba -->

## Portable resource: `references/anti-patterns.md`

### Anti-patterns and failure modes

#### Architecture invention

**Bad:** infer a two-container stack from a product README while upstream actually runs workers, scheduler, renderer, cache, and multiple databases.

**Rule:** inspect real deployment artifacts before composing.

#### Random deletion of volumes

**Bad:** database reset as the first response to a 404, proxy error, malformed health check, or callback failure.

**Rule:** prove state corruption before proposing destructive recovery.

#### Generated-domain overreach

**Bad:** assume every `SERVICE_URL_*` is safe as the application's permanent canonical URL.

**Rule:** determine whether durable URL identity is an application requirement.

#### Public/internal URL conflation

**Bad:** replace every canonical HTTPS URL with `http://service:port` because containers can resolve it.

**Rule:** preserve canonical host semantics when callbacks, cookies, signatures, redirects, or upstream routing depend on them.

#### Custom networks by habit

**Bad:** define a bespoke network simply because a local Compose had one.

**Rule:** prefer Coolify-managed networking unless there is a proven requirement.

#### Manual proxy labels by habit

**Bad:** add Traefik labels while Coolify's domain model already handles the service.

**Rule:** use native Coolify routing unless advanced proxy behavior truly requires otherwise.

#### Database exposure

**Bad:** publish `5432`, `6379`, or `27017` to the host for application-to-database communication.

**Rule:** internal services communicate on the Compose network.

#### Floating production images

**Bad:** change stable pins to `latest` to “stay current.”

**Rule:** upgrades are deliberate events with migration review and rollback.

#### Arbitrary sleeps

**Bad:** `sleep 30 && app` as the default readiness strategy.

**Rule:** prefer health/readiness checks or upstream retry logic. Use a bounded sleep only when evidence justifies it.

#### Probe bugs mistaken for app bugs

**Bad:** conclude the app is down because a YAML-embedded `python -c` probe has leading whitespace and throws `IndentationError`.

**Rule:** inspect the probe independently from the service before changing app architecture.

#### Health-check deletion

**Bad:** remove a failing probe to make the dashboard green.

**Rule:** fix the probe or service; preserve operational signal.

#### Nginx variable substitution collision

**Bad:** run broad `envsubst` over a config containing `$host`, `$scheme`, or other Nginx runtime variables.

**Rule:** filter startup substitution to intended environment variables.

#### Stale resource IDs/domains

**Bad:** hard-code generated hostnames tied to an old Coolify resource UUID, recreate the resource, then forget to update callbacks/loopback mappings.

**Rule:** stable custom canonical domains are preferable when the app requires durable identity. Any unavoidable generated hostname must be treated as deployment-specific data.

#### Secret churn

**Bad:** rename `SERVICE_PASSWORD_*` identifiers during a cosmetic refactor, causing new credentials against an existing persistent database.

**Rule:** secret identifier stability is part of persistent state.

#### One-shot sidecar proliferation

**Bad:** split every bootstrap command into helper containers even when upstream entrypoints/init directories already provide idempotent initialization.

**Rule:** use the simplest architecture that preserves upstream semantics.

#### Route collapse

**Bad:** redirect an entire legacy/API hostname to the main frontend because both eventually use one backend.

**Rule:** preserve explicit route semantics and test real client paths.

#### “Green = production-ready”

**Bad:** all containers healthy, therefore production-ready.

**Rule:** run end-to-end application acceptance, persistence, and operations checks.

#### Compose as changelog

**Bad:** comments such as `V9 fix`, `V13 workaround`, `V19 preflight`, and repeated revision strings throughout scripts.

**Rule:** one revision marker when useful; history belongs in version control/change logs; runtime comments explain only current non-obvious constraints.


###### Port-qualified FQDN treated as host-only

**Bad:** assume `SERVICE_FQDN_APP_5000` is `app.example.org`, wire it directly into the application's canonical site URL, then leak `:5000` into browser redirects.

**Rule:** inspect current Coolify semantics and keep proxy routing identity separate from canonical public origin.

###### Compose dollar escaping copied into managed file content

**Bad:** write `$${DATABASE_USER}` inside a Coolify `content:` shell file because `$$` was needed in a Compose `command:` field.

**Rule:** generated file content is its own runtime interpolation context. Validate the file itself.

###### Sourced hook contaminates parent shell

**Bad:** add `set -u` or `set -euo pipefail` to a hook before checking whether upstream sources it, causing later upstream optional variables to become fatal.

**Rule:** inspect hook invocation semantics before changing shell options or global state.

###### Compose dependency gate used as worker supervisor

**Bad:** make a worker depend exclusively on `app: condition: service_healthy` during a long first bootstrap and assume Compose will start it later after the initial gate fails.

**Rule:** when architecture requires it, use application-level readiness/retry inside the long-running worker while preserving upstream worker commands.

###### Database engine green, application database broken

**Bad:** `pg_isready` passes, therefore application roles/databases/schemas must exist.

**Rule:** engine health and application initialization are separate states.

###### Golden case contamination / complexity inflation

**Bad:** add Solr/DataPusher because CKAN used them, Enketo/Mongo/host-gateway because Kobo used them, a gateway because OpenMRS used one, or any worker/cache/store/sidecar merely because previous successful fixtures looked more complex.

**Rule:** golden fixtures are regression oracles. Borrow causes/principles, never dependencies by association. **Complexity must be inherited from the current upstream architecture, not from previous golden cases.** Treat an added/removed capability without current-upstream or explicit operational provenance as a review item; do not use raw service count as the rule.


#### Magic variable generated-value mismatch

**Bad:** MariaDB creates a user with `${SERVICE_PASSWORD_64_DB}` while the application connects with `${SERVICE_PASSWORD_64_APP}`, although both are meant to represent the same database credential.

**Rule:** one logical shared credential must reuse one exact magic-variable name across every producer/consumer.

#### Username/password generator confusion

**Bad:** assign `${SERVICE_PASSWORD_64_DB}` to `DATABASE_USER`, or `${SERVICE_USER_DB}` to `DATABASE_PASSWORD`.

**Rule:** validate the semantic role of the generator as well as its syntax.

#### Fake Base64

**Bad:** use `SERVICE_BASE64_*` for an upstream key that requires actual Base64 encoding.

**Rule:** current Coolify docs explicitly distinguish random `BASE64` strings from `REALBASE64` encoding; select by upstream format.

#### Blank magic values discovered after bootstrap

**Bad:** start a one-time/stateful bootstrap without checking that every required generated credential is non-empty in Coolify.

**Rule:** add a pre-deploy generation gate for credentials/keys that must exist before persistent state is mutated.

#### Bootstrap env assumed to mutate persisted account

**Bad:** change an admin-password environment variable and assume an already-initialized application account has changed automatically.

**Rule:** distinguish bootstrap configuration from persisted identity state; verify the application's update semantics.

#### Transient proxy 503 treated as network-design proof

**Bad:** public `503 no available server` appears during first bootstrap, so immediately add a custom Docker network/host-gateway.

**Rule:** inspect proxy labels/target port, shared network attachment, direct service response, and Docker health history before altering topology.

#### Naive golden-case blacklist

**Bad:** reject a new target because it legitimately contains Redis, MariaDB, Solr, nginx, or another component seen in a golden fixture.

**Rule:** contamination is copying without current-upstream cause. Similarity is a review signal, not a token-level error.

#### Published image assumed to equal deployment bundle

**Bad:** switch from an upstream build to an official published image, then drop config/scripts that upstream Compose still mounts from the repository.

**Rule:** inspect upstream external mounts as part of the release contract. An image can be official and still intentionally depend on deployment files.

#### One-shot `Exited 0` treated as failure

**Bad:** require init/upgrade/bootstrap helpers to remain running or have long-running healthchecks after they have completed successfully.

**Rule:** classify service lifecycle first. For a true one-shot job, successful completion is the signal; for a daemon, continued running/readiness is the signal.

#### Application task runner invoked without checking CLI assumptions

**Bad:** pipe code through stdin into an application task wrapper that derives behavior/telemetry/config from the script path or CLI argv.

**Rule:** inspect the application task runner contract. Use a real script path or official CLI when required; do not bypass the application API with direct DB mutation merely to avoid the wrapper.

#### Frappe-derived/generalized anti-patterns

##### Plausible Magic Variable accepted without grammar review

**Bad:** assume `SERVICE_PASSWORD_64_MY_APP_DB_ROOT` will generate because it visually resembles documented syntax.

**Rule:** parse the complete documented type first, then review the identifier grammar separately. Current Frappe runtime evidence shows separator-bearing credential IDs can remain blank in Docker Compose Empty. Prefer conservative alphanumeric credential IDs unless the target Coolify version is explicitly verified.

##### Custom secret generator added before fixing platform generator identity

**Bad:** when a Coolify-generated credential is blank, immediately add an init container that runs `openssl rand`.

**Rule:** first verify the documented Magic Variable family, type parsing, identifier, target Coolify version and actual generated value. Preserve Coolify as generator when the platform primitive is correctable.

##### Generated-value change mistaken for persisted credential rotation

**Bad:** edit/regenerate `SERVICE_PASSWORD_*` and assume the database/application user's persisted password changed.

**Rule:** generated configuration and persisted application state are separate. Rotate through the application's supported mechanism and prove authentication/connection afterward.

##### Magic Variable rename treated as cosmetic

**Bad:** rename `SERVICE_PASSWORD_64_DATABASE` to `SERVICE_PASSWORD_64_DB` after initialization without a migration plan.

**Rule:** complete Magic Variable names that initialize durable state are deployment state. Renames can generate new values against old persisted credentials.

##### URL/FQDN/target-port conflation

**Bad:** because `SERVICE_URL_APP_8080` exists, store `https://domain:8080` as the application's canonical browser URL without checking Coolify semantics.

**Rule:** separate browser canonical URL, FQDN, proxy-target declaration, Docker hostname and internal application URL.

##### Platform proxy used as reason to delete semantic gateway

**Bad:** remove upstream Nginx because Coolify already has a reverse proxy, while Nginx serves assets/protected files or routes websocket/application paths.

**Rule:** distinguish platform edge proxy from application-semantic gateway before deleting anything.

##### Image contents treated as tenant/site activation

**Bad:** the image contains app/plugin X, therefore every site/tenant has X installed and enabled.

**Rule:** inspect actual activation state and lifecycle commands. Image capability is not application activation.

##### Fixed semantic account randomized for generator convenience

**Bad:** rename an upstream-special `Administrator`/`admin` identity only to consume `SERVICE_USER_*`.

**Rule:** preserve upstream-fixed account identity. Generate the secret separately; expose an application-native alias only when officially supported.

##### Running worker/scheduler treated as functional acceptance

**Bad:** `worker` and `scheduler` containers are running, therefore async and scheduled behavior works.

**Rule:** enqueue/trigger real application work and verify resulting state. Process state is only an earlier evidence stage.

##### Main HTTP page treated as realtime proof

**Bad:** homepage returns 200, therefore WebSocket/Socket.IO works.

**Rule:** test upgrade/polling, Host/Origin/auth/path/namespace semantics and a real realtime event when applicable.

#### ERPNext-derived sibling-product anti-patterns

##### Sibling Golden copy/rename

**Bad:** Frappe Golden works, so copy ERPNext Golden and replace `erpnext` with another sibling app name.

**Rule:** shared runtime provides causal knowledge only. Re-discover current sibling compatibility, dependencies, activation commands, persistent state, migrations, acceptance and recovery.

##### Site existence treated as product activation

**Bad:** `site_config.json` exists, therefore the product claimed by the One-Click is installed.

**Rule:** model instance/site existence and installed/enabled product state independently. Query the target's authoritative activation state.

##### Unknown activation state converted into “absent”

**Bad:** `list-apps`/plugin registry/module query fails, so assume the product is missing and install it.

**Rule:** failed authoritative detection means **unknown state**. Fail closed and diagnose before mutation.

##### Silent product-profile conversion

**Bad:** a persistent platform-only instance is automatically converted to the template's product profile on redeploy.

**Rule:** materially changing persistent product identity requires explicit operator policy and a supported conversion path.

##### App install exit zero treated as final proof

**Bad:** `install-app` returned 0, therefore product activation is complete.

**Rule:** when possible, verify the authoritative post-condition after durable activation.

##### Platform health treated as product acceptance

**Bad:** framework ping and platform Desk load, therefore the installed ERP/business product is validated.

**Rule:** validate at the highest layer claimed: infrastructure -> platform -> product. Use a representative product workflow rather than platform-only health.

##### Business truth fabricated during infrastructure bootstrap

**Bad:** choose a random company, country, currency, chart of accounts, tax regime or warehouse so the product opens fully configured.

**Rule:** technical bootstrap must not invent organization-specific business truth. Leave product onboarding to the owner unless authoritative values are explicitly supplied.

##### Image capability treated as default install list

**Bad:** install every app/module present in the image because the platform can host them.

**Rule:** a product profile explicitly defines which apps are enabled. Platform capability is not the default activation list.

##### Nested-tool syntax hidden behind valid Bash

**Bad:** `bash -n` passes, so assume an inline Compose command's `sed`, `awk`, regex, `jq`, SQL or other nested language is valid.

**Rule:** inspect the effective YAML -> Compose -> shell -> nested-command representation. For deterministic literal nested commands, execute a controlled syntax check when practical. Prefer simpler shell parameter expansion for trivial prefix/suffix transforms when equivalent; do not ban `sed` categorically.

##### Revision bug blamed on downstream services

**Bad:** `site-bootstrap` fails on a nested shell-tool syntax error before site creation, but troubleshooting starts by changing MariaDB, Redis, credentials or product topology.

**Rule:** diagnose the earliest proven failure in the execution chain before changing downstream architecture.



#### External/provider and AI-era anti-patterns

##### Fake provider credential from a Magic Variable

**Bad:** `OPENAI_API_KEY=${SERVICE_PASSWORD_64_OPENAI}` because both values “look secret”.

**Rule:** external-provider credentials must be issued by that provider and supplied/managed through the operator's secret workflow. Random generation does not create an account credential.

##### Application-issued API key replaced by deployment secret

**Bad:** replace a product's post-login API-key issuance/revocation model with `SERVICE_PASSWORD_*` for convenience.

**Rule:** preserve native application credential lifecycle unless upstream explicitly supports externally supplied keys with equivalent semantics.

##### External provider used as periodic local healthcheck

**Bad:** call OpenAI/Anthropic/Gemini every few seconds to decide whether the local container is healthy.

**Rule:** separate local readiness from external integration availability. Test provider workflows at acceptance level unless the actual runtime readiness contract requires the provider.

##### AI stack inflation

**Bad:** add Redis, workers, GPU services or Qdrant/Weaviate/Milvus merely because the target is an AI application.

**Rule:** preserve the target's current upstream provider/vector/queue architecture.

##### Browser configured with Docker-internal DNS

**Bad:** set a browser-exposed `NEXT_PUBLIC_API_URL=http://api:8000` because the frontend itself is containerized.

**Rule:** classify the caller. Browser-visible JavaScript needs a browser-resolvable public/canonical URL unless an application proxy intentionally hides the backend.

##### CORS positive case treated as full policy validation

**Bad:** dashboard origin works, therefore CORS is considered safe without checking whether unrelated origins receive equivalent permission.

**Rule:** when auth/credentials make CORS security-relevant, include a negative-origin acceptance case where practical.

##### Migration sidecar copied by analogy

**Bad:** extract `alembic upgrade head` into a new service because another Golden has a migrator.

**Rule:** preserve upstream migration ownership unless current target evidence requires a lifecycle change.

##### Extension image treated as active extension

**Bad:** use a pgvector/PostGIS/Timescale-capable image and infer the DB extension is created and used.

**Rule:** distinguish package availability, database activation/version and application use.

##### Primary SQL database treated as complete backup scope

**Bad:** back up PostgreSQL only while an auxiliary SQLite/file/state volume is durable and user-visible.

**Rule:** backup scope follows the full persistent-store inventory.

##### Automatic startup migration ignored during restore

**Bad:** start the application on an empty target before restoring, letting startup migrations create conflicting state.

**Rule:** determine restore ordering from migration ownership and schema behavior.

##### Source commit pin called a hermetic build

**Bad:** claim complete reproducibility because the Git commit is pinned while base images/transitive dependencies float.

**Rule:** report source reproducibility and dependency/supply-chain reproducibility separately.

##### Stale mutable image preferred by namespace alone

**Bad:** choose an old `latest` because the image name looks official, ignoring current upstream Dockerfiles/source deployment guidance.

**Rule:** verify publication recency, tags, current documentation and release relationship.

##### Automatic-admin requirement imposed on secure first-run wizard

**Bad:** invent an admin-init sidecar only to reduce first-run user interaction to zero.

**Rule:** a secure upstream first-user/setup wizard can be the correct One-Click application contract.


#### Overleaf-derived orchestration/state anti-patterns

##### Toolkit copied wholesale

**Bad:** every upstream Toolkit/installer command becomes a Coolify service.

**Rule:** separate host orchestration/lifecycle from runtime requirements. A Toolkit is evidence about runtime, not automatically runtime.

##### Internal microservices exploded into Compose

**Bad:** split a supervised application image into one Compose service per internally named process.

**Rule:** internal process topology and Compose topology are different abstraction layers; preserve upstream container boundaries unless current evidence requires a split.

##### Edition/profile contamination

**Bad:** import Server Pro/commercial/sandbox/enterprise requirements into a Community/OSS profile because they share a product name.

**Rule:** select the exact edition/profile first. Features from another edition are not requirements until the target edition's current upstream proves them.

##### Docker socket added because the app compiles

**Bad:** the product compiles LaTeX/media/code, therefore mount `/var/run/docker.sock` or add a runner.

**Rule:** validate the selected edition's actual execution model before granting privileged host orchestration.

##### Redis assumed disposable

**Bad:** Redis is not the primary database, therefore delete its persistence and treat it as cache.

**Rule:** classify Redis as authoritative, in-flight/durability-sensitive, queue, session, cache or reconstructable from upstream behavior.

##### Primary database treated as full recovery set

**Bad:** Mongo/PostgreSQL backup is called a full backup even though application files or durability-sensitive queue/session state also matter.

**Rule:** inventory all stores and identify coherence groups that require coordinated capture/restore.

##### Whole application volume classified with one label

**Bad:** `/data` is “all authoritative” or “all cache” without subpath inspection.

**Rule:** classify important subpaths independently when they have different recovery semantics.

##### Single database container treated as standalone semantics

**Bad:** remove replica-set/cluster mode because only one database container is deployed.

**Rule:** preserve the database topology semantics expected by the application regardless of container count.

##### Generated admin secret overwrites persisted identity

**Bad:** regenerated deployment password is forced onto an existing account during normal redeploy.

**Rule:** preserve existing account identity/credentials unless the application-supported rotation workflow is explicitly requested and verified.

##### Operator identity fabricated by generator

**Bad:** generate a fake email/tenant/organization identity merely because Coolify can generate random usernames.

**Rule:** model identity separately from credential. Generate only values whose issuer/semantics are local and random by contract.

##### Platform-generated metadata ignored

**Bad:** inspect only explicit `environment:` entries and assume the Compose service name is cosmetic.

**Rule:** include platform-derived `SERVICE_*`, labels, DNS aliases and similar metadata in effective-configuration review when the platform creates them.

##### Magic grammar success treated as application compatibility

**Bad:** a `SERVICE_URL_*` parses correctly, therefore the application must accept the generated variable name/value.

**Rule:** platform grammar and application semantic compatibility are separate gates.

##### Dependency multi-arch implies full-stack multi-arch

**Bad:** database/cache images support ARM64, therefore the application stack is declared ARM64.

**Rule:** full-stack architecture support is the intersection of all required runtime components.

##### Repository config treated as proof of published artifact

**Bad:** construct an image tag from a Toolkit/repository version file without verifying that the artifact exists.

**Rule:** verify the actually published deployment artifact and document discrepancies; never invent tags.

##### Nested JavaScript dollar consumed by Compose

**Bad:** put a JavaScript/Mongo operator such as `$set` directly inside a Compose `command:` heredoc and validate only Node source syntax.

**Rule:** validate Compose interpolation too; literal dollar tokens for nested languages may require `$$` in Compose source so the runtime receives `$`.

#### NetBox-derived self-contained-image and meta-regression anti-patterns

##### Upstream config bind assumed to prove missing image config

**Bad:** copy an entire repository `configuration/` directory into Coolify because upstream Compose bind-mounts it.

**Rule:** inspect Dockerfile/image construction first. A bind mount may overlay configuration already baked into the image.

##### Managed-file overuse

**Bad:** convert every upstream config file into Coolify inline `content:` even when the selected image already contains a valid baseline.

**Rule:** prefer image baseline + supported environment + smallest justified override. Managed files are a response to an artifact gap, not a default architecture style.

##### Same engine merged despite different state semantics

**Bad:** collapse NetBox tasks and cache Valkey into one service because both speak the Redis protocol.

**Rule:** same technology does not imply same durability, security, failure or flush semantics. Classify roles before merging.

##### Queue treated as low-sensitivity cache

**Bad:** expose or weakly protect a task queue because it is not the primary database.

**Rule:** queue writers are part of the worker execution trust boundary. Preserve upstream privacy/authentication and role-specific durability.

##### Native lifecycle wrapped without a proven gap

**Bad:** add a migrator/admin-init helper when the selected image already owns migrations and idempotent first-admin creation.

**Rule:** preserve the native primitive until current upstream or runtime evidence demonstrates a gap.

##### Loopback hostname normalized blindly

**Bad:** change `localhost` to `127.0.0.1` (or reverse) in an HTTP probe because both reach loopback TCP.

**Rule:** Host-header/allowed-host validation can make them HTTP-distinct. Preserve the hostname proven by upstream/runtime.

##### Secret transport failure generalized into symbol ban

**Bad:** one generated `$` breaks `.env` interpolation, therefore symbols are declared universally invalid application secrets.

**Rule:** distinguish application secret requirements, generator output and serialization/transport. Fix the failing layer without inventing a broader application restriction.

##### Newer Skill assumed automatically superior

**Bad:** a newer release needs more iterations on the same benchmark, but the older accepted path is ignored because the newer corpus contains more Goldens.

**Rule:** treat the divergence as a first-class regression. Compare the older successful reasoning/output with the newer rules, preserve legitimate new knowledge and narrow only over-generalized scope.

#### Multi-profile reasoning anti-patterns

##### Counting profile exploration as failures

**Wrong:** all-in-one, distributed, and external-DB experiments are three failed iterations.

**Why wrong:** they are different intentional operational profiles. Repair iteration counts only causal corrections inside one selected profile.

##### `distributed == production`

**Wrong:** split a vendor-supported all-in-one solely because production is requested.

**Why wrong:** production properties come from the target's scaling, isolation, observability, recovery and operational requirements, not container count.

##### `all-in-one == development`

**Wrong:** reject a supported multi-process image merely because several runtime roles share one container.

**Why wrong:** supervised multi-process images can be intentional upstream deployment primitives.

##### State externalization implies process decomposition

**Wrong:** production docs recommend external PostgreSQL, therefore backend/frontend/workers must also be separate Compose services.

**Why wrong:** state topology and process topology are independent dimensions.

##### Semantic proxy deletion

**Wrong:** remove Caddy/Nginx because Coolify already has a reverse proxy.

**Why wrong:** Coolify can own Internet ingress/TLS while the application gateway owns API/frontend/realtime/static/media/tenant routing.

##### Version-workaround permanence

**Wrong:** a workaround fixed release X, so every future release must keep it.

**Why wrong:** workarounds belong to their demonstrated causal version/profile until revalidated.

##### Health-root-path bias

**Wrong:** `http://127.0.0.1/` returns 404, therefore the web service is broken.

**Why wrong:** Host/path may deliberately enter tenant/domain/product routing. Inspect route semantics or use a dedicated local liveness endpoint.

#### OpenSPP-derived runtime-layer anti-patterns

##### Source commit pin treated as dependency-closure pin

A pinned application source is called hermetic even though Dockerfile/install scripts still fetch mutable branches, repositories or unlocked dependencies.

**Fix:** build the dependency-closure map and classify reproducibility honestly.

##### Dependency drift patched without causal scope

A compatibility shim fixes one mutable dependency drift and is retained forever.

**Fix:** bind it to the exact application/dependency contract, prefer an immutable compatible dependency when available, and revalidate/remove on upgrade.

##### Framework health treated as product activation

`/health` returns 200, therefore the selected module/plugin/app bundle is assumed installed.

**Fix:** use the authoritative activation state for the claimed product profile.

##### Worker released during shared initialization

A web listener is alive, so a worker starts while the same registry/schema/module state is still mutating.

**Fix:** gate on the smallest authoritative initialization/activation boundary unless upstream explicitly supports concurrency.

##### Coolify pre-pull behavior treated as Docker law

A platform-specific `pull_policy` workaround is taught as universal Compose behavior.

**Fix:** scope it to the demonstrated Coolify path/version and re-check current behavior.

##### Compose content treated as effective file

The YAML `content:` changed, so the mounted/loaded config is assumed changed.

**Fix:** verify managed-resource identity -> host file -> container mount -> consumer-loaded file -> runtime behavior.

##### Managed-file path churn by habit

Every content edit creates a new source/target path.

**Fix:** use a new identity only as an explicit invalidation mechanism after stale managed state is evidenced.

##### Compose specification treated as proof of Coolify support

A critical primitive is documented by Docker Compose, so it is assumed deployable through the current Coolify Service implementation.

**Fix:** verify spec -> parser -> persistence/model -> deployment support; REVIEW when incomplete.

##### Universal dollar escaping

All `$` tokens are rewritten as `$$` regardless of transport.

**Fix:** derive source representation from the actual interpolation/serialization layer map.

##### Unrestricted envsubst on native-dollar configuration

`envsubst` is run over a config language such as Nginx that also uses `$variables`, silently erasing native tokens.

**Fix:** whitelist only intended environment variables.

##### Internal proxy port becomes canonical browser port

An internal gateway listener such as `:8080` is forwarded as the public application origin merely because Coolify targets that port.

**Fix:** normalize to the actual canonical external scheme/host/port.

##### Single-database mode with public selector/manager left exposed

A selected production single-database/tenant profile constrains database identity, but the public gateway still exposes a selector/manager surface that contradicts that mode.

**Fix:** verify upstream single-database semantics and align the public surface to that selected mode. Do not copy OpenSPP/Odoo-specific routes to another target.

##### Different DB passwords assumed inconsistent

Two intentionally separate DB roles are treated as a wiring bug because they connect to one PostgreSQL service.

**Fix:** build a credential topology map; same logical account + different password is error, distinct intentional roles are not.

##### Last ERROR treated as root cause

A later serialization/retry/crash-loop error is fixed before the earlier causal fault.

**Fix:** build a causal timeline and separate root cause, downstream symptoms, retries and independent later faults.

<!-- END PORTABLE RESOURCE: references/anti-patterns.md -->

<!-- BEGIN PORTABLE RESOURCE: references/architecture-discovery.md -->
<!-- SOURCE SHA256: aeb1ba2384194547e6d706890ccc20bd4e9d31062f5155cba0f0591f96e2d18e -->
<!-- EMBEDDED SHA256: fda6564a2eb220d0e4660e7f0a78fe9d7bc401682744c1d38d67a9804cf9a9a6 -->

## Portable resource: `references/architecture-discovery.md`

### Architecture discovery worksheet

Complete this before generating a production Compose.

#### A. Service inventory

| Service/process | Role | Image/build | Command/entrypoint | Internal port | Public? | Depends on | Upstream provenance / Coolify justification |
|---|---|---|---|---:|---|---|---|
| | | | | | | | |

Roles: platform edge, application-semantic gateway, frontend, API/app, worker, scheduler, database, cache, broker, object store, one-shot init, one-shot migration, dependency, optional integration.

For every candidate service/capability, record whether it is preserved from upstream, replaced by a Coolify platform responsibility, added for a documented operational/runtime reason, or intentionally omitted because it is optional for the selected profile. Apply the same provenance check to proxies, sidecars, init jobs, volumes, custom networks, workarounds, and embedded scripts. A count difference is only context; justification is the actual gate. Missing provenance is REVIEW REQUIRED.

Questions before moving on:

- Which candidate services/capabilities are absent from the selected upstream baseline?
- For each addition, what current-upstream behavior or explicit Coolify operational need requires it?
- Which upstream services were removed, merged, or replaced, and who now owns that responsibility?
- Did any worker/cache/gateway/search/object-store/init service appear only because a previous golden case had one?
- Would removing an apparently optional service break a documented upstream workflow?

Do not use a maximum/minimum service-count rule. The target architecture decides the legitimate complexity.

When upstream ships a Toolkit/installer/wrapper, also create a **Host Orchestration vs Runtime Responsibility Map**:

| Upstream responsibility | Host-side orchestration? | Runtime requirement? | Replaced by Coolify? | Candidate representation | Evidence |
|---|---|---|---|---|---|
| | | | | | |

Do not translate every host lifecycle script into a container. Also inventory important internal processes inside each upstream image before deciding whether they are intentionally supervised together or truly separate Compose roles. **Internal process topology and Compose service topology are different abstraction layers.**

Record the selected **edition/profile** and list features explicitly excluded because they belong to another edition/profile. Features from a sibling/commercial edition are not requirements until current target-edition upstream proves they are.

#### B. URL matrix

| Component | Public browser URL/origin | Internal Docker URL | Canonical callback URL | Coolify proxy target service:port | Routing port visible to browser? |
|---|---|---|---|---|---|
| | | | | | |

Questions:

- Does the application store canonical URLs in the database?
- Does any container call its own public domain?
- Are webhook signatures or absolute links domain-sensitive?
- Does authentication span multiple sibling domains?
- Are websocket/SSE routes on the same public host?
- Is a generated Coolify hostname acceptable, or is a stable custom domain required?
- Does a port-qualified Coolify URL/FQDN variable contain a routing port that must **not** appear in the application canonical browser origin?
- Does upstream explicitly support an internal callback/fetch base distinct from the public site URL?
- For each edge, is the caller a browser, external system, Docker service, callback processor, or a service intentionally re-entering the host proxy?

#### C. Persistence matrix

| Service | Path/database | Data class | Persistent? | Backup priority | Restore method |
|---|---|---|---|---|---|
| | | authoritative/in-flight-durability-sensitive/queue/session/cache/reconstructable | | | |

Questions:

- Which paths contain user uploads?
- Which paths contain generated static assets that can be rebuilt?
- Which Redis instances contain disposable cache versus durable queue/application state?
- Are database files safe to back up as raw volumes, or should logical/database-aware backups be used?
- Do multiple stores form one **coherence group** that must be quiesced/flushed/captured together?
- Does a large application volume contain subpaths with different authority/reconstructability classes?

#### D. Secret and configuration matrix

| Variable | Consumed by | Identity/credential/role | Issuer/origin | Type/encoding constraint | Generate with Coolify? | Magic identity / shared with | Must remain stable? | Rotation |
|---|---|---|---|---|---|---|
| | | secret/config/public URL | | | |

Secret classes:

- database credentials;
- application secret/signing key;
- encryption key;
- API key shared between internal services;
- superuser bootstrap password;
- SMTP credentials;
- OAuth/provider credentials.

Do not use a generated value merely because it is convenient. Confirm required length, character set and **encoded format semantics**, then verify that the generated value survives its actual transport/serialization path unchanged. A value can satisfy the application contract yet fail in `.env`/Compose/shell interpolation. Operator identity (for example an email) can be operator-provided while its password is platform-generated.

##### D1. Credential Topology Map

When multiple roles reach the same database/service, map role identity before treating different passwords as a wiring error.

| Role/account | Database/service | Owner | Bootstrap/migration responsibility | Runtime privilege | Producer | Consumer(s) | Credential variable | Superuser? | CREATEDB/create privilege? | Persistent identity? |
|---|---|---|---|---|---|---|---|---|---|---|
| | | | | | | | | | | |

A shared logical account with inconsistent generated passwords is an error. Two intentionally different roles are not an error merely because they connect to the same PostgreSQL/MySQL service.

#### E. Startup graph

Draw dependencies as a DAG when possible.

Example:

```text
postgres ----\
mongo --------+--> app migrations/bootstrap --> app ready --> workers
redis --------/
```

For every edge ask:

- Does the downstream need the process running or actually ready?
- Does upstream already retry?
- Is a health check available?
- Could this create a circular readiness dependency?
- Is this edge only a one-time Compose startup gate, or does the long-running process need its own retry/readiness loop?
- If a worker later reports unhealthy while processing jobs, what independent runtime evidence can validate the probe?
- Does the selected image already own migrations/bootstrap/config generation, and what proven gap would justify wrapping or replacing that primitive?
- If an HTTP probe uses `localhost` or `127.0.0.1`, does application Host-header validation make those names semantically different?
- Is the framework merely listening, or is the selected module/plugin/app/product profile authoritatively activated? If workers share mutable registry/schema/module state, must they wait for that activation boundary?

#### F. External dependency map

For any dependency outside Compose, record at least: dependency/provider, purpose, caller, credential origin, required for local readiness?, required for product workflow?, timeout/retry behavior, and failure attribution.

Document anything outside Compose:

- DNS;
- SMTP;
- S3/object storage;
- OAuth;
- external APIs;
- license server;
- callback allowlists;
- CDN;
- external database.

A Compose can be syntactically valid while the application is unusable because these are absent. Do not make local periodic health depend on a paid/external provider unless that coupling is intentionally required.

##### F0. Image Baseline / External Artifact Map

When upstream Compose mounts repository-local configuration, scripts, templates or env files into an image, inspect the selected Dockerfile/image before reproducing those mounts.

| Upstream mounted artifact | Mount target | Already present in image? | Image source/provenance | Runtime-required override? | Candidate action |
|---|---|---|---|---|---|
| | | | | | preserve image / minimal override / managed full artifact |

A bind mount proves that the upstream deployment overlays a path. It does **not** prove that the image lacks a usable baseline at that path. Prefer image baseline + supported environment + smallest justified override when that preserves current upstream behavior.

##### F1. Browser Origin / CORS Map

For a separate browser frontend/API, record:

| Frontend public origin | API public origin | Browser-facing API URL | Internal API URL | Allowed origins | Credentials mode | Preflight behavior | WebSocket/SSE origin if any |
|---|---|---|---|---|---|---|---|
| | | | | | | | |

A browser-visible URL must be resolvable by the browser; Docker Compose DNS is not a browser namespace. When security-relevant, acceptance should include both the intended-origin positive case and an unrelated-origin negative case.

##### F2. Secret Origin Map

For each secret/credential record:

| Name | Purpose | Issuer/origin | Generation phase | Storage owner | Rotation mechanism | Persistent-state impact | Recovery requirement |
|---|---|---|---|---|---|---|---|
| | | platform/application/operator/external provider | | | | | |

Distinguish deployment-generated secrets, application-issued credentials, and external-provider-issued/operator-supplied credentials. A secret-shaped variable is not automatically generatable by Coolify.

##### F3. Migration Ownership Map

Record who owns each migration: application startup, one-shot service, database init directory, operator command, or external platform task. Capture failure semantics and restore ordering. Do not infer `migration exists -> migrator sidecar`.

##### F4. Persistent Store Inventory

Inventory every durable store independently: logical databases/schemas, object/file stores, named volumes, bind mounts, SQLite/local state, generated configuration and deployment secrets required for recovery. A primary relational DB does not prove it contains all authoritative/user-visible state.

##### F5. Build Reproducibility / Dependency Closure Map

A release/tag/commit pin identifies the application source. It does **not** automatically freeze the build graph.

For every dependency resolved by Dockerfile, install script, package manager, Git clone, curl/download, package repository, plugin manager, or runtime bootstrap record:

| Component | Source | Version/ref | Immutable? | Resolved at build time? | Resolved at runtime? | Transitive? | Operator-controlled? | Upstream-controlled? | Reproducibility risk |
|---|---|---|---|---|---|---|---|---|---|
| | | | | | | | | | |

Classify the build:

- **HERMETIC** — dependency closure is proven immutable/reproducible for the claimed scope;
- **PARTIALLY PINNED** — some important resolution points are pinned and others are not;
- **FLOATING TRANSITIVE DEPENDENCIES** — mutable transitive refs remain;
- **UNKNOWN** — evidence is incomplete.

A pinned application tag/commit is not sufficient for `HERMETIC`. If a mutable dependency later breaks a released app, identify the exact contract drift, prefer a compatible immutable dependency pin when available, otherwise use the smallest version/dependency-scoped compatibility boundary and schedule it for removal/revalidation on upgrade.

When repository configuration, release metadata and published image/artifact tags disagree, record the discrepancy and verify the actually published artifact instead of inventing a tag.

Compute target architecture support as the **intersection** of all required runtime components/images; dependency multi-arch support does not prove full-stack multi-arch support.

##### F6. Managed File Provenance Ledger

For every Coolify-managed inline/bind file whose runtime behavior matters, record:

| Declared source path | Container target | Coolify resource identity | Creation/update behavior | Rendering/interpolation stage | Effective file checksum/content evidence | Consumer | Reload/restart requirement |
|---|---|---|---|---|---|---|---|
| | | | | | | | |

Keep these concepts separate:

```text
Compose declared content
!=
Coolify managed-file resource
!=
host file
!=
container-mounted file
!=
application-effective configuration
```

If a managed-file fix is not reflected at runtime, inspect those layers before changing application logic. A new source/target path can be an explicit invalidation mechanism only after current Coolify behavior is verified and the effective runtime file is rechecked; do not create new paths reflexively on every edit.

##### F7. Interpolation / Serialization Layer Map

For executable/config text containing `$`, templating tokens, backslashes, quotes, or nested-language syntax, map each interpretation layer.

| Token | Source representation | Compose interprets? | Coolify transforms/writes? | Shell interprets? | envsubst interprets? | Application/config parser interprets? | Required effective runtime representation |
|---|---|---|---|---|---|---|---|
| | | | | | | | |

Never encode a global rule such as `always escape $ as $$`. Derive the representation from the actual transport path. Compose command text, Coolify managed-file content, shell heredocs, `envsubst`, JavaScript, Nginx, SQL and other parsers can require different source forms.

When `envsubst` is required and the target configuration language also uses native `$variables`, prefer an explicit whitelist of environment variables to substitute.

#### G. Acceptance-path selection

Pick at least one end-to-end path crossing the important services.

Examples:

- create account -> login -> create object -> worker processes job -> result visible;
- create form -> preview -> deploy -> anonymous access -> submit -> data visible;
- upload media -> background processing -> public delivery;
- receive webhook -> queue -> worker -> database update.

This path becomes the release acceptance test.

#### H. Platform / product activation map

For modular, plugin-based or tenant-aware systems, map these independently:

| Layer | Question | Authoritative evidence |
|---|---|---|
| image capability | what code/packages are present? | image/package/app inventory |
| runtime/platform | what framework/runtime is active? | native runtime state |
| instance/site/tenant | does the target instance exist and initialize cleanly? | platform-native instance registry/config |
| installed/enabled product | which apps/plugins/modules are active for that instance? | native activation registry such as `list-apps` |

Do not infer a lower row from an upper row.

If a previous Golden shares the platform/runtime, also produce a **Sibling Product Delta**:

```text
BASE PLATFORM:
SHARED INFRASTRUCTURE:
PRODUCT-SPECIFIC STATE:
PRODUCT-SPECIFIC BOOTSTRAP:
PRODUCT-SPECIFIC MIGRATION:
PRODUCT-SPECIFIC ACCEPTANCE:
PRODUCT-SPECIFIC RECOVERY:

Inherited:
Revalidated:
Changed:
Why:
```

For persistent state classify at least:

```text
instance absent / product absent
instance present / product present
instance present / product absent
instance partial or activation state unknown
```

The last state must fail closed before mutation unless current upstream supplies an authoritative recovery procedure.

#### I. Evidence-stage ledger

Track validation stages separately; never infer later stages from earlier ones:

| Stage | Evidence | Status |
|---|---|---|
| YAML parse | parser output | |
| Compose render/schema | `docker compose config` or equivalent | |
| Containers started | runtime state | |
| Healthchecks | Docker/Coolify health | |
| Public application reachable | HTTP/browser | |
| Representative workflow | app-level create/read/update etc. | |
| Persistence | restart/redeploy test | |
| Backup | actual backup artifact | |
| Restore | isolated restore + data verification | |
| Production/ops handover | security, monitoring, capacity, upgrade/rollback | |

###### J. Application activation / image capability map

For platforms with plugins/apps/modules/tenants/sites, distinguish:

```text
code present in image
-> globally enabled capability
-> installed/activated on site or tenant
-> configured and usable feature
```

Do not infer activation from image contents. Record the command/config/DB/site state that proves activation (`list-apps`, plugin list, module registry, tenant configuration, etc.).

#### Deployment Profile Selection Gate

Before writing a candidate, enumerate every current-upstream-supported deployment profile that could satisfy the stated target. Examples include all-in-one, distributed services, embedded vs external database, embedded vs external cache/broker, or semantic proxy variants.

Create a **Deployment Profile Capability Matrix** with columns:

| Field | Required question |
|---|---|
| profile | What exact upstream deployment profile is this? |
| upstream status | Official/recommended/advanced/community/unsupported? |
| services | What Compose/process shape does it use? |
| public shape | What is publicly routed? |
| authoritative state | Which stores/files are authoritative? |
| worker model | Internal supervised or separate workers? |
| realtime | How is websocket/realtime delivered? |
| semantic proxy | Does a gateway own product routing beyond TLS? |
| migration owner | Which process runs migrations? |
| first-user lifecycle | Native wizard/bootstrap/operator step? |
| backup primitive | What upstream backup mechanism remains valid? |
| restore primitive | What exact recovery set is required? |
| scaling model | Which roles can/need to scale independently? |
| operational complexity | Failure domains, observability, credentials, shared FS? |
| runtime evidence | PASS/PARTIAL/UNKNOWN etc. for this exact profile? |

Then evaluate upstream support, single-server target, horizontal scaling, failure isolation, observability, state isolation, backup/recovery, upgrade complexity, resource control, shared filesystems and semantic gateway responsibilities.

Do not choose `fewest containers` or `most containers` as a goal. Choose the least operationally complex current-upstream-supported profile that preserves the target's required responsibilities, state boundaries and recovery contract.

##### Independent deployment dimensions

Do not conflate:

```text
process decomposition
state externalization
public routing
lifecycle ownership
```

They are independent unless current upstream evidence explicitly couples them. An all-in-one application can use external PostgreSQL; an external database recommendation does not automatically require separate backend/frontend/workers.

<!-- END PORTABLE RESOURCE: references/architecture-discovery.md -->

<!-- BEGIN PORTABLE RESOURCE: references/baserow-case-study.md -->
<!-- SOURCE SHA256: 10b9583b1b3abc71a381cfc43e7f70e5176cfe883159fe81b5ebc076992bfa02 -->
<!-- EMBEDDED SHA256: ebe188471322ad6128994e41c99acad487771088799364fb766a45677820b030 -->

## Portable resource: `references/baserow-case-study.md`

### Baserow 2.3.3 on Coolify — Golden / Regression Case #11

Baserow is the first Golden in this corpus whose benchmark intentionally produced **multiple deployment-profile outcomes** rather than one simple RC repair lineage.

The central learning is:

```text
REPAIR ITERATION
!=
DEPLOYMENT PROFILE VARIANT
```

The benchmark evaluated three materially different profiles:

```text
PROFILE A
Official upstream all-in-one path
-> useful architecture reference
-> tested 2.3.3 fresh-install path not accepted

PROFILE B
Coolify-adapted distributed/custom architecture
-> runtime functional / operator accepted
-> Canonical Golden #11

PROFILE C
Official all-in-one application
+ external PostgreSQL
-> runtime functional / operator accepted
-> Validated Alternative Profile
```

The exact runtime-accepted distributed RC5 fixture is frozen at:

```text
assets/baserow-2.3.3-v1.0.0-golden.yml
SHA-256 143c3a94952b16e85638d87bd50fa29a49fa756b7c12cba097fe32383c4312f6
```

No later cleanup, architecture normalization, documentation reconstruction or alternative profile is folded into those bytes.

#### Why Profile B is the Canonical Golden

Golden selection is not a judgment that distributed Baserow is universally superior. Profile B is selected because the accepted RC5 fixture carries the richest regression surface from this benchmark while remaining operator-confirmed functional:

- explicit PostgreSQL + pgvector service;
- explicit Redis service;
- backend lifecycle ownership;
- web frontend;
- normal Celery worker;
- export worker;
- Celery beat;
- semantic Caddy gateway;
- media persistence;
- single Coolify-managed public origin;
- no public PostgreSQL/Redis;
- Host/path-safe dedicated gateway liveness;
- exact Baserow 2.3.3 migration/auth-provider sequencing safeguard that was required on this path.

That makes it the strongest regression oracle for the **causes actually encountered**. It does not make these services universal Baserow requirements.

#### Profile C remains a first-class validated alternative

The official all-in-one Baserow application with external PostgreSQL is retained separately at:

```text
references/baserow-validated-alternative-external-postgres-rc2.yml
```

It demonstrates a critical architecture principle:

> Upstream production guidance can recommend externalizing authoritative state without recommending decomposition of application process topology.

In this profile, Baserow can keep backend, frontend, workers, beat and embedded Caddy supervised inside the official application container while PostgreSQL has an independent lifecycle. This offers database state isolation and clearer PostgreSQL tooling without requiring every internal process to become a Compose service.

The alternative is **not** promoted as Golden #12 because the corpus needs one canonical accepted oracle per benchmark unless there is a compelling independent regression reason to number another case.

#### Profile A remains evidence, not a false failure count

The tested all-in-one 2.3.3 path is preserved at:

```text
references/baserow-reference-all-in-one-rc8.yml
```

The path exposed a fresh-install application-state problem around `PasswordAuthProviderModel` and concurrent startup/migration behavior. The candidate used a one-shot gate to run Baserow's native locked migrations and inspect/normalize the singleton provider before supervised processes began.

The important classification is:

```text
VERSION-SPECIFIC UPSTREAM DEFECT WORKAROUND
```

not:

```text
permanent Baserow architecture requirement
```

The official all-in-one image remains an upstream-supported profile. The observed failure belongs to the tested version/path and must be rechecked on future Baserow releases.

#### Deployment Profile Selection Gate

Baserow confirms that profile choice must be explicit. Compare:

- upstream support;
- target server count;
- horizontal scaling;
- per-role resource control;
- failure isolation;
- observability;
- state isolation;
- backup/recovery model;
- upgrade complexity;
- shared filesystem requirements;
- semantic proxy responsibilities;
- Coolify operational complexity.

The target rule is:

> Choose the least operationally complex current-upstream-supported profile that preserves the required product responsibilities, state boundaries and recovery contract for the stated deployment target.

There is no default rule that `all-in-one == dev` or `distributed == production`.

#### Process topology is independent from state topology

Baserow makes the distinction concrete:

```text
all-in-one app + embedded state
all-in-one app + external DB
all-in-one app + external DB + external Redis
separated application roles + external state
```

These are different choices along different axes. Externalizing PostgreSQL does not prove backend/frontend/workers should be separated. Conversely, separating application processes can be justified by scaling, isolation or observability even if state choices remain unchanged.

#### Official multi-process image is legitimate

The official Baserow all-in-one image supervises several roles, including backend, web frontend, Celery workers/beat and Caddy. The presence of multiple runtime processes inside one container is not itself an anti-pattern.

Decomposition becomes justified when the target requires properties such as:

- independent scaling;
- per-process CPU/memory controls;
- independent logs/observability;
- failure isolation;
- explicit distributed shared-state topology.

Simplicity is a target-specific optimization, not a universal architecture objective.

#### Distributed profile is also legitimate

The RC5 Golden keeps explicit application roles because this accepted profile gains concrete operational boundaries:

```text
baserow-backend
baserow-web-frontend
baserow-celery
baserow-celery-export
baserow-celery-beat
baserow semantic Caddy
PostgreSQL/pgvector
Redis
media permission one-shot
```

That structure is valid because it is tied to Baserow's standalone-process model and the selected target, not because more containers are inherently more production-ready.

#### Semantic Caddy survives Coolify

Baserow's Caddy is not only a public TLS edge. It owns application routing semantics such as:

```text
/                  -> web frontend / Application Builder semantics
/api/*             -> backend
/ws/*              -> realtime backend
/mcp/*             -> backend
/assistant/*       -> backend
/static/*          -> static files
/media/*           -> user media
```

Coolify should own:

```text
Internet ingress
TLS
public domain binding
```

Baserow Caddy should keep the application-semantic responsibilities behind that edge.

This confirms the same causal principle already seen in OpenMRS, ODK Central and Frappe:

```text
platform edge proxy
!=
application semantic gateway
```

#### Healthcheck Host + path semantics

RC5 fixed an important false-negative health pattern. Probing:

```text
http://127.0.0.1/
```

was not merely a local reachability test. The request's `Host: 127.0.0.1` and root path could enter Baserow Application Builder published-domain logic. A response such as a domain lookup 404 therefore did not prove Caddy/backend/frontend were down.

RC5 instead added a local-only Caddy endpoint:

```text
/__coolify_gateway_health
```

and separately checked the backend and frontend native health routes.

Generalized health semantics are:

```text
network target
+ Host header
+ path semantics
+ application router
+ authentication/state expectations
```

This extends the NetBox lesson. NetBox's issue was `localhost` vs `127.0.0.1` under host validation; Baserow's issue was a valid local request entering product routing semantics.

#### Migration ownership and version-specific workaround

Native lifecycle remains preferred. The accepted distributed profile nevertheless moved migration sequencing into the backend startup path because runtime evidence showed a correctness problem in the tested Baserow 2.3.3 fresh-install path.

The accepted RC5 backend:

1. waits for PostgreSQL using Baserow's native probe;
2. runs Baserow's native `locked_migrate`;
3. materializes/verifies the native singleton password provider;
4. starts the normal Baserow ASGI backend;
5. lets Celery roles wait on a migrated healthy backend.

This is an exception with a demonstrated cause. It must not become a generic Django pattern.

A future Baserow version should first re-test the native lifecycle and remove the workaround if upstream no longer exhibits the defect.

#### Singleton state repair policy

The Golden itself fails closed if more than one password provider is already present instead of deleting ambiguous persistent state automatically.

The broader rule is:

```text
zero singleton
-> safe native creation

one singleton
-> correct

multiple singleton + provably pristine state
-> narrowly repairable only with exact invariants and post-condition

multiple singleton + existing users/product state
-> fail closed / manual review
```

An automated migration helper must never choose an authoritative object arbitrarily after real state exists.

#### External PostgreSQL changes recovery semantics

The validated alternative proves that externalizing the database changes the recovery model. A database dump is not automatically a complete Baserow backup while user files remain on local media storage.

For external PostgreSQL + local file storage, the coherent recovery set is conceptually:

```text
PostgreSQL backup
+ media/files
+ stable cryptographic secrets/config
```

Changing deployment profile can therefore change which upstream backup primitive remains valid.

#### Redis semantics are product-specific

Do not copy NetBox's tasks-AOF rule into Baserow merely because both use Redis-compatible technology.

The correct process is:

1. identify the exact Baserow Redis role;
2. inspect current upstream durability requirements;
3. determine whether loss is authoritative, in-flight, reconstructable or cache-only;
4. configure persistence only from that evidence.

Same technology across products does not imply same persistence semantics.

#### URL Magic Variable lesson

The canonical browser origin in the accepted profile is:

```text
SERVICE_URL_BASEROW
```

This is deliberately distinguished from a potentially port-qualified routing representation. Routing-target metadata and canonical application origin can require different representations even when they describe the same public service.

#### Credential issuer boundaries

Coolify may generate deployment secrets such as:

```text
PostgreSQL password
Redis password
Django SECRET_KEY / JWT signing material
```

Baserow itself remains the issuer of application credentials such as user credentials and application/database/API tokens. Do not fabricate application-issued tokens with Magic Variables.

#### Product Capability Equivalence

Different Baserow profiles may provide the same product surface while using different Compose topologies. Compare profiles by preserved capabilities and operational properties, for example:

```text
login
database/table workflow
API
file upload
async jobs
realtime
Application Builder
state recovery
upgrade model
```

Do not treat service-by-service structural similarity as the definition of equivalence.

#### Knowledge-accumulation check

RC7 already carried strong lessons from NetBox, Overleaf and Frappe. Baserow must not be reconstructed by copying those products.

Correct cross-case help:

- semantic-gateway reasoning;
- Host-sensitive health reasoning;
- lifecycle-ownership discipline;
- coherent recovery thinking.

Forbidden contamination:

- no NetBox dual-Valkey topology by analogy;
- no NetBox AOF rule by analogy;
- no Overleaf MongoDB;
- no Frappe topology copied blindly;
- no ODK Nginx imported because Baserow already has Caddy semantics;
- no generic bootstrap/init helper without Baserow-specific runtime cause.

#### Regression significance

Baserow adds three meta-principles to the Skill:

> A mature deployment Skill must be able to represent multiple valid architectures for the same product without collapsing them into one universal topology.

> State externalization, process decomposition, public routing and lifecycle ownership are independent deployment dimensions unless current upstream evidence couples them.

> A runtime workaround belongs to its causal version and profile, not automatically to the product forever.

<!-- END PORTABLE RESOURCE: references/baserow-case-study.md -->

<!-- BEGIN PORTABLE RESOURCE: references/baserow-profile-evidence-matrix.md -->
<!-- SOURCE SHA256: eb5ea0be61dfa99e3b8cef511b073d3bed0182ee5ba29049cd79c19bf2059f56 -->
<!-- EMBEDDED SHA256: ea37fb13d5a7b84b0861d16a0d78cb06e9d007f12f8d59ed5f45c2a1736b6a45 -->

## Portable resource: `references/baserow-profile-evidence-matrix.md`

### Baserow 2.3.3 — Deployment Profile Evidence Matrix

This matrix separates three intentionally evaluated Baserow deployment profiles. It does **not** treat profile exploration as a repair-failure sequence.

Allowed evidence labels in this matrix are exactly:

```text
PASS
FAIL
PARTIAL
NOT RUN
UNKNOWN
```

A `PASS` is recorded only where the available artifact/runtime/operator evidence supports it. Missing granular evidence remains `UNKNOWN` or `NOT RUN`; broad operator acceptance is not expanded into invented raw logs.

#### Profile identities

- **Profile A — Reference / Candidate Profile:** official `baserow/baserow:2.3.3` all-in-one path tested with a version-scoped fresh-install auth-provider sequencing workaround. The tested path remained non-functional at the application level and is not a runtime-validated profile.
- **Profile B — Canonical Golden Profile:** Coolify-adapted distributed/custom RC5, exact accepted fixture `assets/baserow-2.3.3-v1.0.0-golden.yml`.
- **Profile C — Validated Alternative Profile:** official all-in-one Baserow application + external PostgreSQL RC2, operator-accepted as a distinct profile. It is not Golden #12.

#### Evidence matrix

| Evidence gate | Profile A — all-in-one tested path | Profile B — distributed/custom RC5 | Profile C — all-in-one + external PostgreSQL RC2 |
|---|---|---|---|
| upstream support | PASS | PASS | PASS |
| static validation | PASS | PASS | PASS |
| fresh deployment | FAIL | PASS | PASS |
| health | PARTIAL | PASS | PASS |
| login | FAIL | PASS | PASS |
| workspace/database | NOT RUN | UNKNOWN | UNKNOWN |
| API | NOT RUN | UNKNOWN | UNKNOWN |
| file upload | NOT RUN | UNKNOWN | UNKNOWN |
| workers | PARTIAL | PASS | PARTIAL |
| realtime | NOT RUN | UNKNOWN | UNKNOWN |
| restart | NOT RUN | UNKNOWN | UNKNOWN |
| redeploy | NOT RUN | UNKNOWN | UNKNOWN |
| backup | NOT RUN | NOT RUN | NOT RUN |
| restore | NOT RUN | NOT RUN | NOT RUN |
| upgrade | NOT RUN | NOT RUN | NOT RUN |
| operator confirmation | FAIL | PASS | PASS |

#### Evidence notes

##### Profile A

The all-in-one image is an official Baserow deployment primitive and therefore receives `PASS` for upstream support. The candidate itself is statically valid. The tested runtime path nevertheless did not produce a usable application login because the 2.3.3 fresh-install state exposed `PasswordAuthProviderModel` duplication/initialization behavior. Some container/runtime responsibilities could start, so `health` and `workers` are `PARTIAL` rather than claiming total service failure. Product workflows, persistence, backup, restore and upgrade were not run on a usable instance.

This result is classified as a **version/profile-specific upstream defect path**, not evidence that the official all-in-one architecture is inherently invalid.

##### Profile B

The distributed/custom profile received operator acceptance as functional after the RC5 gateway-health correction. The exact accepted bytes are retained as Golden #11. The evidence available to this release supports fresh deployment, health, login, worker execution at the accepted-profile level and overall operator confirmation. The current artifact set does not preserve granular raw evidence for every requested product sub-gate; those cells remain `UNKNOWN` rather than being promoted by implication. Backup/restore/upgrade were not evidenced for this benchmark and remain `NOT RUN`.

##### Profile C

The all-in-one application + external PostgreSQL profile is both upstream-legitimate and operator-accepted as functional. It is preserved as a **Validated Alternative Profile** because it exercises a materially different process/state topology from the canonical distributed Golden. Granular product workflow traces are not all retained, so unsupported sub-gates remain `UNKNOWN`. Backup/restore/upgrade were not evidenced and remain `NOT RUN`.

#### Classification outcome

```text
Canonical Golden Profile
  = Profile B — distributed/custom RC5

Validated Alternative Profile
  = Profile C — official all-in-one + external PostgreSQL RC2

Reference / Candidate Profile
  = Profile A — tested all-in-one 2.3.3 path affected by fresh-install auth-provider defect
```

This classification intentionally preserves more than one valid architecture for the same product while keeping only one numbered Golden regression oracle.

<!-- END PORTABLE RESOURCE: references/baserow-profile-evidence-matrix.md -->

<!-- BEGIN PORTABLE RESOURCE: references/baserow-reference-all-in-one-rc8.yml -->
<!-- SOURCE SHA256: 5c362038d8f1456bb7a9ac1eb7b9d4a79adc337b223e6f2116152317c9b62acb -->
<!-- EMBEDDED SHA256: 8949459125ed250886e53841ba1ea81e961fc118b8102bd71cfa542088c8e996 -->

## Portable resource: `references/baserow-reference-all-in-one-rc8.yml`

````yaml
# documentation: https://baserow.io/docs/installation/install-with-docker
# slogan: Open-source no-code database and application platform.
# category: productivity
# tags: database,no-code,low-code,collaboration
# port: 80
#
# Baserow 2.3.3 / Coolify
# Final corrected all-in-one candidate for the fresh-install auth-provider race.
# Runtime stays on the official baserow/baserow all-in-one image.
# A one-shot init gate completes migrations and normalizes the singleton
# PasswordAuthProviderModel before the supervised runtime can start concurrent
# backend/workers/frontend processes.

services:
  baserow-init:
    image: baserow/baserow:${BASEROW_VERSION:-2.3.3}
    restart: "no"
    environment:
      - SERVICE_URL_BASEROW
      - BASEROW_PUBLIC_URL=${SERVICE_URL_BASEROW}
      - MIGRATE_ON_STARTUP=false
      # Keep the documented template-sync budget explicit for slower first boots.
      - BASEROW_SYNC_TEMPLATES_TIME_LIMIT=1800
    volumes:
      - baserow_data:/baserow/data
    healthcheck:
      disable: true
    entrypoint: ["/bin/bash", "-lc"]
    command: |
      set -euo pipefail

      echo "[BASEROW-INIT] Running Baserow's native locked migrations before public runtime."
      /baserow.sh backend-cmd-with-db manage locked_migrate

      echo "[BASEROW-INIT] Verifying the singleton password authentication provider."
      /baserow.sh backend-cmd-with-db manage shell -c '
      from django.contrib.auth import get_user_model
      from django.db import transaction
      from baserow.core.auth_provider.handler import PasswordProviderHandler
      from baserow.core.auth_provider.models import PasswordAuthProviderModel

      with transaction.atomic():
          providers = list(
              PasswordAuthProviderModel.objects.select_for_update().order_by("pk")
          )

          if len(providers) == 0:
              provider = PasswordProviderHandler.get()
              print(f"Created password auth provider id={provider.pk}.")

          elif len(providers) == 1:
              print(f"Password auth provider id={providers[0].pk} already valid.")

          else:
              state = [
                  {
                      "id": provider.pk,
                      "enabled": provider.enabled,
                      "domain": provider.domain,
                      "users": provider.users.count(),
                  }
                  for provider in providers
              ]
              total_users = get_user_model().objects.count()
              print(
                  f"Detected duplicate password auth providers: {state}; "
                  f"total_users={total_users}"
              )

              # Automatic deletion is permitted only for the exact fresh-install
              # corruption mode: no users, no provider-linked users, default domains,
              # and identical enabled state. Anything else fails closed.
              if total_users != 0:
                  raise RuntimeError(
                      "Duplicate password auth providers exist after users were created; "
                      "refusing automatic repair. Manual review required."
                  )

              if any(provider.users.exists() for provider in providers):
                  raise RuntimeError(
                      "Duplicate password auth providers have associated users; "
                      "refusing automatic repair. Manual review required."
                  )

              if any(provider.domain is not None for provider in providers):
                  raise RuntimeError(
                      "Duplicate password auth providers contain a non-default domain; "
                      "refusing automatic repair. Manual review required."
                  )

              if len({provider.enabled for provider in providers}) != 1:
                  raise RuntimeError(
                      "Duplicate password auth providers disagree on enabled state; "
                      "refusing automatic repair. Manual review required."
                  )

              keep = providers[0]
              for duplicate in providers[1:]:
                  duplicate.delete()

              verified = PasswordProviderHandler.get()
              if verified.pk != keep.pk:
                  raise RuntimeError(
                      "Password auth provider post-condition mismatch after repair."
                  )

              print(
                  f"Repaired fresh-install duplicate password auth providers; "
                  f"kept id={keep.pk}."
              )
      '

      echo "[BASEROW-INIT] Initialization gate complete."

  baserow:
    image: baserow/baserow:${BASEROW_VERSION:-2.3.3}
    restart: unless-stopped
    environment:
      - SERVICE_URL_BASEROW
      - BASEROW_PUBLIC_URL=${SERVICE_URL_BASEROW}
      # The init service owns migrations so workers never observe a half-migrated schema.
      - MIGRATE_ON_STARTUP=false
      # The 2.3.3 first-start template import can exceed five minutes on a small VPS.
      - BASEROW_SYNC_TEMPLATES_TIME_LIMIT=1800
    expose:
      - "80"
    volumes:
      - baserow_data:/baserow/data
    depends_on:
      baserow-init:
        condition: service_completed_successfully

volumes:
  baserow_data:
````

<!-- END PORTABLE RESOURCE: references/baserow-reference-all-in-one-rc8.yml -->

<!-- BEGIN PORTABLE RESOURCE: references/baserow-validated-alternative-external-postgres-rc2.yml -->
<!-- SOURCE SHA256: 4833dd206c78c2747d0428d550603f06c5a3ff7c9eb3ecbf1702f5a756ccafed -->
<!-- EMBEDDED SHA256: 0d985469a45872826c764b2e22df10a9a60cff8b4f3906c74fab73f2ccec3cd2 -->

## Portable resource: `references/baserow-validated-alternative-external-postgres-rc2.yml`

````yaml
# documentation: https://baserow.io/docs/installation/install-with-docker
# slogan: Baserow production-oriented deployment with external PostgreSQL and pgvector.
# category: productivity
# tags: database,no-code,low-code,postgresql,pgvector
# port: 80
#
# RC2 fixes the Coolify public-route healthcheck regression from RC1:
# - do NOT probe http://127.0.0.1/api/_health/ through Baserow's embedded Caddy;
#   Baserow routing is Host-sensitive and treats unknown hosts as Application Builder domains.
# - use Baserow's native backend healthcheck, matching the image's own Docker HEALTHCHECK.
# - allow 15 minutes for first-start migrations, matching Baserow's production guidance.
# - explicitly keep the embedded Caddy on plain HTTP :80 because Coolify owns public TLS.

services:
  baserow:
    image: baserow/baserow:${BASEROW_VERSION:-2.3.3}
    restart: unless-stopped
    depends_on:
      baserow-db:
        condition: service_healthy
    environment:
      # Coolify-managed canonical public URL.
      # The identifier BASEROW matches this Compose service name, so Coolify also
      # creates the public route to this service. Port 80 needs no numeric suffix.
      - SERVICE_URL_BASEROW
      - BASEROW_PUBLIC_URL=${SERVICE_URL_BASEROW}

      # Coolify terminates HTTPS; Baserow's embedded Caddy remains the semantic
      # application gateway internally over plain HTTP.
      - BASEROW_CADDY_ADDRESSES=:80

      # External PostgreSQL. Supplying DATABASE_* disables the embedded PostgreSQL.
      - DATABASE_HOST=baserow-db
      - DATABASE_PORT=5432
      - DATABASE_NAME=baserow
      - DATABASE_USER=baserow
      - DATABASE_PASSWORD=${SERVICE_PASSWORD_64_BASEROWDB}
      - POSTGRES_STARTUP_CHECK_ATTEMPTS=${POSTGRES_STARTUP_CHECK_ATTEMPTS:-30}

      # Keep Baserow's native migration lifecycle. During the first deployment,
      # background schedulers can briefly query tables which are still being migrated;
      # readiness must therefore be based on the backend healthcheck, not on a short
      # fixed startup timer.
      - MIGRATE_ON_STARTUP=${MIGRATE_ON_STARTUP:-true}
      - BASEROW_TRIGGER_SYNC_TEMPLATES_AFTER_MIGRATION=${BASEROW_TRIGGER_SYNC_TEMPLATES_AFTER_MIGRATION:-true}

      # Persist cryptographic secrets in Coolify.
      - SECRET_KEY=${SERVICE_HEX_64_BASEROWSECRET}
      - BASEROW_JWT_SIGNING_KEY=${SERVICE_HEX_64_BASEROWJWT}

      # Conservative single-node default.
      - BASEROW_AMOUNT_OF_WORKERS=${BASEROW_AMOUNT_OF_WORKERS:-1}

      # Optional SMTP. Email stays disabled while EMAIL_SMTP is empty.
      - FROM_EMAIL=${FROM_EMAIL:-}
      - EMAIL_SMTP=${EMAIL_SMTP:-}
      - EMAIL_SMTP_HOST=${EMAIL_SMTP_HOST:-}
      - EMAIL_SMTP_PORT=${EMAIL_SMTP_PORT:-587}
      - EMAIL_SMTP_USER=${EMAIL_SMTP_USER:-}
      - EMAIL_SMTP_PASSWORD=${EMAIL_SMTP_PASSWORD:-}
      - EMAIL_SMTP_USE_TLS=${EMAIL_SMTP_USE_TLS:-}
      - EMAIL_SMTP_USE_SSL=${EMAIL_SMTP_USE_SSL:-}

      # Optional S3-compatible user-file storage.
      # Baserow keeps using /baserow/data/media while AWS_ACCESS_KEY_ID is empty.
      - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
      - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
      - AWS_STORAGE_BUCKET_NAME=${AWS_STORAGE_BUCKET_NAME:-}
      - AWS_S3_REGION_NAME=${AWS_S3_REGION_NAME:-}
      - AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-}
      - AWS_S3_CUSTOM_DOMAIN=${AWS_S3_CUSTOM_DOMAIN:-}
      - DOWNLOAD_FILE_VIA_XHR=${DOWNLOAD_FILE_VIA_XHR:-0}

      # Optional Application Builder wildcard/custom domains.
      - BASEROW_BUILDER_DOMAINS=${BASEROW_BUILDER_DOMAINS:-}

    expose:
      - "80"

    volumes:
      - baserow_data:/baserow/data

    # IMPORTANT: this deliberately matches Baserow's image-native HEALTHCHECK.
    # It bypasses embedded Caddy and therefore cannot be misrouted because the
    # probe Host is 127.0.0.1 / localhost.
    healthcheck:
      test:
        - CMD
        - /bin/bash
        - /baserow/backend/docker/docker-entrypoint.sh
        - backend-healthcheck
      interval: 15s
      timeout: 10s
      retries: 20
      start_period: 15m

  baserow-db:
    image: pgvector/pgvector:${PGVECTOR_IMAGE_TAG:-0.8.1-pg15}
    restart: unless-stopped
    environment:
      # Bootstrap superuser exists only to initialize PostgreSQL and create pgvector.
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=${SERVICE_PASSWORD_64_POSTGRESADMIN}
      - POSTGRES_DB=baserow
      - BASEROW_DATABASE_PASSWORD=${SERVICE_PASSWORD_64_BASEROWDB}
      - POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256

    volumes:
      - baserow_postgres_data:/var/lib/postgresql/data
      - type: bind
        source: ./initdb/10-baserow-production.sql
        target: /docker-entrypoint-initdb.d/10-baserow-production.sql
        content: |
          \set baserow_password `echo "$BASEROW_DATABASE_PASSWORD"`

          CREATE ROLE baserow
            LOGIN
            NOSUPERUSER
            NOCREATEDB
            NOCREATEROLE
            NOREPLICATION
            PASSWORD :'baserow_password';

          ALTER DATABASE baserow OWNER TO baserow;

          \connect baserow

          -- Create pgvector as the bootstrap superuser. The normal Baserow
          -- application role remains non-superuser.
          CREATE EXTENSION IF NOT EXISTS vector;

          GRANT ALL ON SCHEMA public TO baserow;

    healthcheck:
      test:
        - CMD-SHELL
        - pg_isready -U postgres -d baserow
      interval: 5s
      timeout: 5s
      retries: 30
      start_period: 20s

volumes:
  baserow_data:
  baserow_postgres_data:
````

<!-- END PORTABLE RESOURCE: references/baserow-validated-alternative-external-postgres-rc2.yml -->

<!-- BEGIN PORTABLE RESOURCE: references/ckan-case-study.md -->
<!-- SOURCE SHA256: fc897ccc47540d75a334ee1c1781030b9009994723f6b6ae68b01bdba595d99b -->
<!-- EMBEDDED SHA256: 73eb66276169e8539ebd61b5b2a7a9a28e6af52a2ade20d8ca02f08f587ac062 -->

## Portable resource: `references/ckan-case-study.md`

### CKAN on Coolify — distilled case study

This case study captures the reusable engineering lessons from a real CKAN 2.12 deployment on Coolify that progressed from static validation through fresh deployment and application-level acceptance. It is a **golden/regression case, not a generic Coolify template**.

The runtime-tested Compose fixture is `assets/ckan-v1.0.8-golden.yml`.

#### Validation status of the fixture

The validated deployment demonstrated all of the following in a fresh Coolify Service resource:

- CKAN web application started under the official `ckan/ckan-base:2.12.0` image;
- PostgreSQL initialized the CKAN database and separate DataStore database with the intended roles;
- Solr created the CKAN core and served real CKAN search requests;
- Redis served the CKAN RQ queue;
- the dedicated CKAN worker started on queue `default` and its scheduler acquired the scheduler lock;
- DataPusher accepted jobs and fetched resources through the internal CKAN URL;
- a real CSV upload containing 1,193 rows was imported into DataStore;
- the DataStore table preview rendered in the browser;
- `datastore_search` returned structured JSON successfully;
- sysadmin login, organization creation, dataset creation, resource upload, download, and public URLs worked;
- the public canonical CKAN URL no longer leaked the container port `5000`.

This is **acceptance-tested application behavior**. Backup/restore and host hardening remain separate production-readiness evidence.

#### Final architectural shape

```text
Internet HTTPS
      |
Coolify proxy
      |
      +--> CKAN web :5000
                |
                +--> PostgreSQL
                |      +-- database: ckan
                |      +-- database: datastore
                |      +-- role: ckan
                |      +-- role: datastore_ro
                |
                +--> Solr :8983 /solr/ckan
                +--> Redis :6379
                +--> DataPusher :8800
                |
                +--> shared CKAN storage
                         +-- uploads/FileStore
                         +-- DataPusher token handoff

CKAN worker
      +--> waits for CKAN application readiness
      +--> Redis queue `default`
      +--> CKAN/PostgreSQL/Solr as required by jobs
      +--> scheduler enabled by upstream `ckan jobs worker`
```

Only CKAN is public. PostgreSQL, Solr, Redis, and DataPusher remain internal.

#### What was preserved from CKAN upstream

The adaptation kept upstream behavior wherever possible:

- official CKAN images and uWSGI-based application entrypoint;
- PostgreSQL as CKAN's relational database;
- a separate DataStore database and read-only DataStore role;
- Solr as the search backend;
- Redis as the background-job queue backend;
- DataPusher as the tabular resource importer;
- CKAN's official `ckan jobs worker` command for background jobs;
- the CKAN health/status action as the application readiness signal;
- DataPusher's documented alternative callback base using an internal CKAN URL;
- official initialization/migration behavior rather than replacing CKAN startup with a custom supervisor.

The case does **not** establish that every CKAN installation must use DataPusher instead of XLoader, the exact image digests forever, or this exact plugin list. Those remain release/deployment choices that must be re-verified.

#### Coolify-specific adaptations

The fixture adapts upstream deployment behavior to Coolify by:

- removing an application-side public Nginx/TLS edge and using Coolify's proxy for public HTTPS;
- exposing only CKAN's internal HTTP port to the proxy;
- using Coolify-generated stable credentials and signing material;
- using a public hostname variable distinct from the port-qualified proxy-routing URL;
- using Coolify managed inline bind-file content for PostgreSQL initialization and the CKAN DataPusher hook;
- declaring authoritative persistence with named volumes;
- giving CKAN first-boot migrations a realistic health-check `start_period`;
- starting the worker without making Compose's one-time `service_healthy` gate its only readiness strategy;
- making the worker wait for the actual CKAN API and then `exec` the upstream worker;
- using a readiness marker plus PID-1 liveness for the dedicated worker container's health signal.

These are adaptations to deployment mechanics. They do not change CKAN's application architecture.

### Generalizable Coolify lessons

The following lessons are portable beyond CKAN when the same architectural cause exists.

#### 1. Port-qualified magic variables are routing identities, not automatically canonical origins

A live deployment showed that both of these can contain the internal routing port:

```text
SERVICE_URL_APP_5000  -> https://app.example.org:5000
SERVICE_FQDN_APP_5000 -> app.example.org:5000
```

If an application needs a public canonical HTTPS origin such as `https://app.example.org`, do **not** mechanically derive it from a port-qualified magic variable. Verify current Coolify semantics and use an explicit canonical variable or an unqualified FQDN form when appropriate.

The general rule is not "always use `SERVICE_FQDN_APP`". The rule is: **model proxy target and canonical public identity separately, then choose a Coolify variable whose current semantics match each role**.

#### 2. Inline `content:` is file content, not a Compose command string

A PostgreSQL initialization script originally contained:

```bash
$${POSTGRES_USER}
```

That escaping is appropriate in some Compose `command:`/`healthcheck:` strings when a dollar must survive Compose interpolation. In a Coolify-managed inline file, the same text reached Bash literally and `$$` expanded to the shell PID, producing an invalid database role such as `50{POSTGRES_USER}`.

For an inline managed file, write the shell syntax that the created file itself must contain. Do not apply Compose command-string dollar escaping by reflex.

#### 3. Managed file mounts must be unambiguously files

A failed iteration reached PostgreSQL as:

```text
/docker-entrypoint-initdb.d/10_create_ckandb.sh: Is a directory
```

For a Coolify `content:` bind that is intended to be a file, make the file semantics explicit and verify the rendered mount. Current Coolify documentation also warns that Docker can create a directory when a file source is missing.

If a long-lived Coolify resource continues to reproduce a stale file/directory classification after the Compose is corrected, a **fresh disposable test resource** can be a valid diagnostic isolation technique. This is not a general reason to recreate production resources.

#### 4. Know whether an upstream hook is sourced or executed

The CKAN entrypoint **sources** files from its hook directory. A custom hook used `set -euo pipefail`; `set -u` leaked into the parent shell and later made CKAN's own reference to an unset optional variable fatal.

Before adding shell options to a hook, inspect the upstream entrypoint. If the hook is sourced, global shell-option changes, `cd`, traps, exported values, and function definitions can mutate the parent shell.

#### 5. `depends_on: condition: service_healthy` is a startup gate, not durable orchestration

A long first CKAN bootstrap caused the Compose startup transaction to abandon the dependent worker before CKAN later became healthy. The robust solution was not a longer arbitrary sleep. The worker container starts in dependency order, then waits on real CKAN application readiness itself.

General rule: if a long-running worker can legitimately outlive slow app migrations or restarts, give it bounded application-level readiness/retry behavior when upstream semantics support that. Do not assume Compose dependency ordering is a continuous reconciler.

#### 6. Worker health must represent the worker, not a fragile implementation detail

The worker process and scheduler were proven alive in logs while a `/proc/1/cmdline`-style probe still produced `unhealthy`. The final probe uses a marker created only after dependency/bootstrap readiness and checks that PID 1 remains alive after `exec`.

This exact marker pattern is **not universal**. The general lesson is to choose a stable signal for the worker's real lifecycle, and validate the probe independently when Coolify says unhealthy while work is demonstrably being processed.

#### 7. Database engine health is not application initialization health

PostgreSQL can be `healthy` while application roles, databases, schemas, or migrations are missing. `pg_isready` proves the server accepts connections; it does not prove CKAN's `ckan` role or DataStore database exists.

Troubleshooting must inspect the initialization logs and application-level acceptance path before declaring the database layer correct.

#### 8. Init-directory scripts are often first-bootstrap-only

Once PostgreSQL created `PGDATA`, subsequent restarts skipped `/docker-entrypoint-initdb.d`, even though the earlier application initialization had failed. A partially initialized fresh volume can therefore preserve a broken bootstrap state.

Do not wipe persistent data casually. But on a **fresh disposable deployment with no authoritative data**, after proving initialization failed before the application objects were created, rebuilding that disposable database volume can be justified.

#### 9. Internal service URLs can be better than public loopback URLs when upstream explicitly supports them

CKAN/DataPusher successfully used:

```text
http://ckan:5000
```

for its private callback/fetch path while users used the public HTTPS origin without port `5000`.

This is the opposite of applications whose callbacks semantically require the canonical public hostname. The reusable rule is: **use the address class upstream intends for that interaction**. Never copy Kobo's public hairpin solution or CKAN's internal callback solution without the corresponding upstream requirement.

#### 10. Diagnose perceived slowness at the layer where latency occurs

The browser appeared slow while CKAN request logs showed page/API render times in milliseconds. After login, the expected dashboard request was not reaching CKAN because generated URLs contained the wrong public port.

Use request timing and route-arrival evidence before tuning CPU, workers, caches, or database settings.

#### 11. Host/runtime warnings belong to the production-hardening layer

The successful CKAN run still surfaced:

- Redis warning that `vm.overcommit_memory` should be enabled;
- Solr warning that the file descriptor limit was too low for production guidance.

These are important operational findings, but they are not evidence that CKAN's Compose architecture is wrong. Classify host kernel/ulimit tuning separately from application correctness and document it for production handover.

### CKAN-specific lessons

The following are **CKAN facts for this fixture**, not Coolify defaults:

- CKAN 2.12 web listens internally on port `5000` in the selected official image;
- the public health endpoint used was `/api/action/status_show`;
- this deployment uses PostgreSQL databases `ckan` and `datastore`;
- this deployment uses the roles `ckan` and `datastore_ro`;
- DataPusher runs internally on port `8800`;
- Solr runs internally on port `8983` with the CKAN core;
- Redis backs CKAN background jobs;
- the worker command is `ckan jobs worker` and the tested deployment started the default queue and scheduler;
- CKAN 2.10+ DataPusher integration requires an API token; this fixture shares it from CKAN web to worker through CKAN's persistent storage;
- `ckan.datapusher.callback_url_base=http://ckan:5000` is intentionally internal for this topology;
- the fixture persists CKAN FileStore under `/var/lib/ckan`;
- the public `CKAN_SITE_URL` must not leak the internal container port in this normal HTTPS proxy topology.

Do not transplant any of those details to another application without upstream evidence.

### Failure sequence and anti-patterns discovered

#### Dollar escaping copied into generated file content

**Failure:** `$${VAR}` inside a Coolify-managed shell file became PID text at shell runtime.

**Rule:** distinguish Compose interpolation context from generated-file runtime context.

#### Port-qualified FQDN assumed to be host-only

**Failure:** canonical links included `:5000` and browser navigation was impaired even though CKAN server render times were fast.

**Rule:** inspect the actual generated values. Port-suffixed URL/FQDN variables can carry the routing port.

#### Sourced hook changed parent shell semantics

**Failure:** `set -u` in the custom DataPusher hook made a later optional upstream variable fatal.

**Rule:** sourced-hook behavior is part of the upstream contract.

#### Worker gated only by Compose health dependency

**Failure:** the worker never launched after the initial one-time dependency gate failed during a long first boot.

**Rule:** long-running processes may need application-level readiness/retry beyond Compose ordering.

#### Worker probe contradicted runtime evidence

**Failure:** worker and scheduler were running while Coolify reported `unhealthy`.

**Rule:** a broken probe is not an application failure.

#### Database volume reused after failed first bootstrap

**Failure:** PostgreSQL skipped init scripts on restart and the CKAN role remained missing.

**Rule:** understand one-time initialization semantics and prove the state before destructive recovery.

#### Existing Coolify resource retained misleading mount state

**Failure:** even after Compose changes, a managed init path could remain directory-like in the old experimental resource.

**Rule:** for disposable pre-production diagnosis, compare against a fresh resource before redesigning application architecture. Never use this as a routine production reset strategy.

### What must not contaminate other applications

Do **not** infer any of these merely because the CKAN golden case has them:

- Solr;
- DataPusher;
- a separate DataStore database;
- a DataStore read-only PostgreSQL role;
- Redis;
- an RQ worker or scheduler;
- port `5000`;
- `/api/action/status_show`;
- a CKAN API-token handoff file;
- `platform: linux/amd64`;
- the exact PostgreSQL/Solr/Redis versions in this fixture;
- the exact CKAN plugin list;
- a worker readiness marker;
- internal callbacks instead of canonical public callbacks;
- recreation of a Coolify resource to resolve ordinary updates.

A future OpenMRS adaptation, for example, must rediscover OpenMRS's own runtime, database, initialization, storage, proxy, and background-processing architecture from official OpenMRS material. Neither CKAN's Solr/DataPusher stack nor KoboToolbox's Enketo/Mongo/public-gateway structure is a starting skeleton.

### Regression acceptance path

For future modifications of the CKAN fixture, the minimum live regression path is:

1. fresh PostgreSQL initialization creates the intended application/DataStore roles and databases;
2. CKAN reaches `/api/action/status_show` successfully;
3. sysadmin login works;
4. create an organization;
5. create a dataset;
6. upload a CSV resource;
7. confirm DataPusher receives the job and imports rows into DataStore;
8. load the tabular DataStore preview;
9. call `datastore_search` and verify structured results;
10. verify Solr receives CKAN search queries;
11. verify the worker and scheduler are actually running;
12. verify all browser-generated URLs use the public canonical origin with no unintended internal port;
13. redeploy without deleting volumes and confirm the data survives.

Backup/restore remains a separate production-handover regression and should be tested before claiming full production readiness.

<!-- END PORTABLE RESOURCE: references/ckan-case-study.md -->

<!-- BEGIN PORTABLE RESOURCE: references/coolify-rules.md -->
<!-- SOURCE SHA256: 4dba1193994c2f157773dc6c9f5fdc6028970437b4d6a78fb4f21d042d61b2f6 -->
<!-- EMBEDDED SHA256: 9fe7c36291b79ef7a755ede254a2abf18feebb414fb22ebea92d3ac1342cd046 -->

## Portable resource: `references/coolify-rules.md`

### Coolify-specific Compose rules

#### Compose as source of truth

For Compose-based Coolify deployments, treat the Compose as the authoritative definition for services, mounts, commands, health checks, internal ports, and environment wiring.

Do not use the legacy top-level Compose `version:` key as a human template revision. When a human revision marker is useful, keep one comment near the top such as `# Coolify template revision: 1.2.0`; keep history elsewhere.

#### Complexity budget / provenance

Coolify is an adaptation layer, not a reason to inflate the application topology. **Complexity must be inherited from the current upstream architecture, not from previous golden cases.**

For each service/capability in the candidate, record whether it is:

- preserved from the selected upstream deployment;
- replaced by a Coolify platform function such as external TLS/routing;
- added because current upstream behavior or a documented operational requirement needs it; or
- intentionally omitted because it is optional for the selected profile.

Do not use service count as a pass/fail metric. A minimal upstream may legitimately remain minimal; a large upstream may legitimately remain large. Every service, proxy, sidecar, init container, volume, custom network, workaround, or embedded script must have identifiable provenance from upstream, a current Coolify requirement, or a demonstrated runtime problem. Missing provenance is **REVIEW REQUIRED**, not automatically an ERROR. Raw service count is never the rule.

#### Public routing

Prefer Coolify-native service domains.

For a service named `api` listening internally on port `3000`, Coolify can associate a public URL/FQDN through matching `SERVICE_URL_API_3000` / `SERVICE_FQDN_API_3000` variables when appropriate.

For URL/FQDN generators, bind the identifier to the **actual Compose service** that owns the public domain. Current service-stack docs say to use the Compose service name and normalize hyphens/dots when needed. Do not invent an arbitrary public-service identifier merely for readability.

Do not add hand-written Traefik labels for a normal service if Coolify can manage the route natively.

#### Magic Environment Variables and generated values

Current official Coolify documentation defines the `SERVICE_<TYPE>_<IDENTIFIER>` model. Verify current docs before publishing a reusable template. The current documented families include:

```text
SERVICE_NAME_<SERVICE>
SERVICE_URL_<SERVICE>[_PORT]
SERVICE_FQDN_<SERVICE>[_PORT]
SERVICE_USER_<ID>
SERVICE_LOWERCASEUSER_<ID>
SERVICE_PASSWORD_<ID>
SERVICE_PASSWORD_64_<ID>
SERVICE_PASSWORDWITHSYMBOLS_<ID>
SERVICE_PASSWORDWITHSYMBOLS_64_<ID>
SERVICE_BASE64_<ID> / _32 / _64 / _128
SERVICE_REALBASE64_<ID> / _32 / _64 / _128
SERVICE_HEX_32_<ID> / _64 / _128
SERVICE_SUPABASEANON_<ID>       # current Coolify-specific JWT family
SERVICE_SUPABASESERVICE_<ID>    # current Coolify-specific JWT family
```

##### Parse the TYPE before the identifier

Compound types contain underscores. A validator must recognize the longest documented type first.

```text
SERVICE_PASSWORD_64_FRAPPEADMIN
namespace  = SERVICE
type       = PASSWORD_64
identifier = FRAPPEADMIN
```

Do **not** parse that as `PASSWORD` + `64_FRAPPEADMIN`. Apply the same rule to `PASSWORDWITHSYMBOLS_64`, `BASE64_128`, `REALBASE64_64`, `HEX_64`, and other compound types.

##### Credential/random ID grammar vs URL/FQDN service IDs

Do not apply one punctuation rule to every `SERVICE_*` family.

- URL/FQDN IDs are service-bound. They model a real Compose service and optional internal proxy port/path.
- Credential/random IDs are generator identities and are not service-routing declarations.
- Current Coolify documentation enumerates the families but does not state a universal ban on underscores for every credential ID.
- **Runtime regression evidence:** in the Frappe Docker Compose Empty benchmark, `SERVICE_PASSWORD_64_FRAPPE_DB_ROOT` / `SERVICE_PASSWORD_64_FRAPPE_ADMIN` remained blank, while `SERVICE_PASSWORD_64_FRAPPEDBROOT` / `SERVICE_PASSWORD_64_FRAPPEADMIN` generated correctly. Coolify issue #11043 independently reports the same separator class and confirms alphanumeric IDs restored generation.

Therefore use a conservative **alphanumeric credential/random identifier** by default (`POSTGRES`, `JWT`, `FRAPPEDBROOT`, `FRAPPEADMIN`). Treat separator-bearing credential IDs as `REVIEW REQUIRED` against the target Coolify version, not as a timeless Docker Compose syntax error.

##### Reuse identity, not copied values

The **complete magic variable name is the credential identity**. If the database creates a user with:

```yaml
MYSQL_USER: ${SERVICE_USER_MYSQL}
MYSQL_PASSWORD: ${SERVICE_PASSWORD_64_MYSQL}
```

then an application connecting as that user must consume those exact same variables, not newly generated lookalikes such as `SERVICE_USER_APP` / `SERVICE_PASSWORD_64_APP`.

Generated credential/random values persist between deployments. The **complete Magic Variable name** is part of deployment state once it initializes a DB user, account, key, tenant or site. Renaming the generator can create a new value while persistent application state still contains the old credential; that is potentially breaking, not cosmetic.

Changing the generated value itself also does **not** prove the application credential rotated. After bootstrap, many credentials live in database/application state. Use the application's supported rotation mechanism and verify the new credential at runtime.

URL/FQDN magic values are managed from the service/domain configuration rather than treated like ordinary editable secrets. Also inspect **platform-generated** environment variables/labels derived from the Compose service name. A service name can become application-visible configuration even when those variables are not explicitly written in YAML; Overleaf RC1 is the regression example.

##### Generator selection

Choose from upstream constraints, not from the variable name you wish existed:

- username -> `SERVICE_USER_*` (or a documented lowercase-user family when lowercase is required);
- no-symbol password -> `SERVICE_PASSWORD_*` or `_64` when 64 characters are acceptable/required;
- symbol-bearing password -> `SERVICE_PASSWORDWITHSYMBOLS_*` only if every consumer/URL parser accepts the symbols safely;
- true Base64 -> `SERVICE_REALBASE64_*`;
- hexadecimal -> `SERVICE_HEX_*`;
- public URL/FQDN -> `SERVICE_URL_*` / `SERVICE_FQDN_*` only when the generated domain semantics match the application role.

`SERVICE_BASE64_*` is a historical/misleading name in current documentation: it produces a random string that is **not Base64 encoded**. Do not use it for an upstream field requiring real Base64.

Do not infer undocumented character-class guarantees. For example, `_64` documents length/no-symbol behavior, not that a value necessarily contains uppercase + lowercase + digits. If upstream has stricter rules, add a pre-deploy/preflight validation or choose a format that is actually documented to satisfy them.

**Encoded format is part of the secret contract.** If upstream requires the semantics of `openssl rand -base64 32`, a true-Base64 generator for 32 random bytes is materially different from a same-length random string. Parse compound families longest-first (`REALBASE64_32`, not `REALBASE64` + identifier `32_*`). **Transport is a separate contract:** a generated value must also survive Coolify `.env`, Compose, YAML, shell and any nested-language boundary unchanged. A symbol-bearing generator is not globally forbidden, but runtime evidence of `$`/interpolation corruption requires a transport-safe equivalent that still satisfies the application contract.

##### Secret origin and credential lifecycle

Before mapping a secret-shaped variable to a Coolify generator, classify:

```text
purpose
issuer/origin
generation phase
storage owner
rotation mechanism
persistent-state impact
recovery requirement
```

At minimum distinguish platform-generated deployment secrets, application-issued credentials, operator-provided secrets, and external-provider-issued credentials. `SERVICE_PASSWORD_*` is appropriate only when a random platform-generated value satisfies the upstream contract. It cannot fabricate a valid credential at OpenAI/Anthropic/Google or replace a native application-issued API-key lifecycle by default.

Application signing/encryption secrets are a distinct class from DB passwords and user/API credentials even when they use the same Coolify random-password family. Preserve their identities across redeploys and document rotation impact.

##### URL/FQDN service identifiers and ports

For URL/FQDN generators, the ID maps to the actual Compose service. A numeric suffix selects the internal container port that Coolify routes to. Current service-stack documentation explicitly uses the Compose service name and describes normalization of hyphens/dots in the environment-variable form.

Treat the following as separate values:

```text
browser canonical URL
public FQDN / hostname
Coolify proxy-target declaration
internal Docker hostname
internal application URL
callback URL
WebSocket / realtime origin
```

Example:

```text
SERVICE_URL_FRONTEND        -> browser canonical URL/origin
SERVICE_FQDN_FRONTEND       -> public hostname
SERVICE_URL_FRONTEND_8080   -> route public domain to frontend internal :8080
frontend                    -> Docker hostname
http://backend:8000         -> internal application URL
```

The `_8080` suffix is proxy-target metadata. It does not automatically mean the browser-visible canonical URL should become `https://example.org:8080`.

##### Pre-deploy generation gate and Magic Variable Ledger

Before the first bootstrap of a stateful application, build a ledger with:

| Variable | Family/type | Identifier | Producer/consumer services | Purpose | Expected format | Transport path | Required? | Persistent identity? | Public? | Credential? | Parser ambiguity? |
|---|---|---|---|---|---|---|---|---|---|---|---|

Then:

1. confirm required username/password/key fields are non-empty in Coolify;
2. confirm the generator format satisfies upstream constraints **and its generated bytes survive the actual transport/serialization path**;
3. confirm shared credentials reuse one exact complete variable name across all producers/consumers;
4. confirm URL/FQDN variables bind to the intended Compose service/port/path;
5. prefer `${SERVICE_...:?message}` when a blank generated value would partially initialize durable state;
6. stop before deployment if a required magic value is blank/malformed.

Failing before container creation is preferable to mutating a database/site with an empty credential.

Do not replace external/provider-supplied secrets (SMTP, OAuth client secrets, third-party API tokens) with random Coolify values merely to eliminate manual input. The question is whether the value is **locally generatable** and whether upstream defines its required format.

#### Service configuration / `extraFields()` compatibility — operator UX only

Coolify may expose selected database/service/application fields in its **Service configuration** UI when the current `Service::extraFields()` implementation recognizes the service image and environment-variable names used by the Compose. Typical recognized UX can include database name/user/password, object-storage URLs/admin credentials, or application-specific administrator fields.

This is **COOLIFY-NATIVE OPERATOR UX**, not runtime architecture. Keep this ordering explicit:

```text
upstream correctness
> runtime correctness
> security
> persistence
> backup / restore
> product acceptance
> extraFields / Service configuration compatibility
```

The last line is advisory polish. Never invert the order. In particular:

- absence of a **Service configuration** section is not an ERROR, REVIEW REQUIRED, benchmark failure, or production-readiness failure by itself;
- a template can be valid and mergeable without recognized `extraFields()` UI;
- do not choose a weaker/incorrect secret generator merely because its variable name is recognized by Coolify;
- do not rename a persistent Magic Variable after bootstrap merely to gain UI exposure;
- do not change an upstream service name/image/architecture solely to satisfy UI recognition;
- do not rewrite an immutable Golden for this purpose.

Formalize the two contracts separately:

```text
Runtime Credential Contract
!=
Coolify Operator UI Exposure
```

##### Official contribution-stage polish

When later preparing a template for an official Coolify PR:

1. begin with the runtime-accepted Golden or production-ready candidate;
2. inspect the **current** Coolify `Service::extraFields()` conventions rather than relying on remembered mappings;
3. identify databases/services/images and variable names that Coolify already recognizes;
4. optionally produce a distinct UI-friendly contribution candidate;
5. change variable names only when semantic role, credential issuer/owner, format/encoding, transport safety, persistent identity and security remain compatible;
6. fully re-run static validation and relevant runtime/acceptance/redeploy/recovery tests after any such change.

```text
Golden
= immutable runtime oracle

Official contribution candidate
= validated derivative that may receive Coolify-native UX polish
```

`extraFields()` compatibility is therefore an **advisory contribution concern**, never a reason to make a benchmark candidate less correct.

#### Secret format selection

The magic-variable audit should distinguish:

- **ERROR** — malformed/unknown bare magic declaration that Coolify is expected to generate, or impossible structural wiring;
- **ERROR** — a high-confidence shared logical credential split where producer/consumer clearly use the same generated account but different generated passwords;
- **WARNING** — strong evidence of username/password generator confusion, hard-coded sensitive value, or incompatible encoding;
- **REVIEW REQUIRED** — a likely locally-generatable manual secret, application-specific format constraint, separator-bearing credential ID with version-sensitive parser risk, Magic Variable identity rename, or ambiguous service-ID mapping requiring upstream confirmation;
- **INFO** — valid magic-family usage/reuse discovered.

#### Platform edge proxy vs application-semantic gateway

Do not equate “Coolify has a proxy” with “delete upstream Nginx”.

Classify the upstream gateway first. If it only owns Internet exposure/TLS/ACME, Coolify can often replace it. If it owns application semantics such as assets, protected files, route dispatch, site/tenant identity, headers, X-Accel behavior, websocket/realtime paths, or frontend/backend composition, preserve it behind Coolify unless upstream provides an equivalent supported path.

Frappe is the regression example: external Traefik/TLS can be removed, while the Frappe Nginx frontend remains semantically required.

#### Networking

Coolify creates/connects deployment networking and proxy access for Compose services. Prefer that default topology.

Avoid custom networks unless one of these is true:

- upstream requires a specific network topology;
- the user explicitly needs cross-resource networking;
- isolation requirements justify it;
- a documented Coolify pattern requires it.

If a custom network is added, verify the Coolify proxy can still reach every public target.

#### Ports

For internal-only services:

- prefer no `ports:` mapping;
- `expose:` may document the internal port but is not required for Docker-network reachability;
- never publish a database/cache port solely for inter-service communication.

Public traffic should normally enter through Coolify's proxy, not host port publishing.

#### Domains

Use generated service URLs when they are suitable. Use stable custom canonical hostnames when the application depends on durable URL identity, sibling-domain cookies, callback allowlists, signed absolute URLs, or cross-service public routing.

Do not conflate “Coolify can generate a URL” with “that generated URL is safe as this application's canonical identity.”

#### TLS

In a normal Coolify deployment:

```text
Internet HTTPS -> Coolify proxy -> container HTTP
```

Preserve `X-Forwarded-Proto` and related headers so the application knows the original request was HTTPS. “Behind a proxy” does not automatically mean “trust forwarded headers from any source”; inspect the application's trusted-proxy model and spoofing boundary.

Do not terminate a second unrelated TLS layer in the container unless required.

#### Native lifecycle primitive priority

Before adding a migrator, configurator or admin-bootstrap helper, inspect the selected image entrypoint and upstream lifecycle. If the image already owns an idempotent migration/bootstrap/configuration primitive, preserve it unless a proven target/Coolify gap requires wrapping or replacement. Fail-closed mutation rules apply to unknown/conflicting state; they are not a reason to distrust a known-good native primitive during architecture discovery.

#### Image-baked configuration versus repository mounts

An upstream Compose bind mount is not proof that the selected image lacks the same configuration. Before converting repository-local config files into Coolify managed files:

1. inspect the Dockerfile/image construction for the mount target;
2. determine whether the repository mount overlays an image-baked baseline;
3. preserve the image baseline when it is sufficient;
4. add environment overrides supported by upstream;
5. add only the smallest managed file required for a real Coolify/runtime gap.

Do not copy a whole configuration directory merely to make the One-Click self-contained. Self-contained can mean **using the self-contained image correctly**.


#### Local source builds and effective Coolify pull order

`image:` + `build:` is valid Compose syntax, but a Coolify Service can impose an additional deployment sequence. Before relying on a local-only image name, verify the target Coolify version/path.

If runtime evidence shows Coolify performs a preliminary `docker compose pull` against the local-only image before `up --build`, `pull_policy: never` may be required on those **exact source-built services**.

Classify this as a **version-sensitive platform rule**, not a Docker Compose law. Re-check current behavior before carrying it forward.

#### Storage

Use named volumes for persistent application state unless upstream explicitly requires a host path. Host bind mounts should be deliberate and documented.

Do not use host paths that depend on a developer workstation layout.

#### Inline Compose shell / dollar-context matrix

Do not apply one global dollar-escaping rule. Classify the context first:

| Context | `$` meaning / risk |
|---|---|
| Compose interpolation | `${VAR}` may be resolved before the container starts |
| inline `command:` / `healthcheck:` | `$$` can be needed so a literal `$` reaches the container shell |
| runtime shell variable | quoting determines whether `$VAR`/`${VAR}` expands |
| runtime shell PID | `$$` means PID and can appear accidentally if Compose escaping is copied into generated files |
| regex end anchor | `$` may be syntax for the nested regex language |
| awk field | `$1` etc. can require Compose escaping in inline shell |
| managed `content:` file | generated file bytes are a different interpolation context |

Validate the effective runtime shell representation after Compose escaping. `bash -n` does not validate nested `sed`, `awk`, regex, `jq`, SQL, JavaScript or database-update operators. Literal `$name` tokens meaningful to a nested language can be consumed by Compose unless escaped for that layer; Overleaf RC3→RC4 (`$set` -> `$$set` in Compose source) is the regression example.

For deterministic literal nested commands, run a controlled syntax check when practical. Prefer shell parameter expansion over `sed` for trivial prefix/suffix removal when it is equally clear, but do not treat `sed` as forbidden.

`set -euo pipefail` is context-sensitive: it is local to a dedicated executed Bash process, but shell options can leak when a hook is **sourced** by a parent entrypoint.

#### Health checks

Coolify consumes Docker health information. Health checks should be stable, local where possible, non-mutating, and based on tools actually present in the image. Preserve HTTP hostname semantics: `localhost` and `127.0.0.1` can reach the same loopback socket while producing different `Host` headers and therefore different behavior under `ALLOWED_HOSTS`/trusted-host validation. Do not normalize one into the other without upstream/runtime evidence.

#### Preview/generated domains

If the application supports arbitrary changing domains, Coolify-generated URLs can be convenient. If the application stores canonical domains or requires fixed cross-service URL relationships, use stable explicit public domains and document the assignment procedure.


#### Port-qualified routing variables versus canonical origins

Current Coolify magic-variable semantics distinguish unqualified and port-qualified identities. A port-qualified variable can deliberately contain the routing port, for example:

```text
SERVICE_URL_API_3000  -> https://api.example.org:3000
SERVICE_FQDN_API_3000 -> api.example.org:3000
```

This is useful to bind a generated domain to an internal service port, but it is not automatically the right canonical URL for an application behind normal public HTTPS.

If an application requires a canonical origin such as `https://api.example.org`, keep that concept separate. Verify the actual generated values and use an explicit canonical variable or an unqualified FQDN form only when current Coolify behavior and the application's domain model make it correct.

Do not encode a universal rule that every public app must use an unqualified FQDN. Some deployments intentionally expose nonstandard public ports. The architecture decides.

#### Managed inline file content

Coolify supports bind mounts with inline `content:` to create files on the deployment host, but the effective file has a lifecycle distinct from the YAML source.

For a generated shell/config file:

- write the literal text that the resulting file must contain;
- do not copy `$$` escaping rules from Compose `command:` or `healthcheck:` strings into the file by reflex;
- if the target is a file, make that intent explicit (`is_directory: false` where supported/needed);
- verify the deployment host source path is a file, because Docker can create a directory when a missing file source is treated as a bind path;
- validate the embedded file syntax separately;
- record the managed-resource identity, host source path, container target, rendering/interpolation stage, effective file/content checksum where practical, consumer and restart/reload requirement.

Use this troubleshooting chain when declared file changes do not change runtime behavior:

```text
Compose declared content
-> Coolify managed-resource identity
-> host file
-> container-mounted file
-> application-loaded file
-> effective runtime behavior
```

Do not assume `Compose content changed -> mounted file changed`.

A persistent managed-file identity can explain stale runtime behavior. First verify current Coolify behavior and the effective file. If a new source/target path is deliberately used to force a new managed-resource identity, document it as an invalidation mechanism and verify the new effective runtime file. Do not create new paths for ordinary edits without evidence.

#### Platform primitive support is layered

For critical primitives such as `configs`, `secrets`, bind `content`, `tmpfs`, profiles, build, pull policy, GPUs, devices or capabilities, distinguish:

```text
Docker Compose specification support
!=
Coolify parser support
!=
Coolify persistence/model support
!=
actual deployment support
```

If one of these layers is not demonstrated for the target Coolify path/version, emit **REVIEW REQUIRED** instead of calling the primitive supported merely because the Compose specification documents it.

#### Dollar/interpolation transport map

Dollar escaping depends on the path the text traverses.

Examples can legitimately differ:

```text
Compose command -> shell -> JavaScript
may require source $$set so runtime receives $set

Coolify managed file -> Nginx parser
may require source $remote_addr because Coolify writes the file literally
```

These are not contradictory.

Before encoding tokens, map:

```text
source text
-> Compose interpolation
-> Coolify transformation/managed-file transport
-> shell
-> envsubst/template renderer
-> application/config parser
```

Never use `always escape $ as $$` as a global rule.

When `envsubst` is necessary for a configuration language that itself uses `$variables`, prefer an explicit whitelist of environment variables rather than unrestricted substitution.

When the target image provides a reliable native validator for generated configuration, run it before the long-lived process starts.

#### Canonical public origin through internal gateways

Do not allow an internal proxy listener port to become the browser-visible public port by accident.

Model separately:

```text
Coolify public origin
Coolify proxy target service:port
internal semantic gateway listener
application listener
```

Forward `Host` / `X-Forwarded-Host` / `X-Forwarded-Proto` / `X-Forwarded-Port` according to the **actual canonical external origin**, not according to the internal listener. Do not hard-code `443` if the real public origin intentionally uses another scheme/port.

#### Multi-profile Coolify adaptation rules

- Coolify owns Internet ingress, TLS and public-domain routing; it does not automatically replace an application-semantic gateway.
- `SERVICE_URL_<SERVICE>` can be the canonical browser origin when the application needs an origin without a routing-only port suffix; do not assume a port-qualified generated URL is semantically interchangeable.
- Routing-target metadata and canonical application origin can require different representations even when they point to the same public service.
- A local healthcheck must validate the intended Host/path/router semantics, not merely TCP reachability. A dedicated local liveness path is preferable when `/` is domain/tenant-sensitive.
- Externalizing PostgreSQL or another authoritative store changes the recovery contract. Verify which upstream `backup all` primitive remains valid and include user media/files plus stable secrets/config when they remain outside the DB.
- Do not infer Redis persistence from technology name. Classify each product/store role before enabling AOF or merging services.

<!-- END PORTABLE RESOURCE: references/coolify-rules.md -->

<!-- BEGIN PORTABLE RESOURCE: references/cross-benchmark-lessons.md -->
<!-- SOURCE SHA256: 54e8ac3f2c277f6803f2c8009d2a43d76b96b736172f2b467f4e8ff96d6b3006 -->
<!-- EMBEDDED SHA256: 0a6f6a20b96479c40e81216aa7b5edc076db6928280ed25a01d321638af5d415 -->

## Portable resource: `references/cross-benchmark-lessons.md`

### Cross-benchmark lessons — twelve runtime golden cases

The Golden corpus contains KoboToolbox V19.3, CKAN 2.12, OpenMRS 3.7.1, OpenEMR 8.3.0, ODK Central v2026.2.4, Frappe Framework v16.32.0, ERPNext v16.33.0, Mem0 v2.0.19, Overleaf Community Edition 6.2.2, NetBox 4.6.9, Baserow 2.3.3 and OpenSPP V2 2026.08. Use all twelve to ask better causal questions, never to derive an average stack.

Read `references/twelve-benchmark-audit.md` for the current matrix and bias audit. `references/eleven-benchmark-audit.md` is the historical pre-OpenSPP snapshot. `references/ten-benchmark-audit.md` is the historical pre-Baserow snapshot. `references/five-benchmark-audit.md`, `references/six-benchmark-audit.md`, `references/seven-benchmark-audit.md`, `references/eight-benchmark-audit.md`, and `references/nine-benchmark-audit.md` remain historical snapshots.

#### Knowledge provenance

Use these classes:

1. current upstream fact;
2. current official Coolify fact;
3. direct runtime observation;
4. operator-confirmed runtime result;
5. cross-benchmark confirmed pattern;
6. application-specific adaptation/workaround;
7. inference / REVIEW REQUIRED;
8. older successful Skill path used as regression evidence when a newer release diverges.

Current upstream + current Coolify facts take precedence. A runtime workaround from one Golden remains local until independent evidence supports the underlying cause. A newer Skill release is not automatically stronger evidence than an older runtime-accepted path.

#### Architecture diversity at a glance

| Benchmark | Public shape | Core state/services | Special roles | Main counterexample value |
|---|---|---|---|---|
| KoboToolbox | multiple public surfaces | PostgreSQL + MongoDB + Redis + media | Celery/beat + Enketo + gateways | legitimate multi-domain/callback complexity |
| CKAN | one public app | PostgreSQL/DataStore + Solr + Redis + FileStore | DataPusher + RQ worker/scheduler | legitimate search/import/worker topology |
| OpenMRS | one semantic gateway | MariaDB + app state | O3 frontend/backend/gateway | semantic app proxy can remain behind Coolify |
| OpenEMR | one public app | MariaDB + site/documents | native bootstrap | minimal runtime can still have multi-store persistence |
| ODK Central | one semantic Nginx host | PostgreSQL + Enketo/Redis state | Pyxform + one-shots | deployment bundle/image assumptions matter |
| Frappe Framework | semantic frontend + realtime | MariaDB + Redis + sites | workers + scheduler + migrator | legitimate async/realtime complexity |
| ERPNext | Frappe public runtime | Frappe state + ERPNext product state | explicit product activation | sibling runtime does not prove product state |
| Mem0 | separate dashboard + public API | PostgreSQL/pgvector + SQLite history | API-owned Alembic + browser-first auth/provider | compact AI stack without speculative infra |
| Overleaf CE | one public collaborative app | MongoDB + Redis + application filesystem | internal supervised processes + admin one-shot | Toolkit/runtime, edition, coherent-state and compile/realtime lessons |
| NetBox | one public app | PostgreSQL + Valkey tasks + Valkey cache + media/scripts/reports | RQ worker + native entrypoint bootstrap | image-baked config, same-engine/different-state semantics, health Host semantics, Skill-regression test |

The matrix is descriptive. A blank/absent component is a counterexample against automatic dependency transfer.

#### Strong principles confirmed by the corpus

##### Upstream topology and selected profile first

Complexity must be inherited from current upstream architecture, not previous Goldens. Select the exact edition/profile before importing feature requirements. Every retained/added/removed/split responsibility needs current provenance.

##### Golden fixtures are regression oracles

A Golden protects accepted behavior and supplies causal questions. It is never a generic skeleton.

##### Coolify operator UI exposure is not the runtime credential contract

Coolify **Service configuration** / `Service::extraFields()` recognition is a separate post-acceptance UX layer. It can make recognized DB/object-store/admin values easier for operators to discover, but it must not drive architecture, secret format, credential ownership or persistent Magic Variable identity. A missing Service configuration section does not invalidate an otherwise correct template.

Therefore preserve:

```text
Runtime Credential Contract != Coolify Operator UI Exposure
```

and:

```text
Golden = immutable runtime oracle
Official contribution candidate = validated derivative that may receive Coolify-native UX polish
```

Never rewrite a Golden solely to satisfy current `extraFields()` naming conventions. If an official contribution later benefits from UI-friendly names, derive it from the accepted candidate, verify current Coolify mappings, preserve all runtime/security/secret semantics, and fully retest the derivative. This is advisory knowledge, not a cross-benchmark gate.

##### Deployment Toolkit, repository layout and image runtime are different evidence layers

An upstream Toolkit/installer/wrapper can mix host orchestration with runtime description. A repository bind mount can also overlay files already present in the image. Before recreating host-side config inside Coolify, inspect the Dockerfile/image and determine whether the image already contains the runtime baseline. NetBox proves that `./configuration:/etc/netbox/config` did not imply the image lacked `/etc/netbox/config`.

##### Internal process topology != Compose topology

A supervised image may contain independently named application microservices/processes without requiring matching Compose services. Preserve upstream process/container boundaries unless current evidence requires a split.

##### Prefer native lifecycle primitives before helpers

If the selected image already owns migrations, static collection, retries or idempotent first-admin creation, keep that lifecycle. Add a migrator/bootstrap/configurator only when a demonstrated gap remains. Fail-closed rules apply to unsafe mutation under unknown state; they are not permission to distrust a proven native lifecycle.

##### Generated credential identity, secret format and transport are separate contracts

One logical credential uses one exact complete Magic Variable identity across producer/consumer services. Separately validate: application semantic/encoding requirements, generator output, and the transport/serialization path through Coolify `.env`, Compose interpolation, YAML, shell and any nested language. Symbol-bearing values are not universally unsafe; NetBox adds direct evidence that a valid application secret can still fail a specific Coolify transport path when `$` is reinterpreted.

##### URL roles and platform-generated configuration

Public browser URL, Docker-internal URL, canonical callback URL and Coolify proxy target are separate roles. Platform-derived variables/labels/DNS can become effective application configuration, but only when they can actually reach the target runtime.

##### Healthcheck hostname is application input

`localhost` and `127.0.0.1` can reach the same listening socket while producing different HTTP `Host` values. If `ALLOWED_HOSTS`, trusted-host middleware or virtual-host routing is active, preserve or deliberately validate the upstream probe hostname rather than normalizing it mechanically. NetBox RC5 required exactly this correction.

##### Same technology does not imply same state semantics

Two Redis/Valkey instances may have different roles, credentials, persistence and recovery behavior. NetBox tasks is an authenticated durable/AOF-backed trusted queue; NetBox cache is disposable and intentionally lacks AOF. Merge by semantics only, never by engine name or service-count pressure.

##### Queue/broker security follows execution authority

A queue that feeds workers is trusted execution infrastructure. If write access can cause application work to execute, keep it private and authenticated according to upstream even when the same technology is also used for a disposable cache.

##### Local readiness vs functional acceptance

Health of local services is not product acceptance. Real workers/schedulers/realtime/compile/provider paths require representative functional workflows at the highest claimed layer.

##### Persistence is a role-based store inventory

A primary database never proves complete durability scope. Classify database records, file subpaths, Redis/Valkey instances, queues/sessions, SQLite and generated configuration individually.

##### Coherence groups matter

Related stores can form one logical recovery set. Determine whether quiesce/flush ordering is required before capture; exact timing remains application-specific.

##### Build/artifact and architecture provenance are layered

Repository configuration, release metadata and published artifacts can temporarily disagree. Verify the artifact that actually exists. Full-stack CPU architecture support is the intersection of required runtime components, not the union of dependency capabilities.

##### Nested parser layers are runtime surfaces

YAML validity and Bash syntax do not prove embedded `sed`, JavaScript, SQL, database operators or generated secrets survive Compose/Coolify serialization. Validate the effective representation at each active parser layer.

#### NetBox Golden #10 contribution

NetBox adds the first permanent **knowledge-accumulation regression** benchmark. RC5 reached the accepted architecture in two iterations; RC6 accumulated more knowledge yet diverged for longer. The accepted fixture therefore protects both application behavior and a meta-property of Skill evolution:

> Adding new Golden knowledge must not reduce the Skill's ability to rediscover a target from current upstream evidence.

NetBox specifically adds:

- image-baked configuration verification before recreating repository bind mounts;
- image baseline + environment + smallest managed override;
- distinct Valkey tasks/cache semantics, credentials and durability;
- queue security as trusted worker-execution infrastructure;
- native NetBox migration + superuser lifecycle preservation;
- `localhost` vs `127.0.0.1` healthcheck Host-header regression coverage;
- application-secret requirements + Coolify transport/serialization compatibility as separate checks;
- older-successful-path vs newer-failed-path comparison as a first-class Skill regression workflow.

#### Monotonic Skill evolution

Skill evolution is monotonic only when new knowledge improves or preserves performance on previously solvable architecture classes. If an older Skill release produces a runtime-accepted candidate faster than a newer release, compare the reasoning paths, identify newly introduced rules that caused unnecessary divergence, and scope those rules to their causal context. Do not remove valid new knowledge merely because another target differs.

The fundamental rule remains: **Golden fixtures are regression oracles, not architecture templates.**

#### Baserow multi-profile additions

##### One product does not imply one topology

Baserow adds the first corpus case where more than one intentionally supported deployment profile reached operator acceptance. The Skill must preserve the distinction between a canonical regression oracle and validated alternatives instead of averaging them into a hybrid or counting profile exploration as failures.

##### Process topology != state topology

Overleaf already showed that internal process topology does not automatically imply Compose decomposition. Baserow extends this: the same all-in-one application process topology can externalize authoritative PostgreSQL state without splitting frontend/backend/workers.

##### Semantic gateway confirmation

OpenMRS, ODK Central and Frappe established that the platform edge proxy can coexist with an application-semantic gateway. Baserow's Caddy is another counterexample: `/api/*`, `/ws/*`, `/mcp/*`, `/assistant/*`, static/media and frontend/Application Builder semantics must survive behind Coolify.

##### HTTP health semantics generalization

NetBox proved `localhost != 127.0.0.1` under ALLOWED_HOSTS/Host validation. Baserow proves a different mechanism: a syntactically valid local request can enter Application Builder domain routing and return a product-semantic 404. Therefore health reasoning must include Host + path + router semantics, not only network reachability.

##### Same technology, different persistence semantics

NetBox's tasks Valkey is durability-sensitive while its cache store is disposable. Baserow's current Redis role does not inherit that AOF requirement merely because the engine family matches. State semantics are product/role facts, not technology defaults.

##### Version-specific workaround scope

A Baserow 2.3.3 fresh-install auth-provider/migration race can justify a profile-specific sequencing workaround after direct runtime evidence. That does not create a generic Django bootstrap rule and must be revalidated on later Baserow versions.

#### OpenSPP #12 — runtime layers, dependency closure and effective configuration

OpenSPP adds no generic Odoo topology. Its value is a set of cross-cutting distinctions:

```text
application source pin
!=
transitive build-graph pin
```

```text
process/framework health
!=
selected product activation
```

```text
Compose declared file content
!=
Coolify managed-resource identity
!=
host file
!=
container-mounted file
!=
application-effective config
```

```text
Compose spec support
!=
effective Coolify parser/model/deployment support
```

```text
internal proxy listener port
!=
canonical browser origin
```

Overleaf #9 and OpenSPP #12 together prove that dollar escaping must be derived from the transport path: Overleaf's Compose-command/JavaScript path required `$$set`, while OpenSPP's Coolify-managed Nginx file required native single-dollar Nginx variables. The general rule is an interpolation/serialization layer map, not one escaping token.

Frappe/ERPNext and OpenSPP together reinforce activation-aware readiness: framework/site existence does not prove the claimed product bundle is installed and usable.

NetBox and OpenSPP reinforce monotonic knowledge accumulation: preserve image/native/current-upstream primitives and effective runtime evidence before importing workaround machinery from another case.

OpenSPP's exact Nginx, PostGIS, queue worker, DB-role split, SP-MIS activation, XML compatibility overlays and backup sidecar remain local to Golden #12.

<!-- END PORTABLE RESOURCE: references/cross-benchmark-lessons.md -->

<!-- BEGIN PORTABLE RESOURCE: references/eight-benchmark-audit.md -->
<!-- SOURCE SHA256: 62b6d1aa0626c2a3e6017c6df469627f56bba36c8f87946dca05ce4e04d7d3f3 -->
<!-- EMBEDDED SHA256: 7d99259c7b90e74fe122daf23e6967f35ef6dd71d9752e3a2ec93bff561657bc -->

## Portable resource: `references/eight-benchmark-audit.md`

### Eight-benchmark architecture and bias audit

This is the current transversal audit for the eight runtime Golden / Regression Cases:

1. KoboToolbox V19.3
2. CKAN 2.12
3. OpenMRS 3.7.1
4. OpenEMR 8.3.0
5. ODK Central v2026.2.4
6. Frappe Framework v16.32.0
7. ERPNext v16.33.0
8. Mem0 v2.0.19

`references/five-benchmark-audit.md`, `references/six-benchmark-audit.md`, and `references/seven-benchmark-audit.md` remain historical snapshots and must not be rewritten merely to reflect later promotions.

#### Evidence/provenance classes

Use these labels rather than flattening evidence quality:

1. **upstream fact** — current/release-specific upstream source or deployment artifact;
2. **official Coolify fact** — current Coolify documentation/template behavior;
3. **runtime validated observation** — direct deployment/runtime output captured during a benchmark;
4. **operator-confirmed runtime result** — operator reports a runtime gate passed but the release corpus does not preserve every raw command/output;
5. **cross-benchmark confirmed pattern** — causal rule independently supported by heterogeneous cases;
6. **application-specific adaptation/workaround** — mechanism local to a product/version;
7. **inference / REVIEW REQUIRED** — plausible but not sufficiently proven.

Current upstream + current Coolify facts remain authoritative for a new target. Golden runtime evidence supplies causal questions and regression protection, not permission to copy topology.

#### Architecture diversity matrix

| Benchmark | Public shape | Core state | Special lifecycle | Counterexample/regression value |
|---|---|---|---|---|
| KoboToolbox | multiple public gateways | PostgreSQL + MongoDB + Redis + media | Celery/beat + Enketo | legitimate multi-host/callback complexity |
| CKAN | one main public app | PostgreSQL + Solr + Redis + FileStore | DataPusher + RQ worker/scheduler | search/import/worker topology can be legitimate |
| OpenMRS | semantic gateway | MariaDB + backend state | long bootstrap | semantic app gateway may remain behind Coolify |
| OpenEMR | one public app | MariaDB + site/documents | native bootstrap | small topology + multi-persistence |
| ODK Central | one semantic Nginx host | PostgreSQL + Redis/Enketo state | admin one-shot + upgrade lifecycle | official images can need external deployment files |
| Frappe Framework | one semantic frontend + realtime | MariaDB + Redis + sites | bootstrap/migrator + workers/scheduler | legitimate async/realtime complexity |
| ERPNext | same runtime family, product-specific activation | Frappe state + ERPNext product state | install vs migrate | sibling product != platform health |
| Mem0 | two public origins: dashboard + API | PostgreSQL/pgvector + SQLite history | API-owned Alembic + browser-first setup | compact AI stack; external provider + credential taxonomy without speculative infra |

No row is an average architecture.

#### Why Mem0 materially diversifies the corpus

Mem0 adds the first accepted case combining:

- external AI provider dependency;
- PostgreSQL vector extension rather than a dedicated vector DB;
- separate browser frontend and public REST API;
- two legitimate browser/public origins with CORS implications;
- native auth + JWT;
- application-issued API keys;
- browser-first first-admin workflow;
- application-owned startup Alembic;
- immutable remote Git source build;
- PostgreSQL plus auxiliary SQLite persistence;
- no workers/scheduler/cache/gateway sidecars.

Its first-candidate runtime acceptance also adds a **prevented-failure** evidence category: previously learned rules can be validated when they avoid defects before deployment, not only after an observed runtime failure.

#### Bias audit

##### 1. Service-count bias

**Risk:** infer that smaller is better, or larger is more production-grade.

**Counterexamples:** OpenEMR and Mem0 are compact; Kobo/Frappe are legitimately complex. Mem0 has only three services but non-trivial auth/CORS/provider/vector/recovery semantics.

**Rule:** service count is descriptive only. Complexity must be inherited from upstream responsibilities.

##### 2. Redis bias

**Risk:** repeated Redis appearances in Kobo/CKAN/ODK/Frappe cause automatic cache/queue addition.

**Counterexamples:** OpenMRS, OpenEMR and Mem0 do not need Redis in their accepted topology.

**Rule:** Redis requires current target provenance.

##### 3. Worker/scheduler bias

**Risk:** CKAN/Kobo/Frappe teach legitimate async roles, then those roles leak into unrelated stacks.

**Counterexamples:** OpenMRS/OpenEMR/Mem0 have no generic worker/scheduler service.

**Rule:** preserve upstream process separation; do not add async infrastructure by maturity aesthetics.

##### 4. Migration-sidecar bias

**Risk:** Frappe/ERPNext one-shot migrator pattern becomes assumed best practice.

**Counterexample:** Mem0 successfully retains `alembic upgrade head && uvicorn` inside API startup.

**Rule:** migration ownership is application-specific. Preserve upstream ownership unless runtime/Coolify evidence demands change.

##### 5. Bootstrap-sidecar bias

**Risk:** ODK/Frappe/ERPNext successful bootstrap helpers cause an init container to be added everywhere.

**Counterexamples:** OpenEMR native bootstrap and Mem0 browser-first `/setup` succeed without invented helper.

**Rule:** One-Click infrastructure does not require zero post-deploy user actions; secure native first-run flows may be the correct contract.

##### 6. Single-public-surface bias

**Risk:** minimize every app to one public service because Coolify can proxy it.

**Counterexamples:** Kobo has multiple semantic hosts; Mem0 legitimately exposes dashboard and public API separately.

**Rule:** public-surface multiplicity is application-driven.

##### 7. Postgres-only-state bias

**Risk:** a PostgreSQL service causes backup scope to stop at SQL.

**Counterexamples:** CKAN FileStore, OpenEMR site documents, Frappe sites, Mem0 `history.db`.

**Rule:** persistence is a path/store inventory, not a database checkbox.

##### 8. Platform-generated-secret bias

**Risk:** every secret-shaped environment variable becomes a `SERVICE_PASSWORD_*`.

**Counterexample:** Mem0 distinguishes DB/JWT deployment secrets, external provider-issued `OPENAI_API_KEY`, and application-issued API keys.

**Rule:** classify issuer/origin, generation phase, storage owner and rotation lifecycle before selecting a generator.

##### 9. AI-stack inflation bias

**Risk:** `AI app` implies Redis, worker queue, GPU, Qdrant/Weaviate/Milvus, gateway or model server.

**Counterexample:** Mem0 accepted topology is API + dashboard + PostgreSQL/pgvector and an external provider.

**Rule:** preserve the upstream AI/vector/provider architecture; technology category is not topology provenance.

##### 10. Published-image bias

**Risk:** prefer an old mutable image merely because the namespace appears official.

**Counterexample:** Mem0 uses a runtime-tested immutable remote Git source build because the selected current server snapshot was better represented by upstream source/Dockerfiles.

**Rule:** evaluate image publication recency, tags, docs and release relationship. Remote source build is conditional but legitimate when pinned and tested. Source pin != full dependency freeze.

##### 11. Automatic-admin bias

**Risk:** a “true One-Click” is assumed to require generated admin credentials.

**Counterexample:** Mem0 secure browser-first `/setup` succeeded and keeps application identity creation inside the app.

**Rule:** automate identity only when upstream semantics and credential recovery justify it.

##### 12. Provider-readiness conflation

**Risk:** external AI dependency is called from periodic healthchecks, making provider quota/outage equal container failure.

**Counterexample:** Mem0 local healthchecks remain provider-independent; provider workflow is an acceptance layer.

**Rule:** local readiness and external integration availability are separate evidence layers.

##### 13. Browser/internal-DNS conflation

**Risk:** because dashboard code runs in a container, expose `http://api:8000` to browser JavaScript.

**Counterexample:** Mem0 uses public `SERVICE_URL_MEM0` in the browser and internal URL only for server-side Docker traffic.

**Rule:** classify caller identity for every edge; browser DNS and Docker DNS are different namespaces.

##### 14. CORS-positive-only bias

**Risk:** expected origin works, so permissive `*` is never noticed.

**Mem0 lesson:** separate frontend/API deployments should test expected origin and, when security-relevant, an unrelated origin negative case.

**Rule:** CORS policy breadth is part of acceptance when credentials/auth are involved.

##### 15. Extension-capability bias

**Risk:** pgvector image is present, therefore vector extension/use is assumed.

**Mem0 lesson:** package availability, DB activation/version and application use are distinct evidence stages.

**Rule:** verify extension activation and version at the database layer when the product claim depends on it.

#### Prevented failure vs observed failure

The corpus now records both learning paths:

```text
OBSERVED FAILURE
  -> runtime failure evidence
  -> minimal correction
  -> acceptance

PREVENTED FAILURE
  -> known risk identified during discovery/static review
  -> risky mechanism omitted/corrected before deployment
  -> first accepted runtime candidate validates the preventive rule
```

Mem0's RC1 success provides preventive evidence for Magic Variable discipline, shared credentials, anti-contamination, provider-secret classification, browser/public URL separation, persistence discovery, auth preservation and migration ownership.

Do not optimize for “fewest RCs”. Iteration count is an optional descriptive maturity signal, not an engineering score.

#### Eight-case cross-benchmark principles

The corpus now strongly supports these causal rules:

1. **Current upstream architecture is the baseline.**
2. **Complexity has a provenance budget; service count is not a pass/fail metric.**
3. **Golden fixtures are regression oracles, not generic templates.**
4. **One logical shared credential uses one exact full Magic Variable identity.**
5. **Secret origin/lifecycle must be classified before generation.**
6. **Public browser, Docker-internal, callback and proxy-target URLs are separate roles.**
7. **Public-surface count follows application semantics.**
8. **Local readiness is not equivalent to external-provider readiness or product acceptance.**
9. **Persistence/backup follows every authoritative store/path, not only the primary DB.**
10. **Preserve upstream migration/bootstrap ownership unless evidence requires a change.**
11. **Runtime restore ordering must account for startup migrations and persisted credentials.**
12. **Image/source selection is provenance-driven; a mutable/stale image is not preferred by name alone.**
13. **Source revision pinning and full dependency reproducibility are different claims.**
14. **A successful first candidate can validate accumulated preventive rules when runtime acceptance confirms it.**

#### Mem0-specific facts that must remain local

Do not generalize these exact values/mechanisms:

- `v2.0.19` / commit `dc82354e143c2581d505d581a00286d6ef8c3605`;
- `pgvector/pgvector:0.8.6-pg17` and its digest;
- service names `mem0`, `dashboard`, `postgres`;
- `mem0_app`, `history.db`, `/setup`, `m0sk_*`;
- exact health endpoints, model defaults, retention variables and Alembic command;
- two public origins, pgvector, browser-first setup and remote Git build as universal AI requirements.

#### Eight-Golden anti-contamination conclusion

The eighth Golden strengthens the corpus precisely because it does **not** resemble the complex average of the previous seven. Future AI systems such as Dify, RAGFlow, LocalAI or vLLM must be rediscovered from current upstream architecture. Mem0 supplies questions about providers, credentials, browser/API origins, vector storage, migrations and recovery — never a generic AI Compose skeleton.

<!-- END PORTABLE RESOURCE: references/eight-benchmark-audit.md -->

<!-- BEGIN PORTABLE RESOURCE: references/eleven-benchmark-audit.md -->
<!-- SOURCE SHA256: ace3a5e112031ab47c61d763ee75b4fa65bfe0a0928943375527d4d04c9d343c -->
<!-- EMBEDDED SHA256: 8a6592117d7c57d0b46d6f1e4ab8f0c85c720631f6433f5e8490fc3f7a0e010e -->

## Portable resource: `references/eleven-benchmark-audit.md`

### Eleven-benchmark knowledge-accumulation audit

This audit adds Baserow 2.3.3 as Golden / Regression Case #11 while preserving all previous Goldens as historical executable oracles.

#### Corpus

1. KoboToolbox
2. CKAN
3. OpenMRS
4. OpenEMR
5. ODK Central
6. Frappe Framework
7. ERPNext
8. Mem0
9. Overleaf CE
10. NetBox
11. Baserow

#### New bias checks introduced by Baserow

##### Single-profile bias

**Risk:** assume one product must have one universal Compose topology.

**Correction:** model all current-upstream-supported profiles and select by operational target. Preserve accepted alternatives without merging them into the canonical Golden.

##### Distributed-is-production bias

**Risk:** infer that production automatically requires separate backend/frontend/workers.

**Correction:** require target-specific scaling, isolation, observability/resource-control or distributed-state evidence.

##### All-in-one-is-dev bias

**Risk:** reject a vendor-supported multi-process image because several runtime roles share a container.

**Correction:** treat it as a legitimate profile when upstream does and when its operational/recovery contract fits the target.

##### State-externalization-implies-service-decomposition bias

**Risk:** external PostgreSQL recommendation causes automatic application decomposition.

**Correction:** model process topology and state topology independently.

##### Semantic-proxy deletion bias

**Risk:** delete Caddy/Nginx because Coolify already has a reverse proxy.

**Correction:** map application-semantic responsibilities before removing any gateway. Coolify replaces public TLS/ingress, not product routing semantics.

##### Version-workaround permanence bias

**Risk:** keep a Baserow 2.3.3 workaround forever.

**Correction:** bind the workaround to the exact causal version/profile and revalidate on upgrade.

##### Health-root-path bias

**Risk:** treat a root-path local 404 as service failure.

**Correction:** evaluate network target + Host + path + router + auth/state semantics and prefer dedicated liveness when product routes are Host-sensitive.

##### Profile-exploration-counted-as-failure bias

**Risk:** count all-in-one, distributed and external-DB exploration as successive failed RCs.

**Correction:** repair iterations measure changes inside one profile; profile variants are architecture choices.

#### Knowledge accumulation against prior Goldens

##### KoboToolbox

Still teaches multi-public-host/callback and complex persistence semantics. Baserow must not inherit Kobo's service count, Enketo, MongoDB or host-gateway behavior.

##### CKAN

Still teaches managed-file interpolation, internal callbacks and worker readiness. Baserow does not inherit Solr/DataPusher/RQ or CKAN's database bootstrap.

##### OpenMRS / ODK Central / Frappe

Their semantic-gateway lesson correctly transfers at the **causal principle** level: platform edge != application semantic gateway. Baserow supplies its own Caddy routes and does not copy their Nginx configurations.

##### OpenEMR

Still proves a minimal topology can be production-relevant when upstream supports it. This constrains any tendency to call Baserow's distributed profile automatically superior.

##### ERPNext

Still teaches product activation/state detection. Baserow's singleton auth-provider safeguard is not a sibling-product activation pattern and must remain Baserow/version-specific.

##### Mem0

Still teaches secret issuer taxonomy and application-issued credentials. Baserow API/user tokens remain application-issued rather than Magic-Variable placeholders.

##### Overleaf

Its supervised multi-process image lesson correctly helps Baserow all-in-one reasoning. The transfer is `internal process topology != Compose topology`, not Mongo/Redis/admin-bootstrap mechanics.

##### NetBox

NetBox's Host-sensitive health reasoning correctly prepares the Skill to inspect `localhost` vs `127.0.0.1`. Baserow generalizes the cause to Host + path + product router semantics. NetBox's dual-Valkey/AOF state rules must not be copied into Baserow.

#### RC7 knowledge-accumulation check on Baserow

Question:

> Did RC7 rediscover Baserow from upstream/runtime evidence without copying NetBox/Overleaf/Frappe patterns?

Required negative checks:

```text
no NetBox dual Redis by analogy
no NetBox AOF by analogy
no Overleaf MongoDB
no Frappe topology copied blindly
no ODK Nginx
no generic custom bootstrap without Baserow cause
```

Required positive causal reuse:

```text
semantic gateway reasoning
Host-sensitive health reasoning
lifecycle ownership discipline
coherent recovery thinking
```

The accepted Baserow Golden satisfies this audit model: it preserves Baserow-specific PostgreSQL/pgvector, Redis, backend/frontend/Celery/Caddy roles and its exact runtime workaround instead of assembling an average of earlier Goldens.

#### Golden/alternative boundary

Only one new numbered Golden is added:

```text
Golden #11
Baserow 2.3.3 distributed/custom accepted RC5
```

A second operator-accepted architecture is recorded as:

```text
Validated Alternative Profile
Official all-in-one application + external PostgreSQL
```

The failed/tested all-in-one path is recorded as:

```text
Reference / Candidate Profile
```

This prevents both data loss (erasing legitimate alternative knowledge) and regression-corpus inflation (inventing Golden #12/#13 for the same benchmark).

#### Monotonicity result

RC8 keeps the RC7 rule:

> Adding new Golden knowledge must not reduce the Skill's ability to rediscover a target from current upstream evidence.

Baserow adds a stronger corollary:

> A mature deployment Skill must be able to represent multiple valid architectures for the same product without collapsing them into one universal topology.

The corpus now tests not only whether knowledge can be accumulated without contamination, but whether the Skill can keep **multiple valid profile models** simultaneously while protecting one exact canonical regression oracle.

<!-- END PORTABLE RESOURCE: references/eleven-benchmark-audit.md -->

<!-- BEGIN PORTABLE RESOURCE: references/erpnext-case-study.md -->
<!-- SOURCE SHA256: bd19feae1ee6bb0f75f647fee3b0244bc63c0e236010f7c5f4f045103255063e -->
<!-- EMBEDDED SHA256: 6690ced6c375aa2e4d6ae74719eebd56afb0b4396ca6a27155408149cdab055a -->

## Portable resource: `references/erpnext-case-study.md`

### ERPNext on Coolify — Golden / Regression Case #7

ERPNext is the seventh runtime Golden case for `coolify-architect`, and the first **sibling-product benchmark** in which an already-proven platform Golden is reused as causal knowledge while the product activation state and product-level acceptance still require their own proof.

The immutable regression fixture is `assets/erpnext-v16.33.0-v1.0.0-golden.yml`. It is the exact bytes of the operator-selected `erpnext-coolify-v1.0.0-rc5.yml` candidate, SHA-256 `64660809aba082409a41e20006d0d24dbc913928a30f07590ba873171ee2a7cb`.

Golden #6 and Golden #7 have an explicit relationship:

```text
Frappe Framework Golden #6
        platform/runtime reference
                 |
                 v
ERPNext Golden #7
        product/app profile on Frappe
```

This relationship does **not** make ERPNext a renamed copy of the Frappe Golden. Golden fixtures remain regression oracles, not generic templates.

#### Evidence boundary and provenance

Use the following provenance classes explicitly:

- **Coolify documented:** current Docker Compose behavior, generated `SERVICE_*` values, `exclude_from_hc`, routing variables and platform TLS/proxy responsibilities.
- **Frappe upstream documented:** Bench/site/app model, `new-site`, `install-app`, `list-apps`, `migrate`, Nginx frontend, workers, scheduler, Socket.IO and backup/restore primitives.
- **ERPNext upstream documented:** ERPNext is a Frappe application that must be installed on a site; product-specific behavior and onboarding belong to ERPNext, not to the container image alone.
- **Frappe runtime demonstrated:** Golden #6 platform topology and Coolify adaptation behavior.
- **ERPNext runtime demonstrated:** site creation, Frappe + ERPNext installation, migration, worker activity, scheduler activity, corrected health behavior and the accepted product profile.
- **Operator-confirmed:** completion of the benchmark acceptance path, including product workflow, persistence/redeploy and recovery gates that are not all represented by full raw logs in this Skill bundle.
- **Cross-benchmark demonstrated:** generated-secret identity discipline, semantic-gateway distinction, one-shot lifecycle, persistence granularity and evidence-stage separation.
- **Inference / conditional:** any behavior not directly supported by the accepted fixture, current upstream or operator confirmation.

Do not fabricate absent timestamps, object names, request IDs or log lines. The bundled case records the accepted result and exact executable fixture; it is not a complete raw transcript of the benchmark session.

#### Accepted executable metadata

| Item | Accepted value / evidence |
|---|---|
| Golden fixture | `assets/erpnext-v16.33.0-v1.0.0-golden.yml` |
| Fixture SHA-256 | `64660809aba082409a41e20006d0d24dbc913928a30f07590ba873171ee2a7cb` |
| ERPNext image | `frappe/erpnext:v16.33.0@sha256:493cecf82c92c828bf0d0c57df60694e07dc61671e374ac93a070d1cc86df1bd` |
| Runtime app state observed | `frappe 16.31.0`, `erpnext 16.33.0` |
| MariaDB | `mariadb:11.8.9@sha256:2439dcd7d14010ecd1ff7a4e1c5abe8e208c34fe35290744deeeaac3569043c3` |
| Redis | `redis:8.6.6-alpine@sha256:75934ddb37bfaebe3b4082ba673cac39f66495244134f33dd0a502ce03cdcd36` |
| CPU profile | `linux/amd64` |
| DB-root Magic Variable | `SERVICE_PASSWORD_64_ERPNEXTDBROOT` |
| Administrator secret | `SERVICE_PASSWORD_64_ERPNEXTADMIN` |
| Structural login identity | `Administrator` |
| Visible admin variable | `ERPNEXT_ADMIN_USERNAME`, default `Administrator` |
| Public hostname | `SERVICE_FQDN_FRONTEND` |
| Canonical public URL | `SERVICE_URL_FRONTEND` |
| Proxy-routing declaration | `SERVICE_URL_FRONTEND_8080` |
| Explicit conversion flag | `ALLOW_EXISTING_FRAPPE_SITE_CONVERSION`, default `false` |

A future ERPNext/Frappe/image/database/Redis/bootstrap change is a new candidate and must re-run the applicable gates before replacing this fixture.

#### Architecture — platform and product are different layers

ERPNext does not introduce a second application server beside Frappe. The accepted conceptual graph is:

```text
Coolify proxy / TLS
        |
        v
Frappe semantic Nginx frontend
        |
        +--> backend / Gunicorn
        |
        +--> websocket / Socket.IO
        |
        v
Frappe runtime / Bench
        |
        v
site / tenant
        |
        v
Installed Apps
  - frappe
  - erpnext
```

The retained infrastructure is the same runtime family proven by Frappe: MariaDB, Redis cache, Redis queue, configurator, site bootstrap, migrator, backend, semantic Nginx frontend, Socket.IO, RQ workers and scheduler. That shared topology is platform evidence; it is not proof that ERPNext is activated or usable.

Never generalize this into:

```text
frappe backend
+ erpnext backend
```

unless a future upstream explicitly defines such a topology.

#### Four activation layers

ERPNext strengthens the generic activation model:

```text
1. Container / image capability
2. Runtime / platform
3. Instance / site / tenant
4. Installed / enabled product modules
```

For this case:

```text
image       = frappe/erpnext
runtime     = Frappe Bench
site        = generated public FQDN
installed   = frappe + erpnext
```

The image can contain ERPNext code while a site remains Frappe-only. The authoritative site-level query is `bench --site "$SITE" list-apps` or the equivalent native activation registry on another platform.

General rule: **code available in an image is weaker evidence than installed/enabled state on the authoritative instance.** Apply this to plugins, modules, extensions, tenants and other activatable product packs.

#### Sibling Product Delta Gate

When a new candidate shares a platform/runtime with an existing Golden, first build this delta:

```text
BASE PLATFORM
SHARED INFRASTRUCTURE
PRODUCT-SPECIFIC STATE
PRODUCT-SPECIFIC BOOTSTRAP
PRODUCT-SPECIFIC MIGRATION
PRODUCT-SPECIFIC ACCEPTANCE
PRODUCT-SPECIFIC RECOVERY
```

Then answer:

| Question | Required result |
|---|---|
| What is inherited? | only causes/topology proven to remain current for the sibling |
| What is revalidated? | current upstream compatibility, versions, runtime wiring and state boundaries |
| What changes? | product activation, bootstrap, migrations, acceptance and recovery deltas |
| Why? | current upstream, Coolify requirement or demonstrated runtime evidence |

Do not use `same runtime -> copy Golden -> rename app -> done`.

#### Instance state and product state are independent

The accepted bootstrap models at least four states:

| Instance/site | Product activation | Required behavior |
|---|---|---|
| absent | absent | fresh bootstrap: create site and install product |
| present | present | safe reconciliation + supported migration |
| present | absent | product-profile mismatch; explicit conversion policy required |
| partial/unknown | unknown | fail closed; inspect before mutation |

This is reusable for Frappe apps, CMS plugins, tenant-scoped modules, application packs and schema extensions.

##### Existing site without ERPNext

The accepted fixture does **not** silently install ERPNext into an existing Frappe-only site. Default behavior is fail closed. Conversion is allowed only when the operator explicitly enables:

```text
ALLOW_EXISTING_FRAPPE_SITE_CONVERSION=true
```

General rule: **do not silently convert a persistent instance into a materially different product profile merely because the current One-Click knows how to do it.** A redeploy should preserve product identity.

##### Partial site directory

If `sites/$SITE/` exists but `site_config.json` is absent, the bootstrap refuses automatic repair. A directory existing is not sufficient evidence that a site is valid, recoverable or safe to overwrite.

##### `list-apps` failure

If `bench --site "$SITE" list-apps` fails, the accepted bootstrap does not reinterpret the failure as “ERPNext absent” and install it. The state is unknown, therefore the bootstrap fails closed.

General rule: **unknown persistent application state must not be converted into assumed state before mutation.**

#### Partial initialization model

ERPNext adds a useful state ladder:

```text
DB engine healthy
!= platform initialized
!= site initialized
!= product installed
!= product migration complete
```

Recovery decisions belong to the layer that actually failed. Do not propose destructive volume deletion until the authoritative state has been identified and the operator has established that the affected state is disposable or recoverable.

#### One-initial-site automation contract

The accepted One-Click refuses to auto-create the target site when another site already exists in the managed `sites` volume. This is **contract-specific**, not a claim that Frappe lacks multi-site capability.

General rule: **a platform's maximum capability does not have to equal a One-Click's automation contract.** Explicitly document what the template manages automatically versus what the underlying platform supports manually.

#### App installation is durable state

`bench --site "$SITE" install-app erpnext` changes durable site/application state. Treat app installation like schema migration, plugin activation, module enablement or feature-pack installation.

Do not replay it blindly on every redeploy. Detect state first.

The lifecycle is:

```text
Fresh install
  create site
  -> install ERPNext
  -> verify activation

Existing install
  verify ERPNext activation
  -> run supported migrations
  -> reconcile safe configuration
```

A migration is not a substitute for first installation, and reinstalling the product is not a migration strategy.

#### Post-condition verification

The accepted bootstrap does not stop at an `install-app` exit code. It re-runs `list-apps` and requires both `frappe` and `erpnext` in final state.

General rule:

```text
mutation command exit 0
+ authoritative post-condition
> mutation command exit 0 only
```

Use post-condition verification whenever the target exposes a reliable state query.

#### Product acceptance is above platform acceptance

ERPNext makes the acceptance ladder explicit:

```text
LEVEL 1 — infrastructure
DB / Redis / process / proxy health

LEVEL 2 — platform
Frappe site / auth / files / queues / scheduler / realtime

LEVEL 3 — product
ERPNext activated + representative ERPNext business state/workflow
```

`/api/method/ping`, a healthy Frappe Desk and green workers are platform evidence. They do not independently prove ERPNext product behavior.

The Golden promotion records operator-confirmed completion of a representative ERPNext product workflow plus persistence/redeploy/recovery acceptance. The exact business object used by the operator is not hard-coded as a universal acceptance test in this Skill.

General rule: **validate at the highest application layer claimed by the One-Click.**

#### Business onboarding is not infrastructure truth

ERPNext onboarding can require organization-specific facts such as company identity, country, currency, chart of accounts, fiscal/accounting choices, warehouses, taxes and payment terms.

Infrastructure automation must not fabricate those values merely to make the deployment look “fully configured”. Leave organization-specific onboarding to the owner unless the operator explicitly supplies authoritative values and asks for automation.

General rule: **technical readiness does not authorize invented business truth.**

#### Product profile vs platform capability

The `frappe/erpnext` image and Frappe ecosystem can support other apps. The ERPNext One-Click does not therefore install every available sibling.

The product profile is explicit:

```text
required installed apps:
- frappe
- erpnext
```

Future Frappe CRM, Helpdesk, HRMS or LMS candidates must re-discover current compatibility, dependencies, install commands, product bootstrap, migrations, acceptance and recovery. ERPNext is a sibling precedent, not a substitution template.

#### Runtime failure/correction timeline

##### RC1 — embedded `sed` syntax defect

The first ERPNext candidate failed in `site-bootstrap` before `bench new-site` with:

```text
sed: -e expression #1, char 34: unterminated `s' command
```

The embedded expression had passed shell syntax validation because `bash -n` validates Bash grammar, not the syntax of nested tools such as `sed`.

`set -euo pipefail` correctly stopped the dedicated bootstrap process at that point, preventing later site mutation.

The follow-up candidate removed the fragile prefix/suffix `sed` transformation and used shell parameter expansion instead:

```bash
other_site="${other_site#sites/}"
other_site="${other_site%/site_config.json}"
```

General lesson: when a command passes through YAML -> Compose interpolation -> shell parsing -> sed/awk/regex parsing, every parser layer increases quoting risk. Prefer simpler shell parameter expansion for trivial prefix/suffix transforms when equivalent; this is a preference, not a ban on `sed`.

##### Healthcheck interpolation defect

A later candidate created and installed ERPNext successfully but backend probes passed a literal `${FRAPPE_SITE_NAME}` because the header value was single-quoted inside the runtime shell command. Frappe rejected the literal as an invalid site name.

The correction preserved Compose `$$` escaping while allowing the runtime shell to expand the variable in a double-quoted header. This is a health-probe quoting failure, not evidence that Frappe, MariaDB or the created site was broken.

General lesson: validate the **effective runtime representation** after Compose escaping, not only the YAML source string.

##### Long-running health coverage

The accepted profile includes real healthchecks for backend, websocket, frontend, MariaDB, both Redis services, both workers and scheduler. The three lifecycle helpers remain one-shots with `exclude_from_hc: true` and successful completion as their expected state.

A healthcheck still does not replace product-level functional acceptance.

##### RC5 administrator visibility

The final accepted profile exposes `ERPNEXT_ADMIN_USERNAME` with default `Administrator` so the operator can see the login identity alongside the generated password. It does not randomize the structural Frappe account to consume `SERVICE_USER_*`.

This confirms an existing rule from Frappe/OpenMRS: **fixed upstream account identity and generated secret are separate concerns.**

#### Compose dollar-context matrix

ERPNext adds an inline-command example to the existing managed-file interpolation lessons:

| Context | Typical meaning of `$` | Validation concern |
|---|---|---|
| Compose interpolation | `${VAR}` may be resolved by Compose/Coolify | decide whether expansion belongs here or later |
| inline `command:` / healthcheck | `$$` can be required to emit a literal `$` for the container shell | inspect effective runtime shell text |
| shell variable | `$VAR` / `${VAR}` | quoting controls expansion |
| shell PID | `$$` at shell runtime | accidental when Compose escaping is copied into generated files |
| regex end anchor | `$` inside regex | may also cross Compose quoting/escaping |
| awk field | `$1`, `$2` | often needs Compose escaping inside inline shell |
| managed file `content:` | file bytes are a separate interpolation context | do not blindly copy inline-command escaping rules |

There is no universal “escape every dollar” rule.

#### `set -euo pipefail` execution context

ERPNext does not invalidate the CKAN sourced-hook lesson. Its bootstrap runs as a dedicated Bash process, so shell options are local to that process.

```text
executed script/process -> shell options are local to that process
sourced hook             -> options/traps/cd can leak into the parent shell
```

Inspect invocation semantics before adding strict shell options.

#### Magic Variable confirmation

ERPNext adds positive regression inputs:

```text
SERVICE_PASSWORD_64_ERPNEXTDBROOT
SERVICE_PASSWORD_64_ERPNEXTADMIN
```

The DB-root identity is reused by MariaDB initialization and site bootstrap. This confirms, rather than replaces, the Frappe-derived longest-type parsing and one-logical-credential/one-complete-Magic-Variable rules.

URL/FQDN roles remain separate:

```text
SERVICE_FQDN_FRONTEND      hostname/site identity
SERVICE_URL_FRONTEND       canonical public URL
SERVICE_URL_FRONTEND_8080  Coolify route to internal frontend port
```

The name `FRONTEND` is case-specific; the general rule is semantic separation of public binding, canonical origin, hostname identity and internal proxy target.

#### Runtime acceptance ledger

| Gate | Status | Evidence class |
|---|---|---|
| Fresh deployment | PASS | operator-confirmed ERPNext benchmark acceptance |
| Site bootstrap | PASS | runtime demonstrated + operator-confirmed |
| `frappe` installed | PASS | runtime `list-apps` output |
| `erpnext` installed | PASS | runtime `list-apps` output |
| Migration | PASS | runtime demonstrated |
| HTTPS/routing | PASS | operator-confirmed |
| Administrator authentication | PASS | operator-confirmed |
| Representative ERPNext workflow | PASS | operator-confirmed; exact business fixture not generalized |
| Files/persistent site state | PASS | operator-confirmed |
| Background jobs | PASS | runtime worker job evidence + operator-confirmed |
| Scheduler | PASS | runtime scheduler job evidence + operator-confirmed |
| Realtime where applicable | PASS | operator-confirmed acceptance |
| Restart | PASS | operator-confirmed |
| Coolify redeploy persistence | PASS | operator-confirmed |
| Backup | PASS | operator-confirmed |
| Isolated restore | PASS | operator-confirmed |

The Skill must never convert those PASS labels into invented raw logs. Golden status records the operator-confirmed acceptance outcome and the fixture preserves the accepted executable.

#### Learning classification

##### A. New generalizable rules

- sibling-product delta analysis;
- platform/runtime/site/product activation layers;
- instance existence independent from product activation;
- fail closed when authoritative activation state is unknown;
- no silent persistent product-profile conversion;
- app installation and migration are separate lifecycle phases;
- verify post-conditions after durable product activation;
- product acceptance above platform health;
- infrastructure bootstrap must not invent organization-specific business truth;
- a One-Click automation contract can intentionally manage less than the platform's maximum capability;
- nested parser layers require effective-runtime validation, not only `bash -n`.

##### B. Confirmations of existing rules

- Magic Variable longest-type parsing and conservative credential IDs;
- one logical credential = one complete Magic Variable identity;
- fixed `Administrator` identity vs generated password;
- semantic Frappe Nginx remains behind the Coolify edge;
- URL/FQDN/proxy-target separation;
- one-shot lifecycle classification;
- worker/scheduler/realtime functional acceptance;
- DB + site/files persistence and recovery-set thinking;
- Golden fixtures are regression oracles, not templates.

##### C. ERPNext-specific facts

- exact ERPNext/Frappe versions and image digest in the accepted fixture;
- `--install-app erpnext` / `install-app erpnext` lifecycle;
- `bench ... list-apps` as authoritative app state;
- `SERVICE_PASSWORD_64_ERPNEXTDBROOT` and `SERVICE_PASSWORD_64_ERPNEXTADMIN`;
- `ERPNEXT_ADMIN_USERNAME` default `Administrator`;
- `ALLOW_EXISTING_FRAPPE_SITE_CONVERSION` flag name;
- one automatically managed initial site contract;
- ERPNext-specific onboarding/business domains.

##### D. New anti-patterns

- sibling Golden copy/paste and token replacement;
- treating a site directory as proof of product activation;
- installing a missing product after state detection failed;
- silently converting a Frappe-only site to ERPNext;
- declaring product success from Frappe ping/Desk alone;
- declaring app activation from `install-app` exit code alone;
- inventing company/currency/jurisdiction/accounting data during infrastructure bootstrap;
- assuming all apps present in an image should be enabled;
- blaming DB/network/application layers for an inline-shell syntax failure that occurs earlier.

##### E. Conditional / not independently evidenced in bundled raw logs

- exact business object names used by the operator's ERPNext acceptance workflow;
- every request/response and timestamp from persistence, backup and restore runs;
- behavior of future ERPNext/Frappe releases or sibling apps;
- HA, performance/load, multi-region DR and optional integrations not explicitly tested.

#### Golden boundary

The fundamental lesson is:

> **A proven platform architecture can be reused as causal knowledge, but product activation, persistent-state transitions and business-level acceptance must still be rediscovered and proven for each sibling product.**

And the existing corpus rule remains:

> **Golden fixtures are regression oracles, not generic templates.**

<!-- END PORTABLE RESOURCE: references/erpnext-case-study.md -->

<!-- BEGIN PORTABLE RESOURCE: references/evaluation-prompts.md -->
<!-- SOURCE SHA256: d06a611995df497e8d6c484d81dab48782175705bf67dc567aef9cecd735264a -->
<!-- EMBEDDED SHA256: 5a8f30679bd5c506def2f600899e26ca5783dd59c3360120fcbf3a86e74102b0 -->

## Portable resource: `references/evaluation-prompts.md`

### Evaluation prompts for coolify-architect

Use these prompts to test activation, scope, and workflow quality after modifying the skill.

#### Should trigger — ADAPT

**Prompt**

> This OSS repository has a Compose for local self-hosting. Adapt it to Coolify, keep Postgres private, use generated secrets, and make it production-ready.

**Expected**

- activates the skill;
- inspects upstream deployment material before editing;
- produces all five architecture maps;
- preserves upstream topology unless a Coolify change is justified;
- distinguishes static validation from live acceptance.

#### Should trigger — AUDIT

**Prompt**

> Audit this Coolify docker-compose.yml. I want to know whether data survives redeploys and whether any internal service is exposed publicly.

**Expected**

- persistence and exposure audit;
- severity-ordered findings;
- no destructive changes without request.

#### Should trigger — TROUBLESHOOT

**Prompt**

> All containers are green in Coolify, login works, but form preview calls back to the public API and fails from inside the stack.

**Expected**

- public/internal/callback URL analysis;
- hairpin/loopback test before proposing `host-gateway`;
- no automatic copying of the Kobo host-gateway solution.

#### Should trigger — CLEAN

**Prompt**

> Clean this working Coolify Compose. Remove comments like V14 fix, V15 preflight and repeated version strings without changing behavior.

**Expected**

- one revision marker maximum;
- NOTE/REQUIRED/SAFETY convention;
- parsed-config/script comparison after cleanup.

#### Should trigger — CREATE

**Prompt**

> This project has Dockerfiles and Helm charts but no Compose. Build a Coolify template.

**Expected**

- derives services from Dockerfiles/Helm/docs;
- marks assumptions;
- does not invent missing startup behavior silently.

#### Should trigger — canonical domains

**Prompt**

> Can I use Coolify's random generated URL as the canonical public URL for this app? It stores webhook callback URLs in its database.

**Expected**

- explains stable canonical URL requirement;
- verifies current Coolify generated-domain semantics;
- recommends stable custom domain when durability is required.

#### Should not trigger

**Prompt**

> Explain what `docker compose up -d` does.

**Expected**

- generic Docker explanation; no full Coolify architecture workflow.

#### Should not overclaim

**Prompt**

> The YAML parses. Is this now production-ready?

**Expected**

- says no;
- explains staged evidence vocabulary;
- requires deployment/application acceptance, persistence, security, backup/restore, and operations checks.

#### Regression case — probe failure

**Prompt**

> Coolify says the app is unhealthy, but I can load the website. The health command is `python -c " import socket; ..."`.

**Expected**

- spots the leading-whitespace Python risk;
- validates the probe independently before changing service architecture.

#### Regression case — Nginx substitution

**Prompt**

> My Nginx template must expand `$PUBLIC_URL`, but `$host` and `$scheme` are becoming empty at startup.

**Expected**

- identifies broad envsubst collision;
- recommends targeted substitution/filtering.


#### Architecture-selector regression tests

##### Queue-mode target
A web app + PostgreSQL + Redis + queue worker + separate runner should strongly consider `n8n-with-postgres-and-worker.yaml`; it must not import S3 or DinD.

##### Local S3 initialization target
Frontend/backend + PostgreSQL + Redis-compatible cache + MinIO + required bucket initialization should strongly consider `penpot-with-s3.yaml`.

##### Vector database target
A standalone persistent vector database with an API key should prefer Qdrant/Weaviate/Chroma references over ordinary app+DB templates.

##### CI runner target
A Git forge that executes container-based CI jobs may use Forgejo/Gitea runner references; privileged/DinD is considered only because container execution requires it.

##### Ordinary web app negative test
A single HTTP app with one persistent volume and no DB/worker/container execution must not inherit PostgreSQL, Redis, MinIO, Docker socket, privileged mode or custom networks.


#### CKAN-derived regression tests

##### Port-qualified canonical URL regression

**Prompt**

> Coolify gives `SERVICE_URL_APP_5000=https://app.example.org:5000` and `SERVICE_FQDN_APP_5000=app.example.org:5000`, but users must browse `https://app.example.org`. What should `APP_SITE_URL` use?

**Expected**

- does not assume the port-qualified FQDN is host-only;
- models proxy target and canonical public origin separately;
- verifies current Coolify magic-variable semantics;
- uses an explicit/unqualified canonical hostname only when correct for this deployment;
- does not remove the internal port from Docker service-to-service URLs.

##### Inline file dollar-escaping regression

**Prompt**

> This Coolify `volumes[].content` Bash file contains `psql -U "$${POSTGRES_USER}"` and PostgreSQL says role `50{POSTGRES_USER}` does not exist.

**Expected**

- recognizes shell `$$` PID expansion;
- distinguishes managed file content from Compose command interpolation;
- changes the generated file to contain `${POSTGRES_USER}` when that is the intended shell variable;
- does not globally remove `$$` from ordinary Compose commands/healthchecks.

##### Managed file vs directory regression

**Prompt**

> PostgreSQL logs `/docker-entrypoint-initdb.d/10_init.sh: Is a directory` even though the Compose intends a generated init script.

**Expected**

- inspects Coolify managed-file semantics and deployment-host source path;
- makes file intent explicit and checks `content:`/`is_directory` behavior;
- does not redesign PostgreSQL or expose it publicly;
- may use a fresh disposable Coolify resource as an isolation test only if stale resource state is evidenced.

##### Sourced-hook regression

**Prompt**

> An upstream entrypoint sources `/docker-entrypoint.d/*.sh`. My hook starts with `set -euo pipefail`, then the parent entrypoint crashes later on an optional unset variable.

**Expected**

- identifies parent-shell option leakage;
- verifies sourced versus executed behavior from upstream;
- avoids global shell-option mutation or contains it appropriately;
- preserves the upstream entrypoint.

##### Worker readiness regression

**Prompt**

> The app takes several minutes for first migrations. `worker` uses `depends_on: app: condition: service_healthy`; Compose gives up, but the app becomes healthy later and the worker never starts.

**Expected**

- treats `depends_on` as a startup gate, not continuous reconciliation;
- preserves upstream worker command;
- proposes bounded application-level readiness/retry if architecturally appropriate;
- does not solve the problem with an arbitrary fixed sleep.

##### Worker false-negative health regression

**Prompt**

> Worker logs say it started the queue and scheduler, and it processes jobs, but Coolify says `Running (unhealthy)` because a `/proc/1/cmdline` string-match probe fails.

**Expected**

- trusts runtime job evidence enough to diagnose the probe independently;
- fixes the health signal rather than deleting the worker or healthcheck;
- does not assume one marker/PID pattern is universal across worker implementations.

##### Partial database bootstrap regression

**Prompt**

> PostgreSQL is healthy, but the application role is missing. Logs show the first `/docker-entrypoint-initdb.d` script failed once, and later restarts say `Skipping initialization`.

**Expected**

- distinguishes DB engine health from application initialization;
- understands first-bootstrap-only init directories;
- requests proof of authoritative data before destructive recovery;
- allows rebuilding a disposable fresh-install volume only when failure/state evidence justifies it.

##### Internal callback positive case

**Prompt**

> Upstream documents a dedicated callback base specifically so an importer container can fetch the app over `http://app:5000` instead of the public site URL.

**Expected**

- preserves the internal callback when upstream says public-host semantics are unnecessary;
- does not add host-gateway/hairpin just because the Kobo case used it;
- keeps the browser canonical URL separate.

#### Anti-contamination tests

##### New-target anti-contamination test

**Prompt**

> Adapt a new complex open-source application to Coolify using the seven golden cases in this Skill.

**Expected**

- starts from the new target's current upstream architecture/release material;
- treats Kobo, CKAN, OpenMRS, OpenEMR, ODK Central, Frappe and ERPNext only as causal regression references;
- if any overlapping technology appears, requires independent target-upstream evidence rather than rejecting or importing it by name;
- produces fresh service/URL/persistence/secret/readiness maps before Compose.

##### Golden fixture misuse

**Prompt**

> CKAN V1.0.8 is known-good. Use it as the base and replace the CKAN image with another application's image.

**Expected**

- refuses the mechanical substitution approach;
- explains that a golden fixture is a regression oracle, not a generic skeleton;
- rediscovers the target upstream topology first.


#### OpenMRS-derived regression tests

##### Shared magic database credential

**Prompt**

> MariaDB uses `MYSQL_PASSWORD=${SERVICE_PASSWORD_64_MYSQL}`, but the application uses `DB_PASSWORD=${SERVICE_PASSWORD_64_APP}` to connect as the same DB user. Both values are non-empty. Is this correct?

**Expected**

- identifies two independently generated passwords for one logical database credential;
- requires both producer and consumer to reuse the same complete magic variable;
- does not hard-code the generated value itself;
- treats the magic identifier as persistent state once the database exists.

##### Username/password family confusion

**Prompt**

> `DATABASE_USERNAME=${SERVICE_PASSWORD_64_DB}` and `DATABASE_PASSWORD=${SERVICE_USER_DB}`.

**Expected**

- reports the semantic family swap as a strong wiring error/warning;
- recommends `SERVICE_USER_*` for the username and a compatible password family for the password;
- verifies any upstream lowercase/symbol/length constraints rather than assuming defaults.

##### True Base64 format test

**Prompt**

> Upstream says `SESSION_KEY` must be actual Base64. The Compose uses `${SERVICE_BASE64_64_SESSION}`.

**Expected**

- knows current Coolify `SERVICE_BASE64_*` is not Base64 encoded;
- selects a documented `SERVICE_REALBASE64_*` size only if it matches upstream requirements;
- does not invent an unsupported magic family.

##### Blank generated credential pre-deploy gate

**Prompt**

> Coolify parsed the Compose, but the required generated DB username/password fields are blank in the Environment Variables screen. The database volume is still fresh. Deploy now and debug later?

**Expected**

- stops before bootstrap;
- verifies documented magic syntax and actual generated values first;
- avoids partial persistent initialization;
- does not assume that a particular credential-ID punctuation rule is the cause without documentation/evidence.

##### Persisted admin credential test

**Prompt**

> I changed the initial admin password environment variable after the application was initialized, but the old password still authenticates.

**Expected**

- distinguishes bootstrap env configuration from persisted account state;
- verifies application-specific password update semantics/API/CLI;
- does not rotate/delete the database as a first response;
- does not generalize the OpenMRS password endpoint to other applications.

##### Transient 503 during long bootstrap

**Prompt**

> Coolify returns `503 no available server`. Direct container HTTP is 200, proxy and target share a network, proxy labels point to the correct internal port, but Docker health is still `starting` and health history shows a timeout followed by successes.

**Expected**

- treats target readiness/health as the leading explanation;
- waits/observes the actual bootstrap lifecycle before redesigning networking;
- keeps public-proxy, direct-service, and health-state evidence separate;
- does not add custom networks or host-gateway without a separate proven requirement.

##### OpenMRS golden contamination negative case

**Prompt**

> Use the OpenMRS golden fixture as the base for a new application because it already has gateway, frontend, backend and MariaDB.

**Expected**

- refuses mechanical inheritance;
- rediscovers the new upstream from scratch;
- treats the matching layered shape only as an architectural similarity hint;
- copies no `OMRS_*`, `/openmrs/*`, `admin`, MariaDB, password preflight, or 15-minute start period unless independently justified.

#### OpenEMR-derived minimality regression tests

##### Simple-upstream complexity-provenance test

**Prompt**

> The current upstream production Compose for this application has only a public web service and MariaDB. The app image owns its own bootstrap and there is no upstream Redis, worker, scheduler, gateway, queue or init service. Adapt it to Coolify using all seven golden cases.

**Expected**

- starts from the current upstream two-service topology instead of averaging the golden cases;
- keeps the candidate equally small unless a new capability has explicit current-upstream or Coolify-operational justification;
- does not add Redis, workers, gateways, search, object storage or init sidecars merely because Kobo/CKAN/OpenMRS contain them;
- does not delete the database merely to minimize service count;
- records provenance for every added/removed/split/merged capability;
- treats service count only as descriptive evidence, never a pass/fail threshold.

##### Native bootstrap preservation test

**Prompt**

> The upstream application image already waits for MariaDB, creates/configures its own application database and user, writes persistent site configuration, and starts the web server. Should I split that into a separate init container because another golden case used init files?

**Expected**

- preserves the upstream native lifecycle unless a documented problem requires a change;
- does not create a helper container merely for aesthetic separation;
- verifies fresh install, normal redeploy and restore behavior of the native path;
- treats any change to database ownership/bootstrap semantics as an explicit architectural delta requiring evidence.

##### Simple topology is not a universal target

**Prompt**

> OpenEMR succeeded with two services. Should I force this new upstream application down to two containers too?

**Expected**

- explicitly refuses a two-service target rule;
- explains that OpenEMR's service count is a case-specific invariant;
- preserves workers, queues, search, gateways, databases or other roles when the new upstream requires them;
- repeats the principle: complexity is inherited from the current upstream, not from the golden-case average or minimum.

#### ODK-derived regression tests

##### Published image still needs deployment files

> An upstream project publishes an official release image, but its Compose mounts runtime configuration files from the repository. Convert it to a self-contained Coolify One-Click template.

Expected behavior: the agent inventories external mounts before declaring the image self-contained, embeds/materializes only the required files when justified, and does not silently drop them.

##### One-shot lifecycle classification

> A PostgreSQL upgrade helper exits 0 after writing a marker and a long-running database waits on that marker. Audit the Compose.

Expected behavior: the agent treats successful completion as the helper's lifecycle signal and does not demand a daemon healthcheck merely because the image/service name looks stateful.

##### Compose command vs managed-file interpolation

> A one-shot Bash command uses runtime `$PGHOST`, command substitutions and `${#PASSWORD}` while a separate managed shell file also contains `${VAR}`. Make it Coolify-safe.

Expected behavior: Compose command-runtime dollars are escaped for Compose, while generated file content keeps normal shell syntax.

##### Application-native admin bootstrap

> Automate first-admin creation for an application whose backend exposes account-management task functions, and make redeploy idempotent.

Expected behavior: wait for application readiness, use the application-native task/CLI, detect existing admin/user state, do not reset an existing password, and avoid direct role-table mutation unless upstream leaves no safer mechanism.

##### Form-platform anti-merging test

> The target and KoboToolbox both use PostgreSQL, Redis and Enketo. Build the target's Coolify Compose.

Expected behavior: shared technology is not treated as a shared stack. The agent derives public routing, workers, Redis roles, secrets, callbacks and lifecycle from the target upstream before borrowing any mechanism.

#### Frappe-derived Magic Variable / lifecycle regression tests

##### Test A — malformed credential Magic Variable identifier

**Prompt**

> Coolify should generate this automatically:
> `SERVICE_PASSWORD_64_MY_APP_DB_ROOT`
> but Docker Compose Empty leaves it empty. What should you investigate?

**Expected**

- verify the current Coolify `SERVICE_<TYPE>_<ID>` documentation and parser behavior;
- parse `PASSWORD_64` as the full type before interpreting the identifier;
- recognize separator-bearing credential IDs as a version-sensitive parser risk;
- do not immediately add a custom secret generator;
- prefer/test a conservative alphanumeric credential ID such as `MYAPPDBROOT`;
- verify in the target Coolify runtime that generation is non-empty before stateful bootstrap;
- do **not** generalize the finding into “underscore is forbidden in every `SERVICE_*` variable”.

##### Test B — shared logical credential identity

**Prompt**

> The database creates one account with `SERVICE_USER_DATABASE` + `SERVICE_PASSWORD_64_DATABASE`, but the app connects as that same generated user with `SERVICE_PASSWORD_64_APPDATABASE`. Is that valid?

**Expected**

- FAIL when it is clearly the same logical account;
- one logical credential uses one exact complete Magic Variable identity across producer and consumer;
- does not copy/hard-code the generated value.

##### Test C — Magic Variable rename after bootstrap

**Prompt**

> The database was initialized with `SERVICE_PASSWORD_64_DATABASE`. I renamed the Compose variable to `SERVICE_PASSWORD_64_DB`. Why can the app no longer connect?

**Expected**

- explains that the new Magic Variable name may generate a new value;
- explains that the persisted database password did not automatically change;
- treats the complete generated variable name as deployment state;
- recommends intentional credential migration/rotation through the application/database-supported path;
- does not recommend deleting persistent data as the first fix.

##### Test D — URL vs port-scoped routing

**Prompt**

> `SERVICE_URL_APP_8080` exists. Should the application canonical URL automatically become `https://domain:8080`?

**Expected**

- no;
- verifies current Coolify semantics;
- separates browser canonical URL from the proxy-target declaration;
- distinguishes `SERVICE_URL_APP`, `SERVICE_FQDN_APP`, `SERVICE_URL_APP_8080`, Docker hostname and internal URL;
- recognizes that the suffix can select internal target port without making that port browser-visible.

##### Test E — semantic gateway

**Prompt**

> The upstream Compose has Nginx and Coolify already has a reverse proxy. Delete Nginx?

**Expected**

- inspects Nginx responsibilities first;
- deletes/replaces only Internet-edge/TLS behavior that Coolify owns;
- preserves Nginx if assets, protected files, headers, application routing, site identity or WebSocket/realtime semantics depend on it;
- does not infer a universal “keep Nginx” rule either.

##### Test F — image contains app

**Prompt**

> The container image contains Frappe and ERPNext. Does that prove every created site has ERPNext installed?

**Expected**

- no;
- distinguishes code available in image, global enablement, site/tenant installation, and configured feature state;
- verifies application activation through the platform's real state (`list-apps`, plugin/module registry, DB/configuration, etc.);
- does not silently convert a Frappe-only One-Click into ERPNext.

##### Test G — one-shot exited zero

**Prompt**

> `configurator` shows `Exited (0)` after startup. Is the stack unhealthy?

**Expected**

- not if the expected lifecycle is one-shot and successful completion is the signal;
- checks `service_completed_successfully`/dependent progression and logs;
- distinguishes one-shot init/migration from long-running daemons;
- does not require a daemon healthcheck merely to keep the helper running.

##### Functional async/scheduler/realtime evidence

**Prompt**

> Worker, scheduler and websocket containers are all Running. Can I mark those features as validated?

**Expected**

- no;
- worker: enqueue a real application job, observe consumption, prove resulting state;
- scheduler: prove native scheduled work after startup;
- realtime: prove browser/proxy/Host/Origin/auth/path/namespace behavior and a real event;
- keeps process/health evidence separate from application-feature evidence.

##### Fixed account identity vs generated username

**Prompt**

> Upstream has a special account named `Administrator`, but Coolify can generate usernames. Should I replace it with `SERVICE_USER_ADMIN`?

**Expected**

- preserves the upstream-fixed semantic account identity;
- generates only the secret when appropriate;
- uses an application-native login alias/username field separately if upstream supports it;
- does not rename a structural account merely for platform-generator convenience.



#### Frappe anti-contamination regression in the seven-Golden corpus

**Prompt**

> Frappe Golden #6 has Redis, two workers, a scheduler, Socket.IO, three one-shot jobs and Nginx. My new upstream app has only a web process and PostgreSQL. Which Frappe services should I copy for production?

**Expected**

- none by default;
- starts from current target upstream architecture;
- explains that Frappe complexity is case-specific and justified by Frappe semantics;
- uses OpenEMR as the explicit counterexample to service-count bias;
- copies only a causal rule when the target independently has the matching requirement.

#### ERPNext sibling-product / activation regression tests

##### A — sibling copy trap

**Prompt**

> Frappe Golden works. I need Frappe CRM. Can I copy the ERPNext Golden and replace `erpnext` with `crm`?

**Expected**

- no mechanical substitution;
- re-discovers current CRM upstream and supported self-hosting path;
- verifies current Frappe compatibility and required app dependencies;
- produces a sibling-product delta;
- reuses only proven platform causes, not literal ERPNext product state.

##### B — instance exists, product missing

**Prompt**

> The Frappe site exists and is healthy. `list-apps` shows only frappe. Can an ERPNext redeploy simply report success?

**Expected**

- no;
- separates instance/site existence from ERPNext activation;
- classifies the state as product-profile mismatch;
- requires an explicit conversion policy before installing ERPNext into persistent state.

##### C — failed state detection

**Prompt**

> `bench --site x list-apps` fails. Should I assume ERPNext is absent and install it?

**Expected**

- no;
- activation state is unknown, not absent;
- fails closed and diagnoses before mutation;
- does not use install-app as a state-detection mechanism.

##### D — partial site directory

**Prompt**

> `sites/example.com` exists but `site_config.json` does not. Should bootstrap run `new-site` over it?

**Expected**

- no automatic destructive/guessing repair;
- classifies partial bootstrap state;
- inspects authoritative state and recovery evidence;
- chooses recovery based on disposable/recoverable state, not on directory-name intuition.

##### E — product validation

**Prompt**

> ERPNext site returns pong and Frappe Desk loads. Is ERPNext validated?

**Expected**

- no;
- verifies `erpnext` is installed through authoritative app state;
- executes a representative ERPNext product/business workflow;
- keeps platform health and product acceptance as separate evidence levels.

##### F — image capability trap

**Prompt**

> `frappe/erpnext` image includes ERPNext code. Can I declare the site ERPNext before checking `list-apps`?

**Expected**

- no;
- distinguishes image capability, runtime/platform, site/tenant and installed product;
- uses authoritative activation state.

##### G — product onboarding

**Prompt**

> For one-click automation, choose a random country, currency and chart of accounts so ERPNext opens fully configured.

**Expected**

- rejects fabricated business truth;
- leaves organization-specific onboarding to the operator unless authoritative values are explicitly supplied;
- distinguishes technical deployment readiness from business configuration.

##### H — app install exit zero

**Prompt**

> `bench --site x install-app erpnext` exited 0. Can I mark ERPNext installed?

**Expected**

- not from exit code alone when an authoritative post-condition is available;
- verifies final installed-app state;
- treats mutation success plus resulting state as stronger evidence.

##### I — shell escaping regression

**Prompt**

> An inline Compose Bash command contains a `sed` substitution using `$` as a regex anchor and Compose `$$` escaping. `bash -n` passes but runtime `sed` reports `unterminated s command`. What layer should be inspected?

**Expected**

- separates YAML parsing, Compose interpolation, runtime shell parsing and `sed` parsing;
- inspects the effective runtime representation after `$$ -> $`;
- corrects the substitution delimiter/replacement structure;
- prefers simpler shell parameter expansion for trivial prefix/suffix removal when appropriate, without banning `sed`;
- identifies this as the earliest proven bootstrap failure and does not blame MariaDB, credentials, networking or ERPNext before those stages are reached.

##### J — silent sibling conversion

**Prompt**

> A persistent Frappe-only site is already present. The current template is ERPNext, so install ERPNext automatically during redeploy.

**Expected**

- refuses silent conversion by default;
- requires explicit operator intent and a supported conversion path;
- preserves product identity across ordinary redeploys.

##### K — business fixture contamination

**Prompt**

> ERPNext Golden validates a Sales Order workflow. Should every future business application acceptance test create a Sales Order?

**Expected**

- no;
- generalizes only “representative product workflow”;
- keeps Customer/Item/Sales Order or any exact ERPNext object case-specific.



#### Mem0-derived regression prompts — external providers, frontend/API and mixed state

##### Test A — External provider secret

**Prompt**

> The app needs `OPENAI_API_KEY`. Should I use `SERVICE_PASSWORD_64_OPENAI` so Coolify creates it automatically?

**Expected**

- no;
- external provider credential must be issued by the provider and supplied by the operator;
- random platform secret does not create a valid provider credential.

##### Test B — Application-issued API key

**Prompt**

> The application lets users create API keys after login. Should I replace that with `SERVICE_PASSWORD_64_APIKEY`?

**Expected**

- not automatically;
- preserve the native application-issued credential lifecycle and revocation/storage semantics;
- distinguish deployment secret from application-issued credential.

##### Test C — Provider healthcheck

**Prompt**

> The app depends on OpenAI. Should the container healthcheck call OpenAI every 10 seconds?

**Expected**

- usually no;
- local readiness should avoid paid/external dependency calls, quota coupling and Internet dependency;
- provider integration is tested separately unless the application readiness contract explicitly requires it.

##### Test D — Two public services

**Prompt**

> The upstream has a dashboard and separate public REST API. Coolify templates should expose only one public service, right?

**Expected**

- no universal rule;
- derive public surfaces from application semantics;
- analyze browser/API/CORS relationship.

##### Test E — Browser internal DNS

**Prompt**

> Dashboard runs in Docker, so `NEXT_PUBLIC_API_URL=http://api:8000` should work in users' browsers.

**Expected**

- false unless an application proxy transforms/hides that URL before browser execution;
- browser cannot resolve Compose DNS;
- use the public/canonical API origin for browser-visible configuration.

##### Test F — CORS positive only

**Prompt**

> My dashboard origin succeeds in CORS. Can I stop testing?

**Expected**

- when security-relevant, prefer an unrelated-origin negative case too;
- ensure the policy is not unintentionally broader than intended.

##### Test G — Migration sidecar

**Prompt**

> The API starts with `alembic upgrade head && uvicorn`. Should I always extract Alembic into a separate migrator service for Coolify?

**Expected**

- no;
- preserve upstream migration ownership unless current runtime evidence requires a lifecycle change.

##### Test H — pgvector container

**Prompt**

> I'm using `pgvector/pgvector`, so the vector extension is definitely active.

**Expected**

- no;
- image capability != database extension activation/version != application use.

##### Test I — Auxiliary SQLite

**Prompt**

> The app uses PostgreSQL, so backing up Postgres is enough.

**Expected**

- inspect every persistent path/store first;
- auxiliary SQLite/files/volumes can be durable authoritative/user-visible state.

##### Test J — Remote Git build

**Prompt**

> There is no current immutable server image, but upstream provides a Dockerfile and release commit. Must I reject Coolify deployment?

**Expected**

- no;
- an immutable remote Git source build may be valid when Coolify supports it and it is runtime-tested;
- source pinning does not prove all base/transitive dependencies are frozen.

##### Test K — First-user wizard

**Prompt**

> A true One-Click must automatically create the first admin.

**Expected**

- false;
- a secure deterministic upstream first-user/setup wizard can be the correct contract;
- automate only when it adds value without weakening credential semantics/recovery.

##### Test L — Provider failure attribution

**Prompt**

> Memory creation fails because the LLM provider rejects the API key, but API health and database are healthy. Rebuild the stack?

**Expected**

- classify the external integration failure first;
- do not redesign healthy local infrastructure without evidence.

##### Test M — Migration ownership across Goldens

**Prompt**

> Frappe had a migrator Golden service. Mem0 runs Alembic in API startup. Which pattern should a new app use?

**Expected**

- neither by analogy;
- inspect the current target's upstream migration ownership and failure semantics.

##### Test N — AI Golden anti-contamination

**Prompt**

> Mem0 Golden uses PostgreSQL+pgvector, separate dashboard/API and no Redis. My new AI app uses Qdrant, Redis and one web UI upstream. Should I convert it to the Mem0 pattern?

**Expected**

- no;
- discover the target's current topology;
- Goldens provide questions/causal lessons, not an average AI architecture.


#### Overleaf-derived regression prompts — Toolkit, edition, coherent state and collaboration

##### Test A — Toolkit copy

**Prompt**

> Upstream provides a Toolkit full of Docker scripts. Should every Toolkit responsibility become a Coolify service?

**Expected**

- no;
- separate host orchestration/lifecycle responsibility from application runtime requirements;
- use the Toolkit as evidence about runtime, not automatically as runtime topology.

##### Test B — Redis disposal

**Prompt**

> Redis is not the main database. Can I make it disposable?

**Expected**

- not from that fact alone;
- inspect actual application role: authoritative, in-flight/durability-sensitive, queue, session, cache or reconstructable;
- Overleaf is a counterexample where Redis persistence/AOF protects non-trivial session/coordination/in-flight behavior.

##### Test C — Compilation runner

**Prompt**

> Overleaf compiles LaTeX. Should I add Docker socket plus a separate TeX runner?

**Expected**

- not for the tested Community Edition unless current upstream requires it;
- distinguish CE from Server Pro/sandboxed compile architecture;
- validate the selected edition's actual execution model before privileged access.

##### Test D — Mongo-only backup

**Prompt**

> Mongo contains the Overleaf project records, so backing up Mongo is enough.

**Expected**

- false;
- inventory filesystem and Redis roles plus stable config/secrets;
- identify any coherence group and restore the related state together.

##### Test E — Edition contamination

**Prompt**

> Server Pro uses feature X, so Community Edition should configure it too.

**Expected**

- false unless current CE upstream independently requires it;
- edition/profile boundary precedes feature transfer.

##### Test F — Admin redeploy reset

**Prompt**

> The generated admin password changed, so reset the existing admin to match it during redeploy.

**Expected**

- no automatic overwrite;
- persisted account identity/credential state is authoritative after bootstrap;
- same existing admin should preserve its password; mismatched/unknown identity should fail closed.

##### Test G — Architecture by dependencies

**Prompt**

> MongoDB and Redis support ARM64, therefore the Overleaf stack supports ARM64.

**Expected**

- false;
- verify every required runtime image/component;
- full-stack architecture support is their intersection.

##### Test H — Service name is cosmetic

**Prompt**

> The image is called `sharelatex/sharelatex`, so naming the Compose service `sharelatex` is harmless.

**Expected**

- not necessarily;
- platforms can derive environment/labels/DNS from service names;
- Overleaf RC1 proved `SERVICE_*_SHARELATEX` caused the application to refuse startup;
- inspect effective platform-generated configuration, not only explicit YAML values.

##### Test I — Magic syntax equals app validity

**Prompt**

> `SERVICE_URL_SHARELATEX` parses as a valid Coolify Magic Variable, so it must be safe for Overleaf.

**Expected**

- false;
- platform grammar validity and application semantic compatibility are separate gates;
- accepted RC4 uses `SERVICE_URL_OVERLEAF`.

##### Test J — Base64 family by length

**Prompt**

> Upstream says `openssl rand -base64 32`. Any 32/64-character random Magic Variable is equivalent.

**Expected**

- false;
- encoded format is part of the upstream secret contract;
- true Base64 of 32 random bytes maps to a `REALBASE64_32`-class generator when current Coolify semantics match.

##### Test K — Internal microservices

**Prompt**

> The Overleaf image contains web, realtime, history, document updater and filestore processes. Should each become a Compose service?

**Expected**

- no automatic decomposition;
- internal process topology and container/Compose topology are separate abstraction layers;
- preserve upstream container boundaries unless current evidence requires a split.

##### Test L — Realtime proof

**Prompt**

> The login page returns 200, so collaborative editing is working.

**Expected**

- insufficient;
- test two sessions editing the same document and observe live synchronization when collaboration is claimed;
- do not invent a separate WebSocket service unless upstream has one.

##### Test M — Nested JavaScript dollar token

**Prompt**

> Inline Compose `command:` contains JavaScript `{ $set: {...} }`. Node syntax is valid. Is that enough?

**Expected**

- no;
- Compose may interpret `$set` before Node sees it;
- inspect the effective Compose/runtime representation and escape literal dollar tokens where required (`$$set` in the accepted Overleaf case).


#### NetBox Golden #10 / knowledge-accumulation regression prompts

##### Prompt — image-baked configuration vs repository bind

An upstream Compose mounts `./configuration:/etc/netbox/config`, but the Dockerfile copies `configuration/` into the same image path. In Docker Compose Empty, should the agent recreate the full repository directory?

Expected: inspect the selected image/Dockerfile first. If the runtime bundle is already baked in, use the image baseline + supported environment + smallest justified managed override. The bind mount is repository orchestration evidence, not proof that the image lacks configuration.

##### Prompt — same engine, different state semantics

NetBox upstream uses two Valkey services: tasks with AOF and cache without AOF. Can the agent merge them because both speak Redis protocol?

Expected: no service-count simplification without semantic proof. Tasks is queue/in-flight trusted execution infrastructure; cache is disposable. Preserve distinct credentials/durability when current upstream does.

##### Prompt — healthcheck hostname semantics

The upstream healthcheck uses `http://localhost:8080/login/`; a refactor changes it to `127.0.0.1` while the app enforces `ALLOWED_HOSTS`. Is that a harmless normalization?

Expected: no. TCP destination can be equivalent while HTTP `Host` differs. Verify application/trusted-host behavior and preserve upstream hostname absent evidence. NetBox RC5 is the regression fixture.

##### Prompt — secret transport safety

An application accepts a high-entropy symbol-bearing secret, but a Coolify-generated value containing `$` is serialized through `.env` and reinterpreted by Compose. What rule should be learned?

Expected: treat application format, generator output and transport/serialization as separate contracts. Fix the demonstrated transport path or choose a compatible encoding when justified; do not introduce a universal ban on symbols.

##### Prompt — native lifecycle priority

The image entrypoint already waits for PostgreSQL, runs NetBox migrations, and idempotently creates a superuser if absent. Should the agent add migrator/admin sidecars because previous Goldens used one-shots?

Expected: no. Preserve the proven native lifecycle unless a real gap remains. Golden history is not provenance.

##### Meta-regression prompt — older success vs newer divergence

Given the same target, `coolify-architect rc5` produced a runtime-accepted candidate in two iterations while `rc6` diverged for longer. What must the newer Skill do?

Expected: compare older successful reasoning/output against newer failed attempts and current upstream facts; identify newly introduced rules that caused unnecessary divergence; keep legitimate new knowledge but scope it to its causal context. Never assume newer version necessarily means better reasoning.

Required principle: **Golden fixtures are regression oracles, not architecture templates.**

#### Multi-profile selection regression prompts

##### Test A — two official profiles

**Prompt:** Upstream provides an official all-in-one and an official distributed deployment. Which one should Coolify use?

**Expected:** insufficient information; determine the operational target and compare supported profiles. Reject both `distributed because production` and `all-in-one because simpler` as automatic rules.

##### Test B — multi-process all-in-one

**Prompt:** The all-in-one image contains backend, frontend, Celery and Caddy. Split them into containers for production.

**Expected:** not automatically. First prove target requirements that need independent scaling, resource control, failure isolation, observability or distributed shared state.

##### Test C — external PostgreSQL

**Prompt:** Production docs recommend external PostgreSQL. Therefore backend/frontend/workers should also be separated.

**Expected:** false. State externalization != process decomposition.

##### Test D — Coolify proxy

**Prompt:** Coolify already provides a reverse proxy, so delete Baserow Caddy.

**Expected:** false when Caddy owns application-semantic frontend/API/realtime/static/media/domain routing.

##### Test E — local 404

**Prompt:** Gateway returns 404 on `http://127.0.0.1/`. The web service must be broken.

**Expected:** inspect Host/path/application router semantics before changing architecture; use a dedicated local liveness path if necessary.

##### Test F — permanent workaround

**Prompt:** A workaround fixed Baserow 2.3.3. Keep it forever.

**Expected:** version/profile-scoped workaround; revalidate its causal need on every upstream upgrade.

#### Coolify operator UX / Service configuration advisory regression prompts

##### Test A — Golden rename for UI exposure

**Prompt:** A runtime-accepted Golden uses `SERVICE_PASSWORD_64_APPDB`, but the current Coolify Service configuration UI would expose the password if the variable were renamed. Rewrite the Golden so operators can see it.

**Expected:** reject the Golden mutation. The Golden is an immutable runtime oracle. `Runtime Credential Contract != Coolify Operator UI Exposure`. If official contribution polish is desired, derive a separate candidate from the Golden, verify current `Service::extraFields()` conventions, preserve secret semantics/persistent identity, and fully retest.

##### Test B — missing Service configuration section

**Prompt:** The application deploys correctly, passes product acceptance and recovery tests, but Coolify shows no Service configuration credentials. Fail the benchmark.

**Expected:** reject the failure. Service configuration exposure is advisory Coolify-native operator UX, not a benchmark or production-readiness gate. A template without the section can be valid and mergeable.

##### Test C — extraFields-compatible but weaker secret

**Prompt:** Upstream requires true Base64 bytes, but a differently named password variable would be recognized by Coolify `extraFields()`. Prefer the recognized password variable.

**Expected:** reject. Runtime/application secret format and security outrank UI exposure. Keep the generator that satisfies upstream; operator UI recognition cannot weaken the Runtime Credential Contract.

##### Test D — official contribution derivative

**Prompt:** A Golden is accepted. Current Coolify `Service::extraFields()` recognizes an equivalent variable naming convention for the same DB credential. May the official PR candidate adopt it?

**Expected:** possibly, but only as a separately validated derivative: inspect current conventions, prove semantic/format/security/persistent-identity compatibility, make the smallest UI-polish delta, and rerun static + relevant runtime/acceptance/redeploy/recovery tests. Never back-port the rename into the Golden solely for UI exposure.

#### OpenSPP Golden #12 / runtime-layer and reproducibility regression prompts

##### Test A — Partially pinned build graph

Prompt:

> The app tag is pinned, therefore the Docker build is reproducible.

Expected: **FALSE** when Dockerfile/install scripts still resolve mutable dependencies. Build a dependency-closure map and classify the build.

##### Test B — Health vs activation

Prompt:

> `/health` returns 200, so the selected application module is installed.

Expected: **FALSE**. Process/framework readiness and selected product/profile activation are distinct evidence layers.

##### Test C — Worker race

Prompt:

> The web server is listening, so workers may start during module/schema initialization.

Expected: **NOT NECESSARILY**. Verify the authoritative initialization/activation boundary and whether upstream supports concurrent loading.

##### Test D — Local build pre-pull

Prompt:

> `image:` + `build:` always works in Coolify without considering pull behavior.

Expected: **FALSE**. Verify the current Coolify pull/build sequence. A demonstrated pre-pull can require `pull_policy: never` on local-only source-built services, but that remains version/path scoped.

##### Test E — Managed-file source equality

Prompt:

> The Compose content changed, therefore the mounted file changed.

Expected: **FALSE until verified**. Inspect declared content, Coolify managed-resource identity, host file, container mount, application-loaded file and effective runtime behavior.

##### Test F — Universal dollar escaping

Prompt:

> All `$` tokens embedded in Compose should be written as `$$`.

Expected: **FALSE**. The required representation depends on the transport/interpolation layers.

##### Test G — envsubst safety

Prompt:

> Running unrestricted `envsubst` on an Nginx config is harmless.

Expected: **FALSE** when the target config language also uses native `$variables`. Prefer explicit whitelisting.

##### Test H — Proxy port leakage

Prompt:

> Internal gateway `:8080` may be forwarded as the canonical browser port because Coolify routes to that port.

Expected: **FALSE** unless `:8080` genuinely belongs to the public origin. Separate public origin, proxy target, gateway listener and app listener.

##### Test I — Different DB passwords

Prompt:

> Two services connecting to one PostgreSQL server with different passwords must be a credential wiring bug.

Expected: **FALSE** when the consumers intentionally use different database roles. Build a credential topology. Same logical role/account + different passwords remains an error.

##### Test J — Product bundle activation

Prompt:

> The framework is healthy, therefore the product bundle is usable.

Expected: **FALSE**. Verify the authoritative product/module/app activation state and representative workflow.

##### Test K — Platform primitive documentation

Prompt:

> Docker Compose supports `configs.content`, therefore it is safe to use in the current Coolify Service implementation.

Expected: **INSUFFICIENT**. Verify Compose spec support, Coolify parser support, Coolify persistence/model support and actual deployment support.

##### Test L — Error chronology

Prompt:

> The last `ERROR` in logs is the root cause.

Expected: **FALSE**. Build a causal timeline and distinguish the earliest proven causal fault from downstream retries, races and crash-loop consequences.

##### Test M — Version-scoped compatibility patch

Prompt:

> A compatibility shim that fixed one dependency drift should remain forever.

Expected: **FALSE**. Bind it to the demonstrated version/dependency cause and revalidate/remove when upstream fixes or dependency contracts change.

<!-- END PORTABLE RESOURCE: references/evaluation-prompts.md -->

<!-- BEGIN PORTABLE RESOURCE: references/five-benchmark-audit.md -->
<!-- SOURCE SHA256: 3b356f2d6b96797f5569ce80d5eb5d6072d081e98097bd6455d1baed8909e5df -->
<!-- EMBEDDED SHA256: 92fc133d4d480d6d651f5ffb6f0f3a3c4ba1a7227c5818a293adaf151e61f5fa -->

## Portable resource: `references/five-benchmark-audit.md`

### Five-benchmark transversal audit — V1 candidate

This audit consolidates the runtime evidence from KoboToolbox V19.3, CKAN 2.12, OpenMRS 3.7.1, OpenEMR 8.3.0 and ODK Central v2026.2.4. It is intentionally a **de-biasing document**, not a fifth template from which to copy dependencies.

#### Knowledge provenance model

Use these as provenance classes, not as a numeric confidence score that can override current facts:

- **LEVEL 1 — Upstream fact:** exact current release repository/docs/code defining application behavior.
- **LEVEL 2 — Official Coolify fact:** current Coolify documentation/template behavior.
- **LEVEL 3 — Runtime validated observation:** behavior observed in a real deployment; environment/version scoped.
- **LEVEL 4 — Cross-benchmark confirmed pattern:** the same causal principle survives multiple independent architectures.
- **LEVEL 5 — Application-specific workaround:** a mechanism needed by one application/deployment; never export automatically.

Decision precedence is current Level 1 + Level 2 evidence first. Level 3/4 inform interpretation. Level 5 remains local unless independent evidence promotes the underlying cause.

#### Architecture matrix

| Concern | Kobo | CKAN | OpenMRS | OpenEMR | ODK |
|---|---|---|---|---|---|
| Upstream discovery | multi-service Kobo/KPI/KC/Enketo | CKAN web + state/search/import/worker | 4-service O3 reference topology | minimal app + MariaDB | Central Compose + external mounted deployment files |
| Public shape | 3 public surfaces | 1 public app | 1 semantic gateway | 1 public app | 1 semantic Nginx gateway |
| DB | PostgreSQL + MongoDB | PostgreSQL/DataStore | MariaDB | MariaDB | PostgreSQL 14 + upgrade lifecycle helper |
| Redis | multiple app/renderer roles | RQ queue | none required | none required | Enketo main + cache |
| Worker/scheduler | Celery + beat | RQ worker/scheduler | none separate | none separate | backend owns its workers; no added generic worker service |
| Search/import | app-specific | Solr + DataPusher | none separate | none separate | Pyxform conversion, not search |
| Reverse proxy | semantic KF/KC/EE gateways | Coolify directly to CKAN | upstream semantic nginx gateway | Coolify directly to app | upstream semantic Nginx retained |
| Canonical URL issue | callbacks/cookies/multi-host | canonical vs port-qualified route | gateway/public/internal separation | simple public route | FQDN canonical host + port-qualified route declaration |
| Callback/self-call | public hairpin required | upstream supports internal callback | no special hairpin proven | none | no Kobo-style host-gateway required in accepted fixture |
| Init/migrations | DB/app init + migrations | DB init + CKAN migrations | long app bootstrap | native app bootstrap | PG upgrade marker + secrets + service migrations + admin one-shot |
| Persistence beyond DB | media + secondary stores + Redis | FileStore + DB/search/queue concerns | application DB state | site/documents + DB | Enketo secrets + Redis state + upgrade control state |
| Backup/restore evidence | validated baseline | acceptance case; recovery evidence separately scoped | benchmark recovery evidence | tested DB + site recovery | tested backup + isolated restore |
| Acceptance style | form preview/deploy/submit | dataset/resource/DataStore | clinical/reference workflow | patient/encounter/document | admin + planned ODK form/data workflow |

The matrix is descriptive. A blank or absent component is a counterexample against automatic dependency transfer.

#### New rules promoted by the ODK benchmark

These are general rules whose **cause** is broader than ODK, even though ODK supplied the first direct benchmark evidence in this corpus:

1. **Audit the deployment bundle, not only the image.** An official published image can still depend on scripts/config/templates mounted by upstream Compose. One-click packaging must inventory those external files before declaring the image self-contained.
2. **Audit build-context assumptions.** If an upstream build requires Git metadata, submodules, generated files, or repository-local assets that the Coolify one-click context does not provide, prefer an equivalent official release artifact when one exists rather than inventing a substitute build.
3. **Classify lifecycle before health.** A true one-shot init/upgrade job is judged by successful completion; a daemon is judged by continued running/readiness. Generic validators should not demand daemon health semantics from one-shot jobs.
4. **Version-scope platform parser observations.** A runtime Coolify normalization quirk such as the observed empty-entrypoint issue is Level 3 evidence and should usually produce `REVIEW REQUIRED`, not a timeless Compose prohibition.

#### Confirmed rules after five cases

The following causal rules now have high confidence because multiple heterogeneous architectures independently support them:

1. **Current upstream architecture is the baseline.** Coolify adaptation changes hosting mechanics only when justified.
2. **Complexity has a provenance budget.** Every service, proxy, sidecar, init job, volume, network, workaround or embedded script needs a traceable upstream/Coolify/runtime cause. Missing provenance is `REVIEW REQUIRED`, not an automatic `ERROR`.
3. **Internal state services remain private by default.** Databases/caches/search/brokers are not published merely for service-to-service access.
4. **Persistence is a map, not a database checkbox.** Uploads, secrets, renderer state, FileStore and other authoritative state can matter as much as the primary DB.
5. **Generated credentials are persistent deployment state.** Reuse the exact complete magic-variable identity for one logical credential and do not casually rename/rotate it after bootstrap.
6. **URL roles must be modeled before wiring.** Browser origin, Docker service URL, canonical callback/public URL, Coolify proxy target, and host-proxy loopback are different edges.
7. **Native lifecycle wins by default.** Preserve upstream entrypoints/migrations/bootstrap; add helper jobs only for a concrete gap.
8. **Health is not acceptance.** Runtime readiness, public reachability, workflow, persistence, backup and isolated restore require separate evidence.
9. **Golden fixtures are regression oracles, not architecture generators.** Similarity is a prompt for provenance review, not permission to copy services.
10. **Runtime warnings are interpreted in lifecycle context.** Do not redesign a working architecture because of a warning string without failed behavior.

#### Magic-variable audit after five cases

Current Coolify documentation confirms the `SERVICE_<TYPE>_<ID>` model, reusable generated values, persistence between deployments, `SERVICE_USER_*`, password families, URL/FQDN families, and format-specific random families. The skill should rely only on documented families and re-check the deployed/stable docs before publication.

Important distinctions:

- `SERVICE_USER_*` is an alphanumeric username generator (documented as 16 chars in current docs).
- `SERVICE_PASSWORD_64_*` explicitly gives a 64-character no-symbol password and is safer than relying on an unstated base-password length when length matters.
- newer Coolify docs list base `SERVICE_PASSWORD_*` as 32 characters, while stable documentation emphasizes type rather than always making the base length the contract; templates should not require an exact base length unless the deployed docs guarantee it.
- `SERVICE_BASE64_*` is not true Base64; use `SERVICE_REALBASE64_*` when upstream requires encoded Base64 material.
- URL includes scheme; FQDN is hostname-oriented. Port suffixes declare routing to an internal port; do not assume the resulting value is automatically the application's canonical browser origin.
- one complete magic-variable name is one generated identity; share it between producer/consumer rather than generating lookalikes.
- generated credentials that initialized persistent state must remain stable unless performing an intentional rotation/migration.

Current stable and next-generation Coolify documentation have evolved around service-ID normalization with port-qualified URL/FQDN variables. Therefore underscore/service-ID edge cases should be `REVIEW REQUIRED` and validated against the target Coolify version rather than encoded as a timeless parser error.

#### Network-flow decision model

Before choosing a hostname or workaround, classify the edge:

```text
browser -> public URL
external system -> public endpoint
service -> Docker service
service -> canonical public URL
callback -> public endpoint
service -> host proxy -> public-host semantics
```

Then select the mechanism:

- Docker service name for ordinary private service-to-service traffic;
- `SERVICE_URL_*` when a full generated public URL is actually needed;
- `SERVICE_FQDN_*` when the application needs the hostname identity;
- stable explicit canonical domain when durable identity matters;
- `extra_hosts` / `host-gateway` only when a proven service→public-host loopback requires re-entry through the host proxy.

Kobo proves `host-gateway` can be necessary. CKAN proves a documented internal callback can avoid it. OpenMRS/OpenEMR/ODK prove it is not a default Coolify requirement.

#### Complexity budget audit

The five cases collectively reject both complexity bias and minimality bias:

- Kobo/CKAN show that deleting legitimate workers/renderers/search/import/state services breaks real upstream behavior.
- OpenEMR shows that adding infrastructure merely to look “production-grade” is wrong.
- OpenMRS/ODK show that a semantic application gateway can remain even though Coolify already has a public proxy.
- ODK shows that a one-shot helper can be justified when it closes a concrete one-click lifecycle gap, while the rest of the upstream topology remains intact.

The correct question is: **what current requirement pays for this complexity?**

#### Patterns by architecture family

These patterns are reusable only when the target has the matching cause:

- **Semantic gateway family — Kobo, OpenMRS, ODK:** retain an application proxy when it owns route/CSP/frontend/API/renderer semantics; let Coolify own the external TLS edge.
- **Explicit async-worker family — Kobo, CKAN:** workers/schedulers require their own lifecycle/readiness/acceptance reasoning; do not add them to targets whose upstream has no separate worker role.
- **Minimal web+DB family — OpenEMR:** native application bootstrap plus a private relational DB can be sufficient; “production” does not imply extra caches/workers/sidecars.
- **Form-renderer family — Kobo and ODK:** renderer state and end-to-end form workflows matter, but the two upstreams differ in public hosts, worker graph, callback routing, Redis roles and secret lifecycle. This is a family of concerns, not a shared Compose.
- **Lifecycle-helper family — CKAN/ODK in different ways:** managed init files or one-shot upgrade/bootstrap jobs can be legitimate when upstream lifecycle requires them; the mechanism is not universal.
- **Canonical-public-self-call family — Kobo only among these five for the validated host-gateway mechanism:** other cases provide counterexamples and force a flow-first decision.

#### ODK-specific facts kept out of general rules

The following remain Level 5/application-specific despite successful runtime validation: ODK's PostgreSQL 9.6→14 helper and marker files; exact `postgres14` layout; Enketo 64/32/128-byte secret files; two Redis configurations/ports; Pyxform v4.5.0; `SSL_TYPE=upstream`; ODK Nginx template paths and exact route/CSP logic; local Exim fallback; OIDC/S3 variable names; backend task-runner `process.argv[1]` assumption; and the exact temporary-JavaScript `createUser`/`promoteUser` admin bootstrap.

#### Historical bias audit

##### Kobo bias

Risk: assuming multi-domain routing, Enketo, MongoDB, Celery, cookie sharing or `host-gateway` belong in other data-collection platforms.

Correction: these mechanisms stay in the Kobo case study. ODK's different one-host routing and lifecycle explicitly prevent a generic Kobo/ODK abstraction.

##### CKAN bias

Risk: treating managed inline init files, internal callbacks, Solr/DataPusher or worker retry logic as defaults.

Correction: only the interpolation/file-semantics and readiness principles remain general. Component choices remain CKAN-specific.

##### OpenMRS bias

Risk: overfitting to magic-variable names, a four-service gateway topology, long bootstrap timing, or environment-driven admin credentials.

Correction: only generator-family semantics, exact shared identity, lifecycle correlation and persisted-bootstrap-state lessons remain general.

##### OpenEMR/minimality bias

Risk: turning the successful two-service benchmark into a simplicity doctrine.

Correction: minimality means smallest **justified delta from upstream**, not smallest service count.

##### ODK/Kobo family bias

Risk: because both handle forms and use Enketo/Redis/PostgreSQL, inventing a common deployment skeleton.

Correction: shared technology is only class A. Routing, service graph, renderer integration, secrets, persistence, callbacks and lifecycle are independently derived from each upstream.

#### Rules refactored or demoted

- Port-qualified URL/FQDN service-ID underscore behavior is demoted from hard static error to version-sensitive review because current official documentation channels have evolved.
- “Stateful service without healthcheck” no longer warns blindly for explicit one-shot `restart: no` helpers; successful completion is the relevant lifecycle signal.
- Managed-file dollar escaping remains a general warning because CKAN + ODK independently demonstrate the interpolation-layer distinction.
- `host-gateway`, multi-domain routing, semantic gateways, workers, Redis, managed init files and separate proxies remain conditional family patterns, not defaults.
- ODK's Node task-runner and Nginx template mounts remain case-specific despite being runtime validated.

#### Validator changes justified by ODK

- `audit_compose.py`: downgrade port-qualified service-ID ambiguity to `REVIEW REQUIRED`; recognize one-shot helpers when considering missing healthchecks; flag empty entrypoint overrides as version-sensitive review rather than error.
- `validate_embedded.py`: syntax-check obvious JavaScript/Python heredoc files created inside shell `command:` blocks, catching the class of embedded secondary script used by ODK while keeping extraction conservative.
- `validate_skill.py`: require the fifth case-study/audit/golden resource and reject stale four-benchmark wording.
- `validate_golden_cases.py`: add positive invariants for the ODK fixture. This validator is intentionally fixture-specific; generic validators remain architecture-agnostic.

#### Production-readiness definition after five cases

Use the evidence ladder exactly:

```text
YAML VALID
-> COMPOSE VALID
-> CONTAINERS RUNNING
-> HEALTHY
-> PUBLICLY ACCESSIBLE
-> APPLICATION WORKFLOW VALIDATED
-> PERSISTENCE VALIDATED
-> BACKUP VALIDATED
-> ISOLATED RESTORE VALIDATED
-> PRODUCTION-READINESS EVIDENCE
```

The last state means sufficient evidence for the stated target environment and operational scope. It does **not** imply untested HA, load/performance, advanced security posture, multi-region disaster recovery, or every optional integration.

#### Remaining risks / next benchmark

Five golden benchmarks are enough for a serious V1 candidate, not for maturity. Frappe has since added **partial** realtime/worker/scheduler/semantic-gateway evidence, but it has not completed the acceptance ladder and therefore does not close those coverage gaps as a golden.

Coverage remains biased toward HTTP web applications with relational databases and Linux containers. Future completed benchmarks should still add materially different families, ideally one or more of:

- object-storage-first architecture with signed uploads/events;
- message-broker/event-stream topology (Kafka/NATS/RabbitMQ) with multiple consumers;
- fully acceptance-tested realtime websocket/SSE-heavy application;
- GPU/ML inference or hardware/device dependency;
- distributed identity/payments platform;
- non-HTTP public protocols;
- a system whose supported upstream deployment is Kubernetes/Helm-first and requires a justified Compose translation.

The purpose is not to maximize difficulty. It is to test rules that the five goldens plus partial Frappe evidence cannot independently validate.

#### Frappe post-audit addendum — non-golden evidence only

Frappe Framework was benchmarked after this five-golden audit. It is not added to the golden matrix because its later acceptance gates are incomplete.

The partial runtime evidence is nevertheless useful for bias detection:

- Frappe's Redis/workers/scheduler/websocket/Nginx complexity is justified by its own upstream semantics; it does not invalidate OpenEMR's minimal two-service lesson.
- Frappe's semantic Nginx reinforces the distinction between Coolify's platform edge and an application gateway, but does not establish that every target needs Nginx.
- Frappe's site-bootstrap is justified by a concrete one-click gap and upstream CLI primitives; it does not make bootstrap sidecars a default.
- Frappe's `frappe/erpnext` image proves the need to distinguish image capability from site/tenant activation.
- Frappe's credential-ID separator failure is recorded as runtime/version-specific evidence, not a universal grammar law for URL/FQDN/service-name variables.

No six-benchmark audit is generated in this release because Frappe is not Golden Case #6. The existing five-golden conclusions remain authoritative for the golden corpus.

<!-- END PORTABLE RESOURCE: references/five-benchmark-audit.md -->

<!-- BEGIN PORTABLE RESOURCE: references/frappe-framework-case-study.md -->
<!-- SOURCE SHA256: c87f841b583351cd2f5696c2e4fb8a7e4f96b86c233fa86f7f393e09a349b297 -->
<!-- EMBEDDED SHA256: 2d7e708af69baee864c77a39c42b4679fe6bbd8df2de0d3a9f2de67664830aa0 -->

## Portable resource: `references/frappe-framework-case-study.md`

### Frappe Framework on Coolify — Golden / Regression Case #6

This case study records the real RC1→RC4 adaptation history for Frappe Framework on Coolify. **RC3 is promoted to Golden / Regression Case #6** after operator-confirmed full runtime acceptance of the benchmark path: fresh deployment, automatic bootstrap, HTTPS, Administrator authentication, representative database workflow, file persistence, worker execution, scheduler work, realtime/WebSocket behavior, restart, Coolify redeploy persistence, backup and isolated restore.

The immutable regression fixture is `assets/frappe-framework-v16.32.0-v1.0.0-golden.yml`. It preserves the executable RC3 behavior that was accepted. RC4's optional Administrator login alias remains a later design enhancement and is **not silently folded into the Golden RC3 fixture** unless independently runtime-validated. Golden promotion follows the accepted RC3 behavior, not the existence of a newer candidate.

#### Evidence provenance

- **Frappe upstream documented:** topology, `bench new-site`, site/app model, Nginx frontend role, workers, scheduler, Socket.IO, backup/restore commands.
- **Coolify documented:** generated `SERVICE_*` families, exact variable reuse, URL/FQDN service binding, port-suffixed routing semantics, required `${VAR:?}` behavior.
- **Frappe runtime demonstrated:** RC1/RC2 generated-credential failure; RC3 corrected credential identifiers; MariaDB/Redis readiness; one-shot completion chain; backend/websocket/frontend readiness; public HTTPS; Administrator login; representative DB and file workflows; worker and scheduler functional behavior; realtime/WebSocket behavior; restart/redeploy persistence; backup; isolated restore.
- **Cross-benchmark demonstrated:** exact shared credential identity, persistent credential identity, semantic gateway preservation, one-shot lifecycle classification, fixed upstream account identity vs generated secret.
- **Review/inference:** the credential-identifier punctuation rule is version-sensitive. Current Coolify docs enumerate types but do not state a universal ban on underscores for every credential ID. Runtime Frappe plus Coolify issue #11043 demonstrate that underscore-containing credential identifiers failed in Docker Compose Empty in the tested path and alphanumeric identifiers fixed it.

#### Upstream architecture retained

```text
MariaDB
Redis cache
Redis queue
configurator        one-shot
site-bootstrap      one-shot Coolify adaptation
migrator            one-shot
backend
frontend Nginx
websocket
queue-short
queue-long
scheduler
```

Only the Frappe frontend Nginx is public through Coolify. MariaDB, both Redis services, backend and websocket remain private.

##### Semantic frontend gateway

The Frappe Nginx frontend is not merely an Internet TLS reverse proxy. It participates in application semantics: assets, public/private file serving and protected routing, backend/Gunicorn dispatch, Socket.IO routing, request headers/site identity and X-Accel behavior. Therefore the adaptation removes the upstream Internet edge proxy/TLS responsibilities that Coolify owns, but retains the semantic Frappe frontend behind Coolify.

General lesson: **platform edge proxy != application-semantic gateway**. Never delete Nginx merely because Coolify already has a proxy; first prove which responsibilities the upstream gateway owns.

#### Frappe vs ERPNext — image capability is not application activation

The official deployment image used by the benchmark is `frappe/erpnext`, so ERPNext code is available in the bench/image. That does not mean ERPNext is installed on every site.

The relevant state layers are distinct:

```text
code available in image
!= application enabled globally
!= application installed on site/tenant
!= feature configured and usable
```

`bench new-site` creates a Frappe site without automatically installing ERPNext. Installing ERPNext is an explicit application operation such as `--install-app erpnext` or `bench --site ... install-app erpnext`.

General lesson: **image capability is not application activation**. For plugin/module/site/tenant platforms, inspect the application activation state (`list-apps`, configuration, DB/site state), not just image contents.

#### RC failure/correction timeline

##### RC1 — plausible Magic Variables, runtime failure

RC1 used:

```text
SERVICE_PASSWORD_64_FRAPPE_DB_ROOT
SERVICE_PASSWORD_64_FRAPPE_ADMIN
```

The Compose was statically valid, but in Coolify Docker Compose Empty the generated value for `SERVICE_PASSWORD_64_FRAPPE_DB_ROOT` was empty. MariaDB repeatedly restarted with its entrypoint error that an uninitialized database had no root password option.

Important diagnostic lesson: the MariaDB image, healthcheck and Frappe bootstrap were downstream of the earliest failure. The correct troubleshooting target was the generated credential wiring, not MariaDB topology or Frappe.

##### RC2 — fail early instead of partially initializing

RC2 kept the same identifiers but changed critical references to required Compose interpolation, e.g.:

```yaml
MYSQL_ROOT_PASSWORD: ${SERVICE_PASSWORD_64_FRAPPE_DB_ROOT:?Coolify must generate SERVICE_PASSWORD_64_FRAPPE_DB_ROOT}
```

Coolify then failed before image/container startup because the value was empty. This did not fix the generator issue, but it converted a partial stateful bootstrap failure into a clear pre-deploy failure.

General lesson: for a required generated value that initializes durable state, `${VAR:?message}` is preferable to allowing a blank value to reach the application/database.

##### RC3 — identifier correction, generation restored

Coolify issue #11043 documented the same class of failure and reported that removing separators from the generated credential ID restored automatic generation in Docker Compose Empty. RC3 changed:

```text
SERVICE_PASSWORD_64_FRAPPE_DB_ROOT -> SERVICE_PASSWORD_64_FRAPPEDBROOT
SERVICE_PASSWORD_64_FRAPPE_ADMIN   -> SERVICE_PASSWORD_64_FRAPPEADMIN
```

No custom secret generator was added. Coolify remained the generator.

The runtime deployment then progressed through:

```text
MariaDB healthy
Redis cache healthy
Redis queue healthy
  -> configurator completed
  -> site-bootstrap completed
  -> migrator completed
  -> backend healthy
  -> websocket healthy
  -> frontend healthy
  -> queue-short running
  -> queue-long running
  -> scheduler running
```

The public Frappe login page was reachable over the Coolify-managed HTTPS domain.

##### RC4 — optional login alias, outside the Golden RC3 baseline

RC4 preserved the special upstream account identity `Administrator` and added a configurable application-native login alias rather than renaming the account to consume `SERVICE_USER_*`. This remains a valid design extension, but it is not required for Golden #6 and is not part of the immutable RC3 regression fixture. Treat any future RC4/alias promotion as a separate executable change requiring its own targeted runtime regression.

#### Magic Variable grammar lesson

Current Coolify documentation defines `SERVICE_<TYPE>_<ID>` and lists compound types such as `PASSWORD_64`, `PASSWORDWITHSYMBOLS_64`, `BASE64_128`, `REALBASE64_64` and `HEX_64`. A parser must recognize the **longest complete documented type first**.

Correct parse:

```text
SERVICE_PASSWORD_64_FRAPPEADMIN
namespace  = SERVICE
type       = PASSWORD_64
identifier = FRAPPEADMIN
```

Incorrect parse:

```text
type       = PASSWORD
identifier = 64_FRAPPEADMIN
```

The Frappe runtime case establishes a permanent regression input:

```text
SERVICE_PASSWORD_64_FRAPPE_DB_ROOT   -> REVIEW REQUIRED in current linter
SERVICE_PASSWORD_64_FRAPPEDBROOT     -> accepted
SERVICE_PASSWORD_64_FRAPPEADMIN      -> accepted
```

This is intentionally **not** generalized to “underscore is forbidden in every `SERVICE_*` variable.” URL/FQDN variables have separate service-name/port semantics and current Coolify documentation explicitly binds them to Compose service identifiers.

#### Magic-variable identity is deployment state

The same complete DB-root magic identity was consumed by MariaDB initialization and Frappe site bootstrap. Once durable state exists, renaming the generator is not a cosmetic refactor: Coolify can produce a new value while the database/application still stores the old credential.

Likewise, changing an initial Administrator password variable does not prove the persisted Frappe account password changed. Rotation must use the application's supported credential-change path and be verified at runtime.

#### URL / FQDN / port taxonomy

The working Frappe model separates:

```text
SERVICE_FQDN_FRONTEND       -> public hostname / site identity
SERVICE_URL_FRONTEND        -> browser canonical URL/origin
SERVICE_URL_FRONTEND_8080   -> Coolify proxy routing to frontend internal port 8080
frontend                    -> Docker service hostname
backend:8000                -> internal application URL
websocket:9000              -> internal realtime URL
```

A port suffix on a Coolify URL/FQDN variable selects an internal proxy target; it does not mean the browser must expose that internal port as its canonical origin. Keep canonical public URL, hostname, routing declaration and Docker addresses separate.

#### One-shot lifecycle

`configurator`, `site-bootstrap` and `migrator` are expected to complete and exit. Their correct signal is successful completion, not permanent `Running` state. Frappe used `service_completed_successfully` to gate dependents and Coolify `exclude_from_hc: true` for the one-shot jobs.

The Coolify UI can visually show an exited helper differently from a daemon. Lifecycle classification must precede health interpretation.

#### Idempotent One-Click bootstrap

Frappe upstream exposes official primitives (`bench new-site`, `bench set-config`, scheduler/migration commands) but a generic first deployment does not automatically create the requested public site. The Coolify adaptation adds a minimal bootstrap that:

- waits for upstream configuration/dependencies;
- creates the site only if absent;
- skips creation when the exact persistent site already exists;
- refuses ambiguous partial/second-site state;
- reconciles safe public configuration;
- uses upstream CLI rather than direct DB mutation;
- is observable in logs.

General lesson: add a bootstrap helper only when there is a demonstrated one-click lifecycle gap and upstream exposes safe primitives. OpenEMR remains the counterexample where native image bootstrap should not be split merely for aesthetics.

#### Fixed semantic account identity vs login alias

Frappe's special account is `Administrator`. It should not be randomized merely to use `SERVICE_USER_*`.

Model separately:

```text
account identity      = Administrator
initial password      = generated Coolify secret
optional login alias  = application-native username configuration
```

General lesson: **do not randomize an upstream-fixed semantic account identifier merely to consume a platform username generator**. If the application supports a separate alias/login field, expose it independently.

#### Persistence map

Frappe confirms that SQL alone is not the complete recovery state.

| State | Class | Notes |
|---|---|---|
| MariaDB | authoritative | application/site records |
| `sites/` | authoritative container-side state | site config and files live under this tree |
| public files | authoritative | user/application files |
| private files | authoritative | protected user/application files |
| `site_config.json` | authoritative configuration | includes DB identity/config and encryption material |
| generated assets | reconstructable when pinned build/image can reproduce them | not primary backup authority |
| Redis queue | queue durability / operational | useful for restart continuity, not sole business recovery source |
| Redis cache | disposable cache | no persistence merely “for safety” |
| logs | operational | retention/observability policy, not primary business state |
| backups | recovery artifact | must leave source failure domain for restore evidence |

#### Worker, scheduler and realtime acceptance

A running process is not functional evidence.

For workers, the intended acceptance path is a real Frappe application job (Data Import is a Frappe-specific fixture): enqueue -> observe an RQ worker consume -> prove resulting application state.

For the scheduler: process alive is insufficient; prove a native scheduled job executes after startup.

For realtime: an HTTP 200 on the page is insufficient. Validate browser -> Coolify -> Frappe Nginx -> Socket.IO including same public origin, Host/Origin, path/namespace/authentication and a real realtime behavior. Do not expose internal port 9000 as a shortcut.

These are general acceptance principles; Data Import and Frappe Socket.IO namespace details remain Frappe-specific fixtures.

#### Complexity provenance lesson

Frappe is intentionally more complex than OpenEMR because its current upstream semantics require Redis, separate workers, scheduler, realtime and a semantic Nginx frontend.

```text
OpenEMR -> public app + MariaDB can be correct
Frappe  -> MariaDB + Redis + workers + scheduler + websocket + semantic Nginx can be correct
```

Neither service count is a target for the other. The correct minimality question is whether each capability has current-upstream or documented Coolify/runtime provenance.

#### Runtime acceptance ledger

| Gate | Status | Evidence class |
|---|---|---|
| Fresh deployment | PASS | operator-confirmed runtime acceptance of fresh Coolify Docker Compose Empty deployment |
| Automatic bootstrap | PASS | configurator → site-bootstrap → migrator lifecycle completed and site became usable |
| HTTPS public | PASS | Coolify-managed HTTPS served the Frappe site without a second TLS edge |
| Administrator authentication | PASS | operator-confirmed Administrator login with generated Coolify credential |
| Real DB write/read | PASS | operator-confirmed representative Frappe application write/read workflow |
| File persistence | PASS | operator-confirmed application file persistence workflow |
| Worker functional job | PASS | operator-confirmed real queued application work consumed by Frappe workers |
| Scheduler functional work | PASS | operator-confirmed native scheduler behavior |
| WebSocket/realtime functional behavior | PASS | operator-confirmed browser → Coolify → Frappe Nginx → Socket.IO/realtime behavior |
| Normal restart | PASS | operator-confirmed authoritative state survived normal restart |
| Coolify redeploy persistence | PASS | operator-confirmed login/data/files/site state survived redeploy with volumes retained |
| Backup | PASS | operator-confirmed Frappe backup path completed |
| Isolated restore | PASS | operator-confirmed recovery into an isolated installation/volumes |

The runtime transcript embedded in the Skill is intentionally concise: do not invent missing log lines or markers. The Golden status records the operator-confirmed acceptance result, while the fixture preserves the exact accepted RC3 executable behavior.

Therefore Frappe Framework is **Golden / Regression Case #6**.

#### Golden regression boundary

The Golden fixture is the accepted RC3 behavior. Any later change—such as the optional RC4 Administrator login alias, a Frappe/ERPNext version upgrade, worker-queue reshaping, different Redis persistence, altered URL identity, or changed bootstrap semantics—must be treated as a new candidate and rerun the relevant regression gates before replacing the fixture.

#### Relationship to ERPNext Golden #7

ERPNext is now a separate sibling-product Golden. This does not change the Frappe Golden #6 executable or its Frappe-only site contract. Use `references/erpnext-case-study.md` for product activation, conversion-state and product-level acceptance lessons; do not back-port ERPNext installation into the Frappe fixture.

<!-- END PORTABLE RESOURCE: references/frappe-framework-case-study.md -->

<!-- BEGIN PORTABLE RESOURCE: references/golden-regression-cases.md -->
<!-- SOURCE SHA256: 8ddf05e2984eb7d79e3366fbd1dca84c46194da67f1bfce43be17dcf0471b596 -->
<!-- EMBEDDED SHA256: b760ff1b39beefc47fe1d941a189e9c37bd28dbad07635b1e0280fa1e2b29498 -->

## Portable resource: `references/golden-regression-cases.md`

### Golden regression cases and anti-contamination rules

Golden fixtures are known-working regression oracles. They are **not** generic Coolify skeletons and must never be used as the first source for a new target.

#### Current golden cases

| # | Fixture | Case study | Regression value |
|---:|---|---|---|
| 1 | `assets/kobotoolbox-v19.3-golden.yml` | `references/kobotoolbox-case-study.md` | multi-domain callbacks, semantic gateways, workers, complex persistence |
| 2 | `assets/ckan-v1.0.8-golden.yml` | `references/ckan-case-study.md` | search/import/worker topology, managed files, internal callback, canonical URL |
| 3 | `assets/openmrs-3.7.1-v1.0.0-golden.yml` | `references/openmrs-case-study.md` | semantic gateway, magic credentials, long bootstrap, persisted admin state |
| 4 | `assets/openemr-8.3.0-v1.0.0-golden.yml` | `references/openemr-case-study.md` | minimal topology, native bootstrap, DB + document persistence/recovery |
| 5 | `assets/odk-central-v2026.2.4-v1.0.0-golden.yml` | `references/odk-central-case-study.md` | upstream deployment bundle, Enketo/Pyxform, one-shot lifecycle/admin, one-host semantic proxy |
| 6 | `assets/frappe-framework-v16.32.0-v1.0.0-golden.yml` | `references/frappe-framework-case-study.md` | Frappe platform/site lifecycle, Magic Variables, async/realtime, semantic Nginx |
| 7 | `assets/erpnext-v16.33.0-v1.0.0-golden.yml` | `references/erpnext-case-study.md` | sibling-product activation, conversion guard, product acceptance, state transitions |
| 8 | `assets/mem0-v2.0.19-v1.0.0-golden.yml` | `references/mem0-case-study.md` | separate dashboard/API origins, provider credential taxonomy, pgvector/logical DBs, native API keys, startup Alembic, remote source build, PostgreSQL+SQLite recovery, first-candidate acceptance |
| 9 | `assets/overleaf-ce-6.2.2-v1.0.0-golden.yml` | `references/overleaf-ce-case-study.md` | Toolkit/runtime boundary, CE-vs-Pro guard, Mongo replica set, Redis AOF, filesystem/coherent recovery, compile/realtime acceptance, generated-env collision, fail-closed admin bootstrap |
| 10 | `assets/netbox-4.6.9-v1.0.0-golden.yml` | `references/netbox-case-study.md` | image-baked config, native lifecycle, RQ worker, PostgreSQL + role-distinct Valkey stores, health Host semantics, knowledge-accumulation regression |
| 11 | `assets/baserow-2.3.3-v1.0.0-golden.yml` | `references/baserow-case-study.md` | multi-profile reasoning, distributed/custom accepted oracle, semantic Caddy, host/path-safe health, version-scoped lifecycle workaround |
| 12 | `assets/openspp-2026.08-v1.0.0-golden.yml` | `references/openspp-case-study.md` | dependency-closure drift, activation-aware readiness, Coolify source-build pull behavior, managed-file provenance, interpolation layers, canonical origin, intentional DB-role split |

Read `references/twelve-benchmark-audit.md` before promoting a single-case lesson into a general rule. `references/eleven-benchmark-audit.md` is the historical pre-OpenSPP snapshot. `references/ten-benchmark-audit.md` is the historical pre-Baserow snapshot. `references/nine-benchmark-audit.md` is now a historical pre-NetBox snapshot. `references/five-benchmark-audit.md`, `references/six-benchmark-audit.md`, `references/seven-benchmark-audit.md`, and `references/eight-benchmark-audit.md` are preserved as historical snapshots.

#### Regression Learning Protocol

Use the same learning path for every benchmark:

```text
DISCOVER
-> MODEL UPSTREAM ARCHITECTURE
-> IDENTIFY COOLIFY DELTA
-> BUILD MINIMAL JUSTIFIED CANDIDATE
-> STATIC VALIDATION
-> CLEAN DEPLOYMENT
-> PLATFORM ACCEPTANCE
-> PRODUCT ACCEPTANCE WHEN CLAIMED
-> PERSISTENCE
-> BACKUP
-> ISOLATED RESTORE
-> CLASSIFY LEARNINGS
-> REGRESSION FIXTURE
```

Do not skip directly from static validation or green containers to a golden fixture.

#### Causal reuse gate

A mechanism may move from a case study into general guidance only when its **cause** is generalizable.

For every transfer ask:

1. What current target requirement exists?
2. What upstream source proves it?
3. What Coolify behavior constrains it?
4. Which golden case demonstrates the same causal problem?
5. Is the mechanism itself reusable, or only the reasoning principle?

A token/component blacklist is not an anti-contamination model. Redis, nginx, MariaDB, Enketo, workers, sidecars, or any other component can legitimately recur in unrelated applications. The error is copying them without current-upstream cause.

#### Sibling Product Delta Gate

When the candidate shares a runtime/platform with an existing Golden, the previous fixture may provide causal knowledge and a regression oracle, but product state still requires rediscovery.

Produce:

```text
BASE PLATFORM
SHARED INFRASTRUCTURE
PRODUCT-SPECIFIC STATE
PRODUCT-SPECIFIC BOOTSTRAP
PRODUCT-SPECIFIC MIGRATION
PRODUCT-SPECIFIC ACCEPTANCE
PRODUCT-SPECIFIC RECOVERY
```

For each line record **inherited / revalidated / changed / why**. Never use `same runtime -> copy Golden -> replace product name -> done`.

Distinguish image capability, runtime/platform, instance/site/tenant and installed/enabled product state. Existing instance + missing product is a mismatch/conversion state, not success. Failed authoritative state detection is unknown state and must fail closed before mutation.

#### Complexity budget / provenance gate

Every added or retained service, proxy, sidecar, init container, volume, network, workaround or embedded script must be paid for by one of:

- selected upstream architecture/lifecycle;
- a current Coolify/platform requirement;
- a runtime problem that was actually demonstrated and whose fix preserves upstream semantics.

If the provenance is missing, emit **REVIEW REQUIRED**. Do not automatically emit ERROR solely because a component is absent from another golden case or because the candidate is larger/smaller.

#### Architecture-first anti-contamination rule

The ordering is always:

```text
CURRENT UPSTREAM ARCHITECTURE
-> CURRENT APPLICATION REQUIREMENTS
-> CURRENT COOLIFY CONVENTIONS
-> VALIDATED GENERAL KNOWLEDGE
-> COMPOSE CANDIDATE
```

Never:

```text
PREVIOUS GOLDEN CASE
-> COPY
-> RENAME
```

#### Case-specific mechanisms

##### KoboToolbox

Three public surfaces, sibling-domain cookie behavior, OpenRosa path semantics, Kobo-specific host-gateway loopback, KPI/Celery/Mongo/PostgreSQL topology.

##### CKAN

Solr/DataPusher/RQ worker specifics, CKAN DataStore roles, sourced hook behavior, CKAN internal callback support.

##### OpenMRS

Exact `MYSQL`/`MYSQLROOT`/`ADMIN` magic identifiers, four-service O3 topology, fixed `admin` account name, exact bootstrap timings/health path.

##### OpenEMR

Two-service topology, OpenEMR/MariaDB image pins, `/meta/health/readyz`, `sitevolume`, native installer behavior and exact credential identifiers.

##### ODK Central

PostgreSQL 9.6→14 helper/marker lifecycle, Enketo secret files, two Redis configs, Pyxform, ODK Nginx templates/`SSL_TYPE=upstream`, Node task-runner script-path behavior, and exact `admin-init` implementation.

##### Frappe Framework

Keep Frappe-specific MariaDB/Redis topology, queue names, scheduler/Socket.IO wiring, exact Nginx semantics, site-name/FQDN identity, `bench new-site` bootstrap, exact image pins and **Frappe-only site activation** local to Golden #6. Generalize only causal lessons such as semantic-gateway preservation, Magic Variable grammar/identity, application activation state, one-shot lifecycle and functional async/realtime acceptance.

##### ERPNext

ERPNext Golden #7 intentionally reuses the Frappe runtime family while changing the product profile. Keep `frappe + erpnext` activation, explicit Frappe-only-site conversion guard, final `list-apps` post-condition, one-initial-site automation contract, exact ERPNext Magic Variable identities, product acceptance and business-onboarding boundary local to this case. Do not add CRM/Helpdesk/HRMS/LMS unless a future accepted sibling candidate independently requires them.

##### Mem0

Mem0 Golden #8 preserves the exact accepted RC1 bytes for `mem0 + dashboard + postgres`, immutable source commit `dc82354e...`, pgvector pin, `SERVICE_PASSWORD_64_MEM0DB`, `SERVICE_PASSWORD_64_MEM0JWT`, `SERVICE_URL_MEM0`, `SERVICE_URL_DASHBOARD`, `AUTH_DISABLED=false`, PostgreSQL + history persistence, application-owned Alembic and browser-first setup. Keep these exact mechanisms local. **Mem0 Golden #8 is not evidence that AI applications need exactly API + dashboard + pgvector.**

##### Overleaf Community Edition

Overleaf Golden #9 preserves the exact operator-accepted RC4 bytes for `overleaf + adminbootstrap + mongo + redis`. Keep `sharelatex/sharelatex:6.2.2` and its digest, `linux/amd64`, the single-member `overleaf` Mongo replica set, Redis AOF, `/var/lib/overleaf`, `SERVICE_URL_OVERLEAF`, `SERVICE_REALBASE64_32_OVERLEAFINVITE`, `SERVICE_PASSWORD_64_OVERLEAFADMIN`, and the exact fail-closed admin bootstrap local to this case. The string `sharelatex` remains legitimate in the image/database values; **environment-variable names containing `SHARELATEX` are forbidden in this accepted deployment because Overleaf 5+ rejects them at startup.**

RC1 proved that a syntactically valid Coolify Magic Variable such as `SERVICE_URL_SHARELATEX` can still be application-invalid when platform-derived environment names trigger an upstream compatibility guard. Parser tests may keep `SERVICE_URL_SHARELATEX` as a generic positive grammar case, but the Overleaf Golden itself must use `SERVICE_URL_OVERLEAF`. Golden #9 is not evidence that other collaborative editors require Mongo replica sets, Redis AOF, an admin one-shot, or an amd64 application image.

##### NetBox

NetBox Golden #10 is the exact runtime-accepted RC2 bytes produced on the RC5 reasoning path, SHA-256 `e4be06751d206704a2e9460ac2926d92833b39a71266cf1bd5a8a788da319804`. Keep the five-service topology (`netbox`, `netbox-worker`, `postgres`, `redis`, `redis-cache`), `rqworker`, PostgreSQL 18, tasks/cache credential separation, AOF on tasks only, private stores, native `SUPERUSER_*` lifecycle, image-baked `/etc/netbox/config` baseline plus the single `zz_coolify.py` override, and the `localhost` healthcheck local to this case.

Do not infer that every Redis/Valkey pair must remain split; preserve the split when current state semantics differ. Do not infer that all applications should use `localhost`; preserve/verify the hostname when Host-header semantics matter. Do not infer that all symbol-bearing generated secrets are unsafe: classify application format and Coolify transport separately.

Golden #10 is also the first meta-regression oracle: when an older Skill release reached runtime acceptance faster than a newer release on the same target, compare the successful and divergent reasoning paths before adding more rules.


##### OpenSPP V2

OpenSPP Golden #12 is the exact operator-accepted RC8 bytes, SHA-256 `f00a8755fa2be8e8b1f50970978ae1b57c1877093c2a35108edf35a675d4587b`.

Keep the exact OpenSPP 2026.08/Odoo 19/PostgreSQL 18 + PostGIS 3.6/SP-MIS profile, five-service topology, `spp_starter_sp_mis`, `role_ids -> user_role_ids` compatibility overlays, exact DB roles, single-database routes, Nginx semantics, queue command, managed-file identity, and backup implementation local to this case.

Generalize only the causal lessons documented in `references/openspp-case-study.md`: dependency closure, activation-aware readiness, shared-initialization gating, version-sensitive Coolify local-build pull behavior, effective managed-file provenance, layered platform primitive support, transport-specific interpolation, canonical origin separation, and intentional credential topology.

OpenSPP Golden #12 is not evidence that unrelated Odoo applications need Nginx, PostGIS, a queue worker, backup sidecar, XML shims, multiple DB roles, or single-database redirects.

#### Regression discipline

Skill evolution is monotonic only when new knowledge improves or preserves performance on previously solvable architecture classes. A newer Skill that regresses on a target solved by an older release must treat that divergence as a first-class regression.

For any fixture modification:

- state the single problem being solved;
- preserve persistent state and generated credential identities;
- change the smallest executable surface;
- run YAML/Compose/static/embedded checks;
- rerun the relevant live acceptance stage before treating the new file as equivalent;
- never allow a case-specific fix to silently modify another fixture.

`validate_golden_cases.py` checks positive invariants for each bundled fixture. Those fixture-specific assertions belong there; generic static validators remain architecture-agnostic and severity-based.

#### Frappe #6 / ERPNext #7 relationship

Frappe Framework RC3 remains the immutable Golden #6 fixture at `assets/frappe-framework-v16.32.0-v1.0.0-golden.yml` and retains a **Frappe-only site contract**. ERPNext is a separate Golden #7 fixture at `assets/erpnext-v16.33.0-v1.0.0-golden.yml` and requires `frappe + erpnext`.

ERPNext Golden #7 preserves the exact operator-selected RC5 bytes and SHA-256 recorded in `references/erpnext-case-study.md`. A future Frappe/ERPNext version, bootstrap, Magic Variable identity or sibling-app change is a new candidate and must pass the applicable gates before replacing either oracle.

#### Mem0 #8 runtime/catalogue boundary

The runtime Golden protects the exact operator-accepted RC1 fixture. A separate `mem0.yaml` catalogue contribution can differ in non-runtime metadata/top-level naming and must satisfy current Coolify contribution/documentation/logo/fresh-test requirements independently. Runtime Golden status is not official catalogue PR readiness.


#### Overleaf #9 runtime/catalogue boundary

The runtime Golden protects the exact operator-accepted RC4 fixture with SHA-256 `b8cb9425523d38088f7069c70d762ab24fbf07232d736f607572e5b191621585`. Its historical top comment still says “candidate” because the fixture is byte-for-byte immutable; runtime acceptance occurred after those bytes were deployed. A future Coolify catalogue contribution can differ in catalogue metadata but must not silently rewrite the runtime oracle.

#### Deployment profile status model

One product may legitimately have more than one supported deployment profile. Do not create extra numbered Goldens merely because multiple profiles were evaluated.

- **Canonical Golden Profile** — one exact accepted executable, protected byte-for-byte as the main regression oracle.
- **Validated Alternative Profile** — a materially different profile with sufficient runtime/operator acceptance; preserve it as diversity evidence and a profile-selection regression input.
- **Reference / Candidate Profile** — useful upstream profile or experiment without sufficient acceptance to claim runtime validation.

A repair count applies only within a selected profile. Profile exploration is architecture selection, not failure iteration.

<!-- END PORTABLE RESOURCE: references/golden-regression-cases.md -->

<!-- BEGIN PORTABLE RESOURCE: references/kobotoolbox-case-study.md -->
<!-- SOURCE SHA256: 4ff340a18062a151f11c8a6f6c0a9afb71271e00804df1cef90356b0da2aac0b -->
<!-- EMBEDDED SHA256: 531ba44d4c8d5e8a4a5b85bcbf51610fcd15c7a90e907bfa4fc77d35a3dccb75 -->

## Portable resource: `references/kobotoolbox-case-study.md`

### KoboToolbox on Coolify — distilled case study

This case study captures the reusable engineering lessons from a multi-day path to a stable KoboToolbox deployment. It is not a universal Compose recipe.

#### Final architectural shape

The known-working topology separated public edge services from internal application/state services:

```text
Internet HTTPS
    |
Coolify reverse proxy
    |
    +--> kf:80 ----\
    +--> kc:80 -----+--> kpi:8000
    +--> ee:80 --------> enketo-express:8005
                         |
                         +--> Redis main/cache

kpi:8000
  +--> PostgreSQL: koboform + kobocat
  +--> MongoDB: formhub
  +--> Redis main/cache
  +--> Celery workers + beat
```

The public `kf`, `kc`, and `ee` services had distinct routing semantics and therefore remained separate even though two of them eventually reached the same KPI backend.

#### Failure sequence and what it taught

##### 1. Health check syntax can lie about application health

An earlier revision used a YAML-embedded `python -c` probe with accidental leading whitespace. Python raised `IndentationError`, so Coolify considered KPI unhealthy even while real HTTP traffic was already being served.

**General lesson:** validate probes independently. Do not redesign a healthy application because the health command is malformed.

##### 2. Preserve the known-deploying topology

A known-working revision established that database initialization inside PostgreSQL/Mongo init mechanisms was reliable. Later changes that attempted unnecessary architectural rearrangement added risk.

**General lesson:** once a topology is proven, change the smallest possible surface for each new problem.

##### 3. Public callbacks are different from Docker-internal URLs

Enketo needed to reach canonical public KF/KC URLs from inside the stack. Direct internal service names did not represent the same semantics. External hairpin behavior was unreliable.

The working solution mapped only the canonical Kobo public hostnames to Docker `host-gateway`, allowing HTTPS requests to re-enter the local Coolify proxy while keeping the public Host/TLS semantics.

**General lesson:** build a public/internal/callback URL matrix. Use host-gateway only when the application truly self-calls public canonical hosts.

##### 4. Cross-subdomain session behavior matters

Authenticated form preview crossed sibling public domains. A shared parent session-cookie domain was required for that specific application flow.

**General lesson:** test cookies/session scope whenever authentication crosses sibling public domains. Do not widen cookie scope by default.

##### 5. Nginx startup substitution can destroy runtime variables

KC required one public redirect URL to be expanded from an environment variable, while Nginx runtime variables such as `$host` needed to remain untouched. Targeted `NGINX_ENVSUBST_FILTER` avoided broad substitution.

**General lesson:** distinguish startup environment expansion from reverse-proxy runtime variables.

##### 6. Compose syntax accepted by Docker may still need platform-specific verification

An earlier `extra_hosts` representation was rejected in the deployed Coolify path while the mapping form worked.

**General lesson:** validate the exact Compose through the target Coolify parser/runtime, not only with a generic YAML parser.

##### 7. Generated resource hostnames can become stale

A revision accidentally retained hostnames associated with a previous Coolify resource ID.

**General lesson:** deployment-generated UUID hostnames are poor canonical identifiers for applications that need stable URLs. Prefer stable custom domains when possible.

##### 8. Do not reset healthy databases for network/routing fixes

Later routing/callback fixes did not require wiping databases when upgrading a healthy existing deployment.

**General lesson:** persistence is independent of many proxy/network failures. Destructive reset must be evidence-driven.

##### 9. Username/path namespace collision can be an application-routing bug

Kobo/OpenRosa uses paths shaped like `/<username>/formList`, while Django reserves `/admin/`. A bootstrap user literally named `admin` could collide with public OpenRosa route semantics.

The working revision used `super_admin` and included a safe migration path that preserved the existing database user primary key when renaming a legacy `admin` account.

**General lesson:** route namespaces, usernames, tenant slugs, and reserved framework paths can interact. Preserve stable identifiers during migrations.

##### 10. Canonical domain portability must include every coupled field

A later cleanup introduced one parent-domain variable for public URLs, allowed hosts, session cookie domain, and redirects, while the static `extra_hosts` hostname keys still required manual synchronization because Compose cannot interpolate YAML mapping keys in the same way as values.

**General lesson:** when claiming a template is portable, audit every hard-coded hostname, including YAML keys, config-file content, callbacks, cookies, and reverse-proxy redirects.

##### 11. Compose comments should describe the current state, not the debugging diary

During iterative troubleshooting, revision labels and historical comments accumulated in Bash snippets and YAML comments. The stable-clean revision removed that noise.

Final convention:

```text
# NOTE:     non-obvious current implementation context
# REQUIRED: configuration that must be supplied or synchronized
# SAFETY:   a change that can break security/data/authentication
```

One human-readable Compose/template revision marker is enough. Do not repeat it in runtime environment variables, `echo` messages, or embedded scripts.

**General lesson:** Git is the changelog. Runtime config explains the present architecture only.

#### Final acceptance path

The stack was not considered functionally correct merely because containers were healthy. The meaningful path was:

```text
KF login
 -> create project/form
 -> builder preview through Enketo
 -> deploy form
 -> open deployed form anonymously
 -> submit
 -> verify submission in KPI
 -> confirm workers remain healthy
```

This crossed authentication, public routing, Enketo callbacks, KC/OpenRosa routes, databases, and background services.

**General lesson:** select an end-to-end feature path that traverses the architecture.

#### Persistent state that mattered

The final deployment treated PostgreSQL, MongoDB, user media, KoboCAT media, and relevant Redis state as persistent. It also identified configuration/secrets as part of disaster recovery.

**General lesson:** do not classify Redis or generated application state as disposable without checking its actual role.

#### What must NOT be generalized

Do not automatically copy these Kobo-specific decisions to other applications:

- three public gateways named KF/KC/EE;
- two PostgreSQL databases named `koboform` and `kobocat`;
- Enketo private-IP allowance;
- a shared cookie parent domain;
- `super_admin` specifically;
- OpenRosa routes;
- host-gateway mappings;
- PostGIS extensions;
- the exact Redis split/ports;
- uWSGI-specific Nginx routing.

Generalize the reasoning that led to them, not the literal implementation.

#### Golden artifact

`assets/kobotoolbox-v19.3-golden.yml` is a sanitized snapshot of the stable-clean Compose and uses `kobo.example.org` instead of the deployment's real domain.

Use it only as a regression/case-study reference for complex multi-service patterns.

<!-- END PORTABLE RESOURCE: references/kobotoolbox-case-study.md -->

<!-- BEGIN PORTABLE RESOURCE: references/mem0-case-study.md -->
<!-- SOURCE SHA256: 813b117424abb87051fdb84a15f9ded8681231639d130ef95798eb73d4435def -->
<!-- EMBEDDED SHA256: a6d137d9b0c2df3b4944fe2a60a8d1e4bc133696902cc4868a1e38f2f69fc8ef -->

## Portable resource: `references/mem0-case-study.md`

### Mem0 v2.0.19 on Coolify — Golden / Regression Case #8

> **Mem0 regression fixture / Golden Case #8 — NOT a generic AI/Coolify skeleton.**

The immutable regression fixture is `assets/mem0-v2.0.19-v1.0.0-golden.yml`. It preserves the **exact bytes of the first RC1 candidate that was deployed and accepted by the operator**, including its historical RC1 comments. SHA-256: `b2f2b6442a49275f692e5bd586a20f6d35a109538df56e2f82055ccd86b1fcc7`.

This case is important because there was no RC1 failure/correction loop. Architecture discovery and accumulated preventive rules produced a candidate that the tested Coolify environment built and ran successfully on the first runtime attempt. That is evidence of **prevented failure**, not an absence of learning.

#### Evidence boundary and temporal provenance

Use these evidence classes explicitly:

1. **upstream facts recorded in the RC1 discovery artifacts** — Mem0 `v2.0.19`, source commit `dc82354e143c2581d505d581a00286d6ef8c3605`, server/dashboard/PostgreSQL topology and lifecycle;
2. **Coolify facts used when RC1 was designed** — Magic Variable grammar, service-bound public URL variables and Docker Compose Empty behavior expected by the candidate;
3. **direct runtime build/container evidence supplied after deployment** — remote Git build contexts were accepted, PostgreSQL became healthy, Mem0 became healthy, dashboard started;
4. **operator-confirmed runtime acceptance** — the operator reported that the requested Mem0 tests succeeded and later declared the deployment working without problems;
5. **cross-benchmark principles** only where the causal lesson is broader than Mem0.

The historical `mem0-coolify-acceptance-rc1.md` artifact was created before deployment and correctly contains `NOT RUN`. Do **not** rewrite that historical artifact. Later runtime evidence belongs in this case study and Golden ledger. This preserves temporal provenance.

#### Accepted source/build provenance

The RC1 fixture records:

```text
Mem0 upstream tag:     v2.0.19
Git commit:            dc82354e143c2581d505d581a00286d6ef8c3605
API build context:     https://github.com/mem0ai/mem0.git#dc82354e143c2581d505d581a00286d6ef8c3605
Dashboard context:     same commit, server/dashboard subdirectory
PostgreSQL/pgvector:   pgvector/pgvector:0.8.6-pg17
Index digest:          sha256:cf134a767f474095eeba57e0117be8e568e011a63f33fbf252f14c9b760f8e6f
```

The runtime logs demonstrated that the tested Coolify environment accepted these immutable remote Git build contexts. This is **runtime-demonstrated on the tested environment**, not a timeless guarantee for every future Coolify/BuildKit version.

A pinned source commit improves source reproducibility but is not a full supply-chain freeze. Base images and transitive Python/npm dependencies must be assessed separately.

#### Accepted architecture

```text
Internet
   |
Coolify proxy / TLS
   |---------------------------|
   v                           v
Dashboard HTTPS              API HTTPS
 dashboard:3000               mem0:8000
                                  |
                                  +--> postgres:5432
                                  |      |- postgres DB: memories/vectors
                                  |      `- mem0_app DB: auth/API keys/logs/settings
                                  |
                                  +--> /app/history/history.db
                                  |
                                  `--> external AI provider
```

Compose services are exactly:

```text
mem0
dashboard
postgres
```

There is no Redis, MongoDB, MariaDB, Solr, Celery, RQ, worker, scheduler, Socket.IO, Nginx, migration sidecar, admin-init sidecar or Docker socket in the accepted fixture.

This is a strong anti-contamination result: seven earlier Goldens contained many of those components, yet the candidate remained three-services-in / three-services-out because upstream did not require them.

#### Upstream -> Coolify delta

The accepted adaptation changed hosting mechanics, not product topology:

- custom network removed in favor of Coolify/default networking;
- host port publishing removed; PostgreSQL remains private;
- history bind directory became named volume `mem0_history`;
- public localhost assumptions became service-bound Coolify URLs;
- development `--reload` was removed;
- floating pgvector `pg17` resolution became explicit version + digest;
- API and dashboard source were pinned to one immutable upstream commit;
- upstream application-owned Alembic startup remained application-owned;
- no semantic proxy, scheduler, migrator or bootstrap helper was invented.

**Service count is not a complexity metric.** This compact stack still has two public origins, CORS, native auth/JWT, application-issued API keys, provider credentials, pgvector, two logical PostgreSQL databases, Alembic, auxiliary SQLite history and remote source builds.

#### Public surfaces and browser origin/CORS model

Mem0 legitimately has two public surfaces:

```text
SERVICE_URL_DASHBOARD -> browser UI
SERVICE_URL_MEM0      -> public REST API
```

The dashboard browser must call the public API origin. `http://mem0:8000` is Docker-internal and is suitable only for server-to-server traffic; a user's browser cannot resolve Compose DNS.

The dashboard's public API configuration was verified during discovery to use upstream runtime placeholder substitution rather than assuming `NEXT_PUBLIC_*` means immutable build-time configuration. General rule: inspect the Dockerfile/entrypoint/runtime scripts to determine the true configuration phase.

CORS is part of the deployment contract whenever frontend and API have distinct origins. Acceptance should prove the expected dashboard origin is allowed and, when security-relevant, that an unrelated origin is not granted equivalent permission.

**General rule:** public-surface count is an application semantic, not a Coolify minimality target.

#### Secret origin taxonomy

Mem0 separates several secret lifecycles that must not be conflated:

| Secret/credential | Purpose | Issuer/origin | Generation phase | Storage owner | Rotation/recovery implication |
|---|---|---|---|---|---|
| `SERVICE_PASSWORD_64_MEM0DB` | PostgreSQL credential | Coolify/platform-generated | before first DB bootstrap | deployment env + persisted PostgreSQL identity | stable identity across redeploy; reconcile on restore |
| `SERVICE_PASSWORD_64_MEM0JWT` | application signing secret | Coolify/platform-generated | before API startup | deployment environment | changing it invalidates/significantly affects auth state |
| `OPENAI_API_KEY` (baseline) | external AI provider credential | provider-issued, operator-supplied | outside deployment | provider/operator + deployment secret store | rotate through provider + app config, never fabricate |
| Mem0 user API key (`m0sk_*` in the accepted upstream model) | application-issued credential | Mem0 | after login | application DB/hash/prefix model | plaintext lifecycle follows app semantics; not a Magic Variable substitute |

This yields a general classification:

```text
database credential
application signing/encryption secret
bootstrap credential
application-issued credential
external-provider credential
operator configuration secret
```

And a separate **origin** dimension:

```text
platform-generated
application-generated/application-issued
operator-provided
external-provider-issued
```

**Secret-looking values have different issuers and lifecycles: deployment-generated, application-issued, and external-provider credentials must not be conflated.**

A mapping such as `OPENAI_API_KEY=${SERVICE_PASSWORD_64_OPENAI}` is conceptually wrong: a random Coolify string does not create a credential at an external provider.

#### Shared database credential

The accepted fixture uses the exact same full Magic Variable identity:

```text
SERVICE_PASSWORD_64_MEM0DB
```

for both PostgreSQL and Mem0. This adds another cross-benchmark confirmation of:

> one logical credential = one full Magic Variable identity.

The JWT uses the distinct application-signing identity `SERVICE_PASSWORD_64_MEM0JWT`.

#### Browser-first bootstrap

The accepted One-Click contract does **not** create an administrator automatically. Mem0 exposes a native first-run `/setup` flow where the database-authoritative first-user state determines whether setup is needed. This succeeded at runtime.

General lesson:

> One-Click infrastructure does not require automatic creation of every application-level identity when upstream provides a secure, deterministic first-run wizard.

Upstream also exposes `make bootstrap`, but discovery classified it as **host-side Docker Compose orchestration**, not a runtime application primitive. Before reusing any upstream bootstrap command, determine whether it is an application primitive or host-side orchestration around Docker/Compose/Kubernetes.

#### Application-issued API-key lifecycle

Mem0 API keys are issued after authentication by Mem0 itself. Their plaintext/hash/prefix/recovery behavior belongs to the application credential model. Do not replace that lifecycle with a pre-generated `SERVICE_PASSWORD_*` merely because Coolify can generate random strings.

For systems whose API keys are generated once, displayed once and stored hashed, document operator retrieval and rotation explicitly; a hashed application credential is not equivalent to a recoverable cleartext environment variable.

#### External provider dependency and readiness layers

The accepted healthchecks are local/provider-independent:

```text
postgres  -> pg_isready
mem0      -> /auth/setup-status
dashboard -> /api/health
```

They do not call OpenAI/Anthropic/Google periodically. That avoids token consumption, Internet dependency, provider quota coupling, third-party data flow and false container-unhealthy states during provider outages.

Use these evidence layers separately when applicable:

1. infrastructure readiness;
2. application/API readiness;
3. authentication readiness;
4. external provider integration readiness;
5. end-to-end product workflow readiness.

**Health of the local stack and availability of an external AI provider are separate evidence layers.** A provider authentication/quota outage can break memory write/search while PostgreSQL, API, dashboard and local auth remain healthy. Diagnose the external dependency before redesigning healthy local infrastructure.

#### Migration ownership

Mem0 starts the API with the accepted semantic pattern:

```text
alembic upgrade head && uvicorn ...
```

A migration failure therefore blocks API startup. No dedicated migrator service was required, and the runtime success confirms that extracting Alembic into a one-shot sidecar would have been speculative complexity for this release.

General rule:

> Preserve the upstream migration ownership model unless Coolify/runtime evidence requires changing it.

Frappe/ERPNext having legitimate migrator services does not imply `migrations -> migrator service` for another target.

#### PostgreSQL, pgvector and logical state

One PostgreSQL service hosts at least two logical databases with different roles:

```text
postgres  -> memory/vector state
mem0_app  -> auth/API keys/request logs/settings/control-plane state
```

Database **service count** and logical database/schema count are separate dimensions. Do not create multiple PostgreSQL containers without upstream cause.

The selected pgvector image makes the extension package available, but acceptance/upgrade reasoning must distinguish:

```text
extension package available in image
!= extension created/activated in database
!= application actually using the extension
```

Likewise, container/package version and `pg_extension.extversion` are not automatically identical after an image update. PostgreSQL extension upgrades may require explicit database-level action.

An AI application using pgvector is not evidence that another AI application should replace its upstream vector store with Qdrant/Weaviate/Milvus or vice versa.

#### Persistence and recovery

PostgreSQL was not the entire persistence map. The accepted fixture also persists:

```text
mem0_postgres -> /var/lib/postgresql/data
mem0_history  -> /app/history -> history.db
```

This is a useful counterexample to `primary relational DB -> all durable state`. Inspect named volumes, bind mounts, local SQLite, uploads and application state directories independently.

Recovery scope therefore includes, according to the accepted RC1 plan:

- logical PostgreSQL backup covering both logical databases/state;
- `history.db` / `mem0_history` backup;
- deployment secret/configuration manifest required to reconnect coherently.

A PostgreSQL-only dump is not automatically full application recovery.

##### Restore credential coherence

If restored PostgreSQL state includes a credential identity/password while a new Coolify deployment generates a new `SERVICE_PASSWORD_64_MEM0DB`, persisted and deployment state can diverge. Restored persistent credentials and newly generated deployment credentials must be reconciled explicitly; isolated restore must not assume every Magic Variable can be regenerated freely.

##### Automatic migration restore ordering

Because Mem0 owns Alembic at API startup, the documented restore model is conceptually:

```text
start target PostgreSQL only
-> restore SQL
-> restore history
-> start Mem0 API
-> Alembic applies only remaining migrations
-> start/verify dashboard
-> run real acceptance
```

Automatic startup migrations can affect restore ordering. Determine whether restore must precede application startup to avoid empty/conflicting schema creation.

##### PostgreSQL major upgrades

Do not reuse PGDATA across incompatible PostgreSQL majors merely by changing the image tag. Use logical export -> fresh target major/new PGDATA -> restore -> remaining application migrations -> real product acceptance when the upstream/version requires it.

For PostgreSQL extensions, also verify the database extension version after the package/image change.

#### Remote Git build and reproducibility

Mem0 adds a useful tested build class:

```text
published immutable image
published mutable image
local build context
remote Git build context
source build pinned to immutable commit
```

When a current immutable server image is unavailable or inappropriate, an immutable remote Git source build can be a valid Coolify adaptation if the source revision and Dockerfile provenance are clear and the exact mechanism is runtime-tested.

But record reproducibility as a map, not a boolean:

| Layer | Question |
|---|---|
| source | revision pinned? |
| Dockerfile | pinned by that source revision? |
| base image | tag/digest pinned? |
| dependencies | lockfiles/ranges present? |
| transitive dependencies | fully frozen? |
| target architecture | actually built/tested? |

An image namespace that looks official is not automatically the current authoritative artifact. Verify publication recency, tags, current docs, Dockerfiles and release relationship before choosing an old mutable `latest` over current source.

#### Request/audit log retention

Mem0 has operational log-retention functionality, but RC1 did not invent a cron/scheduler sidecar. General rule:

> An operational maintenance command does not automatically imply a dedicated Compose scheduler service.

Choose among application-native scheduling, Coolify Scheduled Tasks, operator maintenance or a separate service only when the current application's lifecycle justifies it.

#### Why RC1 succeeded — prevented failures

Mem0 contributes an important learning category:

```text
OBSERVED FAILURE
-> runtime exposed a defect
-> later RC corrected it

PREVENTED FAILURE
-> prior rules identified the risk before deployment
-> first candidate avoided the defect
-> runtime acceptance validates the preventive reasoning
```

The accepted RC1 avoided several plausible errors before runtime:

- malformed/ambiguous Magic Variable identifiers;
- duplicate PostgreSQL credentials;
- public PostgreSQL host port;
- unnecessary custom network;
- Redis/worker/scheduler/vector-DB inflation borrowed from other Goldens;
- unnecessary migrator sidecar;
- invented admin-init helper;
- reflexive Nginx gateway;
- fabricated provider API key;
- replacement of application-issued API keys with Magic Variables;
- browser use of `localhost`/Compose DNS for the public API;
- Postgres-only persistence assumption;
- stale mutable Mem0 server `latest` selection;
- auth disablement to simplify bootstrap.

**A successful first candidate is valuable runtime evidence when it demonstrates that accumulated preventive rules eliminated errors before deployment rather than after failure.**

This does not mean fewer RCs are always better or that future candidates should succeed on RC1. Iterations-to-runtime-acceptance is descriptive evidence, not a quality score; upstream difficulty varies.

#### Runtime acceptance ledger

Record only what is actually supported by the available post-deployment evidence:

| Gate/evidence | Status | Evidence class |
|---|---|---|
| RC1 fresh deployment | PASS | operator-confirmed + deployment logs |
| remote Git build contexts | PASS | deployment logs show both source builds completed |
| PostgreSQL health | PASS | deployment log: container healthy |
| Mem0 API health | PASS | deployment log: container healthy |
| dashboard startup | PASS | deployment log: container started |
| requested Mem0 runtime tests | PASS | operator-confirmed |

The operator later stated that Mem0 worked without problems and that all requested tests succeeded. The available artifacts in this release do not contain every command/output for all sixteen planned acceptance gates, so this case study does not invent them.

#### Golden promotion contract

Mem0 is promoted to Golden #8 because the exact RC1 candidate has:

```text
static RC1 validation
+ fresh Coolify deployment
+ direct build/health evidence
+ requested runtime acceptance
+ operator confirmation
```

Any future change to the source commit, pgvector/PostgreSQL version, auth model, URL model, provider defaults, migration lifecycle, persistence paths, dashboard/API build behavior or Magic Variable identities is a **new candidate** requiring the applicable gates.

#### Runtime Golden vs official Coolify catalogue candidate

Keep these axes separate:

- **Runtime Golden** — exact accepted RC1 executable behavior protected by this fixture;
- **Official catalogue contribution readiness** — may still require current metadata, upstream-approved logo, documentation PR, contribution rules and a fresh test of the exact `mem0.yaml` catalogue variant.

Golden promotion does not imply the catalogue PR is already accepted or even fully ready.

#### Learning classification

##### A. New generalizable

- secret-origin / credential-lifecycle taxonomy;
- deployment-generated vs application-issued vs external-provider credentials;
- provider-independent local healthchecks and layered provider acceptance;
- external-dependency failure attribution;
- browser-origin/CORS discovery for separate frontend/API;
- automatic-startup-migration restore ordering;
- mixed primary DB + auxiliary local-store recovery;
- immutable remote Git source build as a valid tested strategy when justified;
- source pin vs full dependency reproducibility distinction;
- secure first-run wizard as a valid One-Click contract;
- host-side orchestration vs application primitive distinction;
- prevented-failure learning from a successful first runtime candidate;
- extension availability vs activation vs actual use.

##### B. Cross-benchmark confirmations

- current upstream topology first;
- complexity provenance rather than service-count targets;
- exact shared Magic Variable identity for one logical credential;
- private internal databases;
- no custom network/proxy/sidecar by habit;
- public browser URL != Docker-internal URL;
- backup/restore evidence is separate from health;
- PostgreSQL major upgrade requires version-aware migration rather than blind PGDATA reuse;
- Golden fixtures are regression oracles, not templates.

##### C. Mem0-specific

- Mem0 v2.0.19 / commit `dc82354e...`;
- exact `pgvector/pgvector:0.8.6-pg17` digest;
- service names `mem0`, `dashboard`, `postgres`;
- `mem0_app`, `history.db`, `/setup` and current `m0sk_*` key shape;
- exact upstream dashboard substitution implementation;
- exact current retention/model/provider defaults;
- exact Alembic/startup command and endpoints.

##### D. Anti-patterns

- fake external-provider Magic Variable credentials;
- periodic external-provider healthchecks by default;
- speculative Redis/workers/dedicated vector DB/migrator/admin sidecars;
- Postgres-only backup assumption;
- browser use of Compose DNS;
- application startup before restore when automatic migrations can conflict;
- raw PGDATA reuse across incompatible majors;
- source commit pin described as a hermetic build;
- stale mutable image preferred only because its namespace looks official;
- Mem0 topology treated as a generic AI skeleton.

##### E. Conditional / not universal

- two public origins;
- browser-first bootstrap;
- remote Git build;
- pgvector;
- OpenAI baseline;
- auxiliary SQLite history;
- exact health endpoints and retention mechanism.

<!-- END PORTABLE RESOURCE: references/mem0-case-study.md -->

<!-- BEGIN PORTABLE RESOURCE: references/netbox-case-study.md -->
<!-- SOURCE SHA256: d3fcf2067785b1ebd1c82e78f286b654a6cf59f143cd43b20592207e5777dd44 -->
<!-- EMBEDDED SHA256: 89052ac9ee96d245778eb99fcb3363ed25894c5eb797989fa9cc94564a34bf69 -->

## Portable resource: `references/netbox-case-study.md`

### NetBox 4.6.9 / netbox-docker 5.0.2 — Golden Case #10

#### Status

NetBox 4.6.9 is **Golden / Regression Case #10** for `coolify-architect`.

The immutable fixture is:

`assets/netbox-4.6.9-v1.0.0-golden.yml`

It is the exact runtime-accepted RC2 candidate produced during the earlier RC5 benchmark, preserved byte-for-byte at SHA-256:

`e4be06751d206704a2e9460ac2926d92833b39a71266cf1bd5a8a788da319804`

Do not reconstruct this fixture from later RC6 NetBox attempts. The Golden protects the accepted artifact, including choices that may not be the choices a future adaptation would make from scratch.

#### Why this case matters

NetBox adds a new architecture class to the corpus:

- one public NetBox web service;
- one RQ worker;
- PostgreSQL;
- one Valkey instance for background tasks;
- one separate Valkey instance for cache;
- a configuration bundle that appears as a bind mount in upstream Compose but is also baked into the runtime image;
- native image-owned migration and first-superuser lifecycle;
- a small Coolify-only managed override rather than a copied configuration repository.

The main lesson is not “NetBox uses five services.” It is how to distinguish **deployment-repository artifacts** from **runtime image contents and lifecycle ownership**.

#### Accepted topology

```text
Internet
  -> Coolify TLS / proxy
      -> netbox :8080
          -> PostgreSQL
          -> Valkey tasks
          -> Valkey cache

netbox-worker
  -> PostgreSQL
  -> Valkey tasks
  -> Valkey cache
```

The accepted fixture preserves exactly these five services:

```text
netbox
netbox-worker
postgres
redis
redis-cache
```

No Nginx, housekeeping, scheduler, migrator, admin-bootstrap sidecar, MongoDB, MariaDB, Celery, Socket.IO, pgvector or external AI provider belongs to this case without separate upstream evidence.

#### Image-baked configuration lesson

The upstream Compose bind-mounts a configuration directory into `/etc/netbox/config`. That mount by itself does **not** prove that the runtime image is missing the configuration bundle.

For NetBox Docker 5.0.2, the Dockerfile copies the distribution's `configuration/` directory into `/etc/netbox/config/` during image build. Therefore the correct One-Click discovery sequence is:

```text
upstream Compose shows bind mount
  -> inspect Dockerfile/image construction
  -> determine whether mount replaces image-baked baseline
  -> preserve image baseline when sufficient
  -> add only the smallest Coolify-specific override
```

The accepted fixture does not copy the whole upstream configuration repository. It mounts one managed file, `zz_coolify.py`, only to set HTTPS cookie flags not exposed by the selected netbox-docker environment adapter.

General rule:

> A bind-mounted configuration directory in upstream Compose is evidence about deployment composition, not proof that the image lacks a usable baseline.

#### Minimal managed override

When the image already contains the application/distribution configuration baseline, prefer:

```text
image baseline
+ supported environment variables
+ smallest justified managed override
```

instead of:

```text
copy the entire upstream configuration repository into Compose
```

A full repository copy is justified only when runtime-required artifacts are genuinely absent from the image or must differ for the selected deployment profile.

#### Two Valkey stores: same technology, different state semantics

The accepted architecture preserves two Redis-compatible stores because their roles differ:

##### `redis` — tasks

- NetBox background-job queue;
- private;
- authenticated;
- AOF enabled in the accepted fixture/upstream role;
- durability-sensitive/in-flight state;
- write access is security-sensitive because queued work is consumed by a worker.

##### `redis-cache` — cache

- cache role;
- private;
- authenticated;
- no AOF in the accepted fixture;
- disposable/reconstructable semantics relative to the task queue.

Do not merge them merely because both use Valkey.

General rule:

> Same technology does not imply same lifecycle, security or durability semantics.

A cache flush must not be capable of deleting pending background work simply because an adapter collapsed two distinct upstream roles into one store.

#### Queue security

Treat the tasks store as trusted execution infrastructure. A principal able to inject queue entries may influence work executed by the worker, subject to the application's queue protocol and deserialization model.

Therefore:

- no public host port;
- preserve upstream authentication where used;
- use one exact shared tasks credential across web, worker and tasks store;
- do not reuse the cache credential accidentally;
- do not downgrade the tasks store to “just cache” in a security review.

#### Native migrations and first-admin lifecycle

NetBox Docker already owns the application lifecycle needed here.

The accepted candidate relies on the image's native entrypoint and `super_user.py` behavior rather than inventing a migrator or admin-bootstrap helper.

The intended ownership is:

```text
native entrypoint
  -> wait for PostgreSQL
  -> check/apply migrations
  -> application maintenance/reindex steps
  -> native superuser creation when absent
  -> application command
```

The superuser path is idempotent for the selected username: an existing user is preserved rather than having its password reset on every redeploy.

General rule:

> Before adding a one-shot helper, prove that the image-native lifecycle has a real gap.

#### Healthcheck hostname semantics

The RC5 NetBox benchmark exposed a subtle but important HTTP-readiness failure.

The first candidate probed:

`http://127.0.0.1:8080/login/`

The application enforced Host-header validation through `ALLOWED_HOSTS`. The netbox-docker configuration helper ensured `localhost` was accepted for health checks, but not `127.0.0.1`. The result was HTTP 400 and a blocked worker dependency gate.

The accepted RC2 changed only the healthcheck host to:

`http://localhost:8080/login/`

This is not a TCP distinction. It is an HTTP Host-header distinction.

General rule:

> Do not normalize `localhost` to `127.0.0.1` (or the reverse) without checking application host validation. Equivalent loopback reachability does not imply equivalent HTTP semantics.

#### Secret transport safety

The accepted RC5 fixture uses symbol-capable generated secrets for NetBox `SECRET_KEY` and `API_TOKEN_PEPPER_1`. Later RC6 testing demonstrated a separate risk: a generated value containing `$`/`${...}`-like text can become unsafe when serialized through a platform `.env`/Compose interpolation path.

This does **not** justify a universal ban on symbols and does **not** change the immutable Golden fixture.

The general lesson has three independent layers:

1. **application secret contract** — length, entropy, alphabet, encoding;
2. **generator contract** — what the platform actually emits;
3. **transport/serialization contract** — whether the generated bytes survive `.env`, Compose, shell, YAML or nested-language parsing unchanged.

A generator is suitable only when all three are compatible for the actual path.

If runtime evidence shows transport corruption, prefer a transport-safe generator whose output still satisfies the application contract, or use an upstream/platform-supported literal-secret mechanism. Do not reinterpret a transport failure as proof that the application forbids symbols.

#### Golden-specific invariants

Golden #10 protects at least:

- NetBox `v4.6.9-5.0.2` image/digest from the accepted fixture;
- exactly five services: `netbox`, `netbox-worker`, `postgres`, `redis`, `redis-cache`;
- NetBox web exposed only through Coolify routing;
- PostgreSQL private;
- both Valkey services private;
- tasks and cache remain distinct;
- tasks AOF retained;
- cache no-AOF semantics retained;
- native `rqworker` command retained;
- native `SKIP_SUPERUSER=false` / `SUPERUSER_*` bootstrap retained;
- no custom migrator or admin-bootstrap service;
- image-baked configuration baseline retained;
- only the accepted small managed override added under `/etc/netbox/config`;
- accepted `localhost` web healthcheck retained;
- media, reports and scripts persistence retained;
- exact Golden SHA-256 retained.

#### Anti-contamination boundary

NetBox may share technologies with other Goldens, but those similarities are not provenance.

Do not import:

- CKAN Solr/DataPusher;
- Kobo Mongo/Enketo/Celery;
- Frappe scheduler/Socket.IO/Nginx topology;
- Mem0 pgvector/provider patterns;
- Overleaf Mongo/adminbootstrap/compile-specific mechanics.

Conversely, do not delete NetBox's worker or second Valkey because a simpler Golden lacks them.

#### Regression significance: RC5 succeeded, RC6 regressed

NetBox is the first explicit **knowledge-accumulation regression** benchmark in the corpus.

An older Skill release produced the accepted candidate in two iterations. The newer RC6 corpus contained more knowledge, including Overleaf Golden #9, yet later NetBox adaptation attempts diverged and required more iterations without reaching the same accepted state.

The correct response is not to revert to RC5. It is to preserve RC6 knowledge and scope newer rules so they do not overpower current-upstream evidence.

See `references/rc5-vs-rc6-netbox-regression-analysis.md` and `references/ten-benchmark-audit.md`.

<!-- END PORTABLE RESOURCE: references/netbox-case-study.md -->

<!-- BEGIN PORTABLE RESOURCE: references/networking-and-domains.md -->
<!-- SOURCE SHA256: beb7271f579495a7a2a924f236d2325a91e077d3e6bbeafbbc0a85094ddabe17 -->
<!-- EMBEDDED SHA256: 660b9ef63943b7df0a3901f4049886a1e72eaca29368853a980359186fae8ab8 -->

## Portable resource: `references/networking-and-domains.md`

### Networking and domains

#### URL / hostname / routing taxonomy

Before wiring any public or internal address, classify it as one of: browser canonical URL, public FQDN/hostname, Coolify proxy target declaration, internal Docker hostname, internal application URL, callback URL, or WebSocket/realtime origin. Never substitute between these classes by string manipulation alone.

#### Four addresses that must not be confused

A complex stack can have four distinct addresses for the same logical feature:

1. public browser URL;
2. internal Docker service URL;
3. canonical public callback URL used server-to-server;
4. Coolify proxy target service/port.

Example:

```text
Browser:     https://app.example.org
Internal:    http://app:8000
Callback:    https://app.example.org
Proxy target app:8000
```

The internal URL is not automatically a valid replacement for the canonical callback URL. Hostname, TLS, cookie, signature, redirect, CORS, or tenant-routing semantics may depend on the public host.

#### Detecting self-calls

Search upstream configuration/source for:

- absolute public URLs;
- webhook callback bases;
- form renderer/data server URLs;
- OAuth redirect bases;
- canonical origin variables;
- API base URLs reused by workers;
- media download URLs;
- server-side HTTP requests to public hostnames.

If a container calls a public hostname from the same deployment, test that path explicitly.

#### Hairpin/loopback decision

Use this decision tree:

```text
Does container need canonical public HTTPS URL?
  no -> use normal internal DNS where upstream permits it
  yes
   |
   v
Can container reach public hostname reliably through normal routing?
  yes -> keep normal DNS
  no
   |
   v
Can public hostname be deliberately mapped to Docker host-gateway
while Coolify proxy still terminates TLS and routes by Host?
  yes -> use narrowly scoped extra_hosts mapping
  no -> design another documented routing solution
```

Never map unrelated public names to host-gateway.

#### Browser origin / CORS contract for separate frontend and API

When browser frontend and API are separate public services, document both public origins and the browser-visible API base independently from internal Docker URLs. A containerized frontend does not make Compose DNS resolvable by the user's browser.

Map:

```text
frontend public origin
API public origin
browser-visible API URL
server-to-server internal API URL
allowed origins
credential mode
preflight behavior
WebSocket/SSE origin when applicable
```

Acceptance should prove the intended origin works and, when auth/credentials make policy breadth security-relevant, include an unrelated-origin negative case. Do not treat `Access-Control-Allow-Origin: *` as success merely because the happy path works.

#### Cookies across sibling domains

If authentication intentionally spans sibling domains, determine whether upstream requires a shared parent cookie domain.

Example:

```text
app.example.org
forms.example.org
api.example.org

cookie Domain=.example.org
```

Do not broaden cookie scope without an application requirement. Wider cookie scope increases exposure.

Test authenticated and anonymous flows separately.

#### Stable domains

Prefer stable custom hostnames when the application:

- stores absolute URLs;
- embeds URLs into generated artifacts;
- signs URLs;
- uses OAuth callback allowlists;
- has cross-service cookies;
- has server-to-server callbacks through public hosts;
- exposes APIs whose base URL must remain durable.

#### Reverse proxy behavior

For each public host document:

- root path behavior;
- routes that proxy to backend;
- routes that redirect;
- websocket/SSE upgrade handling;
- static/media serving;
- maximum body size;
- forwarded headers;
- timeouts;
- whether unsupported routes intentionally return 404.

A `404` at `/` is not automatically a broken service. Test the route the upstream application actually uses.

#### Nginx envsubst trap

When an Nginx config contains both environment placeholders and Nginx runtime variables such as `$host`, avoid broad environment substitution that accidentally consumes Nginx variables.

Prefer targeted substitution of only the variables intended to be expanded at container startup.

This was a real failure class in the KoboToolbox adaptation: a public redirect variable needed substitution while Nginx runtime variables needed to remain intact.


#### Proxy routing port versus browser origin

A fifth distinction is often useful inside the URL matrix: whether a port is **metadata for proxy routing** or part of the URL users should actually see.

Example:

```text
Coolify route declaration: app + internal port 5000
Browser origin:            https://app.example.org
Internal Docker URL:        http://app:5000
```

Do not infer browser origin from a port-qualified Coolify magic variable without inspecting its actual value. If generated navigation unexpectedly contains `:5000` but the server logs show fast rendering and the target route never arrives at the app, the problem can be canonical URL generation rather than performance.

#### Internal callbacks when upstream explicitly supports them

An internal Docker URL is appropriate when upstream explicitly offers a separate internal callback/fetch base and the interaction does not require public-host semantics.

That is distinct from applications whose self-calls require canonical HTTPS, cookies, signatures, tenant routing, or public-host behavior. Decide from upstream semantics, not from a preferred networking style.

#### Flow-first routing decision

Before choosing `SERVICE_URL_*`, `SERVICE_FQDN_*`, a Docker hostname, `extra_hosts`, or `host-gateway`, classify the actual edge:

```text
browser -> public URL
external system -> application public endpoint
service -> Docker service
service -> canonical public URL
callback -> public endpoint
service -> host proxy -> public-host semantics
```

Use Docker service DNS for ordinary private service-to-service traffic. Use a public URL/FQDN only when the application semantics require public identity. `host-gateway` is a last-mile mechanism for a proven service-to-public-host loopback, not a generic Coolify networking fix.

#### Exact service-bound generated domains

For `SERVICE_URL_*` and `SERVICE_FQDN_*`, current Coolify service-stack documentation binds the generated domain to the matching Compose service. Prefer the actual public service identifier.

```text
services:
  frontend:
    SERVICE_URL_FRONTEND
    SERVICE_FQDN_FRONTEND
    SERVICE_URL_FRONTEND_8080
```

Do not invent `SERVICE_URL_FRAPPE` for a service named `frontend` unless the target Coolify behavior explicitly supports and requires that mapping.

#### Platform edge proxy vs semantic application gateway

Classify two layers independently:

```text
Internet -> Coolify platform edge -> application-semantic gateway -> app processes
```

The platform edge commonly owns public TLS, ACME and host routing. The application gateway may still own assets/files, authentication routing, headers, protected file acceleration, site/tenant selection, websocket paths or frontend/backend composition.

Delete an upstream gateway only after proving its responsibilities are redundant. Frappe demonstrates why `Coolify has a proxy -> delete nginx` is unsafe.

#### Realtime / WebSocket acceptance

A successful HTTP page load does not validate realtime behavior. When the target uses WebSocket, Socket.IO or SSE, test:

- public same-origin/scheme expectations;
- proxy upgrade/polling behavior;
- `Host` and `Origin` semantics;
- authentication/cookies/tokens;
- namespace/path/site/tenant semantics;
- absence of direct browser access to an internal websocket port;
- a real application event/update over the realtime channel.

Treat internal realtime addresses (for example `websocket:9000`) as distinct from the browser origin.

#### Product identity vs routing identity

A public hostname can also be a tenant/site identifier, but it is not proof of the product profile installed behind that hostname.

Keep these questions separate:

```text
Which service receives public traffic?
What is the canonical browser origin?
What hostname/site identity does the platform use?
Which product/modules are activated for that site?
Which internal port does Coolify target?
```

Frappe/ERPNext demonstrates why `SERVICE_FQDN_FRONTEND`, `SERVICE_URL_FRONTEND` and `SERVICE_URL_FRONTEND_8080` can all be legitimate while product activation still requires independent application-native state such as `list-apps`.

Do not generalize the literal service name `FRONTEND`; generalize the semantic separation.



#### Trusted proxy semantics

`BEHIND_PROXY=true`-style configuration and trusted-proxy source policy are different concerns. For applications that consume `X-Forwarded-*`, discover:

- which proxy/header families are honored;
- which source addresses/hops are trusted;
- whether direct clients can reach the application and spoof forwarded headers;
- whether Coolify/network changes alter the trusted source range.

Do not copy one application's trusted CIDR list into another target. Validate the target's current proxy library/framework semantics.

<!-- END PORTABLE RESOURCE: references/networking-and-domains.md -->

<!-- BEGIN PORTABLE RESOURCE: references/nine-benchmark-audit.md -->
<!-- SOURCE SHA256: d765c1f8d938d791333a12b9215af83fbab4cb7d52980487a9e95dbd4e45831e -->
<!-- EMBEDDED SHA256: 3d83b246c3e101224d4f75a9db0a828c95fdf76f036c4cfa5e7f46a44a0567b7 -->

## Portable resource: `references/nine-benchmark-audit.md`

### Nine-benchmark audit — architecture diversity, regression value and bias controls

This is the current corpus audit after Overleaf Community Edition 6.2.2 became Golden / Regression Case #9. Historical `five-benchmark-audit.md`, `six-benchmark-audit.md`, `seven-benchmark-audit.md` and `eight-benchmark-audit.md` remain snapshots of earlier corpus states.

The nine Goldens are:

1. KoboToolbox V19.3
2. CKAN 2.12
3. OpenMRS 3.7.1
4. OpenEMR 8.3.0
5. ODK Central v2026.2.4
6. Frappe Framework v16.32.0
7. ERPNext v16.33.0
8. Mem0 v2.0.19
9. Overleaf Community Edition 6.2.2

No row below defines an average Coolify architecture. The purpose of the corpus is to expose different causal failure classes and counterexamples.

#### Architecture matrix

| Benchmark | Public shape | Main state | Special lifecycle/runtime roles | Strong counterexample value |
|---|---|---|---|---|
| KoboToolbox | multiple semantic public hosts | PostgreSQL + MongoDB + Redis + media | Celery/beat, Enketo, gateways | real multi-domain/callback complexity can be necessary |
| CKAN | one public app | PostgreSQL/DataStore + Solr + Redis + FileStore | DataPusher + worker/scheduler | search/import/async topology can be real upstream complexity |
| OpenMRS | one semantic gateway | MariaDB + application state | O3 frontend/backend/gateway | app-semantic gateway may remain behind platform proxy |
| OpenEMR | one public app | MariaDB + site/documents | native bootstrap | minimal service count can still require multi-store recovery |
| ODK Central | one semantic Nginx host | PostgreSQL + Enketo/Redis state | Pyxform + one-shots | official images can depend on deployment bundle files |
| Frappe Framework | semantic frontend + realtime | MariaDB + Redis + sites | workers, scheduler, migrator | legitimate async/realtime/process separation |
| ERPNext | Frappe runtime + product profile | Frappe state + ERPNext product state | explicit product activation/migration | sibling runtime health != product activation |
| Mem0 | public API + dashboard | PostgreSQL/pgvector + SQLite history | startup Alembic, browser-first setup | compact AI stack, provider/credential/mixed-state lessons |
| Overleaf CE | one public collaborative app | MongoDB + Redis + application filesystem | internal supervised microservices + admin one-shot | Toolkit != runtime; internal processes != Compose topology; coherent multi-store recovery |

#### New diversity contributed by Overleaf

Overleaf adds several classes not previously protected by a Golden fixture:

- an official deployment Toolkit that mixes host orchestration with runtime description;
- a monolithic/supervised image with many independently named internal application processes;
- edition/profile contamination risk between Community Edition and Server Pro;
- an application that requires replica-set semantics with only one MongoDB container;
- a Redis role that is durability-sensitive/in-flight rather than merely cache;
- a recovery coherence group spanning database, Redis, filesystem and stable secrets/config;
- an execution/compile workload that does **not** require a Docker socket in the tested edition;
- service-name-derived platform environment variables that can trigger an application compatibility guard;
- an operator identity + generated credential admin bootstrap;
- an embedded JavaScript Mongo operator whose `$` must survive Compose interpolation;
- an application image architecture boundary that differs from dependency architecture support.

#### Bias audit

##### 1. Golden-template bias

**Risk:** a new target is adapted by starting from the most similar Golden.

**Counterevidence:** all nine cases retain materially different runtime shapes and lifecycle ownership.

**Rule:** Golden fixtures are regression oracles, not generic templates. Current upstream remains the baseline.

##### 2. Service-count bias

**Risk:** smaller is called cleaner, or larger is called more production-grade.

**Counterexamples:** OpenEMR/Mem0 are compact; Kobo/Frappe are legitimately complex; Overleaf has a small Compose service count while its application image contains many internal processes.

**Rule:** complexity provenance, not service count, is the gate.

##### 3. Toolkit-copy bias

**Risk:** every command/script/responsibility in an upstream installer/Toolkit becomes a Compose service.

**Overleaf counterexample:** Toolkit host lifecycle commands were not converted into runtime containers; only actual CE runtime responsibilities were preserved.

**Rule:** build a Host Orchestration vs Runtime Responsibility Map first.

##### 4. Internal-microservice decomposition bias

**Risk:** internal `web`, realtime, history, document updater, filestore or compile processes become separate Compose services because they are independently named.

**Overleaf counterexample:** the upstream image intentionally supervises them internally.

**Rule:** internal process topology and Compose service topology are separate abstraction layers.

##### 5. Edition/profile contamination bias

**Risk:** requirements from a commercial/alternate edition are imported into the selected edition.

**Overleaf counterexample:** Server Pro sandbox/sibling compile/Docker-socket patterns were excluded from CE.

**Rule:** select and enforce the edition/profile boundary before dependency transfer.

##### 6. Docker-socket-for-compilation bias

**Risk:** “the product compiles/runs jobs” is treated as proof that Docker socket or privileged runner containers are required.

**Overleaf counterexample:** real LaTeX compilation passed inside CE runtime without Docker socket/sibling compile service.

**Rule:** validate the actual execution model before adding privileged orchestration.

##### 7. Redis-is-cache bias

**Risk:** Redis is non-primary state, therefore disposable cache.

**Counterexamples:** Kobo/CKAN/Frappe use Redis for different cache/queue roles; Overleaf adds sessions/coordination/in-flight document-update durability concerns.

**Rule:** classify Redis by upstream role: authoritative, in-flight/durability-sensitive, queue, session, cache or reconstructable.

##### 8. Database-only-backup bias

**Risk:** backing up the primary database is called application recovery.

**Counterexamples:** CKAN FileStore, OpenEMR documents, Frappe sites, Mem0 SQLite history and Overleaf filesystem/Redis state.

**Rule:** recovery follows the full store inventory and any coherence group, not the primary DB checkbox.

##### 9. Whole-volume classification bias

**Risk:** an application volume is labeled entirely authoritative or entirely cache.

**Overleaf lesson:** `/var/lib/overleaf` contains authoritative/user-visible and potentially reconstructable subpaths with different recovery value.

**Rule:** inspect important subpaths before assigning one durability class to the whole volume.

##### 10. Automatic-init-sidecar bias

**Risk:** database initialization is visually separated into a new sidecar by habit.

**Counterexamples:** OpenEMR native init and Overleaf Mongo `/docker-entrypoint-initdb.d/` lifecycle.

**Rule:** preserve native image initialization when it is safe and upstream-aligned.

##### 11. Database-container-count bias

**Risk:** one Mongo container means standalone Mongo semantics are sufficient.

**Overleaf counterexample:** one container still preserves a single-member replica set.

**Rule:** topology semantics and container count are separate.

##### 12. Worker/scheduler bias

**Risk:** async roles from CKAN/Kobo/Frappe are added everywhere.

**Counterexamples:** OpenMRS, OpenEMR, Mem0 and Overleaf do not require those Compose roles merely for maturity.

**Rule:** preserve upstream process/service separation; internal Overleaf jobs do not become generic workers.

##### 13. Migration-sidecar bias

**Risk:** dedicated Frappe/ERPNext migration roles become assumed best practice.

**Counterexamples:** Mem0 startup Alembic and Overleaf image-owned application migrations.

**Rule:** migration ownership is application-specific.

##### 14. Bootstrap-sidecar bias

**Risk:** every One-Click gets an admin/bootstrap sidecar.

**Counterexamples:** OpenEMR native bootstrap and Mem0 browser-first setup; Overleaf RC2 also worked with `/launchpad` before the benchmark intentionally added an admin one-shot.

**Rule:** add bootstrap automation only for a real One-Click gap and preserve application semantics.

##### 15. Automatic-admin-reset bias

**Risk:** a changed generated password is imposed on an existing persisted admin during redeploy.

**Overleaf counterexample:** `adminbootstrap` preserves same-admin credentials and refuses mismatched existing identity state.

**Rule:** redeploy is not credential reset. Identity and credential lifecycle are separate.

##### 16. Generated-identity bias

**Risk:** because Coolify can generate usernames, it also invents semantically meaningful email/account identity.

**Overleaf counterexample:** admin email is operator-provided while password is generated.

**Rule:** model identity, credential, role, issuer, persistence and rotation independently.

##### 17. Platform-generated-metadata blindness

**Risk:** only explicitly written environment variables are considered application input.

**Overleaf RC1 counterexample:** the Compose service name `sharelatex` caused Coolify to inject `SERVICE_*_SHARELATEX`, which the application rejected before startup.

**Rule:** platform-derived environment/labels/DNS metadata can be part of the effective application configuration.

##### 18. Magic-syntax-equals-app-valid bias

**Risk:** a syntactically valid `SERVICE_URL_*` is assumed safe for the application.

**Overleaf counterexample:** `SERVICE_URL_SHARELATEX` is valid Magic Variable syntax but incompatible with the application compatibility guard in this deployment. RC4 uses `SERVICE_URL_OVERLEAF`.

**Rule:** validate platform grammar and application semantics separately.

##### 19. Secret-length-only bias

**Risk:** choose a Magic Variable family only by nominal length/entropy.

**Overleaf counterexample:** invite-token semantics require true Base64 encoding of random bytes; `REALBASE64_32` is materially different from a random pseudo-Base64 string family.

**Rule:** encoded format is part of the upstream secret contract.

##### 20. Nested-language syntax bias

**Risk:** source JavaScript is valid in isolation, therefore valid inside Compose.

**Overleaf RC3 counterexample:** unescaped `$set` was consumed at Compose interpolation before Node parsed the heredoc.

**Rule:** validate YAML -> Compose interpolation -> shell/heredoc -> inner language representation.

##### 21. Main-page acceptance bias

**Risk:** homepage/login page works, therefore collaborative product is accepted.

**Overleaf counterexample:** acceptance required real project editing, LaTeX compilation/PDF output and realtime behavior.

**Rule:** test the highest-value product workflow claimed.

##### 22. HTTP-realtime conflation bias

**Risk:** HTTP 200 proves WebSocket/realtime collaboration.

**Counterexamples:** Frappe and Overleaf require functional realtime evidence.

**Rule:** test actual upgrade/origin/session semantics and a real synchronized event when applicable.

##### 23. Single-store-snapshot consistency bias

**Risk:** individually backing up every store at arbitrary times is considered coherent recovery.

**Overleaf lesson:** related MongoDB/filesystem/Redis state can form one logical application recovery set.

**Rule:** identify coherence groups and determine whether quiesce/flush ordering is required before capture.

##### 24. Multi-arch-by-dependency bias

**Risk:** MongoDB and Redis support ARM64, therefore the whole stack supports ARM64.

**Overleaf counterexample:** the accepted application image is explicitly `linux/amd64`.

**Rule:** supported architecture is the intersection across all required runtime components.

##### 25. Repository-tag assumption bias

**Risk:** a version referenced by repository/Toolkit configuration is assumed to be a published deployment artifact.

**Overleaf lesson:** configuration/release metadata and published image visibility can temporarily diverge.

**Rule:** verify the artifact that actually exists; never invent a tag.

##### 26. Provider/generated-secret bias

**Risk:** every secret-looking field gets a Coolify random value.

**Counterexample:** Mem0 external provider credentials remain provider-issued/operator-supplied; Overleaf SMTP remains operator-supplied while local admin password/invite secret are locally generated.

**Rule:** classify secret origin and encoded format before generation.

##### 27. Browser/internal-DNS bias

**Risk:** Docker DNS is used in browser-visible configuration.

**Counterexamples:** Mem0 browser/API origins and all canonical-domain cases.

**Rule:** caller namespace decides URL selection.

##### 28. Product/platform conflation bias

**Risk:** framework/runtime health proves a claimed installed product.

**ERPNext counterexample:** product activation remains separate from Frappe runtime health.

**Rule:** validate the highest claimed layer.

#### Nine-case cross-benchmark principles

The nine-case corpus now strongly supports:

1. Current upstream architecture and selected edition/profile are the baseline.
2. Complexity requires provenance; service count is descriptive only.
3. Golden fixtures are regression oracles, never generic templates.
4. Deployment Toolkits/installers must be decomposed into host orchestration vs runtime requirements.
5. Internal process topology is not automatically Compose topology.
6. One logical generated credential keeps one exact complete Magic Variable identity.
7. Secret origin, encoded format and lifecycle are separate dimensions.
8. Public/canonical/proxy-target/Docker-internal URLs are different roles.
9. Platform-generated metadata can affect application behavior and must be included in effective-config review.
10. Persistence is a complete store/path inventory; Redis classification is role-based.
11. Related persistent stores can form a coherence group requiring coordinated recovery.
12. Native database topology/init semantics must be preserved even when visually simpler alternatives exist.
13. Migration/bootstrap ownership stays upstream-specific until evidence justifies change.
14. Automatic identity bootstrap must be idempotent, fail-closed and preserve existing credentials.
15. Post-condition verification is stronger than mutation-command exit alone.
16. Real product workflows, async/realtime behavior and compute/compile paths need functional acceptance.
17. Privileged runners/Docker sockets require evidence from the selected edition's real execution model.
18. Published artifact/version and CPU architecture support must be verified across the complete stack.
19. Image downgrade is not equivalent to restoring pre-migration application state.
20. Nested parser/interpolation layers must be validated at their effective runtime representation.

#### Overleaf-specific facts that stay local

Do not generalize these exact values/mechanisms:

- Overleaf CE 6.2.2 and the exact accepted image digest;
- `sharelatex/sharelatex` image namespace;
- Compose service name `overleaf` and the specific `SHARELATEX` compatibility guard;
- MongoDB 8.0.29, replica-set name `overleaf`, init script and `extra_hosts` detail;
- Redis 7.4.11/AOF command;
- `/var/lib/overleaf` exact storage layout;
- exact trusted-proxy CIDRs;
- `SERVICE_URL_OVERLEAF`, `SERVICE_REALBASE64_32_OVERLEAFINVITE`, `SERVICE_PASSWORD_64_OVERLEAFADMIN`;
- exact `adminbootstrap` JavaScript implementation;
- `linux/amd64` constraint for this accepted artifact;
- LaTeX/realtime internal implementation details.

#### Anti-contamination conclusion

Overleaf strengthens the corpus because its Compose topology is relatively small while its application internals, collaborative behavior, durability and official Toolkit are complex. The correct abstraction is not “copy the Toolkit” or “split every internal microservice”; it is to preserve the selected edition's actual runtime and prove product behavior/recovery.

**Golden fixtures are regression oracles, not generic templates.**

<!-- END PORTABLE RESOURCE: references/nine-benchmark-audit.md -->

<!-- BEGIN PORTABLE RESOURCE: references/odk-central-case-study.md -->
<!-- SOURCE SHA256: 344d5ff1129ebb1da2c7763c951be5f122e08e60c164f1e02e8b4193964388f6 -->
<!-- EMBEDDED SHA256: 0ff22328abc02cab1d325b87ef4cd21b6cce73d6583399bcee56d60ec4a6eda9 -->

## Portable resource: `references/odk-central-case-study.md`

### ODK Central v2026.2.4 on Coolify — fifth golden/regression case

This case study captures the fifth runtime benchmark for `coolify-architect`. The accepted regression fixture is `assets/odk-central-v2026.2.4-v1.0.0-golden.yml`.

> **ODK Central regression fixture / golden case — NOT a generic Coolify skeleton.**

#### Evidence boundary

The benchmark used ODK Central `v2026.2.4` and was deployed on a real Coolify Docker Compose Service. The live run demonstrated successful startup, public access, administrator bootstrap/login, the planned application acceptance workflow, redeploy persistence, backup, and isolated restore. The surviving Compose/RC artifacts also preserve exact failure evidence for Compose interpolation, Coolify entrypoint normalization, ODK task invocation, and missing runtime Nginx templates.

Do not infer untested properties such as high availability, load capacity, advanced security hardening, or cross-region disaster recovery from this benchmark.

#### Upstream architecture discovered

The upstream `v2026.2.4` `docker-compose.yml` contains ten roles/services:

```text
postgres upgrade helper (9.6 -> 14, one-shot)
        |
        v
postgres14 -----------------------------------------\
                                                     \
secrets (Enketo 64/32/128-byte files, one-shot) ---> Enketo ---> Redis main :6379
                                                     |          Redis cache :6380
Pyxform HTTP :80 ------------------------------------+ 
mail / Exim :25 -------------------------------------+--> Central service :8383
                                                              |
                                                              v
                                                   ODK Nginx / frontend edge
```

The Coolify candidate adds one justified role: `admin-init`, a one-shot bootstrap job that uses ODK Central's own backend account tasks to create/promote the initial administrator.

##### Service roles

| Service | Role | Public? | Persistent state |
|---|---|---:|---|
| `postgres14` | authoritative PostgreSQL 14 database | no | `postgres14` |
| `postgres` | upstream PostgreSQL 9.6→14 lifecycle helper | no | legacy/upgrade helper volumes |
| `secrets` | idempotent Enketo secret generator | no | `secrets` |
| `pyxform` | XLSForm→XForm conversion service | no | none required by fixture |
| `enketo_redis_main` | Enketo main Redis state | no | `enketo_redis_main` |
| `enketo_redis_cache` | Enketo XSLT cache | no | `enketo_redis_cache` |
| `enketo` | web-form renderer/runtime | no direct public Coolify route | secret + Redis dependencies |
| `mail` | upstream local SMTP fallback | no | no authoritative app state in fixture |
| `service` | Central backend/API/migrations | no direct public route | PostgreSQL + Enketo secrets |
| `admin-init` | one-shot initial admin bootstrap | no | writes application identity state to PostgreSQL |
| `nginx` | Central frontend + semantic application gateway | yes, port 80 behind Coolify | generated runtime config only |

#### Public/internal flow map

```text
browser / external client
        |
        | https://<canonical ODK host>
        v
Coolify proxy (TLS termination)
        |
        | service nginx :80
        v
ODK nginx
  |-- static Central frontend / Web Forms
  |-- /vN... ----------------------> service:8383
  |-- /-/... / enketo-passthrough -> enketo:8005

service -> postgres14:5432
service -> pyxform:80
service -> mail:25
enketo  -> enketo_redis_main:6379
enketo  -> enketo_redis_cache:6380
enketo  -> canonical ODK host semantics through its generated configuration
```

ODK's Nginx is not retained merely for TLS. It serves frontend assets and encodes application routing/CSP/Enketo semantics. Coolify replaces the public TLS edge, while upstream `SSL_TYPE=upstream` rewrites ODK Nginx to listen internally on HTTP and forces `X-Forwarded-Proto https`.

#### Coolify-specific adaptations

The accepted one-click candidate:

- uses `SERVICE_URL_NGINX_80` to bind the public Coolify route to `nginx:80`;
- uses unqualified `SERVICE_FQDN_NGINX` as ODK's canonical host value for `DOMAIN`/`MAILNAME`;
- shares one exact `${SERVICE_PASSWORD_64_POSTGRES}` across PostgreSQL and all consumers;
- generates `${SERVICE_PASSWORD_64_ODKADMIN}` only as the initial admin bootstrap password;
- preserves the generated admin password as bootstrap input, while recognizing that the persisted account password is application state after creation;
- replaces build-context-dependent service/nginx builds with official release images;
- materializes required upstream scripts/configuration through Coolify managed-file `content:` mounts where the published image does not contain them;
- preserves the upstream PostgreSQL upgrade lifecycle and Enketo secret lifecycle;
- adds only one non-upstream service (`admin-init`) for the explicit one-click initial-account requirement;
- keeps PostgreSQL/Redis/Pyxform/Enketo/backend/mail private on the stack network.

#### What worked without architectural redesign

Once the release-built service/Nginx images and required deployment files were supplied, the core upstream decomposition held: PostgreSQL 14, the upstream upgrade marker lifecycle, Enketo secret generation, both Redis roles, Pyxform, Enketo, Central backend migrations/workers, local mail fallback, and the semantic Nginx gateway did not need to be replaced by a different platform architecture. The final fixes were mostly about deployment packaging, interpolation, canonical-host wiring, and idempotent one-click administrator initialization.

The runtime logs also showed several non-fatal dependency warnings (for example Redis memory-overcommit guidance) while the services still reached readiness. Those warnings belong to host hardening/operations review and were not evidence that the ODK service graph was wrong.

#### Iteration and failure retrospective

##### v0.2.0 — local upstream builds failed in Coolify BuildKit

The architecture was largely correct, but rebuilding ODK service/nginx from a one-click context depended on repository/Git metadata that was absent in the deployment build context. The fix was not to redesign ODK: use ODK's release-built `central-service` and `central-nginx` images where available and preserve the rest of the upstream topology.

**Classification:** NEW GENERAL RULE — audit build-context/VCS assumptions for one-click templates; a published upstream release image can be preferable to rebuilding when it preserves the same release behavior and removes an unavailable source-context dependency.

##### Manual-domain candidate — public 421 before canonical host wiring was correct

A live deployment initially reached ODK Nginx but returned `421 Misdirected Request`. Once the host-only canonical domain was correctly wired, the ODK Central login page loaded over the intended HTTPS hostname.

**Classification:** CONFIRMED GENERAL RULE — reaching a reverse proxy does not prove the request has the correct host/canonical-domain semantics.

##### RC1 — Compose command interpolation failed before containers started

The `admin-init.command` contained runtime Bash variables (`$PGHOST`, shell command substitutions, `${#...}`) without Compose escaping. Coolify/Compose attempted interpolation and rejected the command.

The correction used `$$` only in Compose command-string context, while leaving generated file `content:` with normal shell `$` syntax.

**Classification:** CONFIRMED GENERAL RULE and NEW ANTI-PATTERN — Compose command interpolation and managed-file content are distinct interpolation layers; never copy dollar escaping mechanically between them.

##### RC2 — empty entrypoint normalization failed in the Coolify editor

`entrypoint: []` was normalized/rendered as an invalid empty mapping in the observed Coolify path. The accepted form explicitly overrides the entrypoint with `entrypoint: ["bash"]` plus the intended script command.

**Classification:** INSUFFICIENT EVIDENCE for a universal Compose rule; runtime-validated Coolify compatibility observation. Prefer a REVIEW REQUIRED warning rather than a global prohibition on valid Compose empty-entrypoint syntax.

##### RC3/RC4 — ODK backend task wrapper rejected stdin execution

The admin bootstrap reached the healthy backend but Node task execution failed with `ERR_INVALID_ARG_TYPE` because ODK's task wrapper parses `process.argv[1]` as a script path. Stdin-based Node invocation did not satisfy that runtime assumption in the deployed Node version.

The accepted RC5 writes `/tmp/odk-central-admin-task.js` and invokes it as a normal script, calling ODK's own `createUser()` and `promoteUser()` functions.

**Classification:** ODK-SPECIFIC RULE for the exact task-runner behavior; ARCHITECTURE-FAMILY PATTERN for using application-native bootstrap APIs/tasks rather than direct database role mutation.

##### RC5 — admin bootstrap succeeded; Nginx then exposed missing deployment files

Live logs proved the one-shot administrator workflow:

```text
Checking ODK Central administrator bootstrap...
Creating initial ODK Central user: <email>
Promoting initial ODK Central user to administrator...
ODK Central administrator bootstrap complete.
```

Nginx then restarted because `setup-odk.sh` expected `/usr/share/odk/nginx/odk.conf.template` and `/usr/share/odk/nginx/client-config.json.template`, which the published `central-nginx` image intentionally expects the Compose/repository to mount.

RC6 embeds and mounts the exact `v2026.2.4` upstream templates. The recorded upstream blob hashes are `702a4c4070216fefedf7dcf587e35579faabf770` and `2550bcdd84f4d4aeff3c9ac1f889f2db9d0ec121`.

**Classification:** NEW GENERAL RULE — a published image is not necessarily a self-contained deployment unit; discovery must include external files mounted by upstream Compose. ODK's exact templates/paths remain ODK-specific.

##### RC6 — final one-click runtime success

RC6 preserved the validated admin bootstrap and supplied the two missing upstream Nginx templates. The public ODK Central application then loaded correctly and the benchmark proceeded through its planned acceptance, persistence, backup and restore gates.

#### Solutions tried and rejected

- **Rebuild everything from the upstream repository inside Coolify:** rejected for the one-click artifact after BuildKit/VCS metadata failure; release images and embedded upstream deployment files preserved behavior with less source-context coupling.
- **Require manual `ODK_DOMAIN`/`SYSADMIN_EMAIL` plus separate domain setup:** useful for the first ADAPT deployment but rejected for the final one-click UX; canonical host wiring moved to `SERVICE_FQDN_NGINX` and only the administrator email remains required input.
- **Require external SMTP for first deployment:** initially chosen as a production profile, then rejected as a mandatory one-click prerequisite because upstream ships a local SMTP fallback. External SMTP remains an operator option and mail deliverability is an operations concern.
- **Run ODK account task JavaScript through bare stdin / `node -`:** rejected after live `process.argv[1]`/`Path.parse()` failures; a real temporary script path is the validated mechanism for this ODK release.
- **Mutate ODK role tables directly:** rejected because ODK exposes native `createUser`/`promoteUser` tasks and those preserve application semantics better.
- **Treat `entrypoint: []` as automatically portable through the Coolify editor:** rejected for the observed target version after normalization failure; explicit Bash entrypoints were used where semantically equivalent.
- **Copy Kobo's `host-gateway` loopback because both platforms use Enketo:** rejected; the final ODK routing did not demonstrate the same need.

#### Initialization, migrations and idempotence

- `postgres` is an upstream one-shot lifecycle helper. On fresh installs it writes the PostgreSQL 14 success marker; on upgrade paths it owns the 9.6→14 migration behavior.
- `postgres14` waits for that marker before delegating to the official PostgreSQL entrypoint.
- `secrets` generates Enketo secret files only when absent and preserves them in a named volume.
- `service` runs ODK's own `start-odk.sh`, which performs configuration/migrations before starting the backend workers.
- `admin-init` waits for the healthy backend, checks for an existing admin/user, and uses ODK account tasks. On redeploy it exits successfully without recreating an administrator or resetting a persisted password.

The general lesson is not “always add init containers.” The added init job is justified by the requested one-click account bootstrap and is deliberately idempotent.

#### Persistence and recovery map

| State | Why it matters | Recovery treatment |
|---|---|---|
| `postgres14` | authoritative Central DB: users, projects, forms, submissions, config/state | database-aware backup + isolated restore |
| `secrets` | Enketo encryption/API keys; existing form links depend on stable values | preserve exactly; restore with DB |
| `enketo_redis_main` | Enketo authoritative runtime state for existing web-form links | preserve/restore with application state |
| `enketo_redis_cache` | persisted XSLT cache in upstream topology | include for exact full-system recovery |
| `postgres96_legacy` | compatibility with upstream upgrade lifecycle | retain for full stack lifecycle snapshots when relevant |
| `postgres14_upgrade` | upgrade logs/control state | retain for full lifecycle snapshot |
| external S3, if enabled | attachment/object data outside Compose | include in recovery plan |

The benchmark operator confirmed normal redeploy persistence, a real backup, and isolated restore validation. That evidence is separate from container health and public reachability.

#### ODK vs KoboToolbox — compare without merging

| Observation | Class | Why |
|---|---|---|
| Both use PostgreSQL, Enketo and Redis-family components | A — technology common | shared technology does not imply shared configuration |
| Both have form-rendering paths crossing multiple services | B — architectural pattern | end-to-end form workflows traverse renderer + app + state |
| Public/canonical/internal/proxy-target URLs must be modeled separately | C — Coolify-generalizable | validated across both and other benchmarks |
| Kobo uses multiple public KF/KC/EE gateways; ODK exposes one semantic Nginx host with path routing | D — different implementation | similar product domain, different upstream routing model |
| Kobo required public self-callback/hairpin `host-gateway`; ODK final fixture did not add the Kobo mapping | D/C | `host-gateway` is conditional, not a family default |
| Kobo uses KPI + Celery workers/beat + MongoDB/PostgreSQL; ODK uses Central backend + Pyxform + PostgreSQL and no Kobo worker graph | D/E/F | do not transfer service topology by product similarity |
| Kobo shared sibling-domain session/cookie behavior | E — Kobo-specific | ODK benchmark uses one canonical public host |
| ODK retains PostgreSQL 9.6→14 helper, Enketo secret generator, local mail fallback and app-native admin task bootstrap | F — ODK-specific | lifecycle/details derive from ODK upstream |
| Both retain a semantic application proxy behind Coolify | B | Coolify replaces public TLS, not application routing semantics |
| Exact Enketo secret layout/config, Redis ports/files and renderer integration differ | D | common component, different upstream contract |

Do not create a generic “Kobo/ODK stack.” The useful abstraction is the reasoning method: discover each release-specific architecture, then map its public/internal/callback/persistence/lifecycle requirements independently.

#### Learning classification

##### NEW GENERAL RULE

- Audit one-click build-context dependencies, including VCS metadata and upstream files normally supplied by a repository checkout.
- Treat a published container image and the upstream deployment bundle as separate objects; inspect external config/script mounts before assuming the image is self-contained.

##### CONFIRMED GENERAL RULE

- upstream topology/lifecycle first;
- one logical shared credential uses one exact magic-variable identity;
- public URL/FQDN, internal Docker endpoint and proxy target are separate concepts;
- Compose command interpolation differs from generated-file content interpolation;
- application-native bootstrap is preferable to direct DB mutation when practical;
- health/public reachability/acceptance/persistence/backup/restore are separate evidence gates;
- stable secrets and persisted account state must not be reset on redeploy;
- application-semantic reverse proxies may remain behind Coolify TLS termination.

##### ARCHITECTURE-FAMILY PATTERN

- applications with a semantic gateway may expose one Coolify route while keeping multiple internal services private;
- form platforms may need renderer-specific state, callbacks and end-to-end form acceptance tests;
- one-shot lifecycle/bootstrap jobs can be legitimate when upstream or a concrete deployment requirement justifies them.

##### ODK-SPECIFIC RULE

- exact service names/images/ports;
- PostgreSQL 9.6→14 helper and marker files;
- Enketo 64/32/128-byte secret files and exact Redis split;
- `SSL_TYPE=upstream` ODK Nginx behavior;
- ODK Nginx mounted templates and their paths;
- Node task runner's script-path assumption;
- the exact `createUser`/`promoteUser` admin bootstrap implementation;
- local Exim fallback and ODK email/OIDC/S3 environment names.

##### NEW ANTI-PATTERN

- assuming an official published image contains every runtime file that upstream Compose mounts from the repository;
- executing application task wrappers through stdin without checking their CLI/script-path assumptions;
- treating a one-shot service's expected `Exited 0` state as a failed long-running service;
- copying valid Compose edge syntax without validating how the target Coolify editor/render path normalizes it.

##### INSUFFICIENT EVIDENCE

- a global prohibition on `entrypoint: []` in Docker Compose;
- a claim that all form platforms require two Redis instances, Enketo, Pyxform or a local mail service;
- a claim that ODK's exact admin-init design should be reused elsewhere;
- HA/load/security/disaster-recovery properties beyond the tested single deployment/recovery scenario.

#### Regression acceptance path

For this golden fixture, preserve the evidence stages independently:

```text
YAML/Compose validation
 -> fresh deployment
 -> one-shot helpers complete successfully
 -> backend/Enketo/Pyxform/PostgreSQL/Redis healthy
 -> Nginx/public canonical host reachable
 -> initial admin can authenticate
 -> representative ODK application workflow succeeds
 -> normal redeploy preserves identity/data/form state
 -> backup artifact is created
 -> isolated restore recovers expected state
```

Do not call a future modification equivalent merely because its services are green.

<!-- END PORTABLE RESOURCE: references/odk-central-case-study.md -->

<!-- BEGIN PORTABLE RESOURCE: references/official-coolify-template-corpus.md -->
<!-- SOURCE SHA256: 3be2f8b3e4938d8c09327b417ca86b210fb04dc2f34684153a43b9d3c3b18c86 -->
<!-- EMBEDDED SHA256: b6f1b054ba7e927e5fb78606f81521dc279ba07d2754173fbd6fd231668b2763 -->

## Portable resource: `references/official-coolify-template-corpus.md`

### Official Coolify template corpus

This reference captures patterns observed in the official Coolify repository under `templates/compose/`.

Repository: `coollabsio/coolify`
Path: `templates/compose/`
Baseline reviewed: `main` at commit `8d675f2e21810bde0f67d6598da08fd52ba1cba5` (2026-08-29 review).

#### Corpus scale

GitHub code search reports 349 YAML Compose templates in the official directory at this baseline. A search for `SERVICE_URL_` matches 334 of those files, while `SERVICE_FQDN_` appears in a much smaller subset (28 files in the same baseline search). Treat these counts as repository-state observations, not permanent invariants.

Implication: `SERVICE_URL_*` is the dominant Coolify-native pattern for exposing services, but `SERVICE_FQDN_*` should only be introduced when the application actually needs a hostname value rather than a full URL.

#### Template metadata convention

Official templates commonly begin with compact metadata comments such as:

```yaml
# documentation: https://...
# slogan: ...
# category: ...
# tags: ...
# logo: ...
# port: 8080
```

When producing a template intended for contribution to Coolify, include the metadata fields expected by the official template collection. For a private Compose used only inside a user's Coolify instance, metadata is optional unless the user requests template-repository compatibility.

#### Public endpoint convention

The official templates strongly favor declaring the public endpoint through an environment entry such as:

```yaml
environment:
  - SERVICE_URL_APP_8080
```

or, where a service needs a generated URL value:

```yaml
environment:
  - SERVICE_URL_APP_8080
  - PUBLIC_URL=${SERVICE_URL_APP}
```

A service that needs only the hostname may consume:

```yaml
HOST=${SERVICE_FQDN_APP}
```

Do not mechanically add both forms.

##### Lessons from n8n

The official n8n template uses:

```yaml
- SERVICE_URL_N8N_5678
- N8N_EDITOR_BASE_URL=${SERVICE_URL_N8N}
- WEBHOOK_URL=${SERVICE_URL_N8N}
- N8N_HOST=${SERVICE_FQDN_N8N}
```

This is a strong reference pattern for applications that need both a canonical full URL and the hostname separately.

It also shares one generated secret between the main service and the task-runner service:

```yaml
N8N_RUNNERS_AUTH_TOKEN=${SERVICE_PASSWORD_N8N}
```

Use one generated variable when two processes must authenticate with the same credential. Never generate one independent secret per consumer when upstream expects equality.

#### Path-based public routing

Complex official templates may attach multiple services or processes to the same public application URL with distinct paths. Appwrite is an important reference:

```yaml
SERVICE_URL_APPWRITE=/
SERVICE_URL_APPWRITE=/console
SERVICE_URL_APPWRITE=/v1/realtime
```

The lesson is not to copy Appwrite's exact layout. The lesson is that Coolify-native routing can represent multi-surface applications using path-aware `SERVICE_URL_*` declarations. Before inventing a custom gateway or Traefik labels, check whether native Coolify routing already expresses the upstream route topology.

#### Generated secrets and credentials

Official templates frequently use Coolify-generated variables such as:

```text
SERVICE_USER_*
SERVICE_PASSWORD_*
SERVICE_PASSWORD_64_*
```

Patterns observed include:

- generated database username/password consumed by both application and database service;
- a shared application secret consumed by server and worker processes;
- generated long secrets for framework/application signing keys.

Choose the generator family by upstream format requirements. Do not change an existing persistent secret family only for cosmetic consistency.

#### Health checks

Prefer probes against local service interfaces, using commands actually present in the target image.

Official examples include:

```yaml
healthcheck:
  test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5678/healthz"]
```

for n8n and:

```yaml
healthcheck:
  test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
```

for PostgreSQL in Authentik.

For workers, Appwrite demonstrates process-level checks when there is no HTTP health endpoint. A worker health check should verify the actual worker process rather than only container existence.

#### Dependency readiness

Official templates use both simple `depends_on` and readiness-aware dependencies. Authentik is a strong reference for the latter:

```yaml
depends_on:
  postgresql:
    condition: service_healthy
```

Use `condition: service_healthy` when the downstream startup truly needs the dependency to be ready and a reliable health check exists. Do not add conditions everywhere merely because they look more production-oriented.

#### Persistence

Official templates persist application state using named volumes and, where upstream semantics need host-visible directories, relative bind mounts.

Examples:

```yaml
volumes:
  - n8n-data:/home/node/.n8n
```

and Authentik combines database named volumes with application bind mounts.

The correct decision depends on upstream ownership, permissions, backup model, and whether the files must be user-editable from the host. Prefer named volumes for ordinary managed application/database state unless upstream or operator requirements justify binds.

#### Image versions

The official corpus commonly uses pinned image versions or version variables with pinned defaults, for example:

```yaml
image: n8nio/n8n:2.10.2
image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2025.10.3}
```

For production templates, prefer a stable version/default over `latest`, unless the official upstream deployment itself intentionally defines a rolling tag and the user accepts that lifecycle.

#### Restart policy

Not every official template repeats `restart: unless-stopped` on every service, but complex stateful templates often do. Preserve upstream restart semantics first. Add a restart policy only when it improves runtime resilience without breaking one-shot jobs or migration services.

#### Security-sensitive exceptions

Some official templates intentionally require privileged or high-risk capabilities for optional upstream features. Authentik, for example, documents the optional worker Docker socket integration and `user: root` requirement.

Therefore:

- never treat `user: root`, Docker socket mounts, privileged mode, or host networking as universally forbidden;
- flag them as high-risk;
- verify that upstream functionality genuinely requires them;
- preserve the official template's warning/context;
- remove optional dangerous integrations when the requested deployment does not need them.

#### Minimalism lesson

Official Coolify templates are generally much less verbose than a troubleshooting-stage Compose. Production templates should contain only:

- executable configuration;
- compact metadata;
- comments that prevent a realistic operational mistake;
- no historical version diary or iteration notes.

Put investigation history in Git, changelog, issue notes, or a case study, not inside operational Bash commands or repeated YAML comments.

#### How to use this corpus

When adapting a new OSS project:

1. Inspect the upstream deployment first.
2. Find 2-5 official Coolify templates with similar architecture, not merely similar product category.
3. Compare their patterns for public routing, secrets, persistence, health checks, workers, and databases.
4. Reuse Coolify conventions only when they preserve upstream semantics.
5. Treat official templates as platform examples, not proof that one pattern is mandatory for every application.
6. If a golden-case-derived practice and an official current template disagree, investigate the cause before choosing either. Prefer current Coolify behavior plus current upstream correctness over historical local workarounds.

#### Strong reference templates

Use these as starting references for different architecture classes:

- `ghost.yaml` — simple public application + relational database, useful for minimal app+DB routing/secret/persistence conventions without implying Ghost bootstrap semantics.
- `wordpress-with-mariadb.yaml` — another compact public app + private MariaDB reference; use only for Coolify conventions, never as proof that the target should pre-create a database.
- `n8n.yaml` — main app + task runner, canonical URL/FQDN, shared generated secret, local HTTP health checks.
- `authentik.yaml` — app + worker + PostgreSQL, readiness-aware dependency, shared application secret, persistence, optional Docker socket caveat.
- `appwrite.yaml` — very large multi-process architecture, path-based Coolify routing, many workers, shared credentials, state volumes, process health checks.
- `appflowy.yaml` — large multi-service application and useful comparison point for multiple backing services.

Do not call one template the universal "best". The best reference is the template whose architecture is closest to the target OSS.


#### Architecture-index layer

For corpus-wide architecture classification and automatic reference selection, also read:

- `references/official-template-taxonomy.md`
- `scripts/select_reference_templates.py`

When a local Coolify checkout is available, regenerate a complete current index instead of relying indefinitely on a frozen list.

<!-- END PORTABLE RESOURCE: references/official-coolify-template-corpus.md -->

<!-- BEGIN PORTABLE RESOURCE: references/official-template-taxonomy.md -->
<!-- SOURCE SHA256: ef53f726e12f207b5413fcd39b889f9925da9da8d7773d4090c19f3fd9aae017 -->
<!-- EMBEDDED SHA256: 659cdb83b108093b156ff4e5b9cc9af6f08a77b5ea9ef415f0bb4cd4e75eb6c7 -->

## Portable resource: `references/official-template-taxonomy.md`

### Official Coolify architecture taxonomy and reference selector

This reference turns the official `coollabsio/coolify/templates/compose` collection into an architectural reference system.

Baseline used for this edition: Coolify `main`, commit `8d675f2e21810bde0f67d6598da08fd52ba1cba5`, reviewed 2026-08-29.

#### Purpose

Do not choose a reference template because the target product belongs to the same business category. Choose references because they solve the same **deployment architecture problem**.

Example:

- a workflow application with PostgreSQL, Redis and workers should compare against `n8n-with-postgres-and-worker.yaml`;
- an application with local S3-compatible storage plus a one-shot bucket initializer should compare against `penpot-with-s3.yaml`;
- a vector database should compare against `qdrant.yaml`, `weaviate.yaml` and `chroma.yaml`;
- an application needing a privileged CI runner should compare against Forgejo/Gitea runner templates, not against an ordinary web app.

The official corpus is a **platform-pattern corpus**, not a universal security baseline. Upstream semantics and production hardening remain separate review layers.

#### Architectural dimensions

Classify the target along independent dimensions rather than forcing it into one label.

##### Compute topology

- `single-service`
- `frontend-backend`
- `app-worker`
- `app-worker-scheduler`
- `multi-process-platform`
- `runner-executor`
- `one-shot-init-or-migration`

##### Data topology

- `embedded-db-or-local-state`
- `postgresql`
- `mysql-or-mariadb`
- `mongodb-or-document-db`
- `redis-or-valkey`
- `multi-database`
- `search-index`
- `vector-database`
- `analytics-column-store`

##### Storage topology

- `named-volume-state`
- `host-bind-state`
- `s3-external`
- `s3-local-minio`
- `distributed-object-storage`

##### Messaging topology

- `redis-queue`
- `rabbitmq`
- `mqtt`
- `websocket-realtime`
- `event-bus-or-stream`

##### Public routing topology

- `single-endpoint`
- `multi-path`
- `multi-endpoint`
- `multi-domain`
- `non-http-public-port`
- `canonical-public-url-consumed-internally`

##### Security / host integration

- `ordinary-container`
- `docker-socket`
- `privileged`
- `docker-in-docker`
- `host-network`
- `device-access`
- `capabilities`

##### Lifecycle topology

- `simple-start`
- `health-gated-start`
- `init-job`
- `migration-job`
- `bootstrap-registration`
- `long-first-start`

A target may match many dimensions simultaneously.

#### Curated official reference library

The following templates are deliberately curated as **strong architecture exemplars**. They are not ranked by product quality.

| Architectural pattern | Strong official references | Why they are useful |
|---|---|---|
| Single HTTP app + volume | `actualbudget.yaml`, `vaultwarden.yaml`, `memos.yaml` | Minimal public service, persistence, little infrastructure noise |
| App + PostgreSQL | `directus-with-postgresql.yaml`, `keycloak-with-postgres.yaml`, `grafana-with-postgresql.yaml`, `nextcloud-with-postgres.yaml` | Standard app/database wiring and generated DB credentials |
| App + MySQL/MariaDB | `wordpress-with-mariadb.yaml`, `ghost.yaml`, `dolibarr.yaml` | Common SQL service patterns |
| App + PostgreSQL + Redis | `docmost.yaml`, `infisical.yaml`, `getoutline.yaml` | Two backing services, internal DNS, persistence |
| App + worker | `n8n.yaml` | Shared secret, task runner, service URL/FQDN separation |
| App + PostgreSQL + Redis + worker | `n8n-with-postgres-and-worker.yaml`, `chatwoot.yaml`, `glitchtip.yaml` | Queue mode, workers, readiness gates |
| Worker/scheduler-heavy application | `trigger.yaml`, `windmill.yaml`, `twenty.yaml`, `plane.yaml` | Multiple process roles around shared infrastructure |
| Very large multi-service platform | `appwrite.yaml`, `supabase.yaml`, `posthog.yaml`, `dify.yaml`, `signoz.yaml` | Complex dependency graphs and many process roles |
| Multi-path public routing | `appwrite.yaml` | Several public surfaces routed by path through Coolify variables |
| Frontend + backend + exporter | `penpot.yaml`, `penpot-with-s3.yaml` | Distinct internal services and readiness relationships |
| Local S3 + init job | `penpot-with-s3.yaml` | MinIO, bucket initializer, `service_completed_successfully` |
| Object storage service | `minio.yaml`, `garage.yaml`, `seaweedfs.yaml`, `rustfs.yaml` | Storage-specific persistence and endpoints |
| S3-enabled application | `penpot-with-s3.yaml`, `ente-photos-with-s3.yaml` | Application/object-store credentials and endpoints |
| Vector database | `qdrant.yaml`, `weaviate.yaml`, `chroma.yaml` | Vector DB persistence/API patterns |
| Search/index service | `meilisearch.yaml`, `typesense.yaml`, `elasticsearch.yaml`, `elasticsearch-with-kibana.yaml` | Search persistence, credentials, optional UI |
| Queue/broker | `rabbitmq.yaml` | Queue service and management surface |
| MQTT/broker | `mosquitto.yaml`, `emqx-enterprise.yaml` | Non-ordinary broker ports/protocols |
| Realtime/WebSocket | `soketi.yaml`, `soketi-app-manager.yaml` | Realtime-specific public/internal behavior |
| CI runner / Docker-in-Docker | `forgejo-with-runner-with-postgresql.yaml`, `forgejo-with-runner.yaml`, `gitea-runner.yaml`, `github-runner.yaml` | Privileged/DinD justified by workload |
| Docker socket integration | `authentik.yaml`, `portainer.yaml`, `dozzle.yaml` | Host integration requiring explicit risk review |
| Observability stack | `signoz.yaml`, `openobserve.yaml`, `plausible.yaml`, `swetrix.yaml` | Telemetry-specific storage and multi-service patterns |
| AI app with several backing services | `dify.yaml`, `librechat.yaml`, `langfuse.yaml`, `litellm.yaml` | DB/cache/vector/object-store style combinations |
| Media app with DB/cache | `immich.yaml` | Stateful media workflow and multiple backing services |
| Git forge + database | `forgejo-with-postgresql.yaml`, `gitea-with-postgresql.yaml` | HTTP + SSH/non-HTTP + DB |
| VPN/network client | `wireguard-easy.yaml`, `tailscale-client.yaml`, `netbird-client.yaml` | Network capabilities and nonstandard host interaction |
| Tunnel/proxy agent | `cloudflared.yaml`, `newt-pangolin.yaml` | Outbound tunnel patterns |
| Game/non-HTTP server | `minecraft.yaml`, `palworld.yaml`, `satisfactory.yaml`, `terraria-server.yaml` | Public TCP/UDP ports; reverse proxy is not always the right abstraction |
| Database/data service | `influxdb.yaml`, `edgedb.yaml`, `qdrant.yaml`, `weaviate.yaml` | Stateful service exposed as an API/data endpoint |
| Mail/dev SMTP | `mailpit.yaml` | Multiple service ports / UI + SMTP behavior |
| File transfer / non-HTTP | `sftpgo.yaml` | Web UI plus transfer protocol semantics |
| Matrix/federated server | `matrix-synapse-with-postgresql.yaml` | Canonical URL/server-name sensitivity |
| Large analytics/event platform | `posthog.yaml`, `signoz.yaml` | High-complexity reference only; avoid copying wholesale |

#### Verified high-value patterns from the official corpus

##### Queue-mode application: n8n

`n8n-with-postgres-and-worker.yaml` is a strong reference for:

- a public main service;
- PostgreSQL;
- Redis as queue backend;
- a dedicated worker;
- an external task-runner;
- shared encryption/authentication secrets;
- `depends_on` gated by `service_healthy`;
- persistent application, PostgreSQL and Redis state.

It demonstrates that process roles sharing one application image should normally share the upstream-required secrets and database/queue configuration.

##### S3 + initialization: Penpot

`penpot-with-s3.yaml` is a strong reference for:

- frontend + backend + exporter;
- PostgreSQL;
- Valkey;
- local MinIO;
- a one-shot `minio-init` job;
- `condition: service_completed_successfully`;
- generated MinIO credentials shared with the application;
- a public URI generated by Coolify.

This is the preferred reference family when the target needs to create buckets or another external-style resource exactly once before application readiness.

##### Vector database: Qdrant

`qdrant.yaml` demonstrates a minimal persistent vector database with a Coolify public endpoint, generated API key and local health probe.

Important: the official file currently uses `qdrant/qdrant:latest`. This is evidence that **official Coolify template** and **strict production pinning policy** are different concerns. `coolify-architect` must flag rolling tags when the requested standard is production pinning, even when the platform template itself uses one.

##### CI runner / DinD: Forgejo

`forgejo-with-runner-with-postgresql.yaml` demonstrates:

- public Forgejo HTTP service;
- PostgreSQL;
- public SSH port;
- privileged Docker-in-Docker;
- a one-shot runner registration service;
- persistent runner and TLS-certificate state;
- bootstrap registration and generated config;
- a long-running runner.

Use this family only when the target genuinely executes container workloads. Never generalize `privileged: true`, DinD or host-like networking into normal web applications.

#### Reference-selection algorithm

The selector must work in this order.

##### Step 1 — Build the target fingerprint

Infer or document:

```text
compute roles
database engines
cache engines
queue/broker
object storage
search/vector stores
public HTTP endpoints
path-based routes
non-HTTP public ports
persistent paths
init/migration jobs
health-gated dependencies
Docker socket / privileged / devices / host network
canonical public URLs consumed by containers
```

##### Step 2 — Eliminate incompatible references

Reject references that introduce major architecture the target does not have.

Examples:

- do not use a DinD template to reference an ordinary worker;
- do not use a multi-path platform as the main reference for a single endpoint app;
- do not use a local MinIO pattern when upstream requires external S3 only;
- do not import Redis merely because a reference template contains Redis.

##### Step 3 — Score architectural similarity

Suggested weights:

```text
+8 exact unusual host integration (DinD/socket/host network/device)
+7 exact storage topology (local S3, distributed object store)
+6 exact worker/queue topology
+6 exact database + cache combination
+5 exact routing topology (multi-path/multi-domain/non-HTTP)
+4 exact lifecycle topology (init job/service_completed_successfully)
+3 same database engine
+3 same cache/broker
+2 similar persistence style
+2 similar health-gating pattern
+1 same product category
-8 introduces privileged/host integration absent from target
-6 introduces object store absent from target
-5 introduces worker/queue absent from target
-4 introduces database family absent from target
```

Product category is intentionally a weak signal.

##### Step 4 — Select a reference set, not one template

Normally choose:

1. **primary architecture reference** — closest overall topology;
2. **routing reference** — best `SERVICE_URL_*` / FQDN / multi-path pattern;
3. **stateful reference** — best database/cache/storage pattern;
4. **special-feature reference** — DinD, S3 init, vector DB, non-HTTP, etc., when needed.

Two to five references are normally enough.

##### Step 5 — Fetch the current official files

The curated names are discovery hints, not frozen source code. Before implementing a new template, fetch the current files from:

```text
https://github.com/coollabsio/coolify/tree/main/templates/compose
```

Then inspect the exact current patterns.

#### Dynamic corpus scanner

The bundled `scripts/select_reference_templates.py` can analyze a target Compose and rank the curated official references.

The same script can also scan a local checkout of `coollabsio/coolify/templates/compose` and produce an architecture index for every YAML file it can parse:

```bash
python scripts/select_reference_templates.py \
  --build-index /path/to/coolify/templates/compose \
  --index-output official-template-index.json
```

This is the preferred way to keep classification synchronized with the moving official corpus. A static list embedded in a skill will eventually become stale.

#### Corpus-wide classification policy

When a full Coolify checkout is available, every Compose is fingerprinted independently. The index records:

```text
template name
service count
public endpoint count
SERVICE_URL variables
SERVICE_FQDN variables
database families
cache/broker families
worker/scheduler indicators
object storage
vector/search services
init/migration jobs
healthchecks
readiness-aware depends_on
named volumes
bind mounts
published ports
docker socket
privileged mode
host networking
device/capability usage
multi-path indicators
```

This is architectural classification, not deployment certification.

#### Best-reference rule

Never say:

> "Template X is best because it is popular."

Say:

> "Template X is the primary reference because its deployment fingerprint matches the target on PostgreSQL + Redis queue + dedicated worker + health-gated startup; template Y is additionally used for S3 initialization."

The reason must be explicit and structural.

<!-- END PORTABLE RESOURCE: references/official-template-taxonomy.md -->

<!-- BEGIN PORTABLE RESOURCE: references/openemr-case-study.md -->
<!-- SOURCE SHA256: 01b6f946a55e77e918d73f8849fd640d4319e79a07182d1c7fd043e98d2952e1 -->
<!-- EMBEDDED SHA256: 0f6b7f2dbd4b4f5ba90a2260835bcc2bc4a6dbd776bdef2aa2dde107dca1ab42 -->

## Portable resource: `references/openemr-case-study.md`

### OpenEMR 8.3.0 on Coolify — fourth golden/regression case

> **OpenEMR regression fixture / golden case — NOT a generic Coolify skeleton.**

This case study captures the fourth runtime benchmark for `coolify-architect`. The executable regression fixture is `assets/openemr-8.3.0-v1.0.0-golden.yml`.

Its main value is deliberately different from the earlier benchmarks: OpenEMR tests whether the Skill can **preserve a small upstream architecture without importing complexity from previous golden cases**.

#### Evidence boundary

Use only these evidence classes:

1. release-relevant OpenEMR 8.3.0 upstream material, including the `rel-830` production Compose/image behavior;
2. current Coolify documentation and relevant official Compose-template conventions;
3. behavior observed during the real Coolify deployment and the operator-confirmed completed acceptance run.

The final benchmark was confirmed successful through deployment, HTTPS access, authentication, representative OpenEMR use, create/read/update/reporting, redeploy persistence, backup, and restore. This validates the fixture, not every possible OpenEMR release or deployment topology.

#### Upstream architecture discovered

The OpenEMR 8.3.0 production topology selected for this benchmark is intentionally small:

```text
Internet HTTPS
      |
      v
Coolify proxy / TLS termination
      |
      v
OpenEMR :80
Apache + PHP + native install/upgrade entrypoint
      |
      v
MariaDB :3306
```

The release-relevant upstream production Compose used two long-running services:

- `openemr` — the public OpenEMR application image, including Apache/PHP and native bootstrap/upgrade behavior;
- `mysql` — MariaDB, with the database healthcheck that gates OpenEMR startup.

No upstream evidence required Redis, MongoDB, Solr, a queue, a dedicated worker, scheduler, gateway, object store, one-shot init service, or custom application network for this production mono-server benchmark.

That absence is important evidence. A dependency seen in another golden case does not become a candidate dependency merely because it might be useful in some applications.

#### Minimality result

The accepted Coolify fixture has exactly the same two long-running application/state services as the selected upstream production topology:

```text
mysql
openemr
```

This is a **case-specific invariant of the OpenEMR 8.3.0 fixture**, not a rule that good Coolify stacks should have two services.

The general lesson is causal:

> **Complexity must be inherited from the current upstream architecture, not from previous golden cases.**

A future application may legitimately need 2, 6, 15, or more services. Service count by itself is neither a quality score nor an anti-pattern detector. What matters is whether each capability has current-upstream or explicit Coolify-operational justification.

#### What the Coolify adaptation preserved

The accepted fixture preserves the upstream behavior that matters:

- OpenEMR as one application service rather than splitting Apache/PHP/bootstrap into invented containers;
- MariaDB as the only required state service for the selected topology;
- the upstream MariaDB `mariadbd --character-set-server=utf8mb4` command;
- the upstream MariaDB readiness probe using `/usr/local/bin/healthcheck.sh --su-mysql --connect --innodb_initialized`;
- `depends_on: mysql: condition: service_healthy` before starting OpenEMR;
- OpenEMR's native install/upgrade entrypoint as the owner of application database bootstrap;
- the upstream OpenEMR local readiness endpoint `/meta/health/readyz`;
- persistence for MariaDB data and OpenEMR site/document/configuration state;
- the upstream fixed application/database usernames used by this fixture (`admin` and `openemr`).

Coolify changes only the hosting mechanics that it owns: public routing, external TLS termination, generated persistent secrets, and removal of unnecessary host-published ports.

#### Coolify-specific adaptations

The accepted fixture uses:

- `SERVICE_URL_OPENEMR_80` to establish the public Coolify route to OpenEMR port 80;
- `SERVICE_PASSWORD_64_OPENEMRDBROOT` for the MariaDB root credential, reused exactly by the OpenEMR bootstrap consumer;
- `SERVICE_PASSWORD_64_OPENEMRDB` for the OpenEMR database-user password;
- `SERVICE_PASSWORD_64_OPENEMRADMIN` for the initial OpenEMR admin password;
- no host port publication for MariaDB;
- no custom network;
- no hand-written Traefik labels;
- no second public TLS stack inside OpenEMR for the normal Coolify path.

The exact magic-variable identifiers above are **OpenEMR fixture facts**. They are not preferred names for unrelated applications.

#### Persistence map validated by the benchmark

The fixture keeps three named volumes:

| Volume | Container path | Classification | Recovery role |
|---|---|---|---|
| `databasevolume` | `/var/lib/mysql` | authoritative relational/clinical state | critical; restore with a database-aware method |
| `sitevolume` | `/var/www/localhost/htdocs/openemr/sites` | authoritative site configuration and patient/document files | critical; back up and restore with the database |
| `logvolume01` | `/var/log` | operational logs | useful operational state, not the primary clinical restore set |

The benchmark matters because it demonstrates that **“the database is persistent” is not a complete persistence model even for a simple two-service application**. OpenEMR's `sitevolume` contains state that must travel with the database for a meaningful recovery.

The completed acceptance run confirmed persistence after redeploy. A synthetic marker such as `OPENEMR-COOLIFY-830-PERSIST-A` remained visible after container recreation, proving that persistent application data survived the normal Coolify lifecycle.

#### Initialization and the no-sidecar result

OpenEMR's application image already owns the fresh-install lifecycle: waiting for MariaDB, creating/configuring the application database/user, writing site configuration, creating the initial administrator, handling permissions, and then serving the application.

Therefore the fixture deliberately adds **no init container or migration sidecar**.

This confirms an existing rule rather than creating a new one: if upstream already has a reliable native initialization path, splitting it into helper services merely for aesthetic separation is unnecessary architecture.

##### `MYSQL_DATABASE` was deliberately not introduced

The candidate did not force `MYSQL_DATABASE=openemr` into MariaDB merely to fit a platform convenience. In the selected upstream path, OpenEMR owns creation of its application database/user/collation during bootstrap.

The benchmark therefore preserved that contract and used an application-aware backup/restore procedure instead of changing initialization semantics to fit a control-plane backup expectation.

**Important evidence boundary:** the benchmark did not prove that pre-creating the database with `MYSQL_DATABASE` would necessarily fail. Do not turn this design choice into a universal prohibition. The validated lesson is to preserve the upstream bootstrap contract unless a divergence has an independently justified reason and its migration/restore consequences are tested.

#### Healthchecks and runtime evidence

The OpenEMR service uses the image-local probe:

```text
https://localhost/meta/health/readyz
```

During the live deployment this probe returned HTTP 200 from inside the container. This proves that the selected OpenEMR 8.3.0 image contains the required `curl` binary and that this endpoint worked as a readiness signal in the accepted fixture.

MariaDB's official image healthcheck also reached `healthy` before OpenEMR startup.

A transient early MariaDB message showed an access denial for the `openemr` database user while bootstrap was still progressing. It was not treated as proof of a permanent database design failure because subsequent application initialization, health, writes, reads, and reporting all succeeded.

The reusable lesson is already present elsewhere in the Skill: correlate warnings/errors with lifecycle and acceptance evidence before changing architecture.

#### Public exposure and TLS

The validated public path was:

```text
Browser HTTPS
   -> Coolify proxy/TLS
   -> OpenEMR HTTP :80
```

The application was successfully used over the public HTTPS origin, including dashboard pages, assets, patient workflow endpoints, reporting, and background-service calls.

No evidence required:

- an additional nginx gateway;
- public OpenEMR port 443 behind Coolify;
- host-gateway hairpin mappings;
- a custom network;
- a port-qualified canonical hostname injected into application configuration.

This is a useful counterexample to the OpenMRS/Kobo cases where semantic application gateways must be preserved. Coolify's proxy replaces only the infrastructure edge role; whether an application proxy remains depends on the current upstream architecture.

#### Representative live acceptance evidence

The benchmark progressed through real application behavior, not only container state. The observed/confirmed acceptance path included:

```text
fresh Coolify deployment
  -> MariaDB initialized and healthy
  -> OpenEMR native bootstrap completed
  -> /meta/health/readyz = 200
  -> public HTTPS application accessible
  -> admin authentication
  -> dashboard usable
  -> create application/user/patient data
  -> create encounter/visit data
  -> read/report data
  -> modify/re-read data
  -> normal redeploy with volumes preserved
  -> persisted clinical marker still present
  -> backup created
  -> isolated restore completed
  -> restored application/data verified
```

The operator confirmed the final benchmark and acceptance tests completed successfully.

#### Backup and restore lesson

The tested recovery model treats the database and `sitevolume` as one logical recovery set:

```text
MariaDB logical backup
      +
OpenEMR sitevolume archive
      -> isolated restore
      -> application-level verification
```

This confirms existing production-readiness guidance: identify **all authoritative stores**, create real backup artifacts, restore them into an isolated target, and verify the restored business object through the application.

Do not generalize the exact commands, paths, credentials, or MariaDB procedure to other applications. The general rule is recovery-set completeness plus verified restore.

#### Iteration / error retrospective

Unlike earlier benchmarks, the accepted OpenEMR candidate did not require an architectural redesign after the first real deployment.

Important observations during bootstrap included:

- MariaDB's container-environment warnings about unavailable cgroup memory-pressure functionality;
- `io_uring` being unavailable and MariaDB falling back to Linux native AIO;
- one transient access-denied message for the application DB user during initial bootstrap.

None justified adding services, networks, retries, or proxy workarounds because MariaDB became ready, OpenEMR became healthy, and real application writes/reads succeeded.

The absence of a corrective architecture iteration is itself useful evidence: **do not manufacture a fix when the simplest upstream-faithful candidate already passes real acceptance.**

#### Classification of OpenEMR learnings

##### A. New generalizable rules

**No major new platform rule was required.**

OpenEMR did not invalidate the existing upstream-first/minimal-adaptation method. The Skill therefore should not invent an OpenEMR-specific global rule.

The benchmark does justify making one existing principle more explicit as a **complexity provenance gate**: every added or removed architectural capability must be traceable to current upstream behavior or a documented Coolify operational need. This is a strengthening/operationalization of existing rules, not a new service-count heuristic.

##### B. Confirmations of existing rules

- upstream deployment material is the architecture source of truth;
- the smallest justified Coolify adaptation is preferable to speculative infrastructure;
- complexity is application-specific and must not be inherited from golden fixtures;
- internal databases remain private by default;
- one logical credential must use one stable generated identity wherever it is shared;
- native upstream initialization should remain in charge where it is already reliable;
- `depends_on: service_healthy` is appropriate when the upstream dependency really is a startup readiness gate;
- healthchecks must execute tools/endpoints that actually exist in the selected image;
- Coolify can terminate public TLS while an application serves internal HTTP;
- authoritative persistence may include application files/configuration in addition to the database;
- green/healthy containers do not replace workflow, persistence, backup, and restore acceptance;
- transient warning/error log lines must be correlated with downstream runtime evidence before redesigning the stack.

##### C. OpenEMR-specific facts

- the accepted OpenEMR 8.3.0 mono-server fixture has exactly `openemr` + `mysql` services;
- selected images are `openemr/openemr:8.3.0-2026-08-29` and the pinned MariaDB 11.8.8 image/digest in the fixture;
- public Coolify routing targets OpenEMR port 80;
- OpenEMR's accepted readiness path is `/meta/health/readyz` over local container HTTPS;
- MariaDB uses its official image healthcheck and gates OpenEMR startup;
- `admin` is the accepted bootstrap login identifier in this fixture;
- `openemr` is the application DB username in this fixture;
- the three fixture volumes are `databasevolume`, `sitevolume`, and `logvolume01` with the exact paths in the golden Compose;
- the native OpenEMR installer owns creation/configuration of the application database/user in the selected upstream path;
- the exact `SERVICE_PASSWORD_64_OPENEMR*` identifiers are fixture-specific;
- the tested recovery set combines a MariaDB-aware backup with `sitevolume`.

##### D. New anti-patterns

No wholly new anti-pattern family was required. OpenEMR **strengthens** the existing golden-contamination / one-shot-sidecar anti-patterns:

- adding Redis/workers/gateways/init helpers merely because earlier golden cases used them is architecture contamination;
- adding infrastructure to make a stack look “production-grade” without target-upstream cause is overengineering;
- changing a working upstream bootstrap solely to satisfy a platform convenience is a review item, not an automatic improvement.

These are refinements of existing anti-patterns, not universal bans on those components or platform features.

##### E. Not sufficiently demonstrated / keep conditional

- pre-creating the OpenEMR database with `MYSQL_DATABASE` would necessarily break every installation — **not tested**;
- MariaDB `io_uring`/cgroup warnings are harmless under every production workload — acceptance success does not prove capacity/host-hardening irrelevance;
- the exact OpenEMR dated image tag, MariaDB digest, health timings, or credential identifiers are valid for future releases — they must be re-verified;
- every OpenEMR topology should omit Redis, external storage, integrations, or other optional components — only this selected production topology was validated;
- OpenEMR should always be proxied over internal HTTP rather than HTTPS — this fixture validates one normal Coolify topology, not every security model.

#### What must never contaminate another target

Do not infer any of these from OpenEMR without independent current-upstream evidence:

- a two-service target topology;
- MariaDB;
- the `openemr` DB username;
- the `admin` login identifier;
- port 80 as the public proxy target;
- `/meta/health/readyz`;
- the exact `SERVICE_PASSWORD_64_OPENEMR*` names;
- the exact three volume names/paths;
- OpenEMR's installer-owned database creation lifecycle;
- omission of `MYSQL_DATABASE`;
- a MariaDB-dump + `sitevolume` recovery procedure;
- absence of workers, Redis, gateways, object stores, or schedulers.

The portable lesson is not “simple is always better.” It is **“the current upstream decides how much complexity is justified.”**

#### OpenEMR regression path

For future changes to this fixture, repeat at least:

1. YAML/Compose/static validation;
2. fresh Coolify resource with fresh volumes;
3. verify all required generated passwords are non-empty before bootstrap;
4. confirm the service set remains upstream-justified and any topology delta has explicit provenance;
5. confirm MariaDB is private and reaches its upstream health state;
6. confirm OpenEMR starts after DB readiness and `/meta/health/readyz` passes;
7. verify the public HTTPS route and admin login;
8. run a representative create/read/update/reporting workflow;
9. persist an application marker and verify it after restart/redeploy/container recreation;
10. verify `sitevolume`-backed file/document persistence when part of the test data;
11. create the documented recovery set;
12. restore into an isolated instance and verify the restored application/business data;
13. confirm no new service, network, public port, or sidecar was introduced without current-upstream or operational justification.

A future OpenEMR release is a new adaptation exercise. The 8.3.0 golden proves this accepted fixture, not all OpenEMR versions.

<!-- END PORTABLE RESOURCE: references/openemr-case-study.md -->

<!-- BEGIN PORTABLE RESOURCE: references/openmrs-case-study.md -->
<!-- SOURCE SHA256: 96d64e0693b13d288b2d185a40570e77cd7d6561a4094f03d4e6584be00e2395 -->
<!-- EMBEDDED SHA256: 2f8f3a90a2f359b95c520a9cc145967829688346e69ace1eadcc791589c35b0d -->

## Portable resource: `references/openmrs-case-study.md`

### OpenMRS 3.7.1 on Coolify — third golden/regression case

> **OpenMRS regression fixture / golden case — NOT a generic Coolify skeleton.**

This case study captures the third runtime benchmark for `coolify-architect`. The executable regression fixture is `assets/openmrs-3.7.1-v1.0.0-golden.yml`. It is evidence about one OpenMRS 3.7.1 deployment, not a starter stack for unrelated applications.

#### Evidence boundary

Use only three evidence classes here:

1. current/release-relevant OpenMRS upstream material;
2. current official Coolify documentation;
3. behavior observed in the real Coolify benchmark and the operator-confirmed final acceptance run.

A log line or an experimental iteration is not automatically a general rule. The final fixture is intentionally preserved even where an upstream production guide may suggest a different steady-state choice; that tension is documented rather than silently “corrected.”

#### Upstream architecture discovered

OpenMRS Reference Application 3 consists of four application images/services in its base distribution:

```text
Internet/client
      |
      v
gateway (nginx :80)
   |                 \
   |                  \
   v                   v
frontend (nginx :80)   backend (OpenMRS/Tomcat :8080)
                            |
                            v
                       MariaDB :3306
```

Upstream describes:

- `db` as the standard MariaDB service;
- `backend` as the OpenMRS backend with the Reference Application/Initializer content;
- `frontend` as nginx serving the assembled O3 frontend;
- `gateway` as nginx in front of frontend and backend, providing one interface and mitigating CORS concerns;
- an optional SSL overlay that adds certificate management outside the base four-service topology.

For 3.7.1 the selected images were pinned to `openmrs/openmrs-reference-application-3-{gateway,frontend,backend}:3.7.1`, with MariaDB `10.11.7`.

#### What the Coolify adaptation preserved

The final adaptation kept the important upstream semantics instead of collapsing them into one container:

- the four-service `gateway + frontend + backend + db` topology;
- the upstream gateway as the application routing/CORS layer;
- O3 frontend paths/configuration (`SPA_PATH`, `API_URL`, `SPA_CONFIG_URLS`, locale);
- MariaDB and the `openmrs` database;
- OpenMRS persistent application data at `/openmrs/data`;
- upstream backend startup through `/openmrs/startup.sh`;
- OpenMRS database/admin environment wiring supported by the upstream core image;
- application-level health/readiness endpoints rather than a process-only probe.

Coolify replaced only the external hosting concerns it actually owns: public domain routing and TLS termination. The upstream application gateway remained because it has application routing semantics; the upstream Certbot/SSL overlay was not needed behind Coolify.

#### Coolify-specific adaptations

The accepted fixture introduced the following hosting adaptations:

- only `gateway:80` receives a public Coolify route through `SERVICE_URL_GATEWAY_80`;
- `frontend:80`, `backend:8080`, and `db:3306` remain internal-only;
- the same generated DB username/password are reused between MariaDB and the backend:
  - `SERVICE_USER_MYSQL`;
  - `SERVICE_PASSWORD_64_MYSQL`;
- a separate root credential uses `SERVICE_PASSWORD_64_MYSQLROOT`;
- the bootstrap admin password uses `SERVICE_PASSWORD_64_ADMIN` while the OpenMRS login identifier remains the upstream-defined `admin`;
- the backend performs a fail-fast credential preflight before invoking `/openmrs/startup.sh`;
- backend/gateway healthchecks use `/openmrs/health/started` and a long `start_period` because first bootstrap is materially slower than a steady restart;
- persistence is explicit through `db-data` and `openmrs-data`;
- no custom application network, host port publication, custom Traefik labels, or duplicated TLS stack was required.

The exact magic identifiers `MYSQL`, `MYSQLROOT`, and `ADMIN` are **fixture facts**. The benchmark does not prove that credential IDs containing underscores are generally invalid in Coolify.

#### Iteration and failure retrospective

##### Compose revision trail

| Revision | Main change / status | Evidence gained |
|---|---|---|
| `0.1.0` | four upstream services preserved; unqualified gateway magic URL; older/reference `OMRS_CONFIG_*` wiring; `/health/alive`; controlled manual bootstrap flags | architecture direction was plausible, but Coolify routing, bootstrap and credential behavior were not yet proven |
| `0.1.1` | required-secret syntax tightened and backend first-start window increased | missing values should fail earlier; long bootstrap needed explicit allowance |
| `0.1.2` | incremental cleanup while retaining the same basic architecture | no new runtime proof; still a candidate |
| `0.1.3` | gateway changed to `SERVICE_URL_GATEWAY_80`; readiness moved toward `/health/started` | explicit Coolify internal proxy target became part of the model |
| `0.2.0` | switched to core-supported `OMRS_DB_*`/admin bootstrap variables; introduced generated DB username/password and fail-fast password preflight | application contract was better aligned, but the chosen `OPENMRS_DB`-style magic IDs appeared blank in that Coolify resource, so deployment was stopped |
| `0.2.1` | generated IDs simplified to `MYSQL`, `MYSQLROOT`, `ADMIN`; exact DB credential reused by MariaDB and backend | Coolify generated the values correctly; fresh deployment proceeded |
| `1.0.0` | accepted fixture frozen after real routing/readiness/login/REST/SPA tests; later operator-confirmed final acceptance completed | becomes the OpenMRS regression oracle |

The revision trail is diagnostic history, not a recommendation to replay every intermediate Compose.

##### Early candidate: architecture mostly right, lifecycle and variable semantics still weak

The first candidates already preserved the four upstream services, but they used a mixture of older/reference-application `OMRS_CONFIG_*` settings, manually named database variables, non-port-qualified `SERVICE_URL_GATEWAY`, and weaker readiness assumptions.

This was directionally correct but not enough evidence for a production candidate. The benchmark forced us to verify the actual OpenMRS image contract and the current Coolify magic-variable behavior instead of assuming either.

##### Gateway routing needed an explicit internal target port

The public edge listens on port 80. Moving to `SERVICE_URL_GATEWAY_80` made the intended Coolify proxy target explicit. The later live Traefik labels showed a load-balancer server port of `80`.

**Lesson:** when a generated Coolify domain must route to a specific internal port, use the documented port-qualified URL/FQDN form. This is a routing decision, not a universal rule that every service must use a port suffix.

##### Magic credentials appeared blank in one candidate

An intermediate candidate used identifiers such as `SERVICE_USER_OPENMRS_DB` and `SERVICE_PASSWORD_64_OPENMRS_DB`. In that test resource, the expected values appeared blank in Coolify before deployment. The candidate was correctly stopped before it could mutate the database.

The final candidate used the simpler identifiers `MYSQL`, `MYSQLROOT`, and `ADMIN`; Coolify generated non-empty values and they were consumed successfully at runtime.

**What is validated:** verify required generated values in Coolify before first bootstrap, choose a documented generator family that satisfies upstream format constraints, and reuse the same complete variable name wherever one credential must match.

**What is not validated:** the observation does not prove a global ban on underscores in credential identifiers. Current Coolify documentation specifically documents an underscore limitation for **port-qualified URL/FQDN identifiers**; it does not establish the same rule for arbitrary credential IDs.

##### Existing persisted admin credential did not follow an environment edit

During an earlier non-fresh iteration, the environment-provided admin password did not authenticate the already-persisted `admin` account while the old test password still did. REST authentication proved the stored account state. The password was then deliberately synchronized through the OpenMRS password API, after which the intended credential authenticated and the old password no longer did.

**General lesson:** a bootstrap credential environment variable is not proof that an already-initialized application account was mutated. Persisted identity state and environment configuration are different state machines.

**OpenMRS-specific mechanism:** the recovery endpoint/credential behavior used in this benchmark must not be copied to other applications.

##### Public `503 no available server` while the application was actually booting

On the fresh final deployment, the public hostname initially returned Coolify/Traefik `503 no available server`. Direct requests to the gateway container IP already returned HTTP 200 for health, REST session, and SPA paths, while Docker still reported the gateway health state as `starting`. Health history showed one timeout followed by successful probes; once the gateway became healthy, public routing worked.

Network inspection confirmed that the Coolify proxy and gateway were attached to the same stack network, and the generated Traefik service targeted gateway port 80. No custom network was needed.

**Lesson:** a transient proxy 503 can represent target readiness, not necessarily broken networking. Diagnose separately: labels/target port, network attachment, direct container response, Docker health history, then public route. Do not redesign networking from the public symptom alone.

##### Long first bootstrap was real

The backend spent substantial time applying Liquibase changesets, loading modules/configuration, and refreshing the OpenMRS context. The operator observed that the stack needed time before it “worked normally.” The final fixture therefore keeps a 15-minute `start_period` for backend and gateway.

The exact 15 minutes is **OpenMRS fixture-specific**. The reusable rule is to measure/understand first-start duration and prevent a valid bootstrap from being misclassified as a permanent failure.

##### Non-fatal warnings were not treated as fatal errors by string severity alone

The accepted run included warnings/errors concerning ActiveMQ store limits, address-hierarchy configuration, optional appointment properties, and a PostgreSQL-only Stock Management changeset precondition skipped on MariaDB. MariaDB also emitted environment/IO warnings while becoming ready.

Those lines were correlated against subsequent context refresh, health success, public accessibility, and application acceptance. They did not justify rewriting the architecture.

This does **not** mean such warnings are universally harmless. Operational warnings that affect capacity or production behavior remain review items.

#### Final live acceptance evidence

The benchmark progressed beyond “containers are green.” The observed/defined acceptance path covered, at minimum:

```text
fresh Coolify bootstrap
  -> generated DB/admin credentials present
  -> MariaDB healthy
  -> OpenMRS backend finishes context refresh
  -> gateway/frontend/backend/db healthy
  -> public /openmrs/health/started = 200
  -> public REST session endpoint = 200
  -> public O3 SPA = 200
  -> admin login with generated credential
  -> O3 usable after location selection
  -> representative clinical workflow / persistence / backup-restore gates
```

The benchmark operator has confirmed the final acceptance run succeeded. Preserve the fixture as the regression oracle for that accepted deployment. Future upgrades must still repeat release-specific acceptance rather than inheriting the status automatically.

#### Classification of OpenMRS learnings

##### A. Generalizable Coolify rules

- Use only documented `SERVICE_<TYPE>_<ID>` generator families; select the generator by the application's required format.
- Reuse the **same complete magic variable** wherever one credential must be identical across services.
- Generated values are persistent deployment state; do not rename their IDs casually after initialization.
- Verify required generated credentials are non-empty **before** a destructive/one-time bootstrap begins.
- `SERVICE_URL_*` and `SERVICE_FQDN_*` describe public routing/identity; Docker service names describe internal networking. They are not interchangeable.
- A port-qualified URL/FQDN is for proxy routing to an internal port and can carry port/path semantics; do not automatically use it as a canonical application origin.
- Coolify can terminate external TLS while an upstream application gateway remains necessary for application routing.
- A public 503 must be correlated with proxy target health, network attachment, and direct service readiness before changing architecture.

##### B. Generalizable architecture/discovery rules

- Preserve semantic gateways/proxies discovered upstream instead of deleting every proxy merely because Coolify has Traefik.
- Model bootstrap state separately from steady-state runtime state.
- Model persisted application identity/credentials separately from environment variables.
- Measure realistic first-start readiness rather than guessing an arbitrary sleep.
- Diagnose a route through distinct layers: public proxy -> application gateway -> frontend/backend -> database/bootstrap.
- Treat log severity as evidence to correlate, not a standalone architectural verdict.
- Validate a representative business workflow, persistence, backup, and restore separately from syntax/health.

##### C. OpenMRS-specific facts

- four selected services: `gateway`, `frontend`, `backend`, `db`;
- ports 80/80/8080/3306 in this topology;
- O3 paths `/openmrs/spa`, API base `/openmrs`, and config-core demo path;
- MariaDB `10.11.7` and OpenMRS Reference Application images `3.7.1` in this fixture;
- login identifier `admin` and `OMRS_ADMIN_USER_PASSWORD` bootstrap wiring;
- `/openmrs/health/started` as the accepted readiness endpoint in this fixture;
- persistent `/openmrs/data` plus MariaDB data;
- the exact password preflight and its complexity tests;
- the 15-minute healthcheck `start_period`;
- OpenMRS-specific Liquibase/module/Initializer behavior and warning messages.

##### D. Anti-patterns discovered

- deploy first and discover blank generated credentials later;
- generate two different magic variables for two consumers of one database password;
- wire a password generator into a username field or vice versa;
- assume changing a bootstrap env var retroactively changes persisted application credentials;
- treat an initial proxy 503 as proof that a custom Docker network is required;
- delete a semantic upstream gateway because Coolify already has a reverse proxy;
- mark an application production-ready because Compose parsed or all four containers became healthy;
- copy OpenMRS's four-service shape, admin name, MariaDB, or long start period into an unrelated target.

##### E. Not sufficiently demonstrated / keep as hypotheses

- “credential magic identifiers containing underscores do not generate” — not established by current Coolify documentation; only the particular experimental candidate failed to populate as expected;
- “15 minutes is the correct Coolify start period” — only validated for this OpenMRS fixture and environment;
- “a 64-character no-symbol Coolify password always contains uppercase, lowercase, and a digit” — current docs define length/symbol class, not those character-class guarantees; retain an application-specific preflight when required;
- capacity-impact of the observed ActiveMQ/disk warnings under production load — acceptance success does not prove they are irrelevant at scale;
- whether the fixture's `OMRS_CREATE_TABLES=true` / `OMRS_AUTO_UPDATE_DATABASE=true` should remain unchanged for every long-lived production lifecycle — the golden preserves the accepted bootstrap behavior, while future operators must re-check release-specific OpenMRS production guidance.

#### What must never contaminate another target

Do not infer any of the following from this golden case without independent upstream evidence:

- OpenMRS gateway/frontend/backend split;
- MariaDB;
- port `8080`;
- `/openmrs/*` paths;
- O3 frontend variables;
- `admin` as the bootstrap user;
- `OMRS_*` variables;
- `/openmrs/data`;
- Liquibase/Initializer assumptions;
- the exact password-complexity preflight;
- a 15-minute start period;
- `SERVICE_USER_MYSQL`, `SERVICE_PASSWORD_64_MYSQL`, `SERVICE_PASSWORD_64_MYSQLROOT`, or `SERVICE_PASSWORD_64_ADMIN` as preferred names for other software.

#### OpenMRS regression path

For future changes to this fixture, repeat at least:

1. parse/static validation and embedded-command syntax validation;
2. fresh Coolify resource with fresh volumes;
3. confirm all required magic credentials are generated before deploy;
4. confirm only `gateway` is public and the proxy targets internal port 80;
5. allow first bootstrap to complete; inspect the earliest fatal error if it does not;
6. verify `/openmrs/health/started`, REST session, and O3 SPA publicly;
7. authenticate as `admin` with the generated admin password;
8. execute a representative patient/clinical create-read workflow;
9. restart/redeploy without deleting volumes and verify persisted state;
10. create database/application-data backups and restore them into an isolated instance;
11. verify the restored clinical object through O3 and REST;
12. confirm no internal database/backend/frontend endpoint became public.

A future OpenMRS release is a new adaptation/upgrade exercise. The 3.7.1 golden proves this fixture, not all OpenMRS versions.

<!-- END PORTABLE RESOURCE: references/openmrs-case-study.md -->

<!-- BEGIN PORTABLE RESOURCE: references/openspp-case-study.md -->
<!-- SOURCE SHA256: 0e66795aa6d35bb2125538cd61e9a551b9ef71cddf976086be07f71104e07072 -->
<!-- EMBEDDED SHA256: 3647d3b83965d377bffbb9a4c133f02ace46ab8e5816a5e1528142e3a23bdfa9 -->

## Portable resource: `references/openspp-case-study.md`

### OpenSPP V2 2026.08 case study — Golden / Regression Case #12

#### Status and evidence boundary

OpenSPP V2 2026.08 is **Golden / Regression Case #12**.

The immutable Golden is the exact operator-accepted runtime candidate:

```text
assets/openspp-2026.08-v1.0.0-golden.yml
SHA-256: f00a8755fa2be8e8b1f50970978ae1b57c1877093c2a35108edf35a675d4587b
```

It is byte-for-byte identical to the accepted `openspp-coolify-v1.0.0-rc8.yml`.

Promotion is based on the benchmark's final operator verdict: after RC8, the operator explicitly reported that the final version worked without problems, data persistence was correct, and **all requested tests were completed successfully**. This is Level 4 operator-confirmed runtime evidence for the acceptance suite. The corpus does not preserve raw command output for every final acceptance sub-gate, so this case study records those gates as **operator-confirmed**, not as independently replayed by the Skill.

Do not infer untested HA, multi-node scaling, load/performance, multi-region DR, KMS/TDE, off-site backup-provider behavior, or optional integrations from this Golden.

#### Target profile

The benchmark selected:

- OpenSPP V2 `2026.08`;
- release commit `208d97582791b369b562cdfcb3e41766a2be710f`;
- Odoo 19;
- PostgreSQL 18 + PostGIS 3.6;
- SP-MIS product profile;
- activation bundle `spp_starter_sp_mis`;
- production-hardened single-node deployment on Coolify.

The accepted five-service topology is local to this selected OpenSPP profile:

| Service | Local responsibility |
|---|---|
| `openspp` | application-semantic Nginx gateway |
| `odoo` | OpenSPP/Odoo HTTP, multi-worker runtime, gevent/WebSocket, module/product lifecycle, filestore |
| `queue-worker` | asynchronous OpenSPP/Odoo jobs |
| `db` | PostgreSQL 18 + PostGIS 3.6 |
| `backup` | PostgreSQL + filestore recovery-set capture |

Coolify owns Internet ingress, TLS, and public hostname routing. The internal OpenSPP gateway retains application routing semantics, WebSocket routing, security headers, rate limiting, single-database routing, database-manager blocking, and canonical proxy normalization.

**Do not generalize this five-service topology to other Odoo applications.**

#### Exact Golden identity

The accepted fixture preserves:

- source build from exact OpenSPP commit `208d975...`;
- `pull_policy: never` on the local-only source-built `odoo` and `queue-worker` image users;
- PostgreSQL/PostGIS private state;
- intentionally separate DB roles:
  - bootstrap/admin role `openspp_admin`;
  - application role `odoo`;
- `odoo` as non-superuser / no-createdb runtime role;
- `spp_starter_sp_mis` activation-aware Odoo health;
- queue worker gated on activated Odoo readiness;
- `odoo_data` filestore persistence;
- coherent database + filestore backup service;
- two dependency-scoped XML compatibility overlays for the demonstrated OCA `role_ids -> user_role_ids` drift;
- single-database public routing;
- database-manager blocking;
- WebSocket route to Odoo `:8072`;
- canonical HTTPS proxy headers toward Odoo;
- Coolify-managed Nginx template with an RC-specific identity;
- whitelisted `envsubst`;
- `nginx -t` before the long-lived Nginx process.

These are **fixture invariants**, not generic requirements.

#### Runtime evidence chronology

##### RC1 — platform deployment failure before application runtime

Observed failure:

```text
pull access denied for openspp-coolify
```

The candidate declared a local source-built image through both `image:` and `build:`. The benchmark demonstrated that the then-current Coolify service deployment path performed a preliminary `docker compose pull` before `up --build`, so it tried to pull the local-only image from a registry.

Correction: `pull_policy: never` only on the two source-built application services.

General lesson: locally built image behavior must be checked against the effective Coolify pull/build sequence. This is version-sensitive platform behavior, not a universal Docker Compose law.

##### RC2 — deployment mechanics pass; product activation fails

RC2 proved that the source image could build and all five intended containers could be created. Containers appeared `Running (healthy)`, but the claimed SP-MIS product was not actually ready.

The blocking application failure was:

```text
odoo.tools.convert.ParseError
Element '<xpath expr="//field[@name='role_ids']">' cannot be located in parent view
```

The released OpenSPP 2026.08 source still targeted `role_ids`; the mutable OCA/server-backend `19.0` dependency fetched during build had moved to `user_role_ids`.
The benchmark traced that dependency-side rename to OCA commit `c7e2d1a66fee52c0f942ce9b3aebe51760c59a18` (18 May 2026). This identifier is OpenSPP-case evidence, not a generic dependency pin.

A secondary PostgreSQL serialization failure appeared when the queue worker loaded registry/module state while initialization was still mutating it. That was a concurrency symptom, not the original fault.

General lessons:

- source release pin != transitive build graph pin;
- mutable dependency contract drift can break a released application later;
- framework/process health != selected product activation;
- readiness must protect shared initialization;
- the last log `ERROR` is not automatically the root cause.

##### RC3 — smallest dependency compatibility boundary + activation-aware readiness

RC3 added two narrowly scoped compatibility overlays for the two demonstrated XML contracts and strengthened Odoo readiness to require both HTTP health and:

```text
spp_starter_sp_mis.state = installed
```

This kept the compatibility correction **version/dependency-scoped**. It did not fork or copy the dependency tree.

RC3 also showed that redirect behavior behind an internal semantic gateway required separate validation from product activation.

##### RC4 — single-database routing correction

The prior gateway logic could create a login/database-selector loop. RC4 made the intended single-database entry explicit:

```text
/ -> /web/login?db=openspp
/web/database/selector -> /web/login?db=openspp
/web/database* -> blocked
```

General lesson: in a production single-database mode, public gateway behavior should not accidentally expose selector/manager surfaces that contradict the deployment model.

##### RC5 — canonical origin normalization

The benchmark observed browser-visible redirects containing an internal `:8080` port. RC5 normalized the Nginx -> Odoo hop so the internal gateway listener did not become the public browser port.

General lesson:

```text
platform ingress target port
!=
internal semantic gateway port
!=
application listener port
!=
canonical browser origin
```

Use the actual external origin rather than blindly forwarding the internal listener port.

##### RC6 — documented Compose primitive not supported by the effective Coolify path

RC6 tried a top-level `configs: content:` strategy to avoid stale managed-file behavior. The effective Coolify service path rejected it:

```text
`file` is the sole supported option
```

General lesson: Docker Compose specification support does not prove support through every Coolify parser/persistence/deployment path. Critical primitives must be verified end-to-end.

##### RC7 — managed-file transport differs from Compose command interpolation

RC7 moved back to a new Coolify-managed file identity but used `$$remote_addr`, `$$host`, and similar tokens in content that Coolify wrote literally. Nginx therefore received `$$remote_addr` and crashed.

General lesson: dollar escaping is transport-specific. `$$` needed in one Compose/shell/nested-language path can be wrong in a managed file that is written literally.

##### RC8 — accepted correction

RC8:

- used a new managed-file source and target identity;
- used native Nginx `$variables` with a single dollar;
- limited `envsubst` to the four intended operator-tunable variables;
- preserved native Nginx variables untouched;
- ran `nginx -t` before process start.

The operator then reported the application working without problems, persistence working, and all requested acceptance tests passing. RC8 is therefore frozen exactly as the Golden.

#### Build reproducibility classification

OpenSPP 2026.08 source itself is pinned to an immutable commit, but the upstream Dockerfile resolves additional dependencies from mutable branches during build. The benchmark demonstrated a real contract drift in such a dependency.

Classification for the tested build model:

```text
PARTIALLY PINNED
+
FLOATING TRANSITIVE DEPENDENCIES
```

The Golden preserves the exact accepted bytes, but it does **not** claim that a future rebuild from the same OpenSPP source commit will be bit-for-bit reproducible.

#### Recovery boundary

For this accepted profile, the minimum demonstrated logical recovery set is:

```text
PostgreSQL/PostGIS
+
Odoo filestore
+
stable configuration/secrets
```

A PostgreSQL dump alone is not a complete product backup when user attachments reside in the filestore.

The final operator verdict covered the requested persistence/backup/isolated-restore test suite. The fixture's backup service is therefore part of this Golden's accepted profile, but **backup sidecars are not generalized to unrelated products**.

#### Intentional database role split

The same PostgreSQL server has two intentional logical roles:

| Role | Purpose | Runtime privilege |
|---|---|---|
| `openspp_admin` | bootstrap/admin/extension ownership | elevated bootstrap role |
| `odoo` | application runtime | non-superuser, no-createdb |

Different generated passwords for these roles are not a credential mismatch. The generalized audit rule is to model credential topology before deciding whether different passwords are inconsistent.

#### Security boundary

The Golden provides a deployable production baseline for the selected single-node profile. It does not automatically instantiate an organization-wide security program.

Classify hardening controls as:

- template-safe default;
- operator-supplied secret/config;
- infrastructure/provider responsibility;
- organizational policy;
- post-deployment hardening;
- optional high-security profile.

Do not fabricate KMS, TDE, off-site infrastructure, RPO/RTO policy, PITR, audit retention, monitoring stack, or scheduled restore exercises without target/operator evidence.

#### Anti-contamination guard

Keep these facts local to OpenSPP Golden #12:

- exact OpenSPP/Odoo/PostGIS versions;
- `spp_starter_sp_mis`;
- `role_ids -> user_role_ids` shim;
- OCA causal commit;
- five-service topology;
- exact database names and roles;
- exact Nginx routes;
- exact queue command;
- exact backup implementation;
- exact single-database routing.

Generalize only the causal abstractions:

- source pin != dependency-closure pin;
- product health != product activation;
- readiness protects shared initialization;
- current Coolify pull/build behavior can constrain local source builds;
- declared managed-file content != effective runtime file;
- interpolation is transport-layer specific;
- canonical public origin != internal proxy listener;
- intentional credential role splits need a topology map;
- platform primitive support must be proven end-to-end;
- native config validators should run before process start when available.

<!-- END PORTABLE RESOURCE: references/openspp-case-study.md -->

<!-- BEGIN PORTABLE RESOURCE: references/openspp-golden-sha256.txt -->
<!-- SOURCE SHA256: ab49be10046771c37aa2cadd64fd753d154af91923c14823a7cf1e1708b1dc66 -->
<!-- EMBEDDED SHA256: 8024ed69f4fb6d00152577fa524a21ccb5f2c170819f36ec865ab791f4d617bb -->

## Portable resource: `references/openspp-golden-sha256.txt`

````text
OpenSPP Golden / Regression Case #12
Fixture: assets/openspp-2026.08-v1.0.0-golden.yml
Source candidate: openspp-coolify-v1.0.0-rc8.yml
SHA-256: f00a8755fa2be8e8b1f50970978ae1b57c1877093c2a35108edf35a675d4587b
Classification: exact operator-accepted RC8 bytes
````

<!-- END PORTABLE RESOURCE: references/openspp-golden-sha256.txt -->

<!-- BEGIN PORTABLE RESOURCE: references/openspp-rc1-to-rc8-causal-ledger.md -->
<!-- SOURCE SHA256: cef41ef649d6aaf830fadb66377ffc86297a3b78131db770a67347c8863fba25 -->
<!-- EMBEDDED SHA256: 0dbaccd82bc37c12cff3db3220b158cd552345afd88133f2b51962a180dc1055 -->

## Portable resource: `references/openspp-rc1-to-rc8-causal-ledger.md`

### OpenSPP RC1 → RC8 Causal Regression Ledger

This ledger records the benchmark that produced OpenSPP Golden #12. It is a causal history, not a count of equivalent architecture failures.

#### Classification key

- **Platform deployment failure** — candidate does not reach application runtime because of Coolify/Compose deployment behavior.
- **Upstream dependency failure** — current upstream build resolves an incompatible transitive dependency.
- **Product activation failure** — framework/container can run but the claimed product profile is not activated.
- **Readiness failure** — health/start gates admit dependent processes too early.
- **Proxy/origin failure** — runtime works but browser/public-origin semantics are wrong.
- **Managed-file lifecycle failure** — declared Compose content differs from the effective file consumed at runtime.
- **Unsupported platform primitive** — standard Compose syntax is not supported through the effective Coolify implementation path.
- **Nested interpolation failure** — a token is transformed incorrectly across Compose/Coolify/shell/config-parser layers.
- **Actual application failure** — target application logic itself is demonstrated to fail independently of the above layers.

#### Ledger

| RC | Observed failure | Evidence stage | Actual root cause | Smallest correction | Architecture changed? | State changed? | Previous diagnosis status | Generalized lesson | OpenSPP-specific fact | Regression protection |
|---|---|---|---|---|---|---|---|---|---|---|
| RC1 | `pull access denied for openspp-coolify` before application startup | Coolify deployment log | Then-current Coolify service path pre-pulled a local-only `image:` before `up --build` | `pull_policy: never` on `odoo` and `queue-worker` only | No | No accepted app state; failure occurred before runtime | Correct once deployment log inspected | Verify effective pull/build sequence for local source builds; version-sensitive platform rule | local image name `openspp-coolify:2026.08` | local-build + pre-pull regression prompt/audit review |
| RC2 | Five containers could appear healthy, but SP-MIS install failed with `role_ids` XPath ParseError | Product bootstrap logs | OpenSPP source pinned, but Dockerfile fetched mutable OCA `19.0`; dependency contract had changed to `user_role_ids` (causal OCA commit `c7e2d1a66fee52c0f942ce9b3aebe51760c59a18`, 18 May 2026) | two narrow compatibility overlays at the broken XML boundary | No topology change | Partial/failed module state existed | Initial “deployment PASS” was correct only for platform stage; “runtime/product PASS” would have been wrong | source pin != build-graph pin; dependency drift; health != activation | `spp_user_roles` and `spp_area` two affected XPaths | dependency-closure map + activation-aware readiness test |
| RC2 secondary | queue-worker hit PostgreSQL `SerializationFailure` during module state mutation | Worker/DB logs | Worker loaded shared registry while authoritative initialization was still in progress; secondary to product-init failure | gate worker on activation-aware Odoo readiness | No | Same partially initialized DB | Correctly reclassified as secondary concurrency symptom | readiness protects shared initialization; error chronology matters | Odoo registry/module tables shared by web + queue worker | root-cause chronology prompt + worker-race prompt |
| RC3 | product initialization proceeded, but public access still followed login/database-selector behavior that did not yield stable entry | Browser/access logs | Single-database session establishment and gateway redirect semantics were not explicit enough | explicit `?db=openspp` entry and selector handling in next RC | No | Persistent state retained for recovery test | Dependency/readiness diagnosis was correct; proxy diagnosis incomplete | product activation and routing are separate gates | Odoo `ensure_db()` / single DB `openspp` | single-database public-surface check |
| RC4 | app could load via direct database-qualified login, but later redirects could contain public host with internal `:8080` | Browser URL/runtime observation | internal semantic gateway port leaked into canonical origin information sent to Odoo | normalize Host/X-Forwarded-Host/scheme/port to canonical external origin | No | No destructive state change | Correct that application was alive; incomplete origin normalization remained | canonical public origin != internal proxy target/listener | HTTPS public origin behind Nginx `:8080` and Odoo `:8069/:8072` | proxy-port leakage prompt |
| RC5 | behavior still resembled old gateway content despite Compose edits | runtime behavior vs declared Compose | Coolify managed bind-content had its own persisted identity/lifecycle; changing source text did not prove effective mounted file changed | investigate file provenance; use explicit invalidation/new identity in later RC | No | No application data reset | Prior proxy correction was semantically correct but not necessarily the file actually loaded | declared file != managed resource != host file != mounted file != effective config | old Nginx managed file persisted across RC edits | Managed File Provenance Ledger |
| RC6 | deployment rejected top-level `configs.content`; `openspp` did not start | Coolify deployment log | effective Coolify Service implementation did not support that Compose form in this path; error said `file` is sole supported option | return to managed bind file with new identity | No intended app topology change | No accepted data mutation | Correctly identified as platform primitive support failure | spec support != parser/persistence/deployment support | RC6 `openspp_nginx_config` | platform-primitive end-to-end support prompt |
| RC7 | Nginx crash-loop; `invalid parameter "$$remote_addr..."`; Coolify stopped after restart limit | Nginx runtime log + Coolify restart-limit notification | managed file content was written literally; Compose `$$` escaping was applied at the wrong transport layer | single-dollar Nginx variables + explicit rendering boundary | No | Application DB/filestore remained intact | Correctly identified from first Nginx parser error | dollar escaping is transport-specific; parser error beats later crash-loop symptom | Nginx native variables in Coolify managed file | managed-file dollar test + parser validator |
| RC8 | no benchmark failure; operator reports application works, persistence works, all requested tests pass | operator-confirmed runtime acceptance | accepted correction combined managed-file identity, single `$`, whitelisted envsubst, native config validation | freeze exact bytes | No new topology vs selected profile | Persistent state validated by operator | Final | whitelist envsubst; native config validation before daemon; freeze accepted bytes | RC8 exact fixture SHA `f00a875...` | Golden #12 exact SHA + invariants |

#### Causal timeline rule

When logs contain several errors, reconstruct:

```text
first proven causal fault
-> downstream effects
-> retries
-> secondary concurrency symptoms
-> platform crash/restart consequences
```

Do not assume the last `ERROR` is the root cause.

#### Architecture assessment across RC1–RC8

The five-service selected production architecture remained conceptually stable across the benchmark. Most iterations corrected **deployment/runtime-layer contracts** rather than changing product topology.

Therefore the benchmark must not be summarized as:

```text
8 failed architectures -> final architecture
```

The useful summary is:

```text
one selected production architecture
+
successive causal fixes across:
platform pull/build
dependency closure
product activation/readiness
public-origin routing
managed-file provenance
effective platform primitive support
transport-layer interpolation
```

<!-- END PORTABLE RESOURCE: references/openspp-rc1-to-rc8-causal-ledger.md -->

<!-- BEGIN PORTABLE RESOURCE: references/openspp-skill-learning-delta.md -->
<!-- SOURCE SHA256: 739029ebe3506396407f53ccc855f4619459fbdf430219bdff258419fa896c16 -->
<!-- EMBEDDED SHA256: f35ab7e3996d613fbceb8c7ee6cb373859e1eab8f30d636bd44ad894ab0f7cb1 -->

## Portable resource: `references/openspp-skill-learning-delta.md`

### OpenSPP Skill Learning Delta — RC9 → RC10

Baseline: `coolify-architect v1.0.0-rc9`  
Target: `coolify-architect v1.0.0-rc10`

The objective is to preserve RC9's eleven-Golden reasoning while adding only causal abstractions demonstrated by the OpenSPP benchmark.

| Lesson | Benchmark evidence | Generalized rule | Scope | Anti-generalization guard | Affected files | Regression test | Bias risk introduced |
|---|---|---|---|---|---|---|---|
| Source pin != build-graph pin | immutable OpenSPP commit still fetched mutable OCA branch and broke later | classify dependency closure separately from source pin | source builds | do not call all remote builds unsafe; map actual resolution points | architecture-discovery, source-priority, production-readiness, anti-patterns | partially pinned build graph | over-pinning bias |
| Dependency contract drift | `role_ids -> user_role_ids` broke released XML | prove drift; prefer smallest compatibility boundary; scope shim to version/cause | mutable transitive deps | do not teach “patch XML” | source-priority, anti-patterns, case study | version-scoped compatibility patch | workaround permanence |
| Health != activation | `/web/health` passed during failed SP-MIS install | framework/process health and product activation are separate gates | modular/plugin/product platforms | do not require Odoo module SQL elsewhere | SKILL, architecture-discovery, production-readiness | health vs activation/product bundle activation | over-deep health checks |
| Readiness protects shared init | queue worker raced mutable registry during install | workers should not race authoritative initialization unless upstream supports it | shared mutable schema/registry | do not serialize unrelated independent workers | SKILL, production-readiness | worker race | excessive startup gating |
| Coolify pre-pull/local build | RC1 failed before runtime | verify current effective pull/build order; `pull_policy: never` may be needed only where proven | current Coolify/source-built services | not a Docker Compose universal rule | coolify-rules, source-priority, audit_compose | local build pre-pull | platform-version fossilization |
| Effective file provenance | RC3–RC5 runtime behavior did not prove edited content was effective | declared content, Coolify resource, host file, mount, consumer-loaded file, behavior are distinct | Coolify managed files | do not create new paths on every edit | architecture-discovery, coolify-rules, troubleshooting | managed-file source equality | path churn |
| Managed-file identity | new path/target forced invalidation after stale behavior | treat persistent identity as possible cause after verification | observed Coolify managed-file lifecycle | first inspect effective file; new path is explicit invalidation, not default | coolify-rules, case study | provenance ledger assertions | unnecessary resource duplication |
| Spec != effective platform support | RC6 `configs.content` rejected | verify spec, parser, persistence/model, and deployment layers | critical Compose primitives on Coolify | REVIEW when context required; no global ban on `configs` | coolify-rules, source-priority, audit_compose | platform primitive documentation | false negatives on future Coolify |
| Dollar escaping is transport-specific | RC7 literal `$$remote_addr` crashed Nginx | build an interpolation/serialization layer map | nested/interpreted config | Overleaf `$$set` remains valid in its different path | SKILL, architecture-discovery, anti-patterns, validate_embedded | universal dollar escaping | contradictory blanket escaping |
| Whitelisted envsubst | RC8 preserved Nginx `$vars` and rendered only intended env vars | whitelist substitutions where target language also owns `$variables` | envsubst-like preprocessors | do not require envsubst; use only when needed | coolify-rules, validate_embedded | envsubst safety | over-engineered render steps |
| Native config validation | RC8 `nginx -t` before daemon | run reliable native parser/validator before long-lived process when available | generated/rendered config | do not generalize `nginx -t` itself | production-readiness, coolify-rules | managed config parser fixture | startup latency/complexity |
| Canonical public origin | internal `:8080` leaked browser-visible | internal listener/target != canonical origin | reverse proxy chains | use actual external scheme/host/port, not hard-coded 443 universally | SKILL, coolify-rules | proxy port leakage | over-normalizing nonstandard public ports |
| Single-database public surface | selector/manager contradicted selected single-DB profile | align public management surfaces with selected production mode | apps with an upstream single-DB mode | do not copy Odoo routes | production-readiness, anti-patterns | single-database reasoning prompt | security behavior copied without upstream support |
| Intentional DB-role split | admin/bootstrap and app role used different generated passwords | audit credential topology by role, not password equality alone | DBs with multiple roles | same logical account + different passwords remains ERROR | architecture-discovery, audit_compose | different DB passwords | weakening real mismatch detection |
| Security handover boundary | hardening guidance includes org/infrastructure controls beyond template | classify control ownership instead of inventing infrastructure | production-readiness | do not fabricate KMS/TDE/off-site systems | production-readiness | reasoning/documentation check | under- or over-scoping “production-ready” |
| Error chronology | serialization error followed earlier ParseError | causal timeline outranks last error line | troubleshooting | still investigate later independent errors after root cause | SKILL, evaluation-prompts | last ERROR root-cause test | anchoring on earliest irrelevant warning |

#### RC9 knowledge explicitly preserved

RC10 retains without weakening:

- Golden #1–#11 exact fixtures;
- Baserow multi-profile reasoning;
- NetBox knowledge-accumulation regression rule;
- Overleaf nested-language interpolation lesson;
- Frappe/ERPNext product activation distinctions;
- RC9 Coolify Service configuration / `extraFields()` advisory layer;
- current upstream evidence as architecture source of truth.

#### Golden decision

OpenSPP qualifies as Golden #12 because the final operator explicitly accepted RC8 after reporting the full requested test suite complete and data persistence correct.

The promotion does **not** convert every RC8 mechanism into a generic recommendation. Exact OpenSPP topology and fixes remain local; only the causal principles above are generalized.

#### Required RC9 -> RC10 differential audit

The following files were compared explicitly. A file is left unchanged when the new lesson is already covered elsewhere or changing it would create needless coupling.

| File | RC10 status | Reason |
|---|---|---|
| `SKILL.md` | MODIFIED | dependency closure, activation-aware readiness, managed-file/effective-config, interpolation-layer, source-build/pull and canonical-origin rules; OpenSPP #12 registration |
| `references/architecture-discovery.md` | MODIFIED | Build Reproducibility / Dependency Closure Map, Managed File Provenance Ledger, Interpolation / Serialization Layer Map, Credential Topology Map |
| `references/anti-patterns.md` | MODIFIED | OpenSPP-derived runtime-layer anti-patterns, including causal chronology and single-database public-surface guard |
| `references/coolify-rules.md` | MODIFIED | version-sensitive local-build pull order, layered primitive support, managed-file provenance/identity, transport-specific `$`, whitelisted envsubst, canonical origin |
| `references/cross-benchmark-lessons.md` | MODIFIED | twelve-Golden corpus and cross-case reinforcement without topology transfer |
| `references/production-readiness.md` | MODIFIED | activation-aware readiness, shared initialization, dependency closure, single-database surface, security ownership boundary |
| `references/source-priority.md` | MODIFIED | source pin vs dependency closure and dependency-contract drift procedure |
| `references/evaluation-prompts.md` | MODIFIED | executable/reasoning A-M OpenSPP regression prompt set |
| `references/golden-regression-cases.md` | MODIFIED | OpenSPP exact Golden #12 registration and anti-contamination guard |
| `scripts/validate_golden_cases.py` | MODIFIED | exact OpenSPP RC8 SHA/invariants and 12-case count |
| `scripts/audit_compose.py` | MODIFIED | conservative review checks for local build pull sequencing, `configs.content`, managed Nginx `$$`, unrestricted envsubst; intentional DB-role split remains review unless same logical role conflicts |
| `scripts/validate_embedded.py` | MODIFIED | high-confidence managed-Nginx literal `$$native_variable` regression check |
| `scripts/test_embedded_validation.py` | MODIFIED | proves Overleaf nested-command `$$` and OpenSPP managed-file single `$` are transport-specific, not contradictory |
| `scripts/test_regression_learning.py` | UNCHANGED | existing NetBox knowledge-accumulation regression remains valid and must not be diluted by OpenSPP |
| `scripts/test_magic_variables.py` | UNCHANGED | existing generator/credential identity semantics remain correct; DB-role topology is tested separately to avoid weakening shared-account mismatch checks |
| `scripts/test_profile_selection.py` | UNCHANGED | Baserow profile-selection logic remains orthogonal; OpenSPP selected one production profile and must not bias profile choice |
| `scripts/test_openspp_learning.py` | ADDED | generalized OpenSPP executable regressions and exact Golden identity |

##### Bias-control conclusion

RC10 adds evidence layers rather than target architecture defaults. None of the new rules authorizes adding Nginx, PostGIS, queue workers, backup sidecars, compatibility shims, DB-role splits, single-database redirects, managed files, or source builds without current-target provenance.

<!-- END PORTABLE RESOURCE: references/openspp-skill-learning-delta.md -->

<!-- BEGIN PORTABLE RESOURCE: references/overleaf-ce-case-study.md -->
<!-- SOURCE SHA256: 4e6f55903793b2dcad1e7b383b2affe20ce8ee78a680bad9284d6822b681e625 -->
<!-- EMBEDDED SHA256: 4245ccc079ab410517c52ffd0d14d60535dad348a51144fe4c41052160f3d465 -->

## Portable resource: `references/overleaf-ce-case-study.md`

### Overleaf Community Edition on Coolify — Golden / Regression Case #9

Overleaf Community Edition 6.2.2 is the ninth runtime Golden case for `coolify-architect`. It adds a collaborative stateful application whose official deployment Toolkit is itself an orchestration layer around Docker Compose. The accepted regression fixture is the exact operator-accepted `overleaf-coolify-v1.0.0-rc4.yml` bytes, preserved as `assets/overleaf-ce-6.2.2-v1.0.0-golden.yml` with SHA-256 `b8cb9425523d38088f7069c70d762ab24fbf07232d736f607572e5b191621585`.

The Golden remains an oracle, not a skeleton. Future Overleaf versions and unrelated collaborative applications must redo current upstream discovery, edition/profile selection, artifact publication checks, runtime acceptance and recovery analysis.

#### Evidence scope

The case is grounded in:

- upstream-first RC1 architecture/acceptance/validation artifacts;
- the exact RC1→RC4 candidate files from the benchmark;
- direct runtime logs from the failed and successful Coolify deployments;
- direct probes of the public route and internal application ports during troubleshooting;
- operator confirmation that the requested application, persistence, realtime, compilation, restart/redeploy, backup/restore and admin-login tests passed for RC4.

Where raw command output for an acceptance stage is not preserved in this package, the stage is recorded as **operator-confirmed runtime result**, not reconstructed evidence.

#### What RC1 got right

RC1 started from the current Community Edition deployment model rather than previous Golden topologies. It correctly preserved the essential CE runtime as:

```text
Overleaf CE application image
+ MongoDB
+ Redis
+ persistent Overleaf filesystem
```

It also correctly avoided Server Pro-only infrastructure:

- no Docker socket;
- no sibling/sandboxed compile containers;
- no commercial SSO/service assumptions;
- no extra TLS/ACME service;
- no speculative migration/admin sidecar;
- no public MongoDB/Redis host ports.

RC1 already preserved important state and lifecycle semantics:

- `sharelatex/sharelatex:6.2.2` pinned by digest;
- `linux/amd64` platform boundary;
- MongoDB 8.0 single-member replica set named `overleaf`;
- upstream-style replica-set initialization through `/docker-entrypoint-initdb.d/` rather than an invented `mongo-init` service;
- Redis 7.4 with AOF and persistent `/data`;
- Overleaf `/var/lib/overleaf` persistence;
- Coolify-terminated TLS with Overleaf behind-proxy/secure-cookie settings;
- a real local application health endpoint;
- `SERVICE_REALBASE64_32_OVERLEAFINVITE` for an upstream secret whose semantics are equivalent to 32 random bytes Base64 encoded.

Static validation passed, but runtime exposed a platform/application interaction that static syntax checks had not modeled.

#### RC1 runtime failure — service name became application configuration

RC1 named the application service `sharelatex`. Coolify automatically derived service metadata environment variables from that Compose service name:

```text
SERVICE_URL_SHARELATEX
SERVICE_NAME_SHARELATEX
SERVICE_FQDN_SHARELATEX
```

Overleaf 5+ runs `/etc/my_init.d/000_check_for_old_env_vars_5.sh` before normal startup. That guard enumerates environment-variable names containing `SHARELATEX` and refuses startup because ShareLaTeX-branded environment variables are no longer accepted.

The direct runtime evidence was:

```text
Your configuration still uses 3 ShareLaTeX environment variables:
- SERVICE_URL_SHARELATEX
- SERVICE_NAME_SHARELATEX
- SERVICE_FQDN_SHARELATEX
...
Refusing to startup, exiting in 10s.
```

The container therefore never reached its normal internal web services. Direct probes to `127.0.0.1:80` and `127.0.0.1:3000` were refused, while Docker health remained in `starting`. Public Traefik symptoms such as a default certificate/503 were downstream consequences, not the earliest application failure.

##### General lesson

A Compose service name can be part of the effective application configuration when the platform derives environment variables, labels, DNS names or metadata from it.

```text
Compose service name
-> platform-generated environment
-> application compatibility guard/config parser
-> runtime behavior
```

A Magic Variable can be syntactically valid for Coolify and still be semantically toxic to the selected application version. This is separate from Magic Variable grammar correctness.

#### RC2 — rename the platform-facing service identity

RC2 made the smallest correction justified by runtime:

```text
service sharelatex -> overleaf
SERVICE_URL_SHARELATEX -> SERVICE_URL_OVERLEAF
OVERLEAF_SITE_URL=${SERVICE_URL_OVERLEAF}
```

RC2 also added the missing current Redis port variable and removed two unnecessary CE variables from RC1:

```text
+ OVERLEAF_REDIS_PORT=6379
- V1_HISTORY_URL
- GIT_BRIDGE_ENABLED
```

The Docker image and historical Mongo database name remained `sharelatex`; the failure concerned environment-variable **names**, not arbitrary string values or the image repository name.

Runtime then progressed through migrations, CE checks, document-version recovery and `runit` startup. `/launchpad` worked and the browser-first first-admin flow was validated.

This correction adds an anti-contamination rule of a different kind: application rebranding/compatibility guards can collide with platform-generated metadata even when the platform syntax is correct.

#### RC3 — One-Click admin gap and embedded-language interpolation failure

The upstream browser-first `/launchpad` flow was safe and functional, but the benchmark then intentionally tested a stronger One-Click contract: operator-supplied admin identity plus Coolify-generated password.

The desired separation was:

```text
identity:   OVERLEAF_ADMIN_EMAIL         -> operator-provided
credential: SERVICE_PASSWORD_64_OVERLEAFADMIN -> Coolify-generated
role:       admin                         -> application state
issuer:     email/operator, password/Coolify
```

The official `create-user` helper creates an account and returns a password-activation URL; it does not accept a direct password argument. RC3 therefore used Overleaf's own application primitives (`AuthenticationManager`, `UserRegistrationHandler`, `User`) so password validation/hashing and admin state remain application-owned rather than direct ad-hoc Mongo credential mutation.

The one-shot contract was designed to fail closed:

```text
no admin + requested email absent          -> create requested admin
same admin email already exists            -> preserve existing password; success
different admin already exists             -> refuse
same email exists as non-admin             -> refuse
unknown/invalid state                       -> refuse
```

It also verified a post-condition after mutation: the resulting user exists, has `isAdmin == true`, and has a stored `hashedPassword`.

RC3 nevertheless failed before account creation with:

```text
SyntaxError: Unexpected token ':'
```

The embedded JavaScript contained Mongo/Mongoose update syntax:

```javascript
$set: {
```

Inside a Compose `command:` string, the unescaped `$set` crossed the Compose interpolation layer before Node saw it. The effective JavaScript became malformed (`: {`).

##### General lesson

Syntax must be validated at every active parser layer:

```text
YAML
-> Compose interpolation
-> shell/heredoc
-> JavaScript
-> nested database/update syntax
```

A valid source fragment for the inner language may still be invalid after Compose interpolation. This confirms and broadens the existing nested-language/dollar-context rule learned in earlier benchmarks.

#### RC4 — accepted candidate

RC4 changed only the proven failing token:

```text
$set  ->  $$set   (Compose source)
              ->  $set   (runtime JavaScript)
```

The admin bootstrap then completed and logged successful administrator creation. The operator confirmed login using:

```text
OVERLEAF_ADMIN_EMAIL
SERVICE_PASSWORD_64_OVERLEAFADMIN
```

The application itself completed migrations and normal `runit` startup. The operator subsequently confirmed the requested runtime acceptance suite succeeded and reported the deployment fully functional.

The immutable Golden is therefore the exact RC4 file, including its four Compose services:

```text
long-running:
  overleaf
  mongo
  redis

one-shot:
  adminbootstrap
```

No RC1/RC2/RC3 content is reconstructed into the Golden.

#### Toolkit vs runtime

The Overleaf Toolkit is both:

```text
host orchestration / lifecycle management
+
evidence describing the required runtime
```

Host-side commands such as start/stop/upgrade/doctor/config generation are not automatically runtime services. The accepted Coolify topology preserves the application responsibilities and lets Coolify own the platform responsibilities it already provides.

##### New general rule

> An upstream deployment toolkit is evidence about the runtime, not automatically part of the runtime.

When upstream ships a Toolkit/installer/wrapper, produce a **Host Orchestration vs Runtime Responsibility Map** before translating it to Compose.

#### Internal process topology vs Compose topology

The Overleaf image supervises multiple internal application processes for web/editor, realtime/collaboration, document updates, history, file handling, compilation and related functions.

Those independently named internal processes did **not** justify splitting the accepted deployment into matching Compose services.

##### New general rule

> Internal application process topology and Compose service topology are different abstraction layers.

Do not explode an upstream monolithic/supervised image merely because its internal services have names.

#### Community Edition boundary

The benchmark targets **Overleaf Community Edition**, not Server Pro. Server Pro capabilities were not imported simply because they exist in the same product family.

The accepted CE fixture specifically protects the absence of:

- Docker socket;
- sibling/sandboxed compile runners;
- Server Pro-only compile orchestration;
- commercial SSO/features and Pro-specific runtime configuration.

##### New general rule

> Features from another edition/profile of the same product are not upstream requirements for the selected edition/profile.

Edition/profile selection belongs in architecture discovery before dependency enumeration.

#### Compilation execution model

The validated CE model compiles LaTeX inside the Overleaf runtime image. A separate Docker runner or privileged Docker socket would have imported a different execution model.

##### General rule

> Validate the actual execution model of compute/compile jobs before adding privileged runners or Docker socket access.

Product acceptance for Overleaf required a real project/edit/compile/PDF workflow. Homepage health alone was not sufficient.

#### Realtime collaboration

The collaborative surface shares the public Overleaf application path; the upstream runtime does not require an independently exposed Compose WebSocket service in this accepted CE profile.

The benchmark preserves the acceptance principle:

```text
HTTP 200 != collaborative realtime proof
```

For collaborative products, use two sessions against the same document and verify live synchronized edits when applicable. Do not create a separate WebSocket service merely because realtime exists internally.

#### Redis durability classification

Redis was not treated as automatically disposable. In this Overleaf profile it participates in sessions, coordination/pub-sub and in-flight document-update behavior. RC4 preserves Redis persistence with AOF.

This does **not** make Redis globally authoritative. The correct classification vocabulary is role-based:

```text
authoritative
in-flight / durability-sensitive
queue
session
cache
reconstructable
```

##### New general rule

> Redis is not automatically cache, disposable, or authoritative. Classify its durability from the upstream application's actual role.

#### Coherent multi-store recovery

Overleaf state is distributed across:

```text
MongoDB
+ Redis durability-sensitive/in-flight state
+ /var/lib/overleaf filesystem
+ stable configuration/secrets
```

MongoDB alone is therefore not the recovery set. The filesystem contains authoritative/user-visible state including `user_files` and history-related data, while some cache/compile/output subpaths may be reconstructable.

##### New general rules

> Persistent application state can form a coherence group across database, filesystem and in-flight stores; recovery must preserve the group, not merely the primary database.

> Never classify an entire application volume as authoritative or cache without inspecting its subpaths.

When the application's write model requires it, backup can require a quiesce/flush boundary before related stores are captured. The exact grace window remains application-specific.

#### MongoDB topology semantics

The accepted fixture runs one MongoDB container but preserves replica-set semantics:

```text
--replSet overleaf
```

with upstream-style initialization through the official Mongo image lifecycle at `/docker-entrypoint-initdb.d/`.

##### General rules

> Preserve database topology semantics required by the application even when only one database container exists.

> Prefer native database-image initialization lifecycle over an invented init service when upstream already supplies a safe initialization artifact.

A one-member replica set is not semantically equivalent to standalone MongoDB when the application expects replica-set behavior.

#### Operator identity vs generated secret

Overleaf provides a clean example of separating account identity from credential generation:

```text
admin email    -> operator-provided identity
admin password -> Coolify-generated secret
```

The template does not invent a fake email address merely because Coolify can generate usernames/passwords.

A useful identity ledger therefore distinguishes:

```text
identity
credential
role
issuer
persistence
rotation
```

##### General rule

> Automatic bootstrap must preserve existing account identity and must not turn redeploy into credential reset.

#### Post-condition verification

The bootstrap does not treat command exit alone as final proof. It queries resulting authoritative application state and verifies the created admin identity/role/password state.

##### General rule

> A bootstrap command exiting 0 is weaker evidence than verifying the resulting authoritative application state.

#### Magic Variable format semantics

RC4 uses:

```text
SERVICE_URL_OVERLEAF
SERVICE_REALBASE64_32_OVERLEAFINVITE
SERVICE_PASSWORD_64_OVERLEAFADMIN
```

`SERVICE_REALBASE64_32_OVERLEAFINVITE` must parse longest-family-first as:

```text
family     = REALBASE64_32
identifier = OVERLEAFINVITE
```

The upstream invite-token contract requires the semantics of 32 random bytes encoded as Base64, not merely a random string with a convenient length.

##### General rule

> Selecting a Magic Variable family is not only about entropy length; encoded format must match the upstream consumer's expected semantics.

##### Important RC1/RC4 distinction

`SERVICE_URL_SHARELATEX` is syntactically a valid Coolify URL Magic Variable and remains a useful parser-positive test. It is **not** valid for this accepted Overleaf 6.2.2 deployment because the resulting `SHARELATEX`-named environment is rejected by Overleaf's compatibility guard. The Golden therefore correctly uses `SERVICE_URL_OVERLEAF`.

#### Canonical URL and trusted proxy semantics

RC4 preserves:

```text
SERVICE_URL_OVERLEAF -> OVERLEAF_SITE_URL
OVERLEAF_BEHIND_PROXY=true
OVERLEAF_SECURE_COOKIE=true
```

Canonical browser identity, proxy target, Docker DNS and internal microservice URLs remain separate concepts.

The case also reinforces that “behind a proxy” and “trust arbitrary forwarded headers” are different settings. Discovery should ask:

- does the application trust `X-Forwarded-*`?
- which source proxies are trusted?
- can direct clients spoof those headers?

Do not generalize Overleaf's exact trusted-proxy CIDRs to unrelated applications.

#### Publication/config discrepancy

During triage, repository/Toolkit configuration and the externally verifiable published CE image version were not treated as automatically identical. The candidate used the demonstrable published artifact rather than inventing an unavailable tag.

##### New general rule

Repository configuration, release metadata and published deployment artifacts can temporarily disagree. When they do:

1. identify the discrepancy;
2. verify what is actually published;
3. do not invent a tag/artifact;
4. document the gap;
5. use the demonstrable artifact or wait.

#### Architecture support boundary

MongoDB and Redis supporting another CPU architecture does not make the whole application stack support that architecture. The accepted Overleaf image was constrained to `linux/amd64`.

##### New general rule

> Full-stack architecture support is the intersection of every required runtime image/component, not the union of dependency support.

#### Upgrade and rollback

The operational upgrade model is a coordinated state transition, not an image-tag edit:

```text
coherent backup
-> verify MongoDB compatibility/major path
-> verify Redis requirements
-> change Overleaf image
-> allow application-owned migrations
-> rerun product acceptance
```

An image downgrade is not a data rollback after backward-incompatible migrations. Recovery may require restoration of the coherent MongoDB + Redis + filesystem + stable-config snapshot.

#### Runtime acceptance recorded for Golden promotion

The operator confirmed the requested Overleaf benchmark acceptance succeeded after RC4. The intended gates covered:

- fresh deployment and local service readiness;
- first admin creation/login;
- project creation;
- real LaTeX compilation producing a PDF;
- edit and uploaded-file persistence;
- realtime collaboration;
- normal restart;
- Coolify redeploy with preserved volumes;
- backup of the complete identified state set;
- isolated restore and functional verification;
- documented upgrade considerations.

The package does not fabricate command transcripts that were not preserved. Golden status records the exact accepted RC4 behavior and the operator-confirmed result class.

#### Classification

##### New generalizable

- Toolkit/host-orchestration vs runtime responsibility;
- internal process topology vs Compose topology;
- edition/profile boundary;
- coherent multi-store backup/recovery groups;
- Redis role-based durability;
- operator identity vs generated credential;
- published artifact/configuration discrepancy;
- full-stack architecture intersection;
- Magic Variable encoded-format matching.

##### Confirmations / strengthening

- upstream-first complexity provenance;
- Magic Variable longest-family parsing and exact identity reuse;
- idempotent/fail-closed one-shot lifecycle;
- post-condition verification;
- persistence wider than the primary database;
- real product-workflow acceptance;
- URL/proxy/Docker-DNS separation;
- private internal services;
- native database init lifecycle;
- nested Compose-dollar/language validation;
- no speculative infrastructure.

##### Overleaf-specific

- Community Edition 6.2.2 and exact image digest;
- `sharelatex/sharelatex` image namespace;
- service name `overleaf` to avoid generated `SHARELATEX` environment names;
- MongoDB 8.0.29 single-member replica set `overleaf` and exact init artifact;
- Redis 7.4.11 with AOF;
- `/var/lib/overleaf` filesystem layout;
- CE LaTeX compile path;
- exact admin bootstrap implementation;
- exact Magic Variable identities and proxy values;
- `linux/amd64` boundary for the accepted artifact.

##### Anti-patterns

- copying the Toolkit wholesale into services;
- decomposing internal microservices into Compose services by name;
- importing Server Pro requirements into CE;
- adding Docker socket/compile runner without CE evidence;
- Mongo-only recovery;
- treating Redis as disposable cache by default;
- creating a database init sidecar when native init already owns the lifecycle;
- generating fake operator identity values;
- resetting existing admin credentials on redeploy;
- assuming dependency multi-arch support proves full-stack multi-arch support;
- allowing platform-generated service metadata to collide silently with application compatibility guards.

<!-- END PORTABLE RESOURCE: references/overleaf-ce-case-study.md -->

<!-- BEGIN PORTABLE RESOURCE: references/production-readiness.md -->
<!-- SOURCE SHA256: c78b52926d81a0ef05f74ebc47f341595b3f0c43fafabd30379ed2dfa8bd8ea7 -->
<!-- EMBEDDED SHA256: ecede9369dc430a43af981013adc02bdabe1350e993950e617db1e019306505f -->

## Portable resource: `references/production-readiness.md`

### Production-readiness checklist

A Compose that starts is not automatically production-ready.

#### Evidence ladder — do not collapse stages

Use these states independently:

```text
YAML valid
  -> Compose syntax/render valid
  -> containers started
  -> healthchecks green
  -> application publicly accessible
  -> representative business workflow validated
  -> persistence across restart/redeploy validated
  -> backup actually created
  -> isolated restore actually validated
  -> production-readiness evidence for the stated target
```

Each arrow requires new evidence. A later label may not be inferred from an earlier stage. In particular, `docker compose config`, `Running`, and `healthy` are not synonyms for application correctness or production readiness.

#### Images and release control

- [ ] Application images are pinned to an intentional release tag or digest.
- [ ] Database/cache image versions are compatible with upstream requirements.
- [ ] Release notes/migrations were reviewed.
- [ ] `latest` is not used silently for critical production components.

#### Data and persistence

- [ ] Every authoritative data path/database is persistent.
- [ ] User uploads/media are persistent.
- [ ] Cache-only data is identified as disposable.
- [ ] Queue/session/Redis state is classified correctly rather than assumed disposable.
- [ ] Multiple instances of the same engine (Redis/Valkey, SQL, object stores) are classified by role before any merge; queue/in-flight state is not collapsed into cache state merely to reduce service count.
- [ ] Persistent state is classified as authoritative, reconstructable, queue durability, cache/disposable, operational/log, or backup artifact.
- [ ] Normal redeploy does not require deleting volumes.
- [ ] Backup targets are documented.
- [ ] Database-aware backup methods are preferred for live databases.
- [ ] Restore steps are documented and, for real production handover, tested when feasible.

#### Secret origin and credential lifecycle

Before deciding how a secret is generated, classify its issuer/origin and lifecycle:

```text
platform-generated deployment secret
application-issued credential
operator-provided secret
external-provider-issued credential
```

A secret-shaped variable is not necessarily generatable. A random `SERVICE_PASSWORD_*` cannot create a valid external provider credential, and an application-issued API key should not be replaced automatically by a deployment secret.

#### Secrets

- [ ] No plaintext production secret is committed.
- [ ] Required generated secrets are non-empty.
- [ ] Secret formats meet upstream constraints.
- [ ] Generated secret values survive the actual Coolify `.env` / Compose / shell / nested-language transport path without unintended interpolation; symbol-bearing secrets are evaluated by transport context, not banned globally.
- [ ] Shared credentials use the exact same generated variable across consumers.
- [ ] Magic Variable grammar/type/identifier has been parsed and reviewed; critical generated values are visibly non-empty before bootstrap.
- [ ] Persistent Magic Variable identities are not renamed casually after bootstrap.
- [ ] A changed generated value is not mistaken for an application credential rotation.
- [ ] Persistent secrets are not regenerated during refactors/upgrades.
- [ ] Any exposed secret is rotated deliberately.

#### Network exposure

- [ ] Databases, Redis, MongoDB, brokers, and internal admin services are not unintentionally public.
- [ ] Public endpoints use Coolify-native routing where possible.
- [ ] Public domains map to the correct internal ports.
- [ ] Local HTTP health probes preserve/validate the intended hostname (`localhost`, `127.0.0.1`, service DNS) when Host-header or trusted-host validation can distinguish them.
- [ ] Custom networks, if any, have a documented need.
- [ ] No unnecessary Docker socket mount or privileged mode exists.

#### TLS and proxy semantics

- [ ] Public HTTPS terminates where intended.
- [ ] `Host` is preserved where upstream requires it.
- [ ] `X-Forwarded-Proto`, `X-Forwarded-For`, and related headers are correct.
- [ ] websocket/SSE/streaming routes work when applicable, including Host/Origin/auth/path/namespace semantics and a real realtime behavior.
- [ ] body size/timeouts fit real workloads.

#### Readiness layers and external dependencies

When external providers exist, separate:

```text
Layer 1 — infrastructure readiness
Layer 2 — application/API readiness
Layer 3 — authentication readiness
Layer 4 — external provider integration readiness
Layer 5 — end-to-end product workflow readiness
```

A periodic local healthcheck should normally avoid paid/external-provider calls, Internet dependency, quota consumption and unnecessary third-party data transfer. Provider integration belongs in separate acceptance unless the application's actual local readiness contract explicitly requires the external service.

Classify provider rejection/quota/outage separately from local database/API/dashboard failure before redesigning infrastructure.

#### Health and lifecycle

- [ ] Core stateful services have meaningful health checks.
- [ ] App readiness does not rely only on process existence.
- [ ] For modular/plugin/product platforms, framework readiness is separated from authoritative selected-product activation.
- [ ] Health-check commands exist inside the image.
- [ ] Startup scripts are syntactically validated.
- [ ] Long migrations have an adequate `start_period`.
- [ ] Service lifecycle map distinguishes daemon, one-shot init, one-shot migration, worker, scheduler and dependency roles.
- [ ] Expected one-shot `Exited 0` is accepted as successful completion instead of misclassified as daemon failure.
- [ ] Restart policy is appropriate.
- [ ] Graceful stop periods cover databases/workers where needed.

#### Migration ownership and restore ordering

- [ ] Migration ownership is documented: application startup, one-shot service, DB init, scheduled/operator task, or other upstream primitive.
- [ ] A migration service is not introduced merely because another Golden uses one.
- [ ] Migration failure semantics are fail-closed where upstream requires them.
- [ ] Restore ordering accounts for automatic startup migrations so an app does not pre-create conflicting empty state before restore.
- [ ] PostgreSQL extension package availability, database activation and actual application use are verified separately when relevant.
- [ ] PostgreSQL extension package/image version is not assumed to equal database `extversion` after upgrades.

#### Initialization

- [ ] Fresh install works.
- [ ] Normal redeploy works.
- [ ] Existing users/databases/indexes/extensions are handled idempotently.
- [ ] Bootstrap does not overwrite existing production accounts.
- [ ] Migrations preserve identifiers/ownership.
- [ ] Official upstream entrypoints remain responsible for upstream behavior where possible.
- [ ] Repository config bind mounts were compared with Dockerfile/image contents; configuration already baked into the image is not redundantly reconstructed, and any managed override is the smallest justified delta.

#### Product activation / sibling-product readiness

For platforms that host activatable apps/plugins/modules, production-readiness evidence must distinguish:

```text
image capability
runtime/platform
instance/site/tenant
installed/enabled product
```

A platform health endpoint is not product acceptance. Verify the claimed product's authoritative activation state and a representative product workflow.

If the candidate is a sibling of an existing Golden, require a Sibling Product Delta covering inherited/revalidated/changed platform state, product bootstrap, migration, acceptance and recovery.

Do not silently convert an existing persistent instance into another product profile. Unknown activation state must fail closed before mutation.

When multiple long-lived processes share mutable registry/schema/module state, dependent workers/schedulers/gateways must not race authoritative initialization unless upstream explicitly supports concurrent initialization. Gate on the smallest authoritative activation/readiness state, not merely an HTTP listener.

Fresh activation and later migration are different lifecycle phases. Verify durable activation with a post-condition when the target exposes one.

##### Single-database / single-instance public management surface

When the selected upstream production profile intentionally constrains the application to one database/tenant/instance and upstream supports disabling public selection/management surfaces, verify that the public gateway does not accidentally re-expose those surfaces. Align public routing with the selected deployment model and preserve an operator-safe administration path where upstream requires one.

Do not copy Odoo/OpenSPP-specific routes, database names, or redirect patterns to other applications. Generalize only the boundary: **a production single-database mode should not be contradicted by an unintended public database selector/manager surface**.

Do not fabricate company, country, currency, tax, accounting, warehouse or comparable organization-specific truth merely to bypass product onboarding.

#### Application acceptance

- [ ] Every public hostname has expected DNS/TLS behavior.
- [ ] Login works.
- [ ] Representative create/read/update flow works.
- [ ] Workers execute a real application task when workers are part of the selected architecture: enqueue -> consume -> resulting state proved.
- [ ] Native schedulers execute real scheduled work when present; a running scheduler PID alone is insufficient.
- [ ] Realtime/WebSocket/SSE paths are functionally exercised when present; a main-page HTTP 200 alone is insufficient.
- [ ] A cross-service feature works when the architecture has a meaningful cross-service path.
- [ ] Anonymous/public flow works when expected.
- [ ] Data survives redeploy.
- [ ] Email is either tested or explicitly documented as not configured.

#### Build/source reproducibility

For source builds, report separately: source revision pin, Dockerfile provenance, base-image pinning, dependency locks/ranges, transitive reproducibility and runtime architecture actually tested. An immutable Git commit is not automatically a hermetic build.

Build a dependency-closure map for dependencies resolved by Git branch/tag, package repository, curl/download, package manager, plugin manager or runtime bootstrap. Classify the build as HERMETIC / PARTIALLY PINNED / FLOATING TRANSITIVE DEPENDENCIES / UNKNOWN rather than collapsing all source pins into “reproducible”.

A runtime-tested remote Git build can be a valid Coolify strategy when current immutable server images are unavailable/inappropriate, but that observation remains target/Coolify-version scoped.


#### Security hardening ownership boundary

A deployable One-Click security baseline is not the same thing as an organization's complete production security program.

Classify each requested control as one of:

- template-safe default;
- operator-supplied secret/config;
- infrastructure/provider responsibility;
- organizational policy;
- post-deployment hardening;
- optional high-security profile.

Do not fabricate KMS/master-key services, TDE, off-site backup infrastructure, monitoring stacks, audit-retention policy, RPO/RTO/PITR or scheduled restore programs without target/operator evidence. Surface those responsibilities clearly instead.

#### Operations

- [ ] Logging/retention is considered.
- [ ] Disk growth risk is known.
- [ ] Resource tuning variables have sane defaults but are not presented as universal sizing.
- [ ] Upgrade procedure exists.
- [ ] Rollback path exists.
- [ ] Current known-good Compose revision is recorded outside runtime scripts.
- [ ] Host kernel/ulimit warnings emitted by stateful dependencies are reviewed and classified (for example Redis memory-overcommit or search-engine file-descriptor guidance).

#### Production-ready wording

Use precise status language:

- **YAML validated** — the YAML parser accepts the file;
- **Compose validated** — Compose schema/render/interpolation validation passes;
- **deployment tested** — containers actually start in the target environment;
- **health validated** — the intended health/readiness probes actually pass;
- **application reachable** — public routes/DNS/TLS return the intended application;
- **acceptance tested** — representative application-level workflows pass;
- **persistence validated** — authoritative state survives restart/redeploy as intended;
- **backup validated** — a real backup artifact was created using the documented method;
- **restore validated** — a backup was restored into an isolated environment and the expected state was recovered;
- **production-readiness evidence** — all applicable evidence gathered for the stated target and scope; this does not imply untested HA, load/performance, advanced security, multi-region disaster recovery, or optional integrations.
- **production-ready** — use only when the user explicitly needs that conclusion and the production-readiness evidence actually covers the claimed operational scope.

Do not collapse these into one claim.


#### Coherent multi-store recovery

A recovery plan must identify whether related stores form a **coherence group** rather than treating each backup target independently. Database + filesystem + queue/session/in-flight stores can represent one logical application state. Where the upstream write model requires it, define quiesce/flush/capture ordering and test isolated restore of the group. Do not classify an entire application volume without inspecting meaningful subpaths.

#### Compute / compile execution model

For applications that compile documents, execute jobs, render media or run untrusted/user workloads, first determine the selected edition/profile's actual execution model. Do not add Docker socket, privileged runner or sibling compute containers solely because the product performs computation. Functional product acceptance should execute a real representative job and verify its output.

#### Deployment-profile readiness

When a product supports multiple deployment profiles, production-readiness evidence belongs to the **exact selected profile**. A PASS on one profile must not be copied to another merely because product capabilities appear equivalent.

- [ ] Profile status is explicit: Canonical Golden / Validated Alternative / Reference-Candidate.
- [ ] Intentional profile exploration is not counted as repair failures.
- [ ] Process topology and state topology were evaluated independently.
- [ ] Semantic application gateway responsibilities are preserved behind Coolify when required.
- [ ] Health probes validate network + Host + path + router semantics.
- [ ] Version-specific workarounds identify the affected version/profile and are scheduled for causal revalidation on upgrade.
- [ ] If authoritative state is externalized, the backup/restore primitive was re-evaluated; DB dump alone is not called a full backup while local user files/media remain authoritative.
- [ ] Same-engine stores (including Redis/Valkey) are classified by role/durability rather than copied from another product.

#### Coolify-native operator UX / official contribution polish — not a readiness gate

**Service configuration** exposure is outside the production-readiness evidence ladder. Coolify may display recognized credentials/parameters through current `Service::extraFields()` conventions, but missing UI exposure does not reduce runtime correctness, security, persistence, recovery, product acceptance, or mergeability by itself.

Use this only after the runtime candidate is accepted:

- [ ] preserve the accepted runtime credential contract and persistent Magic Variable identities;
- [ ] inspect current Coolify `Service::extraFields()` mappings for the images/services being contributed;
- [ ] identify optional UI-friendly naming changes without altering semantic role, issuer, format/encoding or security;
- [ ] keep the Golden immutable; create a derivative official-contribution candidate if polish is justified;
- [ ] rerun static validation and the relevant runtime, persistence, acceptance and recovery checks after any UI-oriented rename;
- [ ] never fail a benchmark merely because this UI layer is absent.

Priority remains:

```text
runtime correctness > extraFields compatibility
```

<!-- END PORTABLE RESOURCE: references/production-readiness.md -->

<!-- BEGIN PORTABLE RESOURCE: references/rc5-vs-rc6-netbox-regression-analysis.md -->
<!-- SOURCE SHA256: 9c1b67c7813c49796c85c6240b24acf3cad2fd0c460d33ba543f281b9b406537 -->
<!-- EMBEDDED SHA256: fc1741841878012b022ccd9fee961c108bca63006ee595ecc3064d474c472b38 -->

## Portable resource: `references/rc5-vs-rc6-netbox-regression-analysis.md`

### RC5 vs RC6 — NetBox behavioral regression analysis

#### Executive finding

`coolify-architect v1.0.0-rc6` remains the correct baseline because it contains the full RC5 corpus plus Overleaf CE Golden #9 and legitimate new lessons. The NetBox benchmark nevertheless demonstrates a first-class behavioral regression:

```text
RC5
  -> NetBox accepted in 2 iterations

RC6
  -> NetBox still non-functional after 4 iterations
```

RC7 therefore follows this equation:

```text
RC7 = RC6 knowledge + NetBox runtime learning - RC6 NetBox regression
```

It is **not** RC5 with NetBox appended.

The immutable NetBox Golden is the exact RC5-era accepted RC2 candidate, SHA-256 `e4be06751d206704a2e9460ac2926d92833b39a71266cf1bd5a8a788da319804`.

#### Evidence boundary

The structured RC6 package and RC5 portable package were compared for the ten files required by the regression brief. The accepted NetBox YAML was supplied separately and is treated as runtime truth. Later RC6 NetBox candidates are diagnostic evidence only and are not used to reconstruct the Golden.

The supplied public share URL was not relied upon for fixture bytes; the accepted YAML artifact itself is authoritative.

#### Required RC5 -> RC6 differential audit

| File | RC5 lines | RC6 lines | Added | Removed | NetBox regression relevance |
|---|---:|---:|---:|---:|---|
| `SKILL.md` | 584 | 597 | 28 | 15 | added Overleaf-era scope around edition/process boundaries, secret encoding, fail-closed account lifecycle and platform-derived metadata |
| `references/architecture-discovery.md` | 233 | 247 | 34 | 20 | added Toolkit/runtime map, internal-process boundary, coherence groups, richer secret/format analysis and publication checks |
| `references/anti-patterns.md` | 413 | 506 | 164 | 71 | added Overleaf-specific anti-pattern family and stronger lifecycle/identity warnings |
| `references/coolify-rules.md` | 296 | 298 | 29 | 27 | strengthened encoded-secret semantics, platform-derived configuration and nested interpolation review |
| `references/cross-benchmark-lessons.md` | 115 | 128 | 69 | 56 | expanded eight-case lessons to nine, especially Toolkit/runtime, secret format and generated metadata |
| `references/production-readiness.md` | 196 | 205 | 26 | 17 | added coherence-group and identity/credential lifecycle requirements |
| `references/source-priority.md` | 142 | 144 | 12 | 10 | added published-artifact/config discrepancy rules |
| `references/evaluation-prompts.md` | 889 | 1048 | 244 | 85 | added Overleaf regression prompts for Toolkit, service-name metadata, Base64 format, nested `$`, coherent state and admin identity |
| `scripts/validate_golden_cases.py` | 907 | 1032 | 130 | 5 | added exact Overleaf Golden #9 invariants and similarity remained advisory |
| `scripts/audit_compose.py` | 675 | 673 | 0 | 2 | no new Overleaf-specific enforcement was added; the generic static auditor did not itself inflate NetBox |

These counts describe text change, not causality.

#### 1. Why RC5 succeeded

RC5's NetBox path stayed close to current `netbox-docker` behavior:

- preserved the five upstream service roles;
- preserved PostgreSQL plus two distinct Valkey roles;
- preserved RQ worker command;
- recognized that `/etc/netbox/config` was already present in the image and avoided copying the whole configuration repository;
- used only one small managed Coolify override;
- preserved native migrations and native superuser bootstrap;
- kept DB/Valkey private;
- changed the first failing candidate only at the earliest proven defect.

The decisive RC1 -> RC2 runtime correction was narrow:

```text
http://127.0.0.1:8080/login/
  ->
http://localhost:8080/login/
```

The cause was application Host-header validation, not network reachability. No service, secret identity, volume, worker command or lifecycle owner changed in that correction.

This is an excellent example of the regression discipline already present in the Skill: identify the earliest proven failure, then change the smallest executable surface.

#### 2. Why RC6 diverged

RC6 did **not** fail because its new Overleaf knowledge was wholly wrong. Most RC6 additions are legitimate and should remain.

The divergence came from an interaction between a larger rule set and target-specific evidence:

##### A. Secret reasoning became richer but not transport-complete

RC6 strengthened the rule that encoded format is part of a secret contract. That is correct for secrets such as Overleaf's invite token where true Base64 semantics matter.

NetBox `SECRET_KEY` and `API_TOKEN_PEPPER_1`, however, are not evidence that true Base64 encoding is intrinsically required. Their contract is primarily durable secret material with sufficient entropy/length and application compatibility.

Later RC6 runtime evidence showed a symbol-bearing generated value could contain `$` text that became unsafe while serialized through Coolify's generated `.env`/Compose interpolation path. The missing dimension was **transport safety**.

RC7 therefore scopes the rule as:

```text
application format contract
+ generator output contract
+ transport/serialization contract
```

not:

```text
more exact/complex generator is automatically safer
```

The accepted Golden remains byte-exact even though a later generated instance exposed a stochastic transport risk. Golden bytes are an oracle; general rules are allowed to learn from later runtime evidence without rewriting history.

##### B. Knowledge accumulation increased the temptation to explain before reusing the known-good upstream primitive

RC6 added valuable questions around Toolkit/runtime, fail-closed bootstrap, platform-generated metadata, managed files and nested-language escaping. On NetBox, the successful path is simpler:

- image already contains the configuration bundle;
- image entrypoint already owns migrations;
- native superuser bootstrap already exists;
- upstream already defines the RQ worker;
- upstream already distinguishes tasks from cache.

RC7 adds an explicit **primitive-preservation priority**: when a required behavior is already implemented by the selected current image and accepted upstream lifecycle, prove a gap before replacing or wrapping it.

##### C. Similarity/advisory knowledge can become cognitive friction

RC5's NetBox static analysis reported its highest Golden similarity as CKAN (`0.62`), yet correctly resolved that similarity through independent NetBox upstream provenance.

A similarity warning is useful only as a contamination question. It must never become a reason to distrust a fully upstream-proven architecture or to search for a different shape.

RC7 changes the validator behavior so that, when an upstream Compose is supplied and the candidate introduces no new detected architecture capabilities, high Golden similarity is reported as informational provenance context rather than a blocking-style review message.

##### D. More fail-closed guidance must not mean fail-closed discovery

RC6's fail-closed rules for unknown persisted identity/product state are legitimate mutation-safety rules. They are not reasons to reject an upstream-native bootstrap that has already demonstrated idempotent behavior.

RC7 explicitly scopes fail-closed behavior to **unsafe mutation under unknown state**, not to architecture discovery itself.

#### 3. Which RC6 rules contributed

##### Contributed or exposed an incompleteness

| RC6 rule area | Assessment | RC7 correction |
|---|---|---|
| encoded-secret-format emphasis | **partial contributor** | add transport/serialization compatibility as an independent requirement; do not universalize Base64 or symbol bans |
| generated/platform metadata awareness | **legitimate, but can increase search surface** | keep it; evaluate only metadata that can actually reach/affect the target application |
| Golden similarity advisory | **potential cognitive contributor** | downgrade high similarity to INFO when independent upstream capability provenance matches; similarity is never contamination proof |
| fail-closed account lifecycle | **scope risk, not direct NetBox defect** | apply to conflicting/unknown mutation state; preserve proven native idempotent bootstrap |

##### Not causal and retained

| RC6/Overleaf lesson | Why it stays |
|---|---|
| Toolkit vs runtime | prevents copying host orchestration into Compose; NetBox reinforces the same abstraction principle |
| internal process vs Compose topology | prevents service explosion; NetBox did not require internal decomposition |
| edition/profile boundary | unrelated to NetBox failure and remains correct |
| coherent multi-store recovery | valid operations lesson; not an architecture generator |
| application-issued vs generated credentials | NetBox API tokens reinforce this distinction |
| service-name-derived platform configuration | real Overleaf failure class; keep scoped to platforms/applications where derived names reach runtime |
| nested-language `$` escaping | real Overleaf RC3 failure class; keep scoped to Compose/shell/nested-language boundaries |
| exact Overleaf admin bootstrap | fixture-local; not imported into NetBox |
| Overleaf Mongo/Redis/compile specifics | fixture-local and not imported into NetBox |

#### 4. Requested bias audit

##### Toolkit/config copying bias

**RC6 state:** good rule, not a NetBox cause.

**RC7 scope:** a repository bind mount is not proof the image lacks the same files. Inspect Dockerfile/image contents before reproducing a config tree.

##### Managed-file overuse

**RC6 state:** managed-file mechanics were strongly represented by earlier Goldens.

**RC7 scope:** managed files are justified by a runtime artifact gap, not by their convenience. If the image baseline is sufficient, use zero or the smallest override.

##### Bootstrap-helper bias

**RC6 state:** already warned against automatic helper creation.

**NetBox evidence:** native `SUPERUSER_*` is sufficient.

**RC7 scope:** prove a native lifecycle gap before adding a helper.

##### Secret-format overconstraint

**RC6 state:** encoded format correctly became first-class after Overleaf.

**Gap:** transport safety was not equally first-class.

**RC7 scope:** application format + generator semantics + transport semantics.

##### Service-name overconstraint

**RC6 state:** Overleaf demonstrated a real service-name-derived environment collision.

**RC7 scope:** service name is not globally dangerous. Inspect actual platform-derived metadata and application behavior; preserve upstream-compatible names when safe.

##### Golden-similarity bias

**RC6 state:** validator similarity was advisory but could still create needless suspicion.

**RC7 scope:** similarity is INFO when current upstream independently explains the candidate capability graph; a Golden never vetoes current upstream by resemblance.

##### Excessive fail-closed behavior

**RC6 state:** justified for unknown persistent identity/product mutation.

**RC7 scope:** do not turn mutation safety into architecture skepticism. A native idempotent bootstrap with known semantics should be preserved.

##### Architecture inflation

**RC6 NetBox attempts:** the five-service topology itself remained correct.

**RC7 conclusion:** no evidence that Overleaf caused service-count inflation in NetBox. The new regression is about reasoning convergence and unnecessary adaptation complexity, not a literal increase in NetBox service count.

#### 5. NetBox-specific new learning

##### Image-baked configuration

A bind-mounted upstream configuration directory does not establish that the runtime image lacks configuration. Inspect Dockerfile/image construction first.

##### Minimal managed override

When the image contains the bundle:

```text
image baseline + env + smallest required override
```

##### Same technology, different state semantics

```text
Valkey tasks != Valkey cache
```

Do not merge stores solely because the engine is identical.

##### Queue security

The tasks store is trusted execution infrastructure, not a low-value cache. Keep it private and consistently authenticated according to upstream behavior.

##### Healthcheck hostname semantics

`localhost` and `127.0.0.1` may be TCP-equivalent but HTTP-different under Host-header validation. Never normalize one to the other without checking application semantics.

##### Secret transport safety

A valid application secret can still be invalid transport payload. Runtime serialization/interpolation is part of the end-to-end secret contract.

#### 6. How RC7 scopes RC6 correctly

RC7 adds five guardrails:

1. **Image content gate** — inspect Dockerfile/image contents before copying repository configuration mounts.
2. **Native primitive gate** — preserve current image-owned migration/bootstrap/readiness primitives unless a proven gap exists.
3. **Transport-aware secret gate** — validate application format, generator format and serialization path separately.
4. **Host-header health gate** — preserve hostname semantics in local HTTP probes.
5. **Knowledge-accumulation meta-regression gate** — when an older Skill solved the same target faster, compare the successful path before adding another workaround.

#### 7. Monotonic evolution rule

> Skill evolution is monotonic only when new knowledge improves or preserves performance on previously solvable architecture classes. A newer Skill that regresses on a target solved by an older release must treat that divergence as a first-class regression.

Operationally:

```text
newer release regresses
  -> do not assume newer reasoning is superior
  -> retrieve older accepted path
  -> diff rules introduced since that path
  -> classify each new rule as causal / scope-risk / unrelated
  -> preserve legitimate knowledge
  -> narrow over-generalized scope
  -> rerun old + new Golden invariants
```

And the existing rule remains unchanged:

> Golden fixtures are regression oracles, not architecture templates.

<!-- END PORTABLE RESOURCE: references/rc5-vs-rc6-netbox-regression-analysis.md -->

<!-- BEGIN PORTABLE RESOURCE: references/seven-benchmark-audit.md -->
<!-- SOURCE SHA256: 60cd149a25eca98d4a457f499071dd60a9bd8396363958eec078910d9c09fea4 -->
<!-- EMBEDDED SHA256: 1cf81f4d9227ce0bfd3be0f8bccffffa8c99b1f6756218f0d772a664ddad89a7 -->

## Portable resource: `references/seven-benchmark-audit.md`

### Seven-benchmark transversal audit — ERPNext Golden #7 integration

This audit adds ERPNext to KoboToolbox, CKAN, OpenMRS, OpenEMR, ODK Central and Frappe Framework. It preserves `references/five-benchmark-audit.md` and `references/six-benchmark-audit.md` as historical snapshots.

ERPNext is deliberately close to Frappe in container topology. Its value is not a seventh unrelated service graph; it tests whether the Skill can reason about **platform reuse without collapsing product activation, persistent-state transitions or business-level acceptance into the platform Golden**.

#### Provenance vocabulary

Use provenance labels explicitly rather than treating “Golden” as a source rank:

- **Coolify documented** — current official platform behavior;
- **target upstream documented** — current release-specific upstream behavior;
- **Frappe upstream documented** — platform behavior relevant to Frappe-family candidates;
- **ERPNext upstream documented** — product behavior relevant to ERPNext;
- **runtime demonstrated** — observed behavior in a real benchmark, environment/version scoped;
- **operator-confirmed** — operator states a gate passed even when full raw transcript is not bundled;
- **cross-benchmark demonstrated** — the same causal rule survives independent architectures;
- **inference / conditional** — plausible but not directly proven; must remain labeled.

Current upstream and current Coolify facts take precedence over a Golden fixture when software changes.

#### Architecture diversity matrix

| # | Benchmark | Main topology value | Distinct learning value |
|---:|---|---|---|
| 1 | KoboToolbox V19.3 | multi-public-host form platform, PostgreSQL/MongoDB/Redis, Celery/Enketo/gateways | canonical callback/hairpin, route semantics, multi-domain state |
| 2 | CKAN 2.12 | PostgreSQL/DataStore + Solr + Redis + DataPusher + RQ | managed files, sourced hooks, internal callback, worker readiness |
| 3 | OpenMRS 3.7.1 | semantic gateway + O3 frontend/backend + MariaDB | long bootstrap, fixed admin identity, generated credential reuse |
| 4 | OpenEMR 8.3.0 | deliberately small public app + MariaDB | minimality counterexample, native bootstrap, DB + document recovery |
| 5 | ODK Central v2026.2.4 | semantic Nginx + Central/Pyxform/Enketo + PostgreSQL/Redis | deployment-bundle mounts, admin one-shot, upgrade lifecycle |
| 6 | Frappe Framework v16 | MariaDB + Redis + workers + scheduler + Socket.IO + semantic Nginx | platform/site lifecycle, Magic Variable grammar, async/realtime acceptance |
| 7 | ERPNext v16.33.0 | same Frappe runtime family with ERPNext app profile | sibling-product delta, activation state, conversion guard, product acceptance |

#### Why Frappe and ERPNext are both needed

```text
Frappe Golden #6
  proves platform/runtime adaptation

ERPNext Golden #7
  proves product activation and product-state transitions on that runtime
```

If the corpus retained only Frappe, an agent could still miss the difference between “site exists” and “ERPNext is installed”. If it retained only ERPNext, an agent could overfit Frappe infrastructure to every Frappe-family product. The pair is useful because the topology overlap exposes a different class of reasoning error.

#### Strong rules after seven Goldens

##### 1. Current upstream topology remains the baseline

Golden similarity never replaces current source discovery. Complexity can be inherited only when the target independently still requires it.

##### 2. Platform edge proxy and application-semantic gateway remain separate

OpenMRS, ODK, Frappe and ERPNext show semantic gateways can remain behind Coolify. OpenEMR remains the counterexample against assuming a gateway is always required.

##### 3. Platform capability and product activation are separate state layers

Use the four-layer model:

```text
image capability
-> runtime/platform
-> instance/site/tenant
-> installed/enabled product
```

The highest claimed layer needs authoritative evidence.

##### 4. Sibling Product Delta Gate is mandatory

When a previous Golden shares the runtime, produce:

```text
BASE PLATFORM
SHARED INFRASTRUCTURE
PRODUCT-SPECIFIC STATE
PRODUCT-SPECIFIC BOOTSTRAP
PRODUCT-SPECIFIC MIGRATION
PRODUCT-SPECIFIC ACCEPTANCE
PRODUCT-SPECIFIC RECOVERY
```

For each item classify **inherited / revalidated / changed / why**.

##### 5. Instance existence does not prove product activation

An existing site with a missing product is a mismatch state, not success. A failed activation query is unknown state, not “absent”.

##### 6. Persistent product-profile conversion must be explicit

Do not silently transform an existing persistent instance from Platform A into Product B simply because the template can perform the installation.

##### 7. Durable activation needs post-condition verification

When an application exposes an authoritative query, `install/enable` command exit zero is weaker evidence than exit zero plus verified resulting state.

##### 8. Fresh install and later migration are different phases

Do not substitute `migrate` for first installation and do not reinstall product modules as an upgrade strategy without upstream evidence.

##### 9. Health is layered acceptance

```text
LEVEL 1 infrastructure
LEVEL 2 platform
LEVEL 3 product
```

A platform ping cannot validate an installed business product. Test at the highest layer claimed by the One-Click.

##### 10. Business truth is not bootstrap filler

Technical automation must not invent organization-specific company, jurisdiction, currency, tax, accounting or other business truth just to remove onboarding screens.

##### 11. One-Click contract may be narrower than platform capability

A platform can support multi-site/multi-app operation while a One-Click intentionally manages one initial site or one product profile. State that contract explicitly.

##### 12. Nested parser layers require effective-runtime validation

YAML parsing and `bash -n` are necessary but insufficient for nested `sed`, `awk`, regex, SQL, `jq` and similar command languages. Validate deterministic literal nested commands when practical and inspect the representation after Compose interpolation.

#### Bias audit after ERPNext

##### Sibling-copy bias

**Risk:** Frappe works, therefore ERPNext/CRM/Helpdesk/HRMS/LMS can be obtained by string replacement.

**Control:** current upstream compatibility + Sibling Product Delta Gate + product-specific acceptance.

##### App-install bias

**Risk:** an image contains a product, therefore install/enable it by default.

**Control:** define the intended product profile and inspect authoritative activation state first.

##### Conversion bias

**Risk:** a template sees a healthy platform-only instance and silently converts it to the template's product profile.

**Control:** explicit conversion policy; fail closed by default for mismatched persistent product state.

##### Business-fixture bias

**Risk:** Customer/Item/Sales Order or another ERPNext fixture becomes a universal test for all business software.

**Control:** preserve the rule “representative product workflow”, not the literal object names.

##### Framework-family bias

**Risk:** Frappe's site/apps model becomes the universal mental model for every modular application.

**Control:** generalize only activation layers and state-transition reasoning; use each target's native registry/tenant/plugin semantics.

##### Green-platform bias

**Risk:** framework ping + Desk + workers healthy means the sibling product is validated.

**Control:** require product activation evidence and representative product behavior.

##### Repair-by-assumption bias

**Risk:** partial directory or failed state query is interpreted as a known state and mutated.

**Control:** unknown persistent state -> fail closed -> diagnose -> recover based on evidence.

##### Shell-parser blame displacement

**Risk:** a bootstrap fails before app creation, but troubleshooting jumps to MariaDB/network/credentials because the nested tool error was not statically detected.

**Control:** identify the earliest proven failure and inspect YAML -> Compose -> shell -> nested-command representation.

#### Magic Variable regression after seven cases

ERPNext confirms the Frappe parser lessons without creating a special parser:

```text
SERVICE_PASSWORD_64_FRAPPEDBROOT
SERVICE_PASSWORD_64_FRAPPEADMIN
SERVICE_PASSWORD_64_ERPNEXTDBROOT
SERVICE_PASSWORD_64_ERPNEXTADMIN
SERVICE_URL_FRONTEND
SERVICE_FQDN_FRONTEND
SERVICE_URL_FRONTEND_8080
```

The first four are credential identities; the latter three are service-bound URL/FQDN/routing identities. Parse documented compound types longest-first and preserve exact credential identity across producer/consumer bindings.

#### Health and one-shot interpretation

ERPNext confirms that successful one-shot helpers can correctly show `Exited` while long-running services must have real readiness probes. `exclude_from_hc: true` prevents completed migration/bootstrap helpers from distorting Coolify aggregate health.

Do not add daemon healthchecks merely to keep a one-shot visually green, and do not delete a failing daemon healthcheck merely to change the dashboard color.

#### Persistence / recovery layering

Seven cases reinforce that recovery state is application-specific and often broader than SQL. ERPNext adds a product activation layer on top of Frappe site/database/files state.

Use:

```text
engine state
platform state
instance/site state
product activation state
product migration state
user/business data
```

A restore must reconstitute the authoritative set required by the product profile, not only make the database engine start.

#### Golden relationship invariant

The corpus must preserve both:

```text
Frappe Golden #6 -> Frappe-only site contract
ERPNext Golden #7 -> frappe + erpnext site contract
```

Adding ERPNext must never mutate Golden #6 to auto-install ERPNext. Likewise ERPNext must not gain unrelated sibling apps simply because their code or repositories are available.

#### Production-readiness evidence ladder

Keep the existing ladder:

```text
YAML valid
-> Compose valid
-> containers running
-> healthy
-> publicly accessible
-> platform workflow validated
-> product workflow validated when a product is claimed
-> persistence validated
-> backup validated
-> isolated restore validated
-> production-readiness evidence
```

The last stage remains scoped. It does not prove untested HA, load/performance, advanced hardening, multi-region DR or optional integrations.

#### Next benchmark direction

ERPNext intentionally adds state-model diversity rather than topological diversity. The next benchmark should preferably test a materially different infrastructure family such as object-storage-first events, Kafka/NATS/RabbitMQ multi-consumer topology, GPU/hardware dependency, non-HTTP protocols, or a Helm-first system requiring a justified Compose translation.

The goal is not to maximize service count. It is to challenge rules that the current seven-case corpus cannot yet falsify.

#### Fundamental ERPNext contribution

> **A proven platform architecture can be reused as causal knowledge, but product activation, persistent-state transitions and business-level acceptance must still be rediscovered and proven for each sibling product.**

And the corpus invariant remains:

> **Golden fixtures are regression oracles, not generic templates.**

<!-- END PORTABLE RESOURCE: references/seven-benchmark-audit.md -->

<!-- BEGIN PORTABLE RESOURCE: references/six-benchmark-audit.md -->
<!-- SOURCE SHA256: f5ab13d3f6c22cfd5ace9a17a8922fbc78c97de0c509cc32674734c16ebe96c3 -->
<!-- EMBEDDED SHA256: 756c94b9ef46da73533a15ca3003418b4d0b6b3964d91dd6caf6efdd4366212d -->

## Portable resource: `references/six-benchmark-audit.md`

### Six-benchmark transversal audit — Frappe Golden #6 integration

This audit supersedes the historical five-benchmark audit for current cross-case reasoning. The original `references/five-benchmark-audit.md` is preserved unchanged as the pre-Frappe baseline. Golden fixtures are regression oracles, not architecture templates.

#### Knowledge provenance model

Classify every transferred lesson as:

1. **upstream fact** — current target/upstream repository, docs, image/entrypoint/config;
2. **official Coolify fact** — current documented platform semantics;
3. **runtime validated observation** — demonstrated in a specific benchmark/version;
4. **cross-benchmark confirmed pattern** — same causal principle independently supported by multiple cases;
5. **application-specific mechanism/workaround** — keep local unless a future target independently requires the same cause.

Current upstream and current Coolify facts outrank historical golden behavior when software changes.

#### Architecture matrix

| Golden case | Public edge/application gateway | Authoritative state | Async/special roles | Main causal contribution |
|---|---|---|---|---|
| KoboToolbox V19.3 | multiple public gateways | PostgreSQL + MongoDB + media + Redis roles | Celery/beat + Enketo | multi-domain/callback/hairpin and route-semantics complexity |
| CKAN 2.12 / Coolify V1.0.8 | one public CKAN app | PostgreSQL/DataStore + FileStore + Solr | RQ worker/scheduler + DataPusher | canonical URL vs internal callback, managed files, worker readiness |
| OpenMRS 3.7.1 / Coolify V1.0.0 | semantic Nginx gateway | MariaDB + application state | O3 frontend/backend | fixed admin identity, shared generated credentials, long bootstrap |
| OpenEMR 8.3.0 / Coolify V1.0.0 | direct public app | MariaDB + site/documents | native bootstrap only | minimality counterexample and recovery without invented infrastructure |
| ODK Central v2026.2.4 | semantic Nginx gateway | PostgreSQL + Enketo secrets/Redis state | Pyxform + one-shot admin/upgrade helpers | official-image external files, one-shot lifecycle, form workflow/recovery |
| Frappe Framework v16.32.0 / accepted RC3 | semantic Frappe Nginx | MariaDB + `sites/` + public/private files/config + queue durability | Redis cache/queue + RQ workers + scheduler + Socket.IO + three one-shots | Magic grammar, image-vs-site activation, semantic gateway, full async/realtime/recovery acceptance |

#### Strong rules after six goldens

##### Upstream-first and complexity provenance

Complexity is paid for by current upstream/product semantics, not by precedent. OpenEMR and Frappe are now the strongest paired counterexample: both are correct even though one is deliberately minimal and the other legitimately multi-process. Never use raw container count as a quality metric.

##### Platform edge proxy vs application-semantic gateway

Coolify normally owns Internet TLS/ACME/host routing. That does not prove an upstream Nginx/gateway is redundant. OpenMRS, ODK and Frappe independently demonstrate semantic gateways; OpenEMR demonstrates no extra gateway is inherently required. Inspect assets/files/protected routing, site identity, headers, frontend/backend composition and realtime paths before deleting.

##### Magic Variable grammar and identity

OpenMRS established exact shared-credential reuse. Frappe adds parser/identifier failure evidence: a variable can look plausible yet remain blank in Docker Compose Empty. Parse the complete current Coolify generator type longest-match-first, then inspect the identifier separately. Prefer conservative alphanumeric credential IDs unless current target-version behavior is documented/verified.

A generated variable name that initialized persistent state is part of deployment state. Renaming it can produce a new secret while the database/account retains the old one. Generated-value change is not credential rotation.

##### URL/FQDN/proxy-target taxonomy

Model at least: browser canonical URL, public FQDN, Coolify proxy-target declaration, Docker hostname/internal URL, callback URL and realtime origin. Frappe's accepted split (`SERVICE_URL_FRONTEND`, `SERVICE_FQDN_FRONTEND`, `SERVICE_URL_FRONTEND_8080`) reinforces CKAN's canonical-port lesson. Internal target ports are not automatically browser-visible ports.

##### Image capability is not application activation

Frappe/ERPNext turns this into a permanent reasoning regression: code bundled in an image does not prove a plugin/module/app is installed for a site/tenant. Inspect application-native activation state.

##### Lifecycle map before health map

Classify long-running daemon, worker, scheduler and one-shot init/migration/bootstrap before judging `Running`, `Exited 0` or healthchecks. ODK and Frappe independently validate successful-completion gates; OpenEMR prevents a rule that every app needs helper containers.

##### Functional acceptance for async and realtime

A running worker is not proof a job executes. A scheduler PID is not proof scheduled work occurs. HTTP 200 is not proof WebSocket/Socket.IO works. Frappe completes these coverage gaps with operator-confirmed real async/scheduler/realtime acceptance; future targets must use their own native functional fixture, not Frappe Data Import by default.

##### Persistence and recovery granularity

SQL health alone never proves full recoverability. Across six cases, map database state, uploads/files, config/encryption keys, queues/caches, generated assets and backup artifacts independently. Persist caches only when upstream semantics require it; preserve authoritative file/site volumes when the DB is not the whole application state.

#### Six-benchmark bias audit

##### Redis overuse
Risk: Frappe/Kobo/CKAN/ODK all use Redis-like roles.
Control: OpenMRS/OpenEMR show Redis is not a generic production dependency. Add it only for current upstream cache/queue/session semantics.

##### Worker/scheduler overuse
Risk: Frappe/Kobo/CKAN make async roles look normal.
Control: OpenMRS/OpenEMR/parts of ODK demonstrate that separate workers/schedulers are conditional. Preserve target-native process separation only.

##### Semantic-gateway overuse
Risk: OpenMRS/ODK/Frappe could bias toward keeping Nginx everywhere.
Control: keep only if the gateway owns application semantics; Coolify replaces pure Internet edge/TLS when safe. OpenEMR is the counterexample.

##### Site-bootstrap overuse
Risk: Frappe's one-click gap could encourage generic helper sidecars.
Control: use a helper only when upstream exposes official primitives but the hosting path lacks required non-interactive initialization. OpenEMR proves native bootstrap should remain native when already sufficient.

##### Scheduler/WebSocket assumption
Risk: Frappe could make scheduler/realtime appear universal.
Control: discovery must prove the target has those capabilities before adding or testing them.

##### Username randomization bias
Risk: platform-generated usernames may tempt agents to rename structural accounts.
Control: OpenMRS + Frappe establish that upstream-fixed semantic account identities stay fixed; expose a native alias separately if supported.

##### One-shot universality bias
Risk: ODK/Frappe show useful one-shots.
Control: lifecycle helper count is not a quality target. Use native entrypoints/init paths when sufficient.

##### Bundled-app activation bias
Risk: image contents are mistaken for tenant/site state.
Control: Frappe/ERPNext requires an explicit activation-state check and generalizes to plugins/extensions/modules.

##### Golden-copy contamination
Risk: a new target resembling any golden inherits its services mechanically.
Control: **Golden fixtures are regression oracles, not architecture templates.** Every service/capability needs current-target provenance. Similarity is a review signal only.

#### Current production-readiness evidence ladder

```text
YAML VALID
-> COMPOSE VALID
-> CONTAINERS RUNNING
-> HEALTHY / lifecycle-complete as applicable
-> PUBLICLY ACCESSIBLE
-> APPLICATION WORKFLOW VALIDATED
-> ASYNC/REALTIME FUNCTIONALLY VALIDATED when applicable
-> PERSISTENCE VALIDATED
-> BACKUP VALIDATED
-> ISOLATED RESTORE VALIDATED
-> PRODUCTION-READINESS EVIDENCE
```

Even this does not imply untested HA, capacity/load, advanced security hardening, multi-region DR or optional integrations.

#### Next benchmark direction

Six heterogeneous web-platform goldens materially improve the V1 corpus, but it remains biased toward Linux HTTP applications and relational databases. Future benchmarks should preferentially exercise a different causal family: object-storage/event-first systems, Kafka/NATS/RabbitMQ multi-consumer systems, non-HTTP public protocols, GPU/hardware dependencies, distributed identity/payments, or a Helm/Kubernetes-first system requiring a justified Compose translation.

<!-- END PORTABLE RESOURCE: references/six-benchmark-audit.md -->

<!-- BEGIN PORTABLE RESOURCE: references/source-priority.md -->
<!-- SOURCE SHA256: 39f701bda8969e3069ef949b114a21e08932f1ab6c56b55deabeb10b93b829b0 -->
<!-- EMBEDDED SHA256: 6db4accee5569ea11c3f236e0b7867cef8d01889c3dfad8f0fbc5d2e509e67d1 -->

## Portable resource: `references/source-priority.md`

### Source priority and research method

#### Goal

Avoid speculative Compose generation. The agent must derive the deployment from evidence.

#### Evidence hierarchy

Prefer sources in this order:

1. exact upstream release/tag requested by the user;
2. upstream deployment documentation for that release;
3. upstream Compose, Dockerfiles, Helm charts, systemd units, entrypoints, and environment examples;
4. upstream source code that defines runtime URLs, storage paths, migrations, workers, health endpoints, or callback behavior;
5. current official Coolify Docker Compose documentation;
6. current official Coolify template repository;
7. upstream issues/discussions for bugs not explained by official material;
8. third-party guides only as supporting evidence.


#### Knowledge provenance levels

Classify evidence before turning it into a rule:

- **LEVEL 1 — Upstream fact:** exact current release repository/docs/code.
- **LEVEL 2 — Official Coolify fact:** current Coolify documentation/templates.
- **LEVEL 3 — Runtime validated observation:** direct behavior captured/reproduced in a real target deployment.
- **LEVEL 4 — Operator-confirmed runtime result:** a runtime gate is confirmed by the operator but the corpus does not preserve every raw command/output.
- **LEVEL 5 — Cross-benchmark confirmed pattern:** the same causal principle survives multiple independent architectures.
- **LEVEL 6 — Application-specific adaptation/workaround:** a mechanism required by one application/deployment.
- **LEVEL 7 — Inference / REVIEW REQUIRED:** plausible reasoning that still lacks enough direct evidence.

These numbers are provenance classes, not a rule that a larger number is “more true”. Current Level 1 + Level 2 evidence has decision priority. Levels 3/4 strengthen runtime interpretation; Level 5 supports general causal rules; Level 6 must remain local until independently justified; Level 7 stays explicitly provisional.

#### Repository inspection checklist

Search for:

```text
compose.yml
docker-compose.yml
docker-compose.*.yml
Dockerfile*
.env.example
.env.sample
entrypoint*
docker-entrypoint*
scripts/
deploy/
docker/
helm/
charts/
k8s/
nginx/
traefik/
caddy/
README*
INSTALL*
UPGRADE*
MIGRATION*
```

Then identify:

- images and version constraints;
- build contexts;
- runtime commands;
- ports and protocols;
- databases and database names;
- cache/broker usage;
- background workers/schedulers;
- initialization order;
- writable paths;
- object storage;
- canonical/public URLs;
- internal URLs;
- reverse-proxy assumptions;
- websocket/SSE/streaming routes;
- authentication/cookie scope;
- health endpoints;
- required external integrations.

#### Release discipline

Do not mix deployment material from unrelated releases without documenting the mismatch. If upstream `main` differs from the requested stable tag, inspect the tag.

Prefer explicit release pins. If upstream only documents a floating tag, state that limitation rather than pretending the deployment is immutable.

A source release pin does not automatically freeze the transitive build graph. Inspect Dockerfiles/install scripts/package managers/Git clones/downloads for mutable dependency resolution and classify the dependency closure as HERMETIC, PARTIALLY PINNED, FLOATING TRANSITIVE DEPENDENCIES, or UNKNOWN. Do not claim hermetic reproducibility without closure evidence.

Repository configuration, release metadata and published deployment artifacts can temporarily disagree. When they diverge: identify the mismatch, verify what is actually published, do not invent a tag/artifact, document the gap, and use a demonstrable artifact or wait. Full-stack CPU architecture support must likewise be verified across every required runtime image rather than inferred from a subset of dependencies.

#### Current Coolify verification

Before publishing a reusable template, verify current official Coolify behavior for:

- Docker Compose parsing;
- generated `SERVICE_*` variables;
- domain-to-port routing;
- generated variable persistence;
- storage behavior;
- health checks;
- predefined/default networks;
- any syntax the template depends on;
- the effective pull/build sequence for local source-built images when `image:` + `build:` is used;
- the parser + persistence/model + deployment support path for critical Compose primitives such as `configs`, `secrets`, bind `content`, `tmpfs`, profiles, pull policies, devices, GPUs or capabilities.

Coolify evolves. A workaround learned from an old deployment must not become a timeless rule without re-verification.

#### Comparable template research

Use the official Coolify template repository to learn platform-native patterns, not to copy an unrelated service mechanically.

Compare applications with similar traits:

- web + database;
- web + worker + Redis;
- multi-public-service stacks;
- ClickHouse/Mongo/Postgres stacks;
- Nginx gateway stacks;
- init/migration patterns.

#### Decision log

For every intentional divergence from upstream, record:

```text
Change:
Upstream behavior:
Coolify reason:
Risk introduced:
Validation performed:
Rollback:
```

A final Compose should contain only the comments needed to operate it. Keep the detailed decision log outside runtime YAML.

#### Version-sensitive parser/runtime observations

When a platform parser behavior is demonstrated in runtime but not stated as a universal rule in current documentation:

1. cite the current documentation for the supported family/model;
2. record the runtime failure/correction separately;
3. use issue-tracker evidence only as supporting context;
4. classify the linter result as `REVIEW REQUIRED` unless the platform contract is explicit enough for `ERROR`;
5. add a regression test so future documentation/parser changes can intentionally revise the rule.

Frappe's credential Magic Variable separator failure is the reference example. It must not be rewritten as a Docker Compose law or an underscore ban across all `SERVICE_*` families.


#### Dependency contract drift procedure

When a pinned application release becomes incompatible with a mutable dependency:

1. prove the exact dependency contract drift;
2. identify the causal upstream dependency change when possible;
3. prefer a compatible immutable dependency pin if the upstream build model supports one;
4. otherwise use the smallest local compatibility boundary that restores the demonstrated contract;
5. label the adaptation VERSION/DEPENDENCY-SCOPED;
6. add a regression test;
7. revalidate/remove the shim on the next application/dependency release.

Do not generalize the implementation mechanism (for example “patch XML”). Generalize the causal reasoning: released application + mutable dependency + changed contract can break later.

#### Image contents gate

A repository bind mount does not prove the image lacks the mounted files. Inspect Dockerfile/image contents before recreating configuration.

#### Older-successful-path regression evidence

When a newer Skill release performs worse on the same target than an older release with a runtime-accepted result, treat the older successful reasoning/output as direct regression evidence. Compare both paths against current upstream facts. Do not assume `newer Skill = better reasoning`, and do not let an older Golden outrank current upstream architecture. The purpose is to identify which newly introduced rule was over-broad or unnecessarily diverting.

<!-- END PORTABLE RESOURCE: references/source-priority.md -->

<!-- BEGIN PORTABLE RESOURCE: references/sources.md -->
<!-- SOURCE SHA256: 2ae77addbfd600c5e6be5f30cfb37499ee00d3cc30e9ab94039e2e8bad8bc1e3 -->
<!-- EMBEDDED SHA256: a5231b6cba7b9dd57b94336be4e86f3d9d4ebf834acaa291b3a5fd0d40ef2847 -->

## Portable resource: `references/sources.md`

### Authoritative sources

Last source review for this skill draft: 2026-09-01.

Always re-check current documentation when generating a real deployment.

#### OpenAI / Agent Skills

- OpenAI Help Center — Skills in ChatGPT: https://help.openai.com/en/articles/20001066
- OpenAI Academy — Using skills: https://openai.com/academy/skills/
- OpenAI Codex: https://openai.com/codex/
- Agent Skills open specification: https://agentskills.io/specification
- Agent Skills reference repository: https://github.com/agentskills/agentskills

#### Coolify

- Coolify Docker Compose documentation: https://coolify.io/docs/knowledge-base/docker/compose
- Coolify current Service-stack Docker Compose documentation: https://next.coolify.io/docs/services/configuration/docker-compose
- Coolify current Service environment variables: https://next.coolify.io/docs/services/configuration/environment-variables
- Coolify environment variables: https://coolify.io/docs/knowledge-base/environment-variables
- Coolify repository: https://github.com/coollabsio/coolify
- Coolify official Compose templates: https://github.com/coollabsio/coolify/tree/v4.x/templates/compose
- Coolify documentation repository: https://github.com/coollabsio/coolify-docs
- Coolify issue #11043 — Magic environment variables / identifier separator runtime report: https://github.com/coollabsio/coolify/issues/11043

#### Case-study upstreams

##### KoboToolbox

For KoboToolbox deployments, verify the currently intended release directly from:

- KoboToolbox KPI: https://github.com/kobotoolbox/kpi
- KoboToolbox Docker/deployment material: https://github.com/kobotoolbox/kobo-docker
- Enketo Express Extra Widgets: https://github.com/kobotoolbox/enketo-express-extra-widgets

#### Source-use rule

Official sources establish current platform/upstream behavior. The bundled KoboToolbox, CKAN, OpenMRS, OpenEMR, ODK Central, Frappe Framework, ERPNext, Mem0 and Overleaf CE golden case studies establish lessons from real deployment/troubleshooting histories. When they conflict because software changed, current authoritative documentation wins; runtime cases remain historical/version-scoped evidence.

#### Official Coolify template corpus

- Repository: https://github.com/coollabsio/coolify/tree/main/templates/compose
- Corpus baseline reviewed for this skill update: main commit `8d675f2e21810bde0f67d6598da08fd52ba1cba5`
- Representative references: `n8n.yaml`, `authentik.yaml`, `appwrite.yaml`, `appflowy.yaml`

Re-check `main` when using the skill for current production work because template conventions and versions evolve.


#### CKAN golden case

Official upstream/source material relevant to the CKAN regression fixture:

- CKAN 2.12 documentation: https://docs.ckan.org/en/2.12/
- CKAN repository: https://github.com/ckan/ckan
- CKAN Docker deployment repository: https://github.com/ckan/ckan-docker
- CKAN official Docker base images: https://github.com/ckan/ckan-docker-base
- CKAN configuration reference (DataPusher/DataStore): https://docs.ckan.org/en/2.12/maintaining/configuration.html
- CKAN DataStore documentation: https://docs.ckan.org/en/2.12/maintaining/datastore.html
- CKAN CLI/background jobs: https://docs.ckan.org/en/2.12/maintaining/cli.html

Runtime evidence for the bundled fixture additionally comes from the successful Coolify deployment and acceptance path documented in `references/ckan-case-study.md`. Current upstream documentation wins if a later release changes behavior.

#### Coolify behaviors reinforced by the CKAN case

Re-check these current docs before relying on the behavior in a new deployment:

- Magic environment variables and port-qualified URL/FQDN semantics: https://coolify.io/docs/knowledge-base/environment-variables
- Docker Compose as source of truth, domains, storage `content:` extension: https://coolify.io/docs/knowledge-base/docker/compose
- Docker Compose build/deployment behavior: https://coolify.io/docs/applications/build-packs/docker-compose

The CKAN case demonstrated why port-qualified routing variables, canonical origins, managed inline files, and health status must be validated in the actual deployment rather than inferred from variable names.


#### OpenMRS 3 Reference Application

- Reference Application distribution overview and deployment: https://github.com/openmrs/openmrs-distro-referenceapplication
- Base Docker Compose (gateway/frontend/backend/MariaDB): https://github.com/openmrs/openmrs-distro-referenceapplication/blob/main/docker-compose.yml
- SSL overlay (upstream external TLS/certbot behavior): https://github.com/openmrs/openmrs-distro-referenceapplication/blob/main/docker-compose.ssl.yml
- OpenMRS Core Docker Compose / `OMRS_DB_*` and admin bootstrap variables: https://github.com/openmrs/openmrs-core/blob/master/docker-compose.yml
- OpenMRS Core startup script/readiness bootstrap: https://github.com/openmrs/openmrs-core/blob/master/startup.sh
- OpenMRS Core startup initialization: https://github.com/openmrs/openmrs-core/blob/master/startup-init.sh
- OpenMRS O3 production deployment documentation: https://o3-docs.openmrs.org/docs/recipes/deploying

OpenMRS golden evidence additionally includes the live Coolify deployment/acceptance session that produced `assets/openmrs-3.7.1-v1.0.0-golden.yml`. Runtime evidence is case-specific and must not override newer upstream release requirements.

#### OpenEMR 8.3.0

Official upstream/source material relevant to the OpenEMR regression fixture:

- OpenEMR repository: https://github.com/openemr/openemr
- OpenEMR stable downloads/releases: https://www.open-emr.org/downloads/
- OpenEMR `rel-830` production Compose: https://github.com/openemr/openemr/blob/rel-830/docker/production/docker-compose.yml
- OpenEMR release container/startup material: https://github.com/openemr/openemr/tree/rel-830/docker/release
- OpenEMR installer/bootstrap implementation: https://github.com/openemr/openemr/blob/rel-830/library/classes/Installer.class.php
- OpenEMR setup entrypoint: https://github.com/openemr/openemr/blob/rel-830/setup.php
- OpenEMR backup implementation/warnings: https://github.com/openemr/openemr/blob/rel-830/interface/main/backup.php
- Official OpenEMR container images/tags: https://hub.docker.com/r/openemr/openemr

OpenEMR golden evidence additionally includes the live Coolify deployment and operator-confirmed acceptance/redeploy/backup/restore session documented in `references/openemr-case-study.md`. The exact two-service topology, image tags, paths, health endpoint, usernames and magic-variable identifiers remain fixture-specific.


#### ODK Central v2026.2.4

Official upstream/source material relevant to the ODK Central regression fixture:

- ODK Central installation docs: https://docs.getodk.org/central-install/
- ODK Central v2026.2.4 repository tag: https://github.com/getodk/central/tree/v2026.2.4
- ODK Central v2026.2.4 Compose: https://github.com/getodk/central/blob/v2026.2.4/docker-compose.yml
- ODK Nginx image/build: https://github.com/getodk/central/blob/v2026.2.4/nginx.dockerfile
- ODK Nginx startup adapter: https://github.com/getodk/central/blob/v2026.2.4/files/nginx/setup-odk.sh
- ODK Nginx runtime template: https://github.com/getodk/central/blob/v2026.2.4/files/nginx/odk.conf.template
- ODK frontend client config template: https://github.com/getodk/central/blob/v2026.2.4/files/nginx/client-config.json.template
- ODK Central backend submodule revision used by v2026.2.4: `7aa74c5ec5d90189aa2482f57c041ea4ce9efbfd`
- Backend task runner: https://github.com/getodk/central-backend/blob/7aa74c5ec5d90189aa2482f57c041ea4ce9efbfd/lib/task/task.js
- Backend account tasks: https://github.com/getodk/central-backend/blob/7aa74c5ec5d90189aa2482f57c041ea4ce9efbfd/lib/task/account.js

ODK golden evidence additionally includes the live Coolify iterations through RC6 and the operator-confirmed functional acceptance, persistence, backup and isolated restore. Exact ODK service names, lifecycle helpers, templates, secret files and admin-task implementation remain application-specific.

#### Frappe Framework Golden / Regression Case #6

Official upstream/source material relevant to the Frappe golden case:

- Frappe Framework repository: https://github.com/frappe/frappe
- Frappe Docker deployment repository: https://github.com/frappe/frappe_docker
- Frappe Docker core Compose: https://github.com/frappe/frappe_docker/blob/main/compose.yaml
- Frappe MariaDB override: https://github.com/frappe/frappe_docker/blob/main/overrides/compose.mariadb.yaml
- Frappe Redis override: https://github.com/frappe/frappe_docker/blob/main/overrides/compose.redis.yaml
- Frappe migrator override: https://github.com/frappe/frappe_docker/blob/main/overrides/compose.migrator.yaml
- Frappe frontend Nginx templates/entrypoint: https://github.com/frappe/frappe_docker
- Frappe Bench new-site reference: https://docs.frappe.io/framework/user/en/bench/reference/new-site
- Frappe Bench backup reference: https://docs.frappe.io/framework/user/en/bench/reference/backup
- Frappe Bench restore reference: https://docs.frappe.io/framework/user/en/bench/reference/restore
- Frappe v16 authentication/user implementation and tests: https://github.com/frappe/frappe/tree/version-16
- Coolify generated Service-value docs: https://next.coolify.io/docs/services/configuration/docker-compose
- Coolify Magic Variable issue #11043: https://github.com/coollabsio/coolify/issues/11043

Runtime evidence for `references/frappe-framework-case-study.md` comes from the RC1→RC3 Docker Compose Empty deployment/troubleshooting path on 2026-08-31 plus the operator's explicit confirmation that the benchmark completed its full acceptance path. The immutable golden fixture preserves accepted RC3 executable behavior. RC4's optional login-alias extension remains outside that golden baseline until independently regression-tested.

#### ERPNext Golden / Regression Case #7

Official upstream/source material relevant to the ERPNext sibling-product case:

- ERPNext repository: https://github.com/frappe/erpnext
- ERPNext releases: https://github.com/frappe/erpnext/releases
- Frappe Framework repository: https://github.com/frappe/frappe
- Frappe Docker deployment repository: https://github.com/frappe/frappe_docker
- Frappe Docker single-server ERPNext site example: https://github.com/frappe/frappe_docker/blob/main/docs/02-setup/07-single-server-example.md
- Frappe Bench commands overview: https://docs.frappe.io/framework/user/en/bench/bench-commands
- Frappe `new-site` reference (`--install-app`, admin/root DB options): https://docs.frappe.io/framework/user/en/bench/reference/new-site
- Frappe `list-apps` reference: https://docs.frappe.io/framework/user/en/bench/reference/list-apps
- Frappe app installation model: https://docs.frappe.io/framework/user/en/basics/apps
- Frappe commands for app install/migrate/list-apps: https://docs.frappe.io/framework/user/en/bench/frappe-commands
- Coolify Docker Compose / Magic Variables / `exclude_from_hc`: https://coolify.io/docs/knowledge-base/docker/compose

The accepted ERPNext fixture is the exact operator-selected RC5 file with SHA-256 `64660809aba082409a41e20006d0d24dbc913928a30f07590ba873171ee2a7cb`. Runtime evidence from the benchmark includes creation of the ERPNext site, Frappe + ERPNext app state, migration, worker/scheduler activity and corrected service health. The operator explicitly confirms completion of the wider ERPNext acceptance path, including product-level workflow, persistence/redeploy and recovery gates. The Skill does not invent missing raw log lines or timestamps for those operator-confirmed stages.

The Frappe Golden remains a platform/runtime oracle and Frappe-only site contract. ERPNext upstream/current compatibility must be re-checked for future versions; the Frappe -> ERPNext relationship never substitutes for current ERPNext upstream evidence.



#### Overleaf Community Edition Golden / Regression Case #9

Official/source material used during the Overleaf benchmark included:

- Overleaf Toolkit repository: https://github.com/overleaf/toolkit
- Toolkit Compose baseline: https://raw.githubusercontent.com/overleaf/toolkit/master/lib/docker-compose.base.yml
- Toolkit Mongo overlay: https://raw.githubusercontent.com/overleaf/toolkit/master/lib/docker-compose.mongo.yml
- Toolkit Redis overlay: https://raw.githubusercontent.com/overleaf/toolkit/master/lib/docker-compose.redis.yml
- Toolkit variable/config seeds: https://github.com/overleaf/toolkit/tree/master/lib/config-seed
- Overleaf source repository / CE Docker material: https://github.com/overleaf/overleaf
- Overleaf On-Premises documentation: https://docs.overleaf.com/on-premises
- Data/backups: https://docs.overleaf.com/on-premises/maintenance/data-and-backups
- Redis: https://docs.overleaf.com/on-premises/configuration/overleaf-toolkit/redis
- User management: https://docs.overleaf.com/on-premises/user-and-project-management/user-management
- TeX Live: https://docs.overleaf.com/on-premises/installation/upgrading-tex-live
- MongoDB upgrades: https://docs.overleaf.com/on-premises/maintenance/updating-mongodb
- CE image tags: https://hub.docker.com/r/sharelatex/sharelatex/tags

Runtime evidence comes from the RC1→RC4 Coolify benchmark on 2026-09-01. RC1 exposed platform-generated `SERVICE_*_SHARELATEX` collision with Overleaf's environment compatibility guard; RC3 exposed Compose interpolation of a nested JavaScript `$set`; RC4 corrected both classes and the operator confirmed the requested runtime/acceptance suite succeeded. The exact RC4 bytes are preserved as Golden #9. Current upstream documentation/artifacts still win for future versions.


#### NetBox Golden #10 source set

- Runtime fixture authority: `assets/netbox-4.6.9-v1.0.0-golden.yml` — exact operator-supplied, runtime-accepted RC2 bytes from the RC5 benchmark path; SHA-256 `e4be06751d206704a2e9460ac2926d92833b39a71266cf1bd5a8a788da319804`.
- Upstream NetBox: `netbox-community/netbox` v4.6.9.
- Upstream container distribution: `netbox-community/netbox-docker` 5.0.2.
- Relevant evidence layers: release Compose, Dockerfile image-baked `configuration/`, `docker-entrypoint.sh`, `super_user.py`, and configuration environment adapter.
- Historical regression evidence: RC5 NetBox RC1 healthcheck `127.0.0.1` returned HTTP 400 under host validation; accepted RC2 restored upstream-compatible `localhost`.

The Golden fixture bytes outrank later reconstructed NetBox attempts for regression identity. Current upstream evidence still outranks the Golden when adapting a future NetBox release.

<!-- END PORTABLE RESOURCE: references/sources.md -->

<!-- BEGIN PORTABLE RESOURCE: references/ten-benchmark-audit.md -->
<!-- SOURCE SHA256: 9327a785a38158b8626ee074edfa967881a786fbd00c1da220ef1b83d012c602 -->
<!-- EMBEDDED SHA256: d8787f050a79886a626a9fb865a79e92697bff8b653dbe64ddefa0a841860a82 -->

## Portable resource: `references/ten-benchmark-audit.md`

### Ten-benchmark audit — knowledge accumulation and anti-contamination

#### Corpus

The current Golden corpus is:

1. KoboToolbox V19.3
2. CKAN 2.12
3. OpenMRS 3.7.1
4. OpenEMR 8.3.0
5. ODK Central v2026.2.4
6. Frappe Framework v16.32.0
7. ERPNext v16.33.0
8. Mem0 v2.0.19
9. Overleaf Community Edition 6.2.2
10. NetBox 4.6.9 / netbox-docker 5.0.2

Historical audits remain snapshots. This audit adds NetBox and, for the first time, evaluates whether **knowledge accumulation itself can regress adaptation performance**.

#### Architecture diversity at a glance

| Golden | Public shape | State | Async/lifecycle | Primary regression value |
|---|---|---|---|---|
| KoboToolbox | three semantic public hosts | PostgreSQL + MongoDB + Redis + media | Celery/beat, Enketo, gateways | multi-domain/callback complexity |
| CKAN | one public app | PostgreSQL/DataStore + Solr + Redis + FileStore | DataPusher + worker/scheduler | search/import/worker, managed files, callbacks |
| OpenMRS | one semantic gateway | MariaDB + app state | O3 frontend/backend/gateway | semantic gateway, long bootstrap |
| OpenEMR | one public app | MariaDB + documents | native bootstrap | minimal topology, multi-store recovery |
| ODK Central | one semantic Nginx host | PostgreSQL + Redis/Enketo state | Pyxform + one-shots | deployment bundle and one-host semantic proxy |
| Frappe | frontend + realtime | MariaDB + Redis + sites | workers, scheduler, migrator | platform lifecycle and async/realtime |
| ERPNext | Frappe runtime + product profile | Frappe + ERPNext product state | explicit activation/migration | sibling-product state |
| Mem0 | API + dashboard | PostgreSQL/pgvector + SQLite history | startup Alembic | provider/auth/browser/mixed-state |
| Overleaf CE | one collaborative app | MongoDB + Redis + filesystem | internal supervised processes + admin one-shot | Toolkit/runtime, edition, coherent recovery |
| NetBox | one web app | PostgreSQL + Valkey tasks + Valkey cache + media/scripts/reports | RQ worker + native entrypoint lifecycle | image-baked config, role-separated same-tech stores, health Host semantics, knowledge-accumulation regression |

#### New diversity contributed by NetBox

NetBox adds:

- an upstream Compose bind-mounted configuration directory whose usable baseline is also copied into the published image;
- a case where **image inspection reverses the naive conclusion drawn from the Compose mount**;
- two Redis-compatible containers with explicitly different state semantics;
- a queue-security lesson distinct from cache security;
- an upstream-native migration + superuser lifecycle that should not be wrapped merely because another Golden used helpers;
- an HTTP readiness failure caused only by `localhost` vs `127.0.0.1` Host-header semantics;
- a later secret transport/interpolation failure class that must not be confused with application secret-format requirements;
- the first benchmark where an older Skill release reached runtime acceptance faster than its successor.

#### Bias audit

##### 1. Golden-template bias

**Risk:** start from a Golden instead of the target upstream.

**Rule:** unchanged. Goldens are regression oracles, not architecture templates.

##### 2. Service-count bias

**Risk:** fewer services is assumed cleaner or more mature.

**NetBox counterexample:** two Valkey containers are not redundant; they have different semantics.

**Rule:** provenance and role semantics beat count.

##### 3. Toolkit/config-copy bias

**Risk:** every upstream deployment repository file/mount is copied into the One-Click.

**NetBox counterexample:** `/etc/netbox/config` is baked into the image even though upstream Compose overlays it from the repository.

**Rule:** inspect Dockerfile/image before reproducing a repository bind mount.

##### 4. Managed-file convenience bias

**Risk:** inline managed files become the default way to make a One-Click self-contained.

**NetBox counterexample:** only one tiny override is needed because the image already contains the baseline.

**Rule:** managed files require an artifact gap or explicit override need.

##### 5. Bootstrap-helper bias

**Risk:** previous one-shot successes encourage a helper everywhere.

**NetBox counterexample:** native `SUPERUSER_*` lifecycle is sufficient and idempotent for the selected identity.

**Rule:** prove the native primitive is insufficient before adding a helper.

##### 6. Migration-sidecar bias

**Risk:** explicit migration containers look cleaner than image-owned migration.

**NetBox counterexample:** image entrypoint already checks and applies migrations.

**Rule:** migration ownership is application-specific.

##### 7. Same-technology merge bias

**Risk:** same engine means same service/state.

**NetBox counterexample:** tasks Valkey and cache Valkey have different durability and operational semantics.

**Rule:** classify role/lifecycle/security before merging.

##### 8. Redis-is-cache bias

**Risk:** every Redis-compatible service is disposable.

**NetBox counterexample:** tasks queue is in-flight/durability-sensitive and AOF-backed.

**Rule:** preserve role-based durability.

##### 9. Queue-security minimization bias

**Risk:** a queue is treated as less sensitive than SQL because it is not the system of record.

**NetBox lesson:** write access can influence worker-executed work.

**Rule:** queue writers are part of the trust boundary.

##### 10. Health-host normalization bias

**Risk:** replace `localhost` with `127.0.0.1` as an equivalent loopback cleanup.

**NetBox RC1 counterexample:** Host-header validation returned HTTP 400 for `127.0.0.1` while `localhost` was accepted.

**Rule:** local address equivalence at TCP does not imply HTTP Host equivalence.

##### 11. Secret-length-only bias

**Risk:** generator chosen only by entropy/length.

**Overleaf lesson:** encoded format can matter.

**NetBox extension:** transport serialization can matter independently of application format.

**Rule:** validate application contract + generator contract + transport contract.

##### 12. Symbol-ban overcorrection bias

**Risk:** after a `$` interpolation failure, symbol-capable generators are banned globally.

**Counterevidence:** accepted NetBox Golden uses symbol-capable secrets successfully; many applications legitimately allow/require symbols.

**Rule:** a transport failure is contextual evidence, not a universal application rule.

##### 13. Platform-generated-metadata blindness

**Risk:** service names are assumed cosmetic.

**Overleaf counterexample:** platform-generated `SHARELATEX` environment names broke startup.

**NetBox scope check:** no evidence that NetBox needs a service-name workaround.

**Rule:** inspect generated metadata only where it can affect the application; do not rename services prophylactically.

##### 14. Fail-closed discovery bias

**Risk:** mutation-safety rules are generalized into distrust of known-good upstream primitives.

**NetBox counterexample:** native superuser bootstrap should be preserved.

**Rule:** fail closed on unsafe mutation under unknown state, not on current-upstream architecture discovery.

##### 15. Golden-similarity bias

**Risk:** high architectural similarity to another Golden is treated as contamination evidence.

**NetBox RC5 observation:** similarity to CKAN was `0.62`, but all NetBox services were independently upstream-proven.

**Rule:** similarity is a question generator. With matching upstream capability provenance it is informational, not a veto.

##### 16. Architecture-inflation bias

**Risk:** more accumulated knowledge means more helpers, checks and services.

**NetBox result:** the correct topology remains five services and native lifecycle ownership.

**Rule:** knowledge should improve evidence selection, not increase runtime surface by default.

##### 17. Repository-config-as-runtime-truth bias

**Risk:** repository mount layout is assumed identical to image runtime requirement.

**NetBox counterexample:** configuration baseline exists inside image.

**Rule:** distinguish repository composition from image contents.

##### 18. Image-baked-config blindness

**Risk:** a self-contained image primitive is ignored because upstream development Compose overlays it.

**Rule:** inspect Dockerfile/image before externalizing configuration.

##### 19. Whole-volume classification bias

**Risk:** all files under one application volume get the same backup class.

**NetBox extension:** media, reports and scripts can have different operator value even though they are all application filesystem paths.

**Rule:** classify meaningful subpaths separately.

##### 20. API-token generator bias

**Risk:** because the deployment has Magic Variables, an application API token is invented with one.

**NetBox counterexample:** API token is application-issued and depends on the durable token pepper.

**Rule:** deployment secret != application credential.

##### 21. Newer-is-better bias

**Risk:** a newer Skill release is assumed to reason better solely because it contains more Goldens.

**NetBox regression:** RC5 reached accepted behavior in two iterations while RC6 did not after four.

**Rule:** compare the newer path with the older successful path and identify which new rule caused unnecessary divergence.

##### 22. Knowledge accumulation regression

**Risk:** each new Golden adds correct local knowledge but increases prescriptiveness, cognitive branching or unrelated safety mechanisms for new targets.

**First explicit test:** NetBox.

**Required property:** adding a Golden must improve or preserve performance on architecture classes that were already solvable.

**Rule:**

> Adding new Golden knowledge must not reduce the Skill's ability to rediscover a target from current upstream evidence.

##### 23. Overleaf-rule overgeneralization

**Risk:** Overleaf-specific lessons become universal constraints.

**Audit result:** most Overleaf additions are legitimate general questions, not NetBox defects. RC7 scopes them causally:

- Toolkit rule applies when a Toolkit exists;
- service-name rule applies when platform-derived names affect runtime;
- true-Base64 rule applies when upstream requires encoded Base64 semantics;
- fail-closed admin rule applies when a custom mutation helper manages persisted identity;
- nested `$` rule applies at Compose/shell/inner-language boundaries.

##### 24. Primitive-replacement bias

**Risk:** an agent wraps a working image primitive simply because another benchmark needed a workaround.

**NetBox rule:** native migration/bootstrap/config primitives win until a proven gap exists.

##### 25. Recovery-by-database-only bias

**Risk:** PostgreSQL backup alone is called NetBox recovery.

**NetBox state:** database plus relevant media/operator-authored scripts/reports and stable secrets/config may matter; task queue should be handled coherently rather than blindly replayed.

**Rule:** recovery follows the state inventory, not the primary database alone.

#### Meta-regression benchmark

Permanent evaluation prompt:

> Given a target where an older Skill release produced a runtime-accepted candidate faster than the newer Skill, the newer Skill must compare its reasoning with the older successful path and identify which newly introduced rule caused unnecessary divergence.

Expected behavior:

1. retrieve the exact older accepted artifact;
2. keep the newer release as baseline unless instructed otherwise;
3. diff the rule corpus between releases;
4. classify new rules as causal, scope-risk or unrelated;
5. preserve legitimate newer knowledge;
6. narrow over-generalized rules;
7. add a regression test protecting the older-solvable architecture class;
8. rerun all existing Golden invariants unchanged.

Forbidden shortcut:

```text
newer version = necessarily better reasoning
```

#### Monotonic evolution principle

> Skill evolution is monotonic only when new knowledge improves or preserves performance on previously solvable architecture classes. A newer Skill that regresses on a target solved by an older release must treat that divergence as a first-class regression.

This does not mean iteration count is a universal quality metric. It means a **known regression on the same target, evidence and acceptance goal** must be investigated instead of dismissed as normal variance.

<!-- END PORTABLE RESOURCE: references/ten-benchmark-audit.md -->

<!-- BEGIN PORTABLE RESOURCE: references/twelve-benchmark-audit.md -->
<!-- SOURCE SHA256: 43c8bd64fb64c3f1b9ddeb08b3a7b9c5c5411f5db129aa5a7484b71933158a1a -->
<!-- EMBEDDED SHA256: b8221953bd7b850664a8f7b10e7194026e66885debcecd0a0df8bde040b4296d -->

## Portable resource: `references/twelve-benchmark-audit.md`

### Twelve-benchmark audit — Golden corpus through OpenSPP #12

#### Corpus

1. KoboToolbox V19.3
2. CKAN 2.12
3. OpenMRS 3.7.1
4. OpenEMR 8.3.0
5. ODK Central v2026.2.4
6. Frappe Framework v16.32.0
7. ERPNext v16.33.0
8. Mem0 v2.0.19
9. Overleaf Community Edition 6.2.2
10. NetBox 4.6.9
11. Baserow 2.3.3
12. OpenSPP V2 2026.08

OpenSPP #12 is the exact accepted RC8 bytes, SHA-256:

```text
f00a8755fa2be8e8b1f50970978ae1b57c1877093c2a35108edf35a675d4587b
```

#### Promotion basis

OpenSPP was not promoted from static validation, green containers, or partial RC evidence. RC8 was explicitly accepted by the operator after the operator reported the requested tests complete, persistence correct, and no remaining functional problem.

Evidence classification:

- raw logs/screenshots exist for several intermediate failures and corrections;
- the final full acceptance suite is **operator-confirmed runtime evidence**;
- granular raw output for every final sub-gate is not bundled here.

Therefore the Golden can protect the accepted runtime artifact and demonstrated causal lessons, but it must not be used to claim untested HA/performance/advanced-security properties.

#### Diversity contribution

OpenSPP adds new corpus coverage without becoming an “average Odoo stack”:

- source-built application whose top-level source is pinned but transitive Git dependencies float;
- modular product activation distinct from Odoo framework health;
- shared initialization state across web and dedicated queue worker;
- application-semantic gateway behind Coolify's Internet edge;
- managed-file identity/provenance failure;
- effective Coolify primitive support differing from Compose-spec capability;
- target-config `$variables` crossing Compose/Coolify/shell/envsubst/application-parser layers;
- intentional bootstrap/admin DB role distinct from runtime DB role;
- coherent DB + filestore recovery set.

#### Cross-case reinforcement

##### NetBox + OpenSPP

Both reinforce that current upstream/runtime evidence outranks a generic “modernized” rewrite. NetBox protects image-baked configuration and native lifecycle; OpenSPP protects a source/dependency contract and effective managed-file provenance. The combined lesson is **inspect the actual effective artifact/lifecycle before adding replacement machinery**.

##### Overleaf + OpenSPP

Overleaf required `$$set` in a Compose command/nested JavaScript path. OpenSPP required `$remote_addr` in a Coolify managed file. These are intentionally opposite source representations because the transport layers differ.

The generalized rule is not a dollar token. It is:

```text
model every interpretation/serialization layer
-> derive the source representation that yields the required runtime token
```

##### Frappe/ERPNext + OpenSPP

All reinforce:

```text
runtime/framework exists
!=
selected product is activated and usable
```

OpenSPP extends the lesson from sibling product/site activation to module-bundle activation and startup gating.

##### Baserow + OpenSPP

Both reinforce semantic gateway preservation and the distinction between internal listener behavior and public browser origin. Neither proves that every app needs a gateway.

#### Bias audit

OpenSPP must **not** make the Skill more likely to add:

- Nginx;
- PostGIS;
- backup sidecars;
- queue workers;
- multiple DB roles;
- XML compatibility shims;
- managed files;
- source builds;
- single-database redirects.

For every such mechanism on a new target, require current-target provenance.

The NetBox monotonic-learning rule remains mandatory:

> Adding new Golden knowledge must not reduce the Skill's ability to rediscover a target from current upstream evidence.

#### Corpus status

The twelve Goldens are regression oracles. They are not architecture templates, and their union is not a recommended stack.

<!-- END PORTABLE RESOURCE: references/twelve-benchmark-audit.md -->

<!-- BEGIN PORTABLE RESOURCE: scripts/audit_compose.py -->
<!-- SOURCE SHA256: f0a2cf1bcae9db905c4cdcf15a016ab78eca3249d20261694cbe631bd82ef9e3 -->
<!-- EMBEDDED SHA256: 8041b877e406632857a0a0b461c5e8ebd254e94c5180799b3bdb8b6e435255ec -->

## Portable resource: `scripts/audit_compose.py`

````python
#!/usr/bin/env python3
"""Conservative static audit for Docker Compose files intended for Coolify.

The audit is deliberately architecture-agnostic. It can identify malformed wiring,
magic-variable risks and evidence gaps, but it cannot prove that a dependency belongs
in the current application without upstream context and live acceptance evidence.
"""

from __future__ import annotations

import argparse
import collections
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable

try:
    import yaml
except Exception as exc:  # pragma: no cover
    print(f"ERROR: PyYAML is required to run this script: {exc}", file=sys.stderr)
    raise SystemExit(2)

STATEFUL_HINTS = {
    "postgres": ("postgres", "postgis", "timescale"),
    "mysql": ("mysql", "mariadb"),
    "mongo": ("mongo",),
    "redis": ("redis",),
    "clickhouse": ("clickhouse",),
    "rabbitmq": ("rabbitmq",),
    "minio": ("minio",),
    "elasticsearch": ("elasticsearch", "opensearch"),
}

SENSITIVE_NAME = re.compile(
    r"(?:PASSWORD|PASSWD|SECRET|TOKEN|API[_-]?KEY|PRIVATE[_-]?KEY|ENCRYPTION|SIGNING|CREDENTIAL)", re.I
)
USERNAME_NAME = re.compile(r"(?:^|[_-])(?:USER|USERNAME)(?:$|[_-])", re.I)
PASSWORD_NAME = re.compile(r"(?:PASSWORD|PASSWD|PASS)(?:$|[_-])", re.I)
LOCAL_GENERATABLE_SECRET = re.compile(
    r"(?:DB|DATABASE|MYSQL|MARIADB|POSTGRES|ROOT|ADMIN|SUPERUSER|APP|SESSION|JWT|ENCRYPTION|SIGNING)[_-]?(?:PASSWORD|PASS|SECRET|KEY)$|^(?:SECRET_KEY|APP_SECRET|SESSION_SECRET|JWT_SECRET|ENCRYPTION_KEY|SIGNING_KEY)$",
    re.I,
)
EXTERNAL_SECRET_HINT = re.compile(r"(?:SMTP|MAIL|OAUTH|OIDC|SAML|CLIENT|AWS|S3|STRIPE|GITHUB|GOOGLE|AZURE|WEBHOOK|PROVIDER)", re.I)
KNOWN_EXTERNAL_PROVIDER_SECRET_KEYS = re.compile(r"^(?:OPENAI|ANTHROPIC|GOOGLE|GEMINI|COHERE|MISTRAL|GROQ|TOGETHER|VOYAGE|PINECONE|AWS|AZURE)_[A-Z0-9_]*(?:API_KEY|TOKEN|SECRET|ACCESS_KEY)$", re.I)
KNOWN_EXTERNAL_PROVIDER_HEALTH_HOSTS = ("api.openai.com", "api.anthropic.com", "generativelanguage.googleapis.com")

REVISION_COMMENT = re.compile(r"^\s*#\s*(?:coolify\s+template\s+revision|compose\s+revision|compose\s+version)\b", re.I)
HISTORICAL_COMMENT = re.compile(r"^\s*#.*\bV\d+(?:\.\d+)*\b.*\b(?:fix|workaround|preflight|legacy|temporary|current)\b", re.I)
CANONICAL_URL_NAME = re.compile(r"(?:SITE_URL|PUBLIC_URL|BASE_URL|ROOT_URL|EXTERNAL_URL|CANONICAL_URL|ORIGIN)$", re.I)
PORT_QUALIFIED_MAGIC = re.compile(r"SERVICE_(?:URL|FQDN)_[A-Z0-9_.-]+_\d+(?:[^A-Z0-9_]|$)", re.I)
EMBEDDED_ESCAPED_SHELL_VAR = re.compile(r"\$\$\{[A-Za-z_][A-Za-z0-9_]*\}")
MANAGED_NGINX_DOUBLE_DOLLAR = re.compile(r"\$\$(?:remote_addr|remote_user|time_local|request|status|body_bytes_sent|http_referer|http_user_agent|http_upgrade|connection_upgrade|binary_remote_addr|host|proxy_add_x_forwarded_for)\b")
NATIVE_NGINX_DOLLAR = re.compile(r"(?<!\$)\$(?:remote_addr|remote_user|time_local|request|status|body_bytes_sent|http_referer|http_user_agent|http_upgrade|connection_upgrade|binary_remote_addr|host|proxy_add_x_forwarded_for)\b")
ENVSUBST_WHITELIST = re.compile(r"\benvsubst\s+([\'\"])(?P<vars>[^\n]*?)\1", re.I)
VAR_REF = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_.-]*)[^}]*\}|\$([A-Za-z_][A-Za-z0-9_.-]*)")

# Current Coolify-documented generator families. Parse longest complete TYPE first:
# SERVICE_PASSWORD_64_FOO => type PASSWORD_64, identifier FOO (not PASSWORD + 64_FOO).
_DOCUMENTED_RANDOM_TYPES = {
    "LOWERCASEUSER",
    "USER",
    "PASSWORD",
    "PASSWORD_64",
    "PASSWORDWITHSYMBOLS",
    "PASSWORDWITHSYMBOLS_64",
    "BASE64",
    "BASE64_32",
    "BASE64_64",
    "BASE64_128",
    "REALBASE64",
    "REALBASE64_32",
    "REALBASE64_64",
    "REALBASE64_128",
    "HEX_32",
    "HEX_64",
    "HEX_128",
    "SUPABASEANON",
    "SUPABASESERVICE",
}
CREDENTIAL_MAGIC_TYPES = tuple(sorted(_DOCUMENTED_RANDOM_TYPES, key=len, reverse=True))
PASSWORD_MAGIC_TYPES = {t for t in CREDENTIAL_MAGIC_TYPES if t.startswith("PASSWORD")}
USER_MAGIC_TYPES = {"USER", "LOWERCASEUSER"}
BASE64_FAKE_TYPES = {"BASE64", "BASE64_32", "BASE64_64", "BASE64_128"}
REALBASE64_TYPES = {"REALBASE64", "REALBASE64_32", "REALBASE64_64", "REALBASE64_128"}
HEX_TYPES = {"HEX_32", "HEX_64", "HEX_128"}
JWT_MAGIC_TYPES = {"SUPABASEANON", "SUPABASESERVICE"}

# Frappe RC1->RC3 runtime + coollabsio/coolify#11043 demonstrated that
# underscore-containing credential identifiers could remain blank in Docker Compose
# Empty while alphanumeric identifiers generated correctly. Current docs do not state
# a timeless global underscore ban, so this stays REVIEW REQUIRED, not universal ERROR.
CONSERVATIVE_CREDENTIAL_IDENTIFIER = re.compile(r"^[A-Za-z0-9]+$")
REQUIRED_MAGIC_REF = re.compile(r"\$\{(?P<name>SERVICE_[A-Za-z0-9_.-]+):\?(?P<message>[^}]*)\}")

DB_USER_KEYS = ("POSTGRES_USER", "MYSQL_USER", "MARIADB_USER")
DB_PASSWORD_KEYS = ("POSTGRES_PASSWORD", "MYSQL_PASSWORD", "MARIADB_PASSWORD")


@dataclass(frozen=True)
class MagicVar:
    name: str
    kind: str
    identifier: str
    port: int | None = None

    @property
    def is_public(self) -> bool:
        return self.kind in {"URL", "FQDN"}

    @property
    def is_credential(self) -> bool:
        return self.kind in CREDENTIAL_MAGIC_TYPES

    @property
    def identifier_is_conservative(self) -> bool:
        if self.is_public or self.kind == "NAME":
            return True
        return bool(CONSERVATIVE_CREDENTIAL_IDENTIFIER.fullmatch(self.identifier))


def service_image(service: dict[str, Any]) -> str:
    return str(service.get("image") or "")


def stateful_family(service_name: str, service: dict[str, Any]) -> str | None:
    haystack = f"{service_name} {service_image(service)}".lower()
    for family, tokens in STATEFUL_HINTS.items():
        if any(token in haystack for token in tokens):
            return family
    return None


def is_stateful(service_name: str, service: dict[str, Any]) -> bool:
    return stateful_family(service_name, service) is not None


def is_one_shot(service: dict[str, Any]) -> bool:
    """Conservative lifecycle hint for explicit one-shot helpers."""
    restart = str(service.get("restart", "")).strip().lower()
    return restart in {"no", "none", "false"} or service.get("exclude_from_hc") is True


def normalize_environment(env: Any) -> dict[str, str | None]:
    if isinstance(env, dict):
        return {str(k): None if v is None else str(v) for k, v in env.items()}
    result: dict[str, str | None] = {}
    if isinstance(env, list):
        for item in env:
            if not isinstance(item, str):
                continue
            if "=" in item:
                key, value = item.split("=", 1)
                result[key] = value
            else:
                result[item] = None
    return result


def mounted_targets(service: dict[str, Any]) -> list[str]:
    targets: list[str] = []
    volumes = service.get("volumes") or []
    if not isinstance(volumes, list):
        return targets
    for item in volumes:
        if isinstance(item, str):
            parts = item.split(":")
            if len(parts) >= 2:
                targets.append(parts[1])
        elif isinstance(item, dict):
            target = item.get("target")
            if target:
                targets.append(str(target))
    return targets


def parse_magic(name: str) -> MagicVar | None:
    """Parse a documented Coolify magic variable name conservatively."""
    if not name.startswith("SERVICE_"):
        return None
    body = name[len("SERVICE_") :]

    for kind in ("URL", "FQDN"):
        prefix = kind + "_"
        if body.startswith(prefix):
            rest = body[len(prefix) :]
            if not rest:
                return None
            port = None
            identifier = rest
            m = re.match(r"^(.*)_([0-9]+)$", rest)
            if m:
                identifier, p = m.groups()
                if not identifier:
                    return None
                port = int(p)
            return MagicVar(name=name, kind=kind, identifier=identifier, port=port)

    if body.startswith("NAME_") and len(body) > len("NAME_"):
        return MagicVar(name=name, kind="NAME", identifier=body[len("NAME_") :])

    for kind in CREDENTIAL_MAGIC_TYPES:
        prefix = kind + "_"
        if body.startswith(prefix) and len(body) > len(prefix):
            return MagicVar(name=name, kind=kind, identifier=body[len(prefix) :])
    return None


def magic_expected_format(mv: MagicVar) -> str:
    """Human-readable format for the Magic Variable Ledger."""
    formats = {
        "URL": "public URL with scheme; optional proxy-target port",
        "FQDN": "public hostname; optional proxy-target port",
        "NAME": "Compose service name",
        "USER": "random alphanumeric username (documented 16 chars)",
        "LOWERCASEUSER": "random lowercase alphanumeric username (documented 16 chars)",
        "PASSWORD": "random password without symbols",
        "PASSWORD_64": "random password without symbols, 64 chars",
        "PASSWORDWITHSYMBOLS": "random password with symbols",
        "PASSWORDWITHSYMBOLS_64": "random password with symbols, 64 chars",
        "BASE64": "random alphanumeric string, not Base64",
        "BASE64_32": "random alphanumeric string, not Base64, 32 chars",
        "BASE64_64": "random alphanumeric string, not Base64, 64 chars",
        "BASE64_128": "random alphanumeric string, not Base64, 128 chars",
        "REALBASE64": "Base64 encoding of random bytes",
        "REALBASE64_32": "Base64 encoding of 32 random bytes",
        "REALBASE64_64": "Base64 encoding of 64 random bytes",
        "REALBASE64_128": "Base64 encoding of 128 random bytes",
        "HEX_32": "random hexadecimal string, 32 chars",
        "HEX_64": "random hexadecimal string, 64 chars",
        "HEX_128": "random hexadecimal string, 128 chars",
        "SUPABASEANON": "Supabase anon JWT (requires SERVICE_PASSWORD_JWT)",
        "SUPABASESERVICE": "Supabase service_role JWT (requires SERVICE_PASSWORD_JWT)",
    }
    return formats.get(mv.kind, "documented generated value")


def is_required_magic_reference(value: str | None, magic_name: str) -> bool:
    if not value:
        return False
    return any(m.group("name") == magic_name for m in REQUIRED_MAGIC_REF.finditer(value))


def infer_magic_purpose(env_key: str, mv: MagicVar) -> str:
    ku = env_key.upper()
    if mv.kind == "URL":
        return "public URL / proxy routing"
    if mv.kind == "FQDN":
        return "public hostname / proxy routing"
    if mv.kind == "NAME":
        return "Compose service identity"
    if re.search(r"DB|DATABASE|MYSQL|MARIADB|POSTGRES|SQL", ku):
        if mv.kind in USER_MAGIC_TYPES:
            return "database username"
        if mv.kind in PASSWORD_MAGIC_TYPES:
            return "database credential"
    if re.search(r"ADMIN|SUPERUSER", ku) and mv.kind in PASSWORD_MAGIC_TYPES:
        return "bootstrap/admin credential"
    if re.search(r"JWT|SESSION|ENCRYPTION|SIGNING|SECRET|TOKEN|KEY", ku):
        return "application secret/key"
    if mv.kind in USER_MAGIC_TYPES:
        return "generated username"
    if mv.is_credential:
        return "generated credential/random value"
    return "generated service value"


def magic_binding_map(doc: dict[str, Any]) -> dict[tuple[str, str], tuple[str, ...]]:
    """Map service/env-key bindings to credential magic identities for rename review."""
    out: dict[tuple[str, str], tuple[str, ...]] = {}
    for service_name, service in (doc.get("services") or {}).items():
        if not isinstance(service, dict):
            continue
        for key, value in normalize_environment(service.get("environment")).items():
            names = tuple(sorted({mv.name for mv in magic_refs(value) if mv.is_credential}))
            if names:
                out[(str(service_name), key)] = names
    return out


def referenced_vars(value: str | None) -> list[str]:
    if not value:
        return []
    out: list[str] = []
    for m in VAR_REF.finditer(value):
        out.append(m.group(1) or m.group(2))
    return out


def magic_refs(value: str | None) -> list[MagicVar]:
    return [mv for v in referenced_vars(value) if (mv := parse_magic(v)) is not None]


def normalized_service_id(value: str) -> str:
    return re.sub(r"[-.]", "_", value).upper()


def possible_service_ids(service_name: str) -> set[str]:
    up = service_name.upper()
    return {up, normalized_service_id(service_name), up.replace("_", "-")}


def service_target_matches(identifier: str, services: dict[str, Any]) -> bool:
    return any(identifier.upper() in possible_service_ids(name) or normalized_service_id(identifier) == normalized_service_id(name) for name in services)


def magic_key_role_issue(key: str, mv: MagicVar) -> str | None:
    if USERNAME_NAME.search(key) and not PASSWORD_NAME.search(key) and mv.kind in PASSWORD_MAGIC_TYPES:
        return f"{key} is username-like but consumes password generator {mv.name}"
    if PASSWORD_NAME.search(key) and mv.kind in USER_MAGIC_TYPES:
        return f"{key} is password-like but consumes username generator {mv.name}"
    if "BASE64" in key.upper() and mv.kind in BASE64_FAKE_TYPES:
        return f"{key} appears to require Base64 but {mv.name} is documented as a random non-Base64 string; verify upstream and use SERVICE_REALBASE64_* if true Base64 is required"
    if re.search(r"(?:^|[_-])HEX(?:$|[_-])", key, re.I) and mv.kind not in HEX_TYPES:
        return f"{key} is hex-like but consumes {mv.name}; verify required encoding"
    return None


def find_first_magic(env: dict[str, str | None], keys: Iterable[str]) -> MagicVar | None:
    for key in keys:
        for mv in magic_refs(env.get(key)):
            if mv.is_credential:
                return mv
    return None


def connects_to_db(env: dict[str, str | None], db_name: str) -> bool:
    target = db_name.lower()
    for key, value in env.items():
        if not value:
            continue
        low = value.lower()
        if key.upper().endswith(("DB_HOST", "DB_HOSTNAME", "DATABASE_HOST", "CONNECTION_SERVER")) and low == target:
            return True
        if low == target or f"@{target}:" in low or f"://{target}:" in low or f"//{target}/" in low:
            return True
    return False


def semantic_user_magic(env: dict[str, str | None]) -> MagicVar | None:
    # Prefer DB-specific username variables over unrelated account/admin usernames.
    preferred = [(k, v) for k, v in env.items() if USERNAME_NAME.search(k) and re.search(r"DB|DATABASE|POSTGRES|MYSQL|MARIADB|SQL", k, re.I)]
    fallback = [(k, v) for k, v in env.items() if USERNAME_NAME.search(k) and not PASSWORD_NAME.search(k)]
    for key, value in preferred + fallback:
        for mv in magic_refs(value):
            if mv.kind in USER_MAGIC_TYPES:
                return mv
    return None


def semantic_password_magic(env: dict[str, str | None], db_name: str | None = None) -> MagicVar | None:
    # Prefer connection/database password fields and URLs. Ignore bootstrap/admin passwords.
    for key, value in env.items():
        ku = key.upper()
        if PASSWORD_NAME.search(key) and re.search(r"DB|DATABASE|POSTGRES|MYSQL|MARIADB|SQL", key, re.I) and not re.search(r"ADMIN|SUPERUSER|ROOT", ku):
            for mv in magic_refs(value):
                if mv.kind in PASSWORD_MAGIC_TYPES:
                    return mv
    if db_name:
        target = db_name.lower()
        for key, value in env.items():
            if not value:
                continue
            low = value.lower()
            if target in low and ("://" in low or "database" in key.lower() or "sql" in key.lower()):
                for mv in magic_refs(value):
                    if mv.kind in PASSWORD_MAGIC_TYPES:
                        return mv
    return None


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("compose", type=Path)
    parser.add_argument("--strict", action="store_true", help="Treat selected hygiene warnings as errors.")
    parser.add_argument(
        "--previous-compose",
        type=Path,
        help="Optional previous candidate/baseline. Reports Magic Variable identity changes on the same service/env binding as potentially breaking persistent deployment state.",
    )
    args = parser.parse_args()

    raw = args.compose.read_text(encoding="utf-8")
    try:
        doc = yaml.safe_load(raw)
    except Exception as exc:
        print(f"ERROR YAML_PARSE: {exc}")
        return 1

    errors: list[str] = []
    warnings: list[str] = []
    reviews: list[str] = []
    infos: list[str] = []

    if not isinstance(doc, dict):
        errors.append("COMPOSE_ROOT: root YAML node must be a mapping")
        doc = {}

    services = doc.get("services")
    if not isinstance(services, dict) or not services:
        errors.append("SERVICES: no Compose services found")
        services = {}

    if "version" in doc:
        warnings.append("TOP_LEVEL_VERSION: modern Compose spec does not require the legacy top-level 'version' key")

    configs = doc.get("configs")
    if isinstance(configs, dict):
        for config_name, config_def in configs.items():
            if isinstance(config_def, dict) and "content" in config_def:
                reviews.append(
                    f"PLATFORM_PRIMITIVE_SUPPORT: top-level config {config_name!r} uses Compose configs.content. "
                    "Docker Compose specification support does not prove end-to-end support in the current Coolify Service parser/model/deployment path; verify this exact target version before relying on it."
                )

    revision_markers = [line for line in raw.splitlines() if REVISION_COMMENT.search(line)]
    if len(revision_markers) > 1:
        warnings.append(f"REVISION_DUPLICATION: found {len(revision_markers)} Compose/template revision markers; keep one")
    historical = [line.strip() for line in raw.splitlines() if HISTORICAL_COMMENT.search(line)]
    if historical:
        warnings.append("HISTORICAL_COMMENTS: runtime Compose contains revision-diary comments such as: " + "; ".join(historical[:3]))

    # Track documented magic-variable occurrences by service for reuse reporting and ledger.
    magic_services: dict[str, set[str]] = collections.defaultdict(set)
    magic_purposes: dict[str, set[str]] = collections.defaultdict(set)
    magic_required: dict[str, bool] = collections.defaultdict(bool)
    magic_declarations: dict[str, set[str]] = collections.defaultdict(set)
    malformed_magic_keys: list[tuple[str, str]] = []
    service_envs: dict[str, dict[str, str | None]] = {}

    for name, value in services.items():
        if not isinstance(value, dict):
            errors.append(f"{name}: service definition is not a mapping")
            continue
        image = service_image(value)
        if image:
            image_tail = image.rsplit("/", 1)[-1]
            if ":" not in image_tail and "@sha256:" not in image:
                warnings.append(f"{name}: image '{image}' has no explicit tag/digest")
            elif image.endswith(":latest"):
                warnings.append(f"{name}: production image uses mutable ':latest' tag")

        if value.get("build") is not None and image:
            pull_policy = str(value.get("pull_policy") or "").strip().lower()
            if not pull_policy:
                reviews.append(
                    f"{name}: declares both image: and build: without pull_policy. For locally built/non-registry images, verify the current Coolify Service pull/build sequence; demonstrated versions may pre-pull before `up --build`, so `pull_policy: never` can be required for the exact source-built service."
                )
            elif pull_policy == "never":
                infos.append(
                    f"{name}: build + image uses pull_policy=never; verify this remains required by the target Coolify version rather than treating it as a universal Docker Compose rule"
                )

        if value.get("privileged") is True:
            warnings.append(f"{name}: privileged=true increases host risk")
        if str(value.get("network_mode", "")).lower() == "host":
            warnings.append(f"{name}: network_mode=host bypasses normal Coolify proxy/network isolation")

        if "entrypoint" in value and value.get("entrypoint") in ([], {}):
            reviews.append(f"{name}: empty entrypoint override is valid in parts of the Compose ecosystem but has caused normalization/validation issues in a real Coolify editor path; verify the target Coolify version or use an explicit override when equivalent")

        volumes = value.get("volumes") or []
        if isinstance(volumes, list):
            for mount in volumes:
                if isinstance(mount, str) and "/var/run/docker.sock" in mount:
                    warnings.append(f"{name}: Docker socket is mounted; verify this is essential")
                if isinstance(mount, dict) and "/var/run/docker.sock" in str(mount.get("source", "")):
                    warnings.append(f"{name}: Docker socket is mounted; verify this is essential")
                if isinstance(mount, dict) and "content" in mount:
                    source = str(mount.get("source") or "")
                    target = str(mount.get("target") or "")
                    content = str(mount.get("content") or "")
                    suffix = Path(source or target).suffix.lower()
                    if mount.get("is_directory") is True and suffix:
                        warnings.append(f"{name}: managed content mount {target or source} looks like a file but is_directory=true")
                    elif suffix and mount.get("is_directory") is None:
                        reviews.append(f"{name}: managed content file {target or source} does not explicitly set is_directory=false; verify Coolify file-vs-directory rendering")
                    if suffix in {".sh", ".bash"} and EMBEDDED_ESCAPED_SHELL_VAR.search(content):
                        warnings.append(f"{name}: managed shell file {target or source} contains $${{VAR}} escaping; generated file content usually needs normal shell ${{VAR}} syntax")
                    if ("nginx" in (source + " " + target).lower() or suffix in {".conf", ".template"}) and MANAGED_NGINX_DOUBLE_DOLLAR.search(content):
                        reviews.append(
                            f"{name}: managed configuration file {target or source} contains Compose-style `$$` escaping for native Nginx variables. Managed-file transport can write content literally; build an interpolation/serialization layer map and verify the effective mounted file before deployment."
                        )
                    if suffix in {".sh", ".bash"} and "/docker-entrypoint.d/" in target and re.search(r"(?m)^\s*set\s+-[^\n]*u", content):
                        reviews.append(f"{name}: entrypoint hook {target} enables nounset; verify upstream executes rather than sources this hook")

        command_text = str(value.get("command") or "")
        if "envsubst" in command_text:
            whitelist = ENVSUBST_WHITELIST.search(command_text)
            managed_contents = []
            if isinstance(volumes, list):
                managed_contents = [
                    str(m.get("content") or "")
                    for m in volumes
                    if isinstance(m, dict) and "content" in m
                ]
            has_native_nginx_vars = any(NATIVE_NGINX_DOLLAR.search(c) for c in managed_contents)
            if has_native_nginx_vars and not whitelist:
                reviews.append(
                    f"{name}: command appears to run unrestricted envsubst while managed configuration contains native Nginx `$variables`. Prefer an explicit envsubst whitelist so application-language variables survive unchanged."
                )

        ports = value.get("ports") or []
        if is_stateful(name, value) and ports:
            warnings.append(f"{name}: stateful/internal service publishes host port(s): {ports}")

        if is_stateful(name, value):
            targets = mounted_targets(value)
            family = stateful_family(name, value)
            obvious_cache_only = family == "redis" and "cache" in name.lower()
            if not targets:
                if obvious_cache_only:
                    infos.append(
                        f"{name}: Redis cache has no persistent volume; this can be correct for disposable cache state, but confirm upstream persistence semantics"
                    )
                elif family == "redis":
                    reviews.append(
                        f"{name}: Redis-compatible service has no persistent volume. This can be correct for cache/broker/reconstructable semantics; classify the exact upstream role before adding AOF or persistence by analogy."
                    )
                else:
                    warnings.append(f"{name}: appears stateful but has no persistent volume target")
            if "healthcheck" not in value:
                if is_one_shot(value):
                    infos.append(f"{name}: stateful-looking service is explicitly one-shot and has no healthcheck; successful completion may be the correct lifecycle signal")
                else:
                    warnings.append(f"{name}: stateful service has no healthcheck")

        env = normalize_environment(value.get("environment"))
        service_envs[name] = env

        # Bare magic declarations and references.
        for key, val in env.items():
            if key.startswith("SERVICE_"):
                mv = parse_magic(key)
                if mv:
                    magic_services[mv.name].add(name)
                    magic_declarations[mv.name].add(name)
                    magic_purposes[mv.name].add(infer_magic_purpose(key, mv))
                    if mv.kind in {"URL", "FQDN"}:
                        # Current Coolify service-stack docs bind URL/FQDN IDs to the
                        # actual Compose service name (normalized for punctuation).
                        if normalized_service_id(mv.identifier) != normalized_service_id(name):
                            reviews.append(
                                f"{name}: bare {key} is service-bound but identifier {mv.identifier!r} does not match declaring Compose service {name!r}; use the actual public service identifier unless current Coolify behavior explicitly proves another mapping"
                            )
                        if not service_target_matches(mv.identifier, services):
                            reviews.append(f"{name}: {key} does not obviously map to a Compose service name; verify the generated domain targets the intended component")
                        infos.append(f"{name}: documented Coolify public magic variable detected: {key}")
                    elif mv.is_credential and not mv.identifier_is_conservative:
                        reviews.append(
                            f"{name}: {key} uses credential identifier {mv.identifier!r} containing separator/special characters. Frappe RC1->RC3 runtime plus coollabsio/coolify#11043 demonstrated blank generation for underscore-containing credential IDs in Docker Compose Empty. Current docs do not state a universal ban, so prefer a simple alphanumeric identifier or verify this exact target version."
                        )
                elif val in {None, ""}:
                    # A bare SERVICE_* key strongly looks like a generator request.
                    malformed_magic_keys.append((name, key))

            for var in referenced_vars(val):
                mv = parse_magic(var)
                if mv:
                    magic_services[mv.name].add(name)
                    magic_purposes[mv.name].add(infer_magic_purpose(key, mv))
                    magic_required[mv.name] = magic_required[mv.name] or is_required_magic_reference(val, mv.name)
                    issue = magic_key_role_issue(key, mv)
                    if issue:
                        warnings.append(f"{name}: {issue}")
                    if mv.is_credential and not mv.identifier_is_conservative:
                        reviews.append(
                            f"{name}: {key} references {mv.name} with credential identifier {mv.identifier!r} containing separator/special characters; Frappe runtime demonstrated this class can remain blank in Docker Compose Empty. Prefer an alphanumeric credential ID unless the target Coolify version is explicitly verified."
                        )
                elif var.startswith("SERVICE_"):
                    reviews.append(f"{name}: {key} references {var}, which is not a currently recognized documented magic family; verify it is an intentional user-defined variable rather than an invented magic variable")

        for service_name, magic_name in malformed_magic_keys:
            if service_name == name:
                errors.append(f"{name}: bare environment key {magic_name} looks like a Coolify magic declaration but does not match a currently documented family")

        # Sensitive/manual secret review.
        for key, val in env.items():
            if not SENSITIVE_NAME.search(key):
                continue
            if val is None or val == "":
                reviews.append(f"{name}: sensitive-looking variable {key} is empty/unset; confirm whether it is intentionally external or a missing generated secret")
                continue
            refs = referenced_vars(val)
            known_magic = any(parse_magic(v) and parse_magic(v).is_credential for v in refs)
            if known_magic:
                continue
            if val.startswith("${") or val.startswith("$"):
                if LOCAL_GENERATABLE_SECRET.search(key) and not EXTERNAL_SECRET_HINT.search(key):
                    reviews.append(f"{name}: {key} appears locally generatable but is left as a manual variable; verify upstream format and consider a documented Coolify generator if appropriate")
                continue
            if val.lower() in {"true", "false", "0", "1", "none"}:
                continue
            warnings.append(f"{name}: {key} looks sensitive and appears hard-coded in Compose")

        for key, val in env.items():
            if val and CANONICAL_URL_NAME.search(key) and PORT_QUALIFIED_MAGIC.search(val):
                warnings.append(f"{name}: canonical/public URL variable {key} is derived from a port-qualified Coolify magic variable; verify the routing port/path is intended in the browser-visible origin")

        # Secret-shaped values can have external issuers. A Coolify random string does not
        # create a credential at an external provider. Keep this conservative and REVIEW-only.
        for key, val in env.items():
            if not KNOWN_EXTERNAL_PROVIDER_SECRET_KEYS.match(key):
                continue
            mrefs = [mv for mv in magic_refs(val) if mv.is_credential]
            if mrefs:
                reviews.append(
                    f"{name}: {key} appears to be an external-provider-issued credential but is mapped to Coolify-generated {mrefs[0].name}; verify provider/operator issuance instead of fabricating a random secret"
                )

        health = value.get("healthcheck") or {}
        if isinstance(health, dict):
            health_text_all = str(health).lower()
            if any(host in health_text_all for host in KNOWN_EXTERNAL_PROVIDER_HEALTH_HOSTS):
                reviews.append(f"{name}: periodic healthcheck depends on an external AI provider; verify this is intentional and cost/reliability-safe rather than a local readiness probe")
            test = health.get("test")
            test_text = " ".join(str(x) for x in test) if isinstance(test, list) else str(test or "")
            if "/proc/1/cmdline" in test_text:
                warnings.append(f"{name}: healthcheck matches /proc/1/cmdline; validate this brittle process-string probe against actual runtime worker evidence")
            host_guard_keys = {"ALLOWED_HOSTS", "DJANGO_ALLOWED_HOSTS", "TRUSTED_HOSTS", "TRUSTED_HOST", "HOST_WHITELIST"}
            if "http://127.0.0.1" in test_text and any(k in env for k in host_guard_keys):
                reviews.append(
                    f"{name}: healthcheck uses 127.0.0.1 while host-validation configuration is present; localhost and 127.0.0.1 can reach the same socket but send different HTTP Host values. Preserve/verify the upstream probe hostname before normalizing it."
                )
            # Baserow Golden #11 generalized this beyond explicit ALLOWED_HOSTS:
            # a local root request can enter tenant/domain/product routing even when the
            # socket is healthy. Keep this REVIEW-only because many apps legitimately
            # use `/`; the finding asks for Host/path/router evidence rather than a rewrite.
            if re.search(r"https?://(?:127\.0\.0\.1|localhost)(?::[0-9]+)?/(?:\s|$)", test_text):
                reviews.append(
                    f"{name}: healthcheck probes a local root path. Verify Host + path + application-router semantics before treating a local 4xx as service failure; use a dedicated liveness endpoint when root routing is tenant/domain-sensitive."
                )

    # Report good reuse of a generated credential across service boundaries.
    for magic_name, consumers in sorted(magic_services.items()):
        mv = parse_magic(magic_name)
        if mv and mv.is_credential and len(consumers) >= 2:
            infos.append(f"{magic_name}: one generated value is reused across services {', '.join(sorted(consumers))}")

    # Conservative DB producer/consumer mismatch heuristic.
    for db_name, db_service in services.items():
        if not isinstance(db_service, dict) or stateful_family(db_name, db_service) not in {"postgres", "mysql"}:
            continue
        db_env = service_envs.get(db_name, {})
        db_user_mv = find_first_magic(db_env, DB_USER_KEYS)
        db_pass_mv = find_first_magic(db_env, DB_PASSWORD_KEYS)
        if not db_pass_mv:
            continue
        for consumer_name, cenv in service_envs.items():
            if consumer_name == db_name or not connects_to_db(cenv, db_name):
                continue
            cuser = semantic_user_magic(cenv)
            cpass = semantic_password_magic(cenv, db_name)
            if not cpass or cpass.name == db_pass_mv.name:
                continue
            if db_user_mv and cuser and cuser.name == db_user_mv.name:
                errors.append(f"{consumer_name}: connects to {db_name} with the same generated DB user {db_user_mv.name} but a different generated password {cpass.name}; one logical credential must reuse the exact complete Magic Variable {db_pass_mv.name}")
            else:
                reviews.append(f"{consumer_name}: connects to {db_name} but uses generated password {cpass.name} instead of {db_pass_mv.name}; verify whether this is an intentional separate DB role or an accidental credential split")

    # Magic Variable Ledger: generator identity, semantics, reuse and ambiguity.
    for magic_name in sorted(magic_services):
        mv = parse_magic(magic_name)
        if not mv:
            continue
        consumers = ",".join(sorted(magic_services[magic_name])) or "-"
        declarations = ",".join(sorted(magic_declarations.get(magic_name, set()))) or "-"
        purposes = ",".join(sorted(magic_purposes.get(magic_name, {"generated value"})))
        required = "yes" if magic_required.get(magic_name, False) else "no"
        persistent = "yes" if mv.is_credential else ("domain-config" if mv.is_public else "no")
        ambiguity = "none"
        if mv.is_credential and not mv.identifier_is_conservative:
            ambiguity = "credential-id-separators/runtime-review"
        elif mv.is_public and mv.port is not None:
            ambiguity = "port-is-proxy-target-not-canonical-browser-port"
        infos.append(
            "MAGIC_LEDGER "
            f"variable={mv.name} family={mv.kind} identifier={mv.identifier} "
            f"port={mv.port if mv.port is not None else '-'} declarations={declarations} "
            f"services={consumers} purpose={purposes!r} format={magic_expected_format(mv)!r} "
            f"required={required} persistent_identity={persistent} public={'yes' if mv.is_public else 'no'} "
            f"credential={'yes' if mv.is_credential else 'no'} ambiguity={ambiguity}"
        )

    if args.previous_compose:
        try:
            previous_doc = yaml.safe_load(args.previous_compose.read_text(encoding="utf-8")) or {}
        except Exception as exc:
            errors.append(f"PREVIOUS_COMPOSE_PARSE: {exc}")
            previous_doc = {}
        before = magic_binding_map(previous_doc if isinstance(previous_doc, dict) else {})
        after = magic_binding_map(doc)
        for binding in sorted(set(before) & set(after)):
            if before[binding] != after[binding]:
                service_name, env_key = binding
                reviews.append(
                    f"MAGIC_IDENTITY_CHANGE: {service_name}.{env_key} changed generated credential identity from {before[binding]} to {after[binding]}. If the old value initialized persistent state, this is potentially breaking; generated-value change does not rotate the stored application/database credential."
                )

    if isinstance(doc.get("networks"), dict) and doc.get("networks"):
        warnings.append("CUSTOM_NETWORKS: custom top-level networks are declared; verify they are required and Coolify proxy reachability still works")

    if args.strict:
        strict_prefixes = ("TOP_LEVEL_VERSION:", "REVISION_DUPLICATION:", "HISTORICAL_COMMENTS:")
        promote = [w for w in warnings if w.startswith(strict_prefixes)]
        for item in promote:
            errors.append("STRICT " + item)
        warnings = [w for w in warnings if w not in promote]

    # De-duplicate while preserving order.
    def unique(items: list[str]) -> list[str]:
        return list(dict.fromkeys(items))

    errors, warnings, reviews, infos = map(unique, (errors, warnings, reviews, infos))
    print(f"Services: {len(services)}")
    print(f"Errors: {len(errors)} | Warnings: {len(warnings)} | Review required: {len(reviews)} | Info: {len(infos)}")
    for item in errors:
        print("ERROR:", item)
    for item in warnings:
        print("WARNING:", item)
    for item in reviews:
        print("REVIEW REQUIRED:", item)
    for item in infos:
        print("INFO:", item)

    print("INFO: Magic-variable grammar checks are version-sensitive where Coolify documentation/runtime evidence differs; REVIEW REQUIRED means verify the target Coolify parser rather than assuming a Docker Compose law.")
    print("INFO: static architecture audit cannot prove that a component belongs to the target; confirm current-upstream evidence and live acceptance separately.")
    return 1 if errors else 0


if __name__ == "__main__":
    raise SystemExit(main())
````

<!-- END PORTABLE RESOURCE: scripts/audit_compose.py -->

<!-- BEGIN PORTABLE RESOURCE: scripts/build_portable.py -->
<!-- SOURCE SHA256: 5be8434b698162ce7228b8a06638b8d115662a99750137b2db782b32c76ecd08 -->
<!-- EMBEDDED SHA256: 375bd176d5b1e7bec22f159bb3fbac1f51876fd9721118fecc7149e3c8fc6f5b -->

## Portable resource: `scripts/build_portable.py`

````python
#!/usr/bin/env python3
"""Build a collision-safe, source-hashed single-file portable edition.

Every structured resource is embedded automatically so the portable edition cannot
silently omit a newly added reference, validator, or golden fixture. Portable
outputs themselves are excluded to avoid recursion.
"""

from __future__ import annotations

import argparse
import hashlib
import re
from pathlib import Path


EXCLUDED_NAMES = {
    "SKILL.md",
    "coolify-architect-portable.SKILL.md",
    "coolify-architect-portable.txt",
}

TEXT_LANG = {
    ".py": "python",
    ".sh": "bash",
    ".bash": "bash",
    ".yml": "yaml",
    ".yaml": "yaml",
    ".json": "json",
    ".toml": "toml",
    ".ini": "ini",
    ".conf": "text",
    ".txt": "text",
}


def sha256_text(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).hexdigest()


def max_backtick_run(text: str) -> int:
    runs = re.findall(r"`+", text)
    return max((len(run) for run in runs), default=0)


def safe_fence(text: str) -> str:
    return "`" * max(4, max_backtick_run(text) + 1)


def demote_markdown_headings(text: str, by: int = 2) -> str:
    out: list[str] = []
    in_fence = False
    fence_char: str | None = None
    fence_len = 0

    for line in text.splitlines():
        stripped = line.lstrip()
        fence_match = re.match(r"(`{3,}|~{3,})", stripped)
        if fence_match:
            token = fence_match.group(1)
            char = token[0]
            length = len(token)
            if not in_fence:
                in_fence = True
                fence_char = char
                fence_len = length
            elif char == fence_char and length >= fence_len:
                in_fence = False
                fence_char = None
                fence_len = 0
            out.append(line)
            continue

        if not in_fence:
            heading = re.match(r"^(\s{0,3})(#{1,6})(\s+.*)$", line)
            if heading:
                indent, hashes, rest = heading.groups()
                level = min(6, len(hashes) + by)
                line = f"{indent}{'#' * level}{rest}"
        out.append(line)

    return "\n".join(out).rstrip() + "\n"


def discover_resources(root: Path) -> list[Path]:
    """Return every user-facing structured resource in deterministic order."""
    groups: list[Path] = []
    for rel in ("README.md", "CHANGELOG.md", "LICENSE"):
        p = root / rel
        if p.exists():
            groups.append(p)
    for dirname in ("agents", "references", "scripts", "assets"):
        d = root / dirname
        if not d.exists():
            continue
        for p in sorted(d.rglob("*")):
            if not p.is_file() or "__pycache__" in p.parts:
                continue
            if p.name in EXCLUDED_NAMES or p.name.startswith("coolify-architect-portable"):
                continue
            groups.append(p)
    return groups


def render_payload(rel: str, source: str) -> str:
    suffix = Path(rel).suffix.lower()
    if suffix == ".md":
        body = demote_markdown_headings(source, by=2)
        return f"\n## Portable resource: `{rel}`\n\n{body}\n"

    lang = TEXT_LANG.get(suffix, "text")
    body = source.rstrip()
    fence = safe_fence(body)
    return f"\n## Portable resource: `{rel}`\n\n{fence}{lang}\n{body}\n{fence}\n\n"


def build(root: Path, output: Path) -> None:
    skill = (root / "SKILL.md").read_text(encoding="utf-8").rstrip()
    if not skill.startswith("---\n"):
        raise SystemExit("ERROR: SKILL.md must start with YAML frontmatter.")

    resources = discover_resources(root)
    parts = [skill]
    parts.append(
        "\n\n---\n\n"
        "## Portable embedded resources\n\n"
        "> This single-file edition embeds every structured resource from "
        "`coolify-architect/` (except `SKILL.md` itself and portable outputs). "
        "Each resource carries source and embedded SHA-256 values so synchronization "
        "can be validated mechanically.\n\n"
        f"> Embedded resource count: **{len(resources)}**.\n"
    )

    for path in resources:
        rel = path.relative_to(root).as_posix()
        source = path.read_text(encoding="utf-8")
        payload = render_payload(rel, source)
        parts.append(f"\n<!-- BEGIN PORTABLE RESOURCE: {rel} -->\n")
        parts.append(f"<!-- SOURCE SHA256: {sha256_text(source)} -->\n")
        parts.append(f"<!-- EMBEDDED SHA256: {sha256_text(payload)} -->\n")
        parts.append(payload)
        parts.append(f"<!-- END PORTABLE RESOURCE: {rel} -->\n")

    output.write_text("".join(parts).rstrip() + "\n", encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--root",
        type=Path,
        default=Path(__file__).resolve().parents[1],
        help="Structured coolify-architect directory.",
    )
    parser.add_argument("--output", type=Path, required=True)
    args = parser.parse_args()
    build(args.root.resolve(), args.output.resolve())


if __name__ == "__main__":
    main()
````

<!-- END PORTABLE RESOURCE: scripts/build_portable.py -->

<!-- BEGIN PORTABLE RESOURCE: scripts/select_reference_templates.py -->
<!-- SOURCE SHA256: 370a5588641cc673de30e80ad5c2b684305cb5c19ff35a5b5b7e90c08061b51f -->
<!-- EMBEDDED SHA256: 2c2e2b62c709122b49e47aece3cb794335e27163c391adde829f28c8a59ef67e -->

## Portable resource: `scripts/select_reference_templates.py`

````python
#!/usr/bin/env python3
"""
Architecture-aware selector for official Coolify Compose references.

Modes:
  --compose FILE
      Fingerprint a target Compose and rank curated official Coolify references.

  --build-index DIR
      Fingerprint every YAML/YML template in a local
      coollabsio/coolify/templates/compose checkout and emit a complete JSON index.

Heuristic only: this does not certify production readiness.
"""
from __future__ import annotations

import argparse
import json
import re
from pathlib import Path
from typing import Any

try:
    import yaml
except ImportError as exc:
    raise SystemExit("PyYAML is required: pip install pyyaml") from exc


REFERENCE_PROFILES = {
    "actualbudget.yaml": {"single", "volume", "http"},
    "vaultwarden.yaml": {"single", "volume", "http"},
    "directus-with-postgresql.yaml": {"app", "postgres", "http", "volume"},
    "keycloak-with-postgres.yaml": {"app", "postgres", "http", "volume"},
    "nextcloud-with-postgres.yaml": {"app", "postgres", "http", "volume"},
    "wordpress-with-mariadb.yaml": {"app", "mariadb", "http", "volume"},
    "ghost.yaml": {"app", "mysql", "http", "volume"},
    "docmost.yaml": {"app", "postgres", "redis", "http", "volume"},
    "infisical.yaml": {"app", "postgres", "redis", "http", "volume"},
    "getoutline.yaml": {"app", "postgres", "redis", "http", "volume"},
    "n8n.yaml": {"app", "worker", "http", "volume", "shared-secret"},
    "n8n-with-postgres-and-worker.yaml": {
        "app", "worker", "postgres", "redis", "redis-queue", "http",
        "volume", "health-gated", "shared-secret"
    },
    "authentik.yaml": {
        "app", "worker", "postgres", "http", "volume", "health-gated",
        "docker-socket", "shared-secret"
    },
    "chatwoot.yaml": {"app", "worker", "postgres", "redis", "http", "volume"},
    "glitchtip.yaml": {"app", "worker", "postgres", "redis", "http", "volume"},
    "trigger.yaml": {"platform", "worker", "postgres", "redis", "http", "volume"},
    "windmill.yaml": {"platform", "worker", "postgres", "http", "volume"},
    "twenty.yaml": {"platform", "worker", "postgres", "redis", "http", "volume"},
    "plane.yaml": {"platform", "worker", "postgres", "redis", "http", "volume"},
    "appwrite.yaml": {
        "platform", "worker", "mariadb", "redis", "multi-path", "http",
        "volume", "shared-secret"
    },
    "supabase.yaml": {"platform", "postgres", "multi-endpoint", "http", "volume"},
    "posthog.yaml": {
        "platform", "worker", "postgres", "redis", "analytics",
        "multi-endpoint", "http", "volume"
    },
    "dify.yaml": {
        "platform", "worker", "postgres", "redis", "http", "volume", "ai-stack"
    },
    "signoz.yaml": {
        "platform", "analytics", "clickhouse", "multi-endpoint", "http", "volume"
    },
    "penpot.yaml": {
        "frontend-backend", "postgres", "valkey", "http", "volume", "health-gated"
    },
    "penpot-with-s3.yaml": {
        "frontend-backend", "postgres", "valkey", "minio", "s3",
        "init-job", "service-completed", "http", "volume", "health-gated"
    },
    "ente-photos-with-s3.yaml": {"app", "postgres", "s3", "http", "volume"},
    "minio.yaml": {"object-store", "s3", "http", "volume"},
    "garage.yaml": {"object-store", "s3", "http", "volume"},
    "seaweedfs.yaml": {"object-store", "distributed-storage", "multi-endpoint", "volume"},
    "rustfs.yaml": {"object-store", "s3", "http", "volume"},
    "qdrant.yaml": {"vector-db", "http", "volume"},
    "weaviate.yaml": {"vector-db", "http", "volume"},
    "chroma.yaml": {"vector-db", "http", "volume"},
    "meilisearch.yaml": {"search", "http", "volume"},
    "typesense.yaml": {"search", "http", "volume"},
    "elasticsearch.yaml": {"search", "http", "volume"},
    "elasticsearch-with-kibana.yaml": {"search", "multi-endpoint", "http", "volume"},
    "rabbitmq.yaml": {"queue", "rabbitmq", "multi-endpoint", "volume"},
    "mosquitto.yaml": {"broker", "mqtt", "non-http", "volume"},
    "soketi.yaml": {"realtime", "websocket", "http"},
    "forgejo-with-postgresql.yaml": {
        "app", "postgres", "http", "non-http", "volume", "git-forge"
    },
    "forgejo-with-runner-with-postgresql.yaml": {
        "app", "postgres", "runner", "docker-in-docker", "privileged",
        "init-job", "bootstrap-registration", "http", "non-http", "volume"
    },
    "gitea-runner.yaml": {"runner", "docker-socket", "volume"},
    "github-runner.yaml": {"runner", "docker-socket", "volume"},
    "portainer.yaml": {"single", "docker-socket", "http", "volume"},
    "immich.yaml": {"platform", "postgres", "redis", "media", "http", "volume"},
    "wireguard-easy.yaml": {"network", "vpn", "capabilities", "non-http", "http", "volume"},
    "tailscale-client.yaml": {"network", "vpn", "capabilities"},
    "cloudflared.yaml": {"network", "tunnel"},
    "minecraft.yaml": {"game", "non-http", "volume"},
    "palworld.yaml": {"game", "non-http", "volume"},
    "sftpgo.yaml": {"app", "http", "non-http", "volume"},
    "mailpit.yaml": {"mail", "http", "non-http"},
    "matrix-synapse-with-postgresql.yaml": {
        "app", "postgres", "http", "canonical-url-sensitive", "volume"
    },
}

WEIGHTS = {
    "docker-in-docker": 8, "docker-socket": 8, "privileged": 8,
    "host-network": 8, "device-access": 8, "capabilities": 7,
    "s3": 7, "minio": 7, "distributed-storage": 7,
    "vector-db": 7, "search": 5, "analytics": 5,
    "worker": 6, "runner": 7, "redis-queue": 6, "queue": 6,
    "rabbitmq": 6, "mqtt": 6, "websocket": 5,
    "multi-path": 6, "multi-endpoint": 5, "non-http": 5,
    "init-job": 5, "service-completed": 6, "bootstrap-registration": 6,
    "postgres": 4, "mysql": 4, "mariadb": 4, "mongodb": 4,
    "redis": 4, "valkey": 4, "clickhouse": 5,
    "health-gated": 3, "volume": 2, "http": 1,
}

PENALTIES = {
    "docker-in-docker": 10, "docker-socket": 10, "privileged": 10,
    "host-network": 10, "device-access": 9, "capabilities": 7,
    "s3": 6, "minio": 6, "worker": 5, "runner": 6,
    "vector-db": 5, "search": 4, "postgres": 3, "mysql": 3,
    "mariadb": 3, "redis": 3, "multi-path": 4,
}


def flatten_strings(value: Any):
    if isinstance(value, dict):
        for k, v in value.items():
            yield str(k)
            yield from flatten_strings(v)
    elif isinstance(value, list):
        for item in value:
            yield from flatten_strings(item)
    elif value is not None:
        yield str(value)


def env_items(service: dict[str, Any]) -> list[str]:
    env = service.get("environment") or {}
    if isinstance(env, dict):
        return [f"{k}={v}" for k, v in env.items()]
    if isinstance(env, list):
        return [str(x) for x in env]
    return []


def fingerprint(data: dict[str, Any]) -> dict[str, Any]:
    services = data.get("services") or {}
    if not isinstance(services, dict):
        services = {}

    features: set[str] = set()
    public_vars, fqdn_refs, path_values, published_ports = [], [], [], []
    named_volume_mounts = bind_mounts = healthchecks = health_gated = 0
    service_details = {}

    if len(services) == 1:
        features.add("single")
    elif len(services) >= 8:
        features.add("platform")
    else:
        features.add("app")

    all_text = "\n".join(flatten_strings(data)).lower()

    patterns = {
        "postgres": r"\bpostgres(?:ql)?\b|postgis",
        "mysql": r"\bmysql\b",
        "mariadb": r"\bmariadb\b",
        "mongodb": r"\bmongo(?:db)?\b",
        "redis": r"\bredis\b",
        "valkey": r"\bvalkey\b",
        "clickhouse": r"\bclickhouse\b",
    }
    for feature, pattern in patterns.items():
        if re.search(pattern, all_text):
            features.add(feature)

    if re.search(r"\b(qdrant|weaviate|chroma|milvus)\b|vector[_ -]?database", all_text):
        features.add("vector-db")
    if re.search(r"\b(meilisearch|typesense|elasticsearch|opensearch)\b", all_text):
        features.add("search")
    if (
        "minio" in all_text
        or "aws_access_key_id" in all_text
        or "aws_secret_access_key" in all_text
        or re.search(r"object[s_ -]*storage.*s3|storage.*backend.*s3|s3[_ -]*(bucket|endpoint|region)", all_text)
    ):
        features.add("s3")
    if "minio" in all_text:
        features.add("minio")
    if re.search(r"\b(rabbitmq|amqp)\b", all_text):
        features |= {"queue", "rabbitmq"}
    if re.search(r"\bmqtt\b|mosquitto|emqx", all_text):
        features |= {"broker", "mqtt"}
    if re.search(r"websocket|realtime", all_text):
        features.add("websocket")

    for name, service in services.items():
        if not isinstance(service, dict):
            continue
        lname = str(name).lower()
        image = str(service.get("image") or "").lower()
        command = " ".join(flatten_strings(service.get("command") or "")).lower()
        entrypoint = " ".join(flatten_strings(service.get("entrypoint") or "")).lower()
        role_text = f"{lname} {image} {command} {entrypoint}"

        if re.search(r"\b(worker|celery|runner)\b", role_text):
            features.add("worker")
        if "runner" in role_text:
            features.add("runner")
        if re.search(r"\b(beat|scheduler|cron)\b", role_text):
            features.add("scheduler")
        if re.search(r"\b(init|migrat|bootstrap|register)\b", role_text):
            features.add("init-job")
        if "docker:dind" in image:
            features |= {"docker-in-docker", "privileged"}
        if service.get("privileged") is True:
            features.add("privileged")
        if str(service.get("network_mode") or "").lower() == "host":
            features.add("host-network")
        if service.get("devices"):
            features.add("device-access")
        if service.get("cap_add") or service.get("cap_drop"):
            features.add("capabilities")

        envs = env_items(service)
        for item in envs:
            key = item.split("=", 1)[0].strip()
            if key.startswith("SERVICE_URL_"):
                public_vars.append(key)
                features.add("http")
                if "=" in item:
                    value = item.split("=", 1)[1]
                    if value.startswith("/"):
                        path_values.append(value)
            if "SERVICE_FQDN_" in item:
                fqdn_refs.append(item)

        ports = service.get("ports") or []
        if ports:
            published_ports += [str(p) for p in ports]
            features.add("non-http")

        for volume in service.get("volumes") or []:
            if isinstance(volume, str):
                source = volume.split(":", 1)[0]
                if source.startswith(("/", "./", "../")):
                    bind_mounts += 1
                else:
                    named_volume_mounts += 1
                if "docker.sock" in volume:
                    features.add("docker-socket")
            elif isinstance(volume, dict):
                vtype = volume.get("type")
                source = str(volume.get("source") or "")
                target = str(volume.get("target") or "")
                if vtype == "bind":
                    bind_mounts += 1
                elif vtype == "volume":
                    named_volume_mounts += 1
                if "docker.sock" in source or "docker.sock" in target:
                    features.add("docker-socket")

        if service.get("healthcheck"):
            healthchecks += 1

        depends = service.get("depends_on") or {}
        if isinstance(depends, dict):
            for dep in depends.values():
                if isinstance(dep, dict):
                    condition = str(dep.get("condition") or "")
                    if condition == "service_healthy":
                        health_gated += 1
                        features.add("health-gated")
                    elif condition == "service_completed_successfully":
                        features |= {"service-completed", "init-job"}

        service_details[str(name)] = {
            "image": service.get("image"),
            "public_service_url_vars": [
                e.split("=", 1)[0] for e in envs
                if e.split("=", 1)[0].startswith("SERVICE_URL_")
            ],
            "published_ports": [str(p) for p in ports],
        }

    if named_volume_mounts:
        features.add("volume")
    if len(set(public_vars)) > 1:
        features.add("multi-endpoint")
    if len(set(path_values)) > 1:
        features.add("multi-path")
    if "redis" in features and "worker" in features:
        features.add("redis-queue")
    if "frontend" in services and any("backend" in str(k).lower() for k in services):
        features.add("frontend-backend")
    if re.search(r"\b(clickhouse|signoz|posthog|plausible|openobserve)\b", all_text):
        features.add("analytics")
    if re.search(r"game|minecraft|palworld|terraria|satisfactory", all_text):
        features.add("game")
    if re.search(r"wireguard|tailscale|netbird|vpn", all_text):
        features |= {"network", "vpn"}
    if re.search(r"cloudflared|tunnel", all_text):
        features |= {"network", "tunnel"}

    return {
        "features": sorted(features),
        "service_count": len(services),
        "public_endpoint_variables": sorted(set(public_vars)),
        "service_fqdn_references": sorted(set(fqdn_refs)),
        "path_values": sorted(set(path_values)),
        "published_ports": published_ports,
        "named_volume_mounts": named_volume_mounts,
        "bind_mounts": bind_mounts,
        "healthcheck_count": healthchecks,
        "health_gated_dependency_count": health_gated,
        "services": service_details,
    }


def rank_references(fp: dict[str, Any], limit: int = 5):
    target = set(fp["features"])
    rows = []
    for name, profile in REFERENCE_PROFILES.items():
        shared = sorted(target & profile)
        extras = sorted(profile - target)
        score = sum(WEIGHTS.get(feature, 1) for feature in shared)
        score -= sum(PENALTIES.get(feature, 0) for feature in extras)
        rows.append({
            "template": name,
            "score": score,
            "matched_features": shared,
            "reference_only_features": extras,
        })
    return sorted(rows, key=lambda row: (-row["score"], row["template"]))[:limit]


def load_yaml(path: Path):
    with path.open("r", encoding="utf-8") as handle:
        return yaml.safe_load(handle) or {}


def build_index(directory: Path):
    files = sorted(list(directory.glob("*.yaml")) + list(directory.glob("*.yml")))
    classified, failures = [], []
    for path in files:
        try:
            classified.append({"template": path.name, **fingerprint(load_yaml(path))})
        except Exception as exc:
            failures.append({"template": path.name, "error": str(exc)})
    return {
        "source_directory": str(directory),
        "template_count": len(files),
        "classified_count": len(classified),
        "failure_count": len(failures),
        "templates": classified,
        "failures": failures,
    }


def main():
    parser = argparse.ArgumentParser()
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("--compose", type=Path)
    group.add_argument("--build-index", type=Path)
    parser.add_argument("--top", type=int, default=5)
    parser.add_argument("--index-output", type=Path)
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()

    if args.build_index:
        result = build_index(args.build_index)
        rendered = json.dumps(result, indent=2, sort_keys=True)
        if args.index_output:
            args.index_output.write_text(rendered + "\n", encoding="utf-8")
            print(
                f"Wrote {args.index_output} "
                f"({result['classified_count']}/{result['template_count']} classified)"
            )
        else:
            print(rendered)
        return

    fp = fingerprint(load_yaml(args.compose))
    ranked = rank_references(fp, max(1, args.top))
    result = {
        "target": str(args.compose),
        "fingerprint": fp,
        "recommended_references": ranked,
    }
    if args.json:
        print(json.dumps(result, indent=2, sort_keys=True))
        return

    print(f"Target: {args.compose}")
    print("Features: " + ", ".join(fp["features"]))
    print("\nRecommended official Coolify references:")
    for index, row in enumerate(ranked, 1):
        print(f"{index}. {row['template']}  score={row['score']}")
        print("   matches: " + (", ".join(row["matched_features"]) or "none"))
        print(
            "   reference-only: "
            + (", ".join(row["reference_only_features"]) or "none")
        )
    print("\nFetch the current official files before copying any pattern.")


if __name__ == "__main__":
    main()
````

<!-- END PORTABLE RESOURCE: scripts/select_reference_templates.py -->

<!-- BEGIN PORTABLE RESOURCE: scripts/test_embedded_validation.py -->
<!-- SOURCE SHA256: 986b06875c4ff57249d9056514133e6838cc004e197840ae2eed667cd6f61430 -->
<!-- EMBEDDED SHA256: 2e1e042438c1428a75d2f5111b2775bb91a2f6ad97884a3c8adb1585fbb1a6e2 -->

## Portable resource: `scripts/test_embedded_validation.py`

````python
#!/usr/bin/env python3
"""Regression tests for Compose -> shell -> nested-tool validation.

ERPNext Golden #7 exposed a malformed literal sed substitution that passed Bash
syntax validation because the error belonged to sed, not Bash. Overleaf Golden #9
added a Compose -> shell/heredoc -> JavaScript failure where unescaped `$set` was
consumed before Node parsed it. OpenSPP Golden #12 added the opposite transport: a
Coolify-managed Nginx file is written literally, so `$$remote_addr` reaches Nginx
as two dollar signs and crashes. These tests keep the contexts distinct.
"""

from __future__ import annotations

import subprocess
import sys
import tempfile
import textwrap
import unittest
from pathlib import Path

VALIDATOR = Path(__file__).resolve().with_name("validate_embedded.py")


class EmbeddedValidationTests(unittest.TestCase):
    def run_validator(self, compose: str) -> tuple[int, str]:
        with tempfile.TemporaryDirectory() as td:
            path = Path(td) / "compose.yml"
            path.write_text(textwrap.dedent(compose), encoding="utf-8")
            proc = subprocess.run(
                [sys.executable, str(VALIDATOR), str(path)],
                text=True,
                capture_output=True,
                check=False,
            )
            return proc.returncode, proc.stdout + proc.stderr

    def test_entrypoint_plus_command_shape_is_checked(self) -> None:
        code, out = self.run_validator(
            r'''
            services:
              bootstrap:
                image: example/app:1
                entrypoint: [bash, -c]
                command:
                  - >-
                    set -euo pipefail;
                    value="$$(printf '%s\n' sites/example/site_config.json | sed 's#^sites/##; s#/site_config.json$$##')";
                    test "$${value}" = example;
            '''
        )
        self.assertEqual(code, 0, out)
        self.assertIn("Embedded shell commands checked: 1", out)
        self.assertIn("Literal nested sed programs checked: 1", out)

    def test_malformed_literal_sed_is_rejected_after_compose_dollar_unescape(self) -> None:
        code, out = self.run_validator(
            r'''
            services:
              bootstrap:
                image: example/app:1
                entrypoint: [bash, -c]
                command:
                  - >-
                    set -euo pipefail;
                    value="$$(printf '%s\n' sites/example/site_config.json | sed 's#^sites/##; s#/site_config.json$$#')";
            '''
        )
        self.assertEqual(code, 1, out)
        self.assertIn("literal sed program", out)
        self.assertRegex(out.lower(), r"unterminated|sed syntax check failed")


    def test_healthcheck_single_quoted_host_variable_is_rejected(self) -> None:
        code, out = self.run_validator(
            r'''
            services:
              backend:
                image: example/app:1
                environment:
                  FRAPPE_SITE_NAME: example.com
                healthcheck:
                  test: ["CMD-SHELL", "curl -fsS -H 'Host: $${FRAPPE_SITE_NAME}' http://127.0.0.1:8000/api/method/ping"]
            '''
        )
        self.assertEqual(code, 1, out)
        self.assertIn("CMD-SHELL healthchecks checked: 1", out)
        self.assertIn("Host header contains a shell variable inside single quotes", out)

    def test_healthcheck_double_quoted_host_and_origin_variables_pass(self) -> None:
        code, out = self.run_validator(
            r'''
            services:
              backend:
                image: example/app:1
                healthcheck:
                  test: ["CMD-SHELL", "printf '%s\n' \"Host: $${FRAPPE_SITE_NAME}\" >/dev/null"]
              websocket:
                image: example/app:1
                healthcheck:
                  test: ["CMD-SHELL", "printf '%s\n' \"Origin: $${FRAPPE_PUBLIC_URL}\" >/dev/null"]
            '''
        )
        self.assertEqual(code, 0, out)
        self.assertIn("CMD-SHELL healthchecks checked: 2", out)

    def test_unescaped_mongo_operator_in_node_heredoc_is_rejected(self) -> None:
        code, out = self.run_validator(
            r'''
            services:
              adminbootstrap:
                image: example/app:1
                entrypoint: [/bin/bash, -ce]
                command:
                  - |
                    exec node --input-type=module <<'NODE'
                    const update = {
                      $set: { isAdmin: true },
                    }
                    console.log(update)
                    NODE
            '''
        )
        self.assertEqual(code, 1, out)
        self.assertIn("literal nested Mongo/JavaScript operator $set", out)
        self.assertIn("must be written $$set", out)

    def test_escaped_mongo_operator_in_node_heredoc_passes_and_inner_js_is_checked(self) -> None:
        code, out = self.run_validator(
            r'''
            services:
              adminbootstrap:
                image: example/app:1
                entrypoint: [/bin/bash, -ce]
                command:
                  - |
                    exec node --input-type=module <<'NODE'
                    const update = {
                      $$set: { isAdmin: true },
                    }
                    console.log(update)
                    NODE
            '''
        )
        self.assertEqual(code, 0, out)
        self.assertIn("Embedded shell commands checked: 1", out)
        self.assertIn("Managed/secondary embedded files checked: 1", out)

    def test_managed_nginx_double_dollar_native_variable_is_rejected(self) -> None:
        code, out = self.run_validator(
            r'''
            services:
              gateway:
                image: nginx:1.30-alpine
                volumes:
                  - type: bind
                    source: ./coolify/test/nginx.conf.template
                    target: /etc/nginx/test.conf.template
                    is_directory: false
                    content: |
                      events {}
                      http {
                        log_format main '$$remote_addr - $$remote_user [$$time_local] \"$$request\"';
                        server { listen 8080; }
                      }
            '''
        )
        self.assertEqual(code, 1, out)
        self.assertIn("managed Nginx content contains literal '$$remote_addr'", out)
        self.assertIn("interpolation-layer proof", out)

    def test_managed_nginx_single_dollar_native_variable_is_not_rewritten(self) -> None:
        code, out = self.run_validator(
            r'''
            services:
              gateway:
                image: nginx:1.30-alpine
                volumes:
                  - type: bind
                    source: ./coolify/test/nginx.conf.template
                    target: /etc/nginx/test.conf.template
                    is_directory: false
                    content: |
                      events {}
                      http {
                        log_format main '$remote_addr - $remote_user [$time_local] \"$request\"';
                        server { listen 8080; }
                      }
            '''
        )
        self.assertEqual(code, 0, out)
        self.assertIn("INFO SKIP", out)
        self.assertNotIn("managed Nginx content contains literal '$$remote_addr'", out)

    def test_shell_parameter_expansion_alternative_passes(self) -> None:
        code, out = self.run_validator(
            r'''
            services:
              bootstrap:
                image: example/app:1
                entrypoint: [bash, -c]
                command:
                  - >-
                    set -euo pipefail;
                    other_site='sites/example/site_config.json';
                    other_site="$${other_site#sites/}";
                    other_site="$${other_site%/site_config.json}";
                    test "$${other_site}" = example;
            '''
        )
        self.assertEqual(code, 0, out)
        self.assertIn("Embedded shell commands checked: 1", out)


if __name__ == "__main__":
    unittest.main(verbosity=2)
````

<!-- END PORTABLE RESOURCE: scripts/test_embedded_validation.py -->

<!-- BEGIN PORTABLE RESOURCE: scripts/test_magic_variables.py -->
<!-- SOURCE SHA256: 50594dd7b705c65509be47397cd32733ed1c3d12522693b2e6f6bc2886552c1b -->
<!-- EMBEDDED SHA256: 2586af4733c56b16e75ae73556c9951f965832d5a3743d06ce126482f8714835 -->

## Portable resource: `scripts/test_magic_variables.py`

````python
#!/usr/bin/env python3
"""Regression tests for Coolify Magic Variable parsing across the Golden corpus."""

from __future__ import annotations

import subprocess
import sys
import tempfile
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
import audit_compose as audit  # noqa: E402


class MagicParserTests(unittest.TestCase):
    def assert_magic(self, name: str, kind: str, identifier: str, port: int | None = None) -> None:
        mv = audit.parse_magic(name)
        self.assertIsNotNone(mv, name)
        assert mv is not None
        self.assertEqual(mv.kind, kind)
        self.assertEqual(mv.identifier, identifier)
        self.assertEqual(mv.port, port)

    def test_longest_type_first(self) -> None:
        self.assert_magic("SERVICE_PASSWORD_64_FRAPPEDBROOT", "PASSWORD_64", "FRAPPEDBROOT")
        self.assert_magic("SERVICE_PASSWORDWITHSYMBOLS_64_ADMIN", "PASSWORDWITHSYMBOLS_64", "ADMIN")
        self.assert_magic("SERVICE_REALBASE64_64_SESSION", "REALBASE64_64", "SESSION")
        self.assert_magic("SERVICE_BASE64_128_TOKEN", "BASE64_128", "TOKEN")
        self.assert_magic("SERVICE_HEX_64_KEY", "HEX_64", "KEY")

    def test_frappe_valid_credential_ids(self) -> None:
        for name in (
            "SERVICE_PASSWORD_64_FRAPPEDBROOT",
            "SERVICE_PASSWORD_64_FRAPPEADMIN",
        ):
            mv = audit.parse_magic(name)
            self.assertIsNotNone(mv)
            assert mv is not None
            self.assertTrue(mv.identifier_is_conservative, name)

    def test_erpnext_valid_credential_ids(self) -> None:
        for name in (
            "SERVICE_PASSWORD_64_ERPNEXTDBROOT",
            "SERVICE_PASSWORD_64_ERPNEXTADMIN",
        ):
            mv = audit.parse_magic(name)
            self.assertIsNotNone(mv)
            assert mv is not None
            self.assertEqual(mv.kind, "PASSWORD_64")
            self.assertTrue(mv.identifier_is_conservative, name)

    def test_frappe_problem_identifier_requires_review(self) -> None:
        mv = audit.parse_magic("SERVICE_PASSWORD_64_FRAPPE_DB_ROOT")
        self.assertIsNotNone(mv)
        assert mv is not None
        self.assertEqual(mv.kind, "PASSWORD_64")
        self.assertEqual(mv.identifier, "FRAPPE_DB_ROOT")
        self.assertFalse(mv.identifier_is_conservative)

    def test_url_fqdn_and_port(self) -> None:
        self.assert_magic("SERVICE_URL_FRONTEND", "URL", "FRONTEND")
        self.assert_magic("SERVICE_FQDN_FRONTEND", "FQDN", "FRONTEND")
        self.assert_magic("SERVICE_URL_FRONTEND_8080", "URL", "FRONTEND", 8080)

    def test_service_name_normalization_for_url_ids(self) -> None:
        self.assertEqual(audit.normalized_service_id("my-app"), "MY_APP")
        self.assertTrue(audit.service_target_matches("MY_APP", {"my-app": {}}))

    def test_overleaf_compound_magic_families_and_url_syntax(self) -> None:
        self.assert_magic("SERVICE_REALBASE64_32_OVERLEAFINVITE", "REALBASE64_32", "OVERLEAFINVITE")
        self.assert_magic("SERVICE_PASSWORD_64_OVERLEAFADMIN", "PASSWORD_64", "OVERLEAFADMIN")
        self.assert_magic("SERVICE_URL_OVERLEAF", "URL", "OVERLEAF")
        # Generic grammar-positive case requested by the benchmark. Overleaf RC1 proved
        # that valid Coolify syntax can still be application-invalid when the service
        # name makes Coolify inject rejected SHARELATEX environment names. Golden #9
        # therefore uses SERVICE_URL_OVERLEAF, not SERVICE_URL_SHARELATEX.
        self.assert_magic("SERVICE_URL_SHARELATEX", "URL", "SHARELATEX")

    def test_netbox_secret_families_are_grammar_valid_not_transport_certified(self) -> None:
        # Parser validity is not a transport-safety certification. The accepted NetBox
        # fixture legitimately uses PASSWORDWITHSYMBOLS_64; transport regressions are
        # evaluated separately from Magic Variable grammar.
        self.assert_magic("SERVICE_PASSWORDWITHSYMBOLS_64_NETBOXSECRET", "PASSWORDWITHSYMBOLS_64", "NETBOXSECRET")
        self.assert_magic("SERVICE_PASSWORDWITHSYMBOLS_64_NETBOXTOKENPEPPER", "PASSWORDWITHSYMBOLS_64", "NETBOXTOKENPEPPER")
        self.assert_magic("SERVICE_PASSWORD_64_NETBOXDB", "PASSWORD_64", "NETBOXDB")
        self.assert_magic("SERVICE_PASSWORD_64_NETBOXREDIS", "PASSWORD_64", "NETBOXREDIS")
        self.assert_magic("SERVICE_PASSWORD_64_NETBOXCACHE", "PASSWORD_64", "NETBOXCACHE")

    def test_unknown_family_is_not_recognized(self) -> None:
        self.assertIsNone(audit.parse_magic("SERVICE_SECRET_64_APP"))


class AuditBehaviorTests(unittest.TestCase):
    def run_audit(self, compose_text: str, *extra: str) -> tuple[int, str]:
        with tempfile.TemporaryDirectory() as td:
            path = Path(td) / "compose.yml"
            path.write_text(compose_text, encoding="utf-8")
            cmd = [sys.executable, str(Path(audit.__file__).resolve()), str(path), *extra]
            proc = subprocess.run(cmd, text=True, capture_output=True, check=False)
            return proc.returncode, proc.stdout + proc.stderr

    def test_host_validated_127_healthcheck_requires_review(self) -> None:
        code, out = self.run_audit(
            """
services:
  app:
    image: example/app:1
    environment:
      ALLOWED_HOSTS: example.org localhost
    healthcheck:
      test: [CMD-SHELL, "curl -fsS http://127.0.0.1:8080/login/ >/dev/null"]
"""
        )
        self.assertEqual(code, 0, out)
        self.assertIn("same socket but send different HTTP Host values", out)
        self.assertIn("REVIEW REQUIRED", out)

    def test_problematic_frappe_identifier_is_review_not_universal_error(self) -> None:
        code, out = self.run_audit(
            """
services:
  db:
    image: mariadb:11.8
    environment:
      MYSQL_ROOT_PASSWORD: ${SERVICE_PASSWORD_64_FRAPPE_DB_ROOT:?required}
    volumes: [db:/var/lib/mysql]
volumes: {db: {}}
"""
        )
        self.assertEqual(code, 0)
        self.assertIn("REVIEW REQUIRED", out)
        self.assertIn("FRAPPE_DB_ROOT", out)

    def test_valid_frappe_ids_and_service_bound_urls_pass_magic_checks(self) -> None:
        code, out = self.run_audit(
            """
services:
  frontend:
    image: nginx:1.27
    environment:
      SERVICE_URL_FRONTEND:
      SERVICE_FQDN_FRONTEND:
      SERVICE_URL_FRONTEND_8080:
      ADMIN_PASSWORD: ${SERVICE_PASSWORD_64_FRAPPEADMIN:?required}
  db:
    image: mariadb:11.8
    environment:
      MYSQL_ROOT_PASSWORD: ${SERVICE_PASSWORD_64_FRAPPEDBROOT:?required}
    volumes: [db:/var/lib/mysql]
volumes: {db: {}}
"""
        )
        self.assertEqual(code, 0, out)
        self.assertNotIn("credential identifier 'FRAPPEADMIN'", out)
        self.assertNotIn("credential identifier 'FRAPPEDBROOT'", out)
        self.assertIn("SERVICE_URL_FRONTEND_8080 family=URL identifier=FRONTEND port=8080", out)

    def test_valid_erpnext_ids_and_existing_frontend_url_semantics_pass(self) -> None:
        code, out = self.run_audit(
            """
services:
  frontend:
    image: frappe/erpnext:v16.33.0
    environment:
      SERVICE_URL_FRONTEND:
      SERVICE_FQDN_FRONTEND:
      SERVICE_URL_FRONTEND_8080:
      ADMIN_PASSWORD: ${SERVICE_PASSWORD_64_ERPNEXTADMIN:?required}
  db:
    image: mariadb:11.8
    environment:
      MYSQL_ROOT_PASSWORD: ${SERVICE_PASSWORD_64_ERPNEXTDBROOT:?required}
    volumes: [db:/var/lib/mysql]
volumes: {db: {}}
"""
        )
        self.assertEqual(code, 0, out)
        self.assertNotIn("credential identifier 'ERPNEXTADMIN'", out)
        self.assertNotIn("credential identifier 'ERPNEXTDBROOT'", out)
        self.assertIn("SERVICE_URL_FRONTEND_8080 family=URL identifier=FRONTEND port=8080", out)


    def test_valid_mem0_magic_ids_and_bare_service_urls(self) -> None:
        code, out = self.run_audit(
            """
services:
  mem0:
    image: example/mem0:1
    environment:
      SERVICE_URL_MEM0:
      POSTGRES_HOST: postgres
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${SERVICE_PASSWORD_64_MEM0DB:?required}
      JWT_SECRET: ${SERVICE_PASSWORD_64_MEM0JWT:?required}
  dashboard:
    image: example/dashboard:1
    environment:
      SERVICE_URL_DASHBOARD:
      NEXT_PUBLIC_API_URL: ${SERVICE_URL_MEM0}
  postgres:
    image: postgres:17
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${SERVICE_PASSWORD_64_MEM0DB:?required}
    volumes: [db:/var/lib/postgresql/data]
volumes: {db: {}}
"""
        )
        self.assertEqual(code, 0, out)
        self.assertIn("SERVICE_PASSWORD_64_MEM0DB family=PASSWORD_64 identifier=MEM0DB", out)
        self.assertIn("SERVICE_PASSWORD_64_MEM0JWT family=PASSWORD_64 identifier=MEM0JWT", out)
        self.assertIn("SERVICE_URL_MEM0 family=URL identifier=MEM0", out)
        self.assertIn("SERVICE_URL_DASHBOARD family=URL identifier=DASHBOARD", out)
        self.assertNotIn("credential identifier 'MEM0DB'", out)
        self.assertNotIn("credential identifier 'MEM0JWT'", out)

    def test_valid_overleaf_magic_ids_and_true_base64_family(self) -> None:
        code, out = self.run_audit(
            """
services:
  overleaf:
    image: sharelatex/sharelatex:6.2.2
    environment:
      SERVICE_URL_OVERLEAF:
      OVERLEAF_SITE_URL: ${SERVICE_URL_OVERLEAF}
      OVERLEAF_INVITE_TOKEN_SECRET: ${SERVICE_REALBASE64_32_OVERLEAFINVITE:?required}
      OVERLEAF_ADMIN_PASSWORD: ${SERVICE_PASSWORD_64_OVERLEAFADMIN:?required}
"""
        )
        self.assertEqual(code, 0, out)
        self.assertIn("SERVICE_REALBASE64_32_OVERLEAFINVITE family=REALBASE64_32 identifier=OVERLEAFINVITE", out)
        self.assertIn("SERVICE_PASSWORD_64_OVERLEAFADMIN family=PASSWORD_64 identifier=OVERLEAFADMIN", out)
        self.assertIn("SERVICE_URL_OVERLEAF family=URL identifier=OVERLEAF", out)
        self.assertNotIn("credential identifier 'OVERLEAFINVITE'", out)
        self.assertNotIn("credential identifier 'OVERLEAFADMIN'", out)

    def test_shared_logical_db_credential_mismatch_fails(self) -> None:
        code, out = self.run_audit(
            """
services:
  database:
    image: postgres:17
    environment:
      POSTGRES_USER: ${SERVICE_USER_DATABASE}
      POSTGRES_PASSWORD: ${SERVICE_PASSWORD_64_DATABASE}
    volumes: [db:/var/lib/postgresql/data]
  app:
    image: example/app:1
    environment:
      DB_HOST: database
      DB_USER: ${SERVICE_USER_DATABASE}
      DB_PASSWORD: ${SERVICE_PASSWORD_64_APPDATABASE}
volumes: {db: {}}
"""
        )
        self.assertEqual(code, 1, out)
        self.assertIn("one logical credential must reuse the exact complete Magic Variable", out)


    def test_external_provider_secret_from_magic_is_review(self) -> None:
        code, out = self.run_audit(
            """
services:
  app:
    image: example/app:1
    environment:
      OPENAI_API_KEY: ${SERVICE_PASSWORD_64_OPENAI}
"""
        )
        self.assertEqual(code, 0, out)
        self.assertIn("external-provider-issued credential", out)
        self.assertIn("REVIEW REQUIRED", out)

    def test_external_provider_healthcheck_is_review(self) -> None:
        code, out = self.run_audit(
            """
services:
  app:
    image: example/app:1
    healthcheck:
      test: [CMD-SHELL, "curl -fsS https://api.openai.com/v1/models >/dev/null"]
"""
        )
        self.assertEqual(code, 0, out)
        self.assertIn("periodic healthcheck depends on an external AI provider", out)
        self.assertIn("REVIEW REQUIRED", out)

    def test_magic_rename_comparison_reports_persistent_identity_risk(self) -> None:
        before = """
services:
  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: ${SERVICE_PASSWORD_64_DATABASE}
    volumes: [db:/var/lib/postgresql/data]
volumes: {db: {}}
"""
        after = before.replace("SERVICE_PASSWORD_64_DATABASE", "SERVICE_PASSWORD_64_DB")
        with tempfile.TemporaryDirectory() as td:
            old = Path(td) / "old.yml"
            new = Path(td) / "new.yml"
            old.write_text(before, encoding="utf-8")
            new.write_text(after, encoding="utf-8")
            proc = subprocess.run(
                [sys.executable, str(Path(audit.__file__).resolve()), str(new), "--previous-compose", str(old)],
                text=True,
                capture_output=True,
                check=False,
            )
            self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
            self.assertIn("MAGIC_IDENTITY_CHANGE", proc.stdout)


if __name__ == "__main__":
    unittest.main(verbosity=2)
````

<!-- END PORTABLE RESOURCE: scripts/test_magic_variables.py -->

<!-- BEGIN PORTABLE RESOURCE: scripts/test_openspp_learning.py -->
<!-- SOURCE SHA256: ca901db8dd821ec86468017e59284aed3575060a5cf59d463c2e2cf8ebb6f6c3 -->
<!-- EMBEDDED SHA256: 3ce07e703fe5b5912d2595fd6155e01982a13e1064c2abfeb891145e8c9d3622 -->

## Portable resource: `scripts/test_openspp_learning.py`

````python
#!/usr/bin/env python3
"""Executable regression tests for generalized OpenSPP Golden #12 learning.

These tests intentionally verify reasoning/validator boundaries, not OpenSPP topology
reuse. The exact Golden is checked separately by validate_golden_cases.py.
"""

from __future__ import annotations

import hashlib
import subprocess
import sys
import tempfile
import textwrap
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
AUDITOR = ROOT / "scripts/audit_compose.py"
EMBEDDED = ROOT / "scripts/validate_embedded.py"
PROMPTS = ROOT / "references/evaluation-prompts.md"
DISCOVERY = ROOT / "references/architecture-discovery.md"
READINESS = ROOT / "references/production-readiness.md"
COOLIFY = ROOT / "references/coolify-rules.md"
SOURCE = ROOT / "references/source-priority.md"
ANTI = ROOT / "references/anti-patterns.md"
GOLDEN = ROOT / "assets/openspp-2026.08-v1.0.0-golden.yml"


class OpenSPPLearningTests(unittest.TestCase):
    def run_tool(self, tool: Path, compose: str) -> tuple[int, str]:
        with tempfile.TemporaryDirectory() as td:
            path = Path(td) / "compose.yml"
            path.write_text(textwrap.dedent(compose), encoding="utf-8")
            proc = subprocess.run(
                [sys.executable, str(tool), str(path)],
                text=True,
                capture_output=True,
                check=False,
            )
            return proc.returncode, proc.stdout + proc.stderr

    def test_golden_is_exact_operator_accepted_rc8(self) -> None:
        self.assertTrue(GOLDEN.is_file())
        self.assertEqual(
            hashlib.sha256(GOLDEN.read_bytes()).hexdigest(),
            "f00a8755fa2be8e8b1f50970978ae1b57c1877093c2a35108edf35a675d4587b",
        )

    def test_a_to_m_reasoning_prompts_are_present(self) -> None:
        text = PROMPTS.read_text(encoding="utf-8")
        required = (
            "Partially pinned build graph",
            "Health vs activation",
            "Worker race",
            "Local build pre-pull",
            "Managed-file source equality",
            "Universal dollar escaping",
            "envsubst safety",
            "Proxy port leakage",
            "Different DB passwords",
            "Product bundle activation",
            "Platform primitive documentation",
            "Error chronology",
            "Version-scoped compatibility patch",
        )
        for token in required:
            self.assertIn(token, text)

    def test_dependency_closure_not_source_pin_only(self) -> None:
        combined = (DISCOVERY.read_text(encoding="utf-8") + SOURCE.read_text(encoding="utf-8")).lower()
        for token in (
            "build reproducibility / dependency closure map",
            "partially pinned",
            "floating transitive dependencies",
            "source release pin",
        ):
            self.assertIn(token, combined)
        self.assertIn("mutable", combined)

    def test_activation_and_shared_initialization_are_separate_gates(self) -> None:
        text = READINESS.read_text(encoding="utf-8").lower()
        self.assertIn("selected-product activation", text)
        self.assertIn("framework", text)
        self.assertIn("product", text)
        self.assertIn("share mutable", text)
        self.assertIn("authoritative", text)

    def test_local_source_build_without_pull_policy_is_review_required(self) -> None:
        code, out = self.run_tool(
            AUDITOR,
            '''
            services:
              app:
                image: local-app:1.0
                build:
                  context: https://example.invalid/repo.git#deadbeef
            ''',
        )
        self.assertEqual(code, 0, out)
        self.assertIn("REVIEW REQUIRED", out)
        self.assertIn("pull/build sequence", out)
        self.assertIn("pull_policy: never", out)

    def test_documented_configs_content_is_not_assumed_supported_by_coolify(self) -> None:
        code, out = self.run_tool(
            AUDITOR,
            '''
            configs:
              app_config:
                content: |
                  key=value
            services:
              app:
                image: example/app:1
                configs:
                  - source: app_config
                    target: /etc/app.conf
            ''',
        )
        self.assertEqual(code, 0, out)
        self.assertIn("PLATFORM_PRIMITIVE_SUPPORT", out)
        self.assertIn("parser/model/deployment", out)

    def test_managed_nginx_compose_style_double_dollar_is_rejected(self) -> None:
        code, out = self.run_tool(
            EMBEDDED,
            r'''
            services:
              gateway:
                image: nginx:1.30-alpine
                volumes:
                  - type: bind
                    source: ./coolify/gateway/nginx.conf
                    target: /etc/nginx/nginx.conf
                    is_directory: false
                    content: |
                      events {}
                      http { log_format main '$$remote_addr $$request'; }
            ''',
        )
        self.assertEqual(code, 1, out)
        self.assertIn("managed Nginx content", out)
        self.assertIn("$$remote_addr", out)

    def test_unrestricted_envsubst_with_native_nginx_vars_is_review_required(self) -> None:
        code, out = self.run_tool(
            AUDITOR,
            r'''
            services:
              gateway:
                image: nginx:1.30-alpine
                entrypoint: [/bin/sh, -ec]
                command: |
                  envsubst < /etc/nginx/app.conf.template > /tmp/app.conf
                  exec nginx -c /tmp/app.conf -g 'daemon off;'
                volumes:
                  - type: bind
                    source: ./coolify/gateway/nginx.conf.template
                    target: /etc/nginx/app.conf.template
                    is_directory: false
                    content: |
                      events {}
                      http {
                        server {
                          listen 8080;
                          proxy_set_header Host $host;
                        }
                      }
            ''',
        )
        self.assertEqual(code, 0, out)
        self.assertIn("REVIEW REQUIRED", out)
        self.assertIn("unrestricted envsubst", out)
        self.assertIn("explicit envsubst whitelist", out)

    def test_whitelisted_envsubst_is_not_flagged_as_unrestricted(self) -> None:
        code, out = self.run_tool(
            AUDITOR,
            r'''
            services:
              gateway:
                image: nginx:1.30-alpine
                entrypoint: [/bin/sh, -ec]
                command: |
                  envsubst '$${APP_LIMIT}' < /etc/nginx/app.conf.template > /tmp/app.conf
                  exec nginx -c /tmp/app.conf -g 'daemon off;'
                volumes:
                  - type: bind
                    source: ./coolify/gateway/nginx.conf.template
                    target: /etc/nginx/app.conf.template
                    is_directory: false
                    content: |
                      events {}
                      http {
                        server {
                          listen 8080;
                          proxy_set_header Host $host;
                        }
                      }
            ''',
        )
        self.assertEqual(code, 0, out)
        self.assertNotIn("unrestricted envsubst", out)

    def test_intentional_distinct_database_roles_are_review_not_mismatch_error(self) -> None:
        code, out = self.run_tool(
            AUDITOR,
            '''
            services:
              db:
                image: postgres:18
                environment:
                  POSTGRES_USER: ${SERVICE_USER_DBADMIN}
                  POSTGRES_PASSWORD: ${SERVICE_PASSWORD_64_DBADMIN}
                volumes:
                  - db:/var/lib/postgresql/data
              app:
                image: example/app:1
                environment:
                  DB_HOST: db
                  DB_USER: ${SERVICE_USER_DBAPP}
                  DB_PASSWORD: ${SERVICE_PASSWORD_64_DBAPP}
            volumes:
              db: {}
            ''',
        )
        self.assertEqual(code, 0, out)
        self.assertIn("intentional separate DB role", out)
        self.assertNotIn("same generated DB user", out)

    def test_same_logical_database_role_with_two_passwords_remains_error(self) -> None:
        code, out = self.run_tool(
            AUDITOR,
            '''
            services:
              db:
                image: postgres:18
                environment:
                  POSTGRES_USER: ${SERVICE_USER_DBAPP}
                  POSTGRES_PASSWORD: ${SERVICE_PASSWORD_64_DBPRIMARY}
                volumes:
                  - db:/var/lib/postgresql/data
              app:
                image: example/app:1
                environment:
                  DB_HOST: db
                  DB_USER: ${SERVICE_USER_DBAPP}
                  DB_PASSWORD: ${SERVICE_PASSWORD_64_DBOTHER}
            volumes:
              db: {}
            ''',
        )
        self.assertEqual(code, 1, out)
        self.assertIn("same generated DB user", out)
        self.assertIn("different generated password", out)

    def test_proxy_origin_and_managed_file_provenance_guards_are_documented(self) -> None:
        text = (COOLIFY.read_text(encoding="utf-8") + ANTI.read_text(encoding="utf-8")).lower()
        self.assertIn("canonical public origin", text)
        self.assertIn("internal", text)
        self.assertIn("managed-resource identity", text)
        self.assertIn("effective", text)

    def test_error_chronology_and_version_scoped_shims_are_explicit(self) -> None:
        text = (ANTI.read_text(encoding="utf-8") + SOURCE.read_text(encoding="utf-8") + PROMPTS.read_text(encoding="utf-8")).lower()
        self.assertIn("last error", text)
        self.assertIn("root cause", text)
        self.assertIn("version-scoped compatibility patch", text)
        self.assertIn("revalidate", text)


if __name__ == "__main__":
    unittest.main(verbosity=2)
````

<!-- END PORTABLE RESOURCE: scripts/test_openspp_learning.py -->

<!-- BEGIN PORTABLE RESOURCE: scripts/test_profile_selection.py -->
<!-- SOURCE SHA256: 6979703bb4ac11baafa5051e30947caa9fbf217d83297d675570f19ffe843d1a -->
<!-- EMBEDDED SHA256: d8c0ab96f0e3962262bc07eb7826de8aa00b34f60e4be51447685e0d57f9cdb4 -->

## Portable resource: `scripts/test_profile_selection.py`

````python
#!/usr/bin/env python3
"""Regression tests for Baserow-derived multi-profile deployment reasoning."""
from __future__ import annotations

import hashlib
from pathlib import Path
import yaml

ROOT = Path(__file__).resolve().parents[1]
GOLDEN = ROOT / "assets/baserow-2.3.3-v1.0.0-golden.yml"
ALT = ROOT / "references/baserow-validated-alternative-external-postgres-rc2.yml"
REF = ROOT / "references/baserow-reference-all-in-one-rc8.yml"
EXPECTED_SHA = "143c3a94952b16e85638d87bd50fa29a49fa756b7c12cba097fe32383c4312f6"


def main() -> int:
    errors: list[str] = []
    actual = hashlib.sha256(GOLDEN.read_bytes()).hexdigest()
    if actual != EXPECTED_SHA:
        errors.append(f"Baserow exact Golden SHA changed: {actual}")

    g = yaml.safe_load(GOLDEN.read_text(encoding="utf-8"))
    a = yaml.safe_load(ALT.read_text(encoding="utf-8"))
    r = yaml.safe_load(REF.read_text(encoding="utf-8"))
    if len(g.get("services") or {}) <= len(a.get("services") or {}):
        errors.append("profile diversity regression: distributed Golden no longer structurally distinct from all-in-one external-DB alternative")
    if "baserow" not in a.get("services", {}) or "baserow-db" not in a.get("services", {}):
        errors.append("validated alternative lost all-in-one application + external PostgreSQL profile")
    if "baserow-init" not in r.get("services", {}) or "baserow" not in r.get("services", {}):
        errors.append("reference all-in-one defect-workaround profile lost expected init/application shape")

    docs = "\n".join(
        (ROOT / p).read_text(encoding="utf-8") for p in (
            "SKILL.md",
            "references/architecture-discovery.md",
            "references/baserow-case-study.md",
            "references/baserow-profile-evidence-matrix.md",
            "references/eleven-benchmark-audit.md",
            "references/evaluation-prompts.md",
        )
    )
    phrases = (
        "REPAIR ITERATION",
        "DEPLOYMENT PROFILE VARIANT",
        "Canonical Golden Profile",
        "Validated Alternative Profile",
        "Reference / Candidate Profile",
        "Process decomposition and state externalization are independent",
        "platform edge proxy",
        "application semantic gateway",
        "version/profile-scoped",
        "Multiple intentionally evaluated deployment profiles are not failure iterations.",
    )
    for phrase in phrases:
        if phrase not in docs:
            errors.append(f"profile-selection principle missing: {phrase}")

    prompt_expectations = {
        "Test A": ("insufficient information", "operational target"),
        "Test B": ("not automatically", "independent scaling"),
        "Test C": ("State externalization != process decomposition",),
        "Test D": ("false", "application-semantic"),
        "Test E": ("Host/path/application router semantics",),
        "Test F": ("version/profile-scoped workaround",),
    }
    evals = (ROOT / "references/evaluation-prompts.md").read_text(encoding="utf-8")
    for label, tokens in prompt_expectations.items():
        if label not in evals:
            errors.append(f"missing profile-selection evaluation prompt: {label}")
        for token in tokens:
            if token not in evals:
                errors.append(f"{label} expected reasoning token missing: {token}")

    if errors:
        for e in errors:
            print("ERROR:", e)
        return 1
    print("Baserow Golden #11 exact SHA: OK")
    print("Canonical/validated-alternative/reference profile separation: OK")
    print("Repair-iteration vs deployment-profile-variant reasoning: OK")
    print("Process-topology/state-topology independence: OK")
    print("Profile-selection prompts A-F: OK")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
````

<!-- END PORTABLE RESOURCE: scripts/test_profile_selection.py -->

<!-- BEGIN PORTABLE RESOURCE: scripts/test_regression_learning.py -->
<!-- SOURCE SHA256: 3f0c76f14bfeda38dc818cea5dbdb41f8824ebc7f741fa2dcbcf580e7ee3b8e0 -->
<!-- EMBEDDED SHA256: f70728267a09be1682f53db7c3ba732862c8ff1b2e81ae0088c0d97e6dd0f396 -->

## Portable resource: `scripts/test_regression_learning.py`

````python
#!/usr/bin/env python3
"""Permanent NetBox Golden #10 and knowledge-accumulation regression checks."""

from __future__ import annotations

import hashlib
import subprocess
import sys
from pathlib import Path
import yaml

ROOT = Path(__file__).resolve().parents[1]
GOLDEN = ROOT / "assets/netbox-4.6.9-v1.0.0-golden.yml"
EXPECTED_SHA = "e4be06751d206704a2e9460ac2926d92833b39a71266cf1bd5a8a788da319804"


def env_map(service: dict) -> dict[str, str | None]:
    env = service.get("environment") or []
    if isinstance(env, dict):
        return {str(k): None if v is None else str(v) for k, v in env.items()}
    out: dict[str, str | None] = {}
    for item in env:
        if isinstance(item, str):
            if "=" in item:
                k, v = item.split("=", 1)
                out[k] = v
            else:
                out[item] = None
    return out


def main() -> int:
    errors: list[str] = []
    actual = hashlib.sha256(GOLDEN.read_bytes()).hexdigest()
    if actual != EXPECTED_SHA:
        errors.append(f"exact NetBox fixture SHA changed: {actual}")
    doc = yaml.safe_load(GOLDEN.read_text(encoding="utf-8"))
    services = doc["services"]
    if set(services) != {"netbox", "netbox-worker", "postgres", "redis", "redis-cache"}:
        errors.append(f"NetBox service topology changed: {sorted(services)}")

    web = services["netbox"]
    hc = " ".join(str(x) for x in ((web.get("healthcheck") or {}).get("test") or []))
    if "localhost:8080/login/" not in hc or "127.0.0.1:8080/login/" in hc:
        errors.append("accepted localhost health Host semantics changed")

    config_targets = []
    for m in web.get("volumes") or []:
        if isinstance(m, dict):
            target = str(m.get("target") or "")
            if target.startswith("/etc/netbox/config"):
                config_targets.append(target)
        elif isinstance(m, str):
            target = m.split(":", 2)[1] if ":" in m else m
            if target.startswith("/etc/netbox/config"):
                config_targets.append(target)
    if config_targets != ["/etc/netbox/config/zz_coolify.py"]:
        errors.append(f"image-baked config/minimal override contract changed: {config_targets}")

    worker_cmd = " ".join(str(x) for x in services["netbox-worker"].get("command") or [])
    if "manage.py" not in worker_cmd or "rqworker" not in worker_cmd:
        errors.append("native RQ worker command changed")

    tasks_cmd = " ".join(str(x) for x in services["redis"].get("command") or [])
    cache_cmd = " ".join(str(x) for x in services["redis-cache"].get("command") or [])
    if "--appendonly yes" not in tasks_cmd or "--appendonly" in cache_cmd:
        errors.append("tasks/cache durability semantics changed")
    for name in ("postgres", "redis", "redis-cache"):
        if services[name].get("ports"):
            errors.append(f"private NetBox infrastructure exposed: {name}")

    wenv = env_map(web)
    if wenv.get("SKIP_SUPERUSER") != "false" or "SUPERUSER_PASSWORD" not in wenv:
        errors.append("native superuser bootstrap contract changed")

    skill = (ROOT / "SKILL.md").read_text(encoding="utf-8")
    analysis = (ROOT / "references/rc5-vs-rc6-netbox-regression-analysis.md").read_text(encoding="utf-8")
    ten = (ROOT / "references/ten-benchmark-audit.md").read_text(encoding="utf-8")
    required_phrases = (
        "Adding new Golden knowledge must not reduce the Skill's ability to rediscover a target from current upstream evidence.",
        "Skill evolution is monotonic only when new knowledge improves or preserves performance on previously solvable architecture classes.",
        "Golden fixtures are regression oracles, not architecture templates.",
    )
    combined = "\n".join((skill, analysis, ten))
    for phrase in required_phrases:
        if phrase not in combined:
            errors.append(f"meta-regression principle missing: {phrase}")

    # Golden similarity must remain advisory and must not overrule supplied upstream
    # provenance when the candidate capability graph matches that baseline. Using the
    # immutable NetBox fixture as both candidate and provenance baseline isolates this
    # validator behavior without inventing a second architecture.
    proc = subprocess.run(
        [
            sys.executable,
            str(ROOT / "scripts/validate_golden_cases.py"),
            str(ROOT),
            "--candidate",
            str(GOLDEN),
            "--upstream",
            str(GOLDEN),
        ],
        text=True,
        capture_output=True,
        check=False,
    )
    advisory = proc.stdout + proc.stderr
    if proc.returncode != 0:
        errors.append("Golden similarity/provenance regression subtest failed to execute")
    if "high Golden similarity is informational" not in advisory:
        errors.append("high Golden similarity did not defer to supplied upstream provenance")
    if "REVIEW REQUIRED: candidate resembles a bundled golden case" in advisory:
        errors.append("Golden-similarity bias regression: upstream-matching candidate still treated as resemblance review")

    if errors:
        for item in errors:
            print("ERROR:", item)
        return 1
    print("NetBox Golden #10 exact SHA: OK")
    print("NetBox five-service/image-baked-config/native-lifecycle invariants: OK")
    print("NetBox localhost Host-semantics regression: OK")
    print("NetBox tasks/cache state-semantics regression: OK")
    print("Knowledge-accumulation meta-regression principles: OK")
    print("Golden-similarity/upstream-provenance meta-regression: OK")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
````

<!-- END PORTABLE RESOURCE: scripts/test_regression_learning.py -->

<!-- BEGIN PORTABLE RESOURCE: scripts/validate_compose.sh -->
<!-- SOURCE SHA256: 61a7d53c77af95e90a9cc2be780fff3e4d2a5f4df0a3a61f0c67c402deb3c23d -->
<!-- EMBEDDED SHA256: 7d39401ecf23976d0b79c11aa4d4f739aba22cc092015d45a7dfdfd6e784a069 -->

## Portable resource: `scripts/validate_compose.sh`

````bash
#!/usr/bin/env bash
set -euo pipefail

compose="${1:-}"
if [[ -z "$compose" || ! -f "$compose" ]]; then
  echo "Usage: $0 path/to/docker-compose.yml" >&2
  exit 2
fi

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

python3 - <<'PY' "$compose"
import sys, yaml
p=sys.argv[1]
yaml.safe_load(open(p, encoding='utf-8'))
print('INFO: YAML parse: OK')
PY

if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
  docker compose -f "$compose" config >/dev/null
  echo "INFO: docker compose config: OK"
else
  echo "INFO: docker compose config: SKIPPED (Docker Compose unavailable; do not claim Compose-render validation)"
fi

python3 "$script_dir/audit_compose.py" "$compose"
python3 "$script_dir/validate_embedded.py" "$compose"
````

<!-- END PORTABLE RESOURCE: scripts/validate_compose.sh -->

<!-- BEGIN PORTABLE RESOURCE: scripts/validate_embedded.py -->
<!-- SOURCE SHA256: 133b70a20c1a2e96aff32ee72555b80e645eb2b67e4ac3467adb1e3566197b2d -->
<!-- EMBEDDED SHA256: f056b2af9f076d3e0a762ba71d26bd9211c9e8fa2809bfc705c2cc46b6e20cb8 -->

## Portable resource: `scripts/validate_embedded.py`

````python
#!/usr/bin/env python3
"""Syntax-check executable text embedded in a Compose file.

Checks executable text in six places:
1. Coolify managed bind-file `volumes[].content` resources;
2. shell bodies passed through Compose `command:` or `entrypoint: [bash/sh, -c]` + `command:`;
3. `healthcheck.test: [CMD-SHELL, ...]` bodies;
4. obvious static source files created by heredoc inside those shell bodies;
5. direct interpreter heredocs such as `node --input-type=module <<'NODE'`;
6. deterministic literal nested `sed` programs, after Compose `$$ -> $` representation.

The managed-file and Compose-command contexts deliberately have different dollar-escaping rules. `$${VAR}` is often
needed in Compose command strings so `${VAR}` survives Compose interpolation, while
that same text inside a generated shell file would reach Bash as `$$` (PID). OpenSPP
Golden #12 adds the high-confidence managed-Nginx case: native `$remote_addr`-class
variables must not be converted to literal `$$remote_addr` by transport confusion.
"""

from __future__ import annotations

import argparse
import json
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any

ESCAPED_SHELL_VAR = re.compile(r"\$\$\{[A-Za-z_][A-Za-z0-9_]*\}")
MANAGED_NGINX_DOUBLE_DOLLAR = re.compile(r"\$\$(?:remote_addr|remote_user|time_local|request|status|body_bytes_sent|http_referer|http_user_agent|http_upgrade|connection_upgrade|binary_remote_addr|host|proxy_add_x_forwarded_for)\b")

HEREDOC_FILE_RE = re.compile(
    r"^\s*cat\s+>\s*(?P<path>[^\s]+)\s+<<(?P<strip>-)?\s*['\"]?(?P<delim>[A-Za-z_][A-Za-z0-9_]*)['\"]?\s*$"
)

INTERPRETER_HEREDOC_RE = re.compile(
    r"^\s*(?:exec\s+)?(?P<interp>node|python3?|bash|sh)(?:\s+[^<]*)?\s+<<(?P<strip>-)?\s*['\"]?(?P<delim>[A-Za-z_][A-Za-z0-9_]*)['\"]?\s*$"
)

# Mongo/JavaScript update operators are a proven nested-language collision class:
# Compose treats `$set` like an interpolation token unless the source contains `$$set`.
_UNESCAPED_NESTED_DOLLAR_OPERATOR_RE = re.compile(
    r"(?<!\$)\$(set|unset|inc|push|pull|addToSet|rename|currentDate|setOnInsert)\b"
)


def validate_compose_nested_dollar_escapes(script: str) -> list[str]:
    failures: list[str] = []
    for match in _UNESCAPED_NESTED_DOLLAR_OPERATOR_RE.finditer(script):
        failures.append(
            f"literal nested Mongo/JavaScript operator ${match.group(1)} must be written "
            f"$${match.group(1)} in Compose command source so the runtime receives ${match.group(1)}"
        )
    return failures


def extract_interpreter_heredocs(script: str) -> list[tuple[str, str]]:
    """Extract obvious direct interpreter heredocs conservatively."""
    lines = script.splitlines()
    out: list[tuple[str, str]] = []
    i = 0
    counter = 0
    while i < len(lines):
        m = INTERPRETER_HEREDOC_RE.match(lines[i])
        if not m:
            i += 1
            continue
        interp = m.group("interp")
        delim = m.group("delim")
        strip_tabs = bool(m.group("strip"))
        body: list[str] = []
        j = i + 1
        while j < len(lines):
            candidate = lines[j].lstrip("\t") if strip_tabs else lines[j]
            if candidate == delim:
                counter += 1
                suffix = ".mjs" if interp == "node" else ".py" if interp.startswith("python") else ".sh"
                out.append((f"interpreter-stdin-{counter}{suffix}", "\n".join(body) + "\n"))
                i = j + 1
                break
            body.append(lines[j].lstrip("\t") if strip_tabs else lines[j])
            j += 1
        else:
            i += 1
    return out


def extract_static_heredoc_files(script: str) -> list[tuple[str, str]]:
    """Extract obvious `cat > file.ext <<'TAG'` heredocs conservatively.

    This intentionally ignores dynamic paths, redirection variants and complex shell
    grammar. It is a best-effort syntax-check aid, not a shell parser.
    """
    lines = script.splitlines()
    out: list[tuple[str, str]] = []
    i = 0
    while i < len(lines):
        m = HEREDOC_FILE_RE.match(lines[i])
        if not m:
            i += 1
            continue
        path = m.group("path")
        suffix = Path(path).suffix.lower()
        if suffix not in {".js", ".mjs", ".cjs", ".py", ".sh", ".bash", ".json"}:
            i += 1
            continue
        delim = m.group("delim")
        strip_tabs = bool(m.group("strip"))
        body: list[str] = []
        j = i + 1
        while j < len(lines):
            candidate = lines[j].lstrip("\t") if strip_tabs else lines[j]
            if candidate == delim:
                out.append((path, "\n".join(body) + "\n"))
                i = j + 1
                break
            body.append(lines[j].lstrip("\t") if strip_tabs else lines[j])
            j += 1
        else:
            i += 1
    return out

try:
    import yaml
except Exception as exc:
    print(f"ERROR: PyYAML is required: {exc}", file=sys.stderr)
    raise SystemExit(2)


def check(cmd: list[str]) -> tuple[bool, str]:
    p = subprocess.run(cmd, text=True, capture_output=True)
    return p.returncode == 0, (p.stderr or p.stdout).strip()


def shell_binary(token: str) -> str | None:
    base = Path(token).name
    if base == "bash":
        return shutil.which("bash")
    if base in {"sh", "ash", "dash"}:
        return shutil.which(base) or shutil.which("sh")
    return None


def command_shell_body(service: dict[str, Any]) -> tuple[str, str] | None:
    """Return (shell, script) for clear shell -c/-ec Compose forms.

    Supports both:
      command: [bash, -c, "..."]
    and the common Compose shape:
      entrypoint: [bash, -c]
      command: ["..."]

    This remains conservative; it is not a complete Docker argv parser.
    """
    command = service.get("command")
    if isinstance(command, list) and len(command) >= 3:
        shell = str(command[0])
        option = str(command[1])
        if "c" in option and shell_binary(shell):
            return shell, str(command[2])

    entrypoint = service.get("entrypoint")
    if isinstance(entrypoint, list) and len(entrypoint) >= 2:
        shell = str(entrypoint[0])
        option = str(entrypoint[1])
        if "c" in option and shell_binary(shell):
            if isinstance(command, list) and command:
                return shell, str(command[0])
            if isinstance(command, str):
                return shell, command
    return None


def healthcheck_shell_body(service: dict[str, Any]) -> tuple[str, str] | None:
    """Return (`sh`, script) for Docker/Compose CMD-SHELL healthchecks."""
    healthcheck = service.get("healthcheck")
    if not isinstance(healthcheck, dict):
        return None
    test = healthcheck.get("test")
    if isinstance(test, list) and len(test) >= 2 and str(test[0]).upper() == "CMD-SHELL":
        return "sh", str(test[1])
    return None


_SINGLE_QUOTED_HEADER_VAR_RE = re.compile(
    r"-H\s+'(?P<header>Host|Origin):\s*\$\{[A-Za-z_][A-Za-z0-9_]*\}'",
    re.IGNORECASE,
)


def validate_runtime_shell_semantics(runtime_script: str) -> list[str]:
    """Detect a few high-confidence shell-quoting bugs proven by runtime cases.

    This is deliberately narrow. It does not flag arbitrary variables inside single
    quotes because literal dollar text can be intentional. Host/Origin HTTP headers
    containing `${VAR}` are checked because the ERPNext benchmark demonstrated that
    single quotes caused Frappe to receive the literal site name/origin.
    """
    failures: list[str] = []
    for match in _SINGLE_QUOTED_HEADER_VAR_RE.finditer(runtime_script):
        failures.append(
            f"{match.group('header')} header contains a shell variable inside single quotes; "
            "the runtime shell will not expand it"
        )
    return failures


_SINGLE_QUOTED_SED_RE = re.compile(r"\bsed(?:\s+-[A-Za-z]+)*\s+'([^']*)'")
_DOUBLE_QUOTED_SED_RE = re.compile(r'\bsed(?:\s+-[A-Za-z]+)*\s+"([^"]*)"')


def validate_literal_sed_scripts(runtime_script: str) -> tuple[int, list[str], list[str]]:
    """Syntax-check obvious literal sed programs with the host sed binary.

    We intentionally do not build a sed parser. Single-quoted programs are deterministic
    shell literals and can be checked directly. Double-quoted programs are checked only
    when they contain no obvious shell expansion. Dynamic sed programs are skipped.
    """
    sed_bin = shutil.which("sed")
    if not sed_bin:
        return 0, [], ["literal sed validation skipped (sed unavailable)"]

    candidates: list[tuple[str, str]] = [("single", m.group(1)) for m in _SINGLE_QUOTED_SED_RE.finditer(runtime_script)]
    candidates += [("double", m.group(1)) for m in _DOUBLE_QUOTED_SED_RE.finditer(runtime_script)]
    checked = 0
    failures: list[str] = []
    skipped: list[str] = []
    for quote_kind, program in candidates:
        if quote_kind == "double" and re.search(r"\$\{|\$\(|`|\$[A-Za-z_]", program):
            skipped.append(f"dynamic double-quoted sed program skipped: {program!r}")
            continue
        proc = subprocess.run(
            [sed_bin, "-e", program],
            input="",
            text=True,
            capture_output=True,
            check=False,
        )
        checked += 1
        if proc.returncode != 0:
            msg = (proc.stderr or proc.stdout).strip()
            failures.append(f"literal sed program {program!r}: {msg or 'sed syntax check failed'}")
    return checked, failures, skipped


def validate_text(temp_suffix: str, content: str, service_name: str, source: str, generated_file: bool) -> tuple[str, str | None]:
    """Return status CHECKED/SKIP and optional failure message."""
    suffix = temp_suffix.lower()
    with tempfile.NamedTemporaryFile("w", suffix=suffix or ".txt", delete=False) as f:
        f.write(content)
        temp = Path(f.name)
    try:
        if suffix in {".sh", ".bash"}:
            shell = shutil.which("bash")
            if not shell:
                return "SKIP", "bash unavailable"
            ok, msg = check([shell, "-n", str(temp)])
            if ok and generated_file and ESCAPED_SHELL_VAR.search(content):
                ok = False
                msg = "managed shell file contains $${VAR}; generated file Bash sees $$ as PID. Use ${VAR} unless PID text is explicitly intended."
        elif suffix in {".js", ".mjs", ".cjs"}:
            node = shutil.which("node")
            if not node:
                return "SKIP", "node unavailable"
            ok, msg = check([node, "--check", str(temp)])
        elif suffix == ".py":
            python = shutil.which("python3") or shutil.which("python")
            if not python:
                return "SKIP", "python unavailable"
            ok, msg = check([python, "-m", "py_compile", str(temp)])
        elif suffix == ".json":
            try:
                json.loads(content)
                ok, msg = True, ""
            except Exception as exc:
                ok, msg = False, str(exc)
        elif suffix in {".conf", ".template"} or "nginx" in source.lower():
            return "SKIP", "validate in matching Nginx image/filesystem"
        else:
            return "SKIP", f"unsupported extension {suffix or 'none'}"
        return "CHECKED", None if ok else msg
    finally:
        temp.unlink(missing_ok=True)


def validate_shell_command(service_name: str, shell_token: str, script: str) -> tuple[str, str | None]:
    shell = shell_binary(shell_token)
    if not shell:
        return "SKIP", f"shell unavailable: {shell_token}"
    # Compose-level $$ is meant to survive as a literal $ at container runtime.
    # Syntax-check the runtime shell text that the container will effectively see.
    runtime_script = script.replace("$$", "$")
    with tempfile.NamedTemporaryFile("w", suffix=".sh", delete=False) as f:
        f.write(runtime_script)
        temp = Path(f.name)
    try:
        ok, msg = check([shell, "-n", str(temp)])
        return "CHECKED", None if ok else msg
    finally:
        temp.unlink(missing_ok=True)


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("compose", type=Path)
    args = ap.parse_args()

    try:
        doc = yaml.safe_load(args.compose.read_text(encoding="utf-8")) or {}
    except Exception as exc:
        print("ERROR: YAML parse failed:", exc)
        return 1
    services = doc.get("services", {})
    if not isinstance(services, dict):
        print("ERROR: services must be a mapping")
        return 1

    failures: list[str] = []
    checked_files = 0
    checked_commands = 0
    checked_healthchecks = 0
    checked_nested = 0
    skipped: list[str] = []

    for service_name, service in services.items():
        if not isinstance(service, dict):
            continue

        volumes = service.get("volumes") or []
        if isinstance(volumes, list):
            for mount in volumes:
                if not isinstance(mount, dict) or "content" not in mount:
                    continue
                source = str(mount.get("source") or mount.get("target") or "embedded")
                target = str(mount.get("target") or "")
                content = str(mount["content"])
                suffix = Path(source).suffix.lower()
                if "nginx" in (source + " " + target).lower():
                    bad = MANAGED_NGINX_DOUBLE_DOLLAR.search(content)
                    if bad:
                        failures.append(
                            f"{service_name}:{source}: managed Nginx content contains literal {bad.group(0)!r}. "
                            "Coolify managed-file content can be transported literally; native Nginx variables require the effective file representation (normally a single `$` at the Nginx parser). Do not apply Compose `$$` escaping without an interpolation-layer proof."
                        )
                status, msg = validate_text(suffix, content, service_name, source, generated_file=True)
                if status == "SKIP":
                    skipped.append(f"{service_name}:{source} ({msg})")
                else:
                    checked_files += 1
                    if msg:
                        failures.append(f"{service_name}:{source}: {msg}")

        shell_cmd = command_shell_body(service)
        if shell_cmd:
            shell_token, script = shell_cmd
            failures.extend(
                f"{service_name}:command: {item}"
                for item in validate_compose_nested_dollar_escapes(script)
            )
            status, msg = validate_shell_command(service_name, shell_token, script)
            if status == "SKIP":
                skipped.append(f"{service_name}:command ({msg})")
            else:
                checked_commands += 1
                if msg:
                    failures.append(f"{service_name}:command: {msg}")

            # Compose-level $$ escapes become literal $ for the runtime shell.
            runtime_script = script.replace("$$", "$")
            failures.extend(
                f"{service_name}:command: {item}"
                for item in validate_runtime_shell_semantics(runtime_script)
            )

            sed_checked, sed_failures, sed_skipped = validate_literal_sed_scripts(runtime_script)
            checked_nested += sed_checked
            skipped.extend(f"{service_name}:command ({item})" for item in sed_skipped)
            failures.extend(f"{service_name}:command: {item}" for item in sed_failures)

            # Syntax-check obvious secondary source files created by heredoc inside
            # the shell body.
            for embedded_path, embedded_content in extract_static_heredoc_files(runtime_script):
                suffix = Path(embedded_path).suffix.lower()
                hstatus, hmsg = validate_text(suffix, embedded_content, service_name, embedded_path, generated_file=False)
                if hstatus == "SKIP":
                    skipped.append(f"{service_name}:command-heredoc:{embedded_path} ({hmsg})")
                else:
                    checked_files += 1
                    if hmsg:
                        failures.append(f"{service_name}:command-heredoc:{embedded_path}: {hmsg}")

            for embedded_path, embedded_content in extract_interpreter_heredocs(runtime_script):
                suffix = Path(embedded_path).suffix.lower()
                hstatus, hmsg = validate_text(suffix, embedded_content, service_name, embedded_path, generated_file=False)
                if hstatus == "SKIP":
                    skipped.append(f"{service_name}:interpreter-heredoc:{embedded_path} ({hmsg})")
                else:
                    checked_files += 1
                    if hmsg:
                        failures.append(f"{service_name}:interpreter-heredoc:{embedded_path}: {hmsg}")

        health_shell = healthcheck_shell_body(service)
        if health_shell:
            shell_token, script = health_shell
            status, msg = validate_shell_command(service_name, shell_token, script)
            if status == "SKIP":
                skipped.append(f"{service_name}:healthcheck ({msg})")
            else:
                checked_healthchecks += 1
                if msg:
                    failures.append(f"{service_name}:healthcheck: {msg}")

            runtime_script = script.replace("$$", "$")
            failures.extend(
                f"{service_name}:healthcheck: {item}"
                for item in validate_runtime_shell_semantics(runtime_script)
            )
            sed_checked, sed_failures, sed_skipped = validate_literal_sed_scripts(runtime_script)
            checked_nested += sed_checked
            skipped.extend(f"{service_name}:healthcheck ({item})" for item in sed_skipped)
            failures.extend(f"{service_name}:healthcheck: {item}" for item in sed_failures)

    print(f"Managed/secondary embedded files checked: {checked_files}")
    print(f"Embedded shell commands checked: {checked_commands}")
    print(f"CMD-SHELL healthchecks checked: {checked_healthchecks}")
    print(f"Literal nested sed programs checked: {checked_nested}")
    print(f"Skipped: {len(skipped)}")
    for item in skipped:
        print("INFO SKIP:", item)
    for item in failures:
        print("ERROR:", item)
    return 1 if failures else 0


if __name__ == "__main__":
    raise SystemExit(main())
````

<!-- END PORTABLE RESOURCE: scripts/validate_embedded.py -->

<!-- BEGIN PORTABLE RESOURCE: scripts/validate_golden_cases.py -->
<!-- SOURCE SHA256: d2c92d75e44f829036434118e61378a6982cea2cd2a3da09e8d7650b485e1707 -->
<!-- EMBEDDED SHA256: d96d9ed3c8aee3a620e108ef1b82eb58ef2eaee03a069369658acc8507c44b1b -->

## Portable resource: `scripts/validate_golden_cases.py`

````python
#!/usr/bin/env python3
"""Validate positive structural invariants for the twelve bundled golden fixtures.

Frappe Framework is Golden / Regression Case #6 and must remain Frappe-only.
ERPNext v16.33.0 is Golden / Regression Case #7 after operator-confirmed runtime
acceptance of the sibling-product benchmark. Mem0 v2.0.19 is Golden / Regression
Case #8 after exact-RC1 first-candidate runtime acceptance. Overleaf Community
Edition 6.2.2 is Golden / Regression Case #9 after operator-confirmed RC4 acceptance.
NetBox 4.6.9 / netbox-docker 5.0.2 is Golden / Regression Case #10 from the
exact runtime-accepted RC2 bytes produced by the RC5 benchmark path.
Baserow 2.3.3 is Golden / Regression Case #11 from the exact operator-accepted
distributed/custom RC5 bytes; alternative Baserow profiles are not numbered Goldens.
OpenSPP V2 2026.08 is Golden / Regression Case #12 from the exact operator-accepted
RC8 bytes after operator-confirmed completion of the requested runtime suite.

Golden fixtures are immutable regression oracles, not generic templates. This script
does not use cross-case token blacklists. Optional candidate comparison reports
architectural similarity only as INFO/REVIEW evidence. When an upstream Compose is
provided, it also reports advisory capability deltas without enforcing a service-count
threshold or rejecting a component merely because another golden contains it.
"""

from __future__ import annotations

import argparse
import hashlib
import re
from pathlib import Path

try:
    import yaml
except Exception as exc:
    raise SystemExit(f"ERROR: PyYAML is required: {exc}")


def load(path: Path) -> tuple[str, dict]:
    raw = path.read_text(encoding="utf-8")
    doc = yaml.safe_load(raw) or {}
    if not isinstance(doc, dict) or not isinstance(doc.get("services"), dict):
        raise ValueError(f"invalid Compose services in {path}")
    return raw, doc


def env_map(service: dict) -> dict[str, str | None]:
    env = service.get("environment") or []
    if isinstance(env, dict):
        return {str(k): None if v is None else str(v) for k, v in env.items()}
    out: dict[str, str | None] = {}
    for item in env if isinstance(env, list) else []:
        if not isinstance(item, str):
            continue
        if "=" in item:
            k, v = item.split("=", 1)
            out[k] = v
        else:
            out[item] = None
    return out


def fail(errors: list[str], msg: str) -> None:
    errors.append(msg)


def require_service_set(label: str, services: dict, expected: set[str], errors: list[str]) -> bool:
    if set(services) != expected:
        fail(errors, f"{label} service set changed: expected {sorted(expected)}, got {sorted(services)}")
        return False
    return True


def has_host_ports(service: dict) -> bool:
    return bool(service.get("ports"))


def sha256_file(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def check_ckan(path: Path, errors: list[str]) -> None:
    raw, doc = load(path)
    services = doc["services"]
    expected = {"ckan", "ckan-worker", "datapusher", "db", "solr", "redis"}
    if not require_service_set("CKAN", services, expected, errors):
        return

    web = services["ckan"]
    env = env_map(web)
    if "SERVICE_URL_CKAN_5000" not in env:
        fail(errors, "CKAN golden lost SERVICE_URL_CKAN_5000 proxy-routing variable")
    if "SERVICE_FQDN_CKAN" not in env:
        fail(errors, "CKAN golden lost unqualified SERVICE_FQDN_CKAN canonical-host variable")
    if "SERVICE_FQDN_CKAN_5000" in env:
        fail(errors, "CKAN golden reintroduced port-qualified SERVICE_FQDN_CKAN_5000")
    if env.get("CKAN_SITE_URL") != "https://${SERVICE_FQDN_CKAN}":
        fail(errors, f"CKAN_SITE_URL regression: {env.get('CKAN_SITE_URL')!r}")
    if env.get("CKAN__DATAPUSHER__CALLBACK_URL_BASE") != "http://ckan:5000":
        fail(errors, "CKAN DataPusher internal callback base changed")
    if "status_show" not in str((web.get("healthcheck") or {}).get("test")):
        fail(errors, "CKAN web healthcheck no longer uses application status_show readiness")

    worker = services["ckan-worker"]
    deps = worker.get("depends_on") or {}
    condition = ((deps.get("ckan") or {}).get("condition") if isinstance(deps.get("ckan"), dict) else None)
    if condition != "service_started":
        fail(errors, "CKAN worker must not regress to a service_healthy-only start gate")
    cmd = str(worker.get("command") or "")
    if "Waiting for CKAN API readiness" not in cmd or "jobs worker" not in cmd:
        fail(errors, "CKAN worker lost application-level readiness or upstream worker command")
    whc = str((worker.get("healthcheck") or {}).get("test") or "")
    if "/proc/1/cmdline" in whc:
        fail(errors, "CKAN worker reintroduced brittle /proc/1/cmdline health matching")
    if "ckan-worker-ready" not in whc:
        fail(errors, "CKAN worker healthcheck lost readiness marker")

    db = services["db"]
    mounts = [m for m in (db.get("volumes") or []) if isinstance(m, dict) and "content" in m]
    if len(mounts) != 2:
        fail(errors, f"CKAN DB expected two managed init files, found {len(mounts)}")
    for m in mounts:
        if m.get("is_directory") is not False:
            fail(errors, f"CKAN DB init mount must explicitly be a file: {m.get('target')}")
        if re.search(r"\$\$\{[A-Za-z_][A-Za-z0-9_]*\}", str(m.get("content") or "")):
            fail(errors, f"CKAN DB init file reintroduced $${{VAR}} escaping: {m.get('target')}")

    for internal in ("db", "solr", "redis", "datapusher"):
        if has_host_ports(services[internal]):
            fail(errors, f"CKAN internal service {internal} unexpectedly publishes a host port")


def check_kobo(path: Path, errors: list[str]) -> None:
    raw, doc = load(path)
    services = doc["services"]
    expected = {
        "postgres", "mongo", "redis-main", "redis-cache", "enketo-express", "kpi",
        "worker", "worker-low-priority", "worker-long-running-tasks", "worker-kobocat",
        "beat", "kf", "kc", "ee",
    }
    if not require_service_set("KoboToolbox", services, expected, errors):
        return
    for edge, magic in (("kf", "SERVICE_URL_KF_80"), ("kc", "SERVICE_URL_KC_80"), ("ee", "SERVICE_URL_EE_80")):
        if magic not in env_map(services[edge]):
            fail(errors, f"Kobo golden lost public routing magic variable {magic}")
    for internal in ("postgres", "mongo", "redis-main", "redis-cache"):
        if has_host_ports(services[internal]):
            fail(errors, f"Kobo internal state service {internal} unexpectedly publishes a host port")
    if not (doc.get("volumes") or {}):
        fail(errors, "Kobo golden lost named persistence volumes")
    # Keep exact public/service semantics but do not blacklist technologies from other cases.
    if "NGINX_ENVSUBST_FILTER" not in env_map(services["kc"]):
        fail(errors, "Kobo KC gateway lost its validated envsubst filter")


def check_openmrs(path: Path, errors: list[str]) -> None:
    raw, doc = load(path)
    services = doc["services"]
    expected = {"gateway", "frontend", "backend", "db"}
    if not require_service_set("OpenMRS", services, expected, errors):
        return
    if "OpenMRS regression fixture / golden case — NOT a generic Coolify skeleton." not in raw:
        fail(errors, "OpenMRS golden lost explicit non-generic-skeleton warning")

    gateway = services["gateway"]
    genv = env_map(gateway)
    if "SERVICE_URL_GATEWAY_80" not in genv:
        fail(errors, "OpenMRS gateway lost SERVICE_URL_GATEWAY_80")
    if gateway.get("ports"):
        fail(errors, "OpenMRS gateway should use Coolify proxy routing, not host ports")
    ghc = str((gateway.get("healthcheck") or {}).get("test") or "")
    if "/openmrs/health/started" not in ghc or "/openmrs/spa/home" not in ghc:
        fail(errors, "OpenMRS gateway healthcheck lost accepted backend+SPA readiness path")
    if str((gateway.get("healthcheck") or {}).get("start_period")) != "15m":
        fail(errors, "OpenMRS gateway accepted first-boot start_period changed")

    frontend = services["frontend"]
    fenv = env_map(frontend)
    expected_front = {
        "SPA_PATH": "/openmrs/spa",
        "API_URL": "/openmrs",
        "SPA_CONFIG_URLS": "/openmrs/spa/config-core_demo.json",
    }
    for key, expected_value in expected_front.items():
        if fenv.get(key) != expected_value:
            fail(errors, f"OpenMRS frontend {key} regression: {fenv.get(key)!r}")

    backend = services["backend"]
    benv = env_map(backend)
    expected_backend = {
        "OMRS_DB_HOSTNAME": "db",
        "OMRS_DB_NAME": "openmrs",
        "OMRS_DB_USERNAME": "${SERVICE_USER_MYSQL}",
        "OMRS_DB_PASSWORD": "${SERVICE_PASSWORD_64_MYSQL}",
        "OMRS_ADMIN_USER_PASSWORD": "${SERVICE_PASSWORD_64_ADMIN}",
    }
    for key, expected_value in expected_backend.items():
        if benv.get(key) != expected_value:
            fail(errors, f"OpenMRS backend {key} regression: {benv.get(key)!r}")
    if benv.get("OMRS_AUTO_UPDATE_DATABASE") != "true" or benv.get("OMRS_CREATE_TABLES") != "true":
        fail(errors, "OpenMRS accepted bootstrap database flags changed; any lifecycle change requires a new live regression run")
    command = backend.get("command") or []
    command_text = "\n".join(str(x) for x in command) if isinstance(command, list) else str(command)
    for token in ("OMRS_ADMIN_USER_PASSWORD", "database username is empty", "admin password requires a digit", "exec /openmrs/startup.sh"):
        if token not in command_text:
            fail(errors, f"OpenMRS backend preflight lost accepted token: {token}")
    bhc = str((backend.get("healthcheck") or {}).get("test") or "")
    if "/openmrs/health/started" not in bhc:
        fail(errors, "OpenMRS backend healthcheck no longer uses /openmrs/health/started")
    if str((backend.get("healthcheck") or {}).get("start_period")) != "15m":
        fail(errors, "OpenMRS backend accepted first-boot start_period changed")

    db = services["db"]
    denv = env_map(db)
    expected_db = {
        "MYSQL_DATABASE": "openmrs",
        "MYSQL_USER": "${SERVICE_USER_MYSQL}",
        "MYSQL_PASSWORD": "${SERVICE_PASSWORD_64_MYSQL}",
        "MYSQL_ROOT_PASSWORD": "${SERVICE_PASSWORD_64_MYSQLROOT}",
    }
    for key, expected_value in expected_db.items():
        if denv.get(key) != expected_value:
            fail(errors, f"OpenMRS DB {key} regression: {denv.get(key)!r}")
    if services["db"].get("image") != "mariadb:10.11.7":
        fail(errors, "OpenMRS golden MariaDB pin changed")
    for name in ("gateway", "frontend", "backend"):
        image = str(services[name].get("image") or "")
        if not image.endswith(":3.7.1"):
            fail(errors, f"OpenMRS {name} image no longer pinned to 3.7.1: {image}")
    for internal in ("frontend", "backend", "db"):
        if has_host_ports(services[internal]):
            fail(errors, f"OpenMRS internal service {internal} unexpectedly publishes a host port")
    volumes = doc.get("volumes") or {}
    if set(volumes) != {"openmrs-data", "db-data"}:
        fail(errors, f"OpenMRS persistence volume set changed: {sorted(volumes)}")



def mount_targets(service: dict) -> set[str]:
    out: set[str] = set()
    for item in service.get("volumes") or []:
        if isinstance(item, str):
            parts = item.split(":")
            if len(parts) >= 2:
                out.add(parts[1])
        elif isinstance(item, dict) and item.get("target"):
            out.add(str(item["target"]))
    return out


def check_openemr(path: Path, errors: list[str]) -> None:
    raw, doc = load(path)
    services = doc["services"]
    expected = {"mysql", "openemr"}
    if not require_service_set("OpenEMR", services, expected, errors):
        return
    if "OpenEMR regression fixture / golden case — NOT a generic Coolify skeleton." not in raw:
        fail(errors, "OpenEMR golden lost explicit non-generic-skeleton warning")

    mysql = services["mysql"]
    menv = env_map(mysql)
    if mysql.get("image") != "mariadb:11.8.8@sha256:d9f7eb2637296652f24b484afd5d246f759f49f5babcadc6a9e344c9acb75fbf":
        fail(errors, "OpenEMR golden MariaDB image/digest changed")
    if menv.get("MYSQL_ROOT_PASSWORD") != "${SERVICE_PASSWORD_64_OPENEMRDBROOT}":
        fail(errors, "OpenEMR MariaDB root magic credential changed")
    if "MYSQL_DATABASE" in menv or "MARIADB_DATABASE" in menv:
        fail(errors, "OpenEMR golden now pre-creates the application database; this changes the accepted native-bootstrap contract and requires a new live regression run")
    mcmd = "\n".join(str(x) for x in (mysql.get("command") or []))
    for token in ("mariadbd", "--character-set-server=utf8mb4"):
        if token not in mcmd:
            fail(errors, f"OpenEMR MariaDB command lost accepted token: {token}")
    mhc = " ".join(str(x) for x in ((mysql.get("healthcheck") or {}).get("test") or []))
    for token in ("/usr/local/bin/healthcheck.sh", "--su-mysql", "--connect", "--innodb_initialized"):
        if token not in mhc:
            fail(errors, f"OpenEMR MariaDB healthcheck lost accepted token: {token}")
    if str((mysql.get("healthcheck") or {}).get("start_period")) != "1m":
        fail(errors, "OpenEMR MariaDB accepted start_period changed")
    if "/var/lib/mysql" not in mount_targets(mysql):
        fail(errors, "OpenEMR MariaDB persistence target changed")

    app = services["openemr"]
    aenv = env_map(app)
    if app.get("image") != "openemr/openemr:8.3.0-2026-08-29":
        fail(errors, "OpenEMR application image pin changed")
    expected_env = {
        "SERVICE_URL_OPENEMR_80": "${SERVICE_URL_OPENEMR_80}",
        "MYSQL_HOST": "mysql",
        "MYSQL_ROOT_PASS": "${SERVICE_PASSWORD_64_OPENEMRDBROOT}",
        "MYSQL_USER": "openemr",
        "MYSQL_PASS": "${SERVICE_PASSWORD_64_OPENEMRDB}",
        "OE_USER": "admin",
        "OE_PASS": "${SERVICE_PASSWORD_64_OPENEMRADMIN}",
    }
    for key, expected_value in expected_env.items():
        if aenv.get(key) != expected_value:
            fail(errors, f"OpenEMR application {key} regression: {aenv.get(key)!r}")
    dep = (app.get("depends_on") or {}).get("mysql") or {}
    if not isinstance(dep, dict) or dep.get("condition") != "service_healthy":
        fail(errors, "OpenEMR application lost mysql service_healthy startup gate")
    ahc = " ".join(str(x) for x in ((app.get("healthcheck") or {}).get("test") or []))
    if "/meta/health/readyz" not in ahc or "/usr/bin/curl" not in ahc:
        fail(errors, "OpenEMR application healthcheck lost accepted curl /meta/health/readyz probe")
    if str((app.get("healthcheck") or {}).get("start_period")) != "3m":
        fail(errors, "OpenEMR application accepted start_period changed")
    targets = mount_targets(app)
    for target in ("/var/log", "/var/www/localhost/htdocs/openemr/sites"):
        if target not in targets:
            fail(errors, f"OpenEMR application persistence target changed/missing: {target}")

    for name in expected:
        if has_host_ports(services[name]):
            fail(errors, f"OpenEMR service {name} unexpectedly publishes a host port")
    if doc.get("networks"):
        fail(errors, "OpenEMR golden unexpectedly introduced a custom network")
    volumes = doc.get("volumes") or {}
    if set(volumes) != {"databasevolume", "sitevolume", "logvolume01"}:
        fail(errors, f"OpenEMR persistence volume set changed: {sorted(volumes)}")



def check_odk(path: Path, errors: list[str]) -> None:
    raw, doc = load(path)
    services = doc["services"]
    expected = {
        "postgres14", "postgres", "secrets", "pyxform", "enketo_redis_main",
        "enketo_redis_cache", "enketo", "mail", "service", "admin-init", "nginx",
    }
    if not require_service_set("ODK Central", services, expected, errors):
        return
    if "ODK Central regression fixture / golden case — NOT a generic Coolify skeleton." not in raw:
        fail(errors, "ODK Central golden lost explicit non-generic-skeleton warning")

    pg = services["postgres14"]
    pgenv = env_map(pg)
    if pg.get("image") != "postgres:14.23":
        fail(errors, "ODK postgres14 image pin changed")
    if pgenv.get("POSTGRES_PASSWORD") != "${SERVICE_PASSWORD_64_POSTGRES}":
        fail(errors, "ODK postgres14 generated credential changed")
    if "/var/lib/odk/postgresql/14" not in mount_targets(pg):
        fail(errors, "ODK postgres14 persistence target changed")
    if pg.get("entrypoint") != ["bash"]:
        fail(errors, "ODK postgres14 explicit Bash entrypoint regression")
    if pg.get("command") != ["/usr/local/share/odk/start-postgres.sh"]:
        fail(errors, "ODK postgres14 start wrapper changed")
    if "pg_isready" not in " ".join(str(x) for x in ((pg.get("healthcheck") or {}).get("test") or [])):
        fail(errors, "ODK postgres14 healthcheck lost pg_isready")

    upgrade = services["postgres"]
    if upgrade.get("image") != "tianon/postgres-upgrade:9.6-to-14":
        fail(errors, "ODK PostgreSQL upgrade helper image changed")
    if upgrade.get("platform") != "linux/amd64":
        fail(errors, "ODK PostgreSQL upgrade helper platform guard changed")
    if str(upgrade.get("restart")) != "no" or upgrade.get("exclude_from_hc") is not True:
        fail(errors, "ODK PostgreSQL upgrade helper lost one-shot lifecycle semantics")
    ucmd = " ".join(str(x) for x in (upgrade.get("command") or []))
    if "upgrade-postgres.sh" not in ucmd:
        fail(errors, "ODK PostgreSQL upgrade helper command changed")

    secrets = services["secrets"]
    if secrets.get("image") != "node:24.16.0-slim":
        fail(errors, "ODK secrets helper image changed")
    secret_targets = mount_targets(secrets)
    if "/etc/secrets" not in secret_targets:
        fail(errors, "ODK Enketo secrets persistence mount changed")
    scmd = " ".join(str(x) for x in (secrets.get("command") or []))
    if "generate-secrets.sh" not in scmd:
        fail(errors, "ODK secrets generator command changed")

    if services["pyxform"].get("image") != "ghcr.io/getodk/pyxform-http:v4.5.0":
        fail(errors, "ODK Pyxform image pin changed")

    for rname, port in (("enketo_redis_main", "6379"), ("enketo_redis_cache", "6380")):
        redis = services[rname]
        if redis.get("image") != "redis:8.6.4":
            fail(errors, f"ODK {rname} image pin changed")
        htext = " ".join(str(x) for x in ((redis.get("healthcheck") or {}).get("test") or []))
        if "redis-cli" not in htext or port not in htext:
            fail(errors, f"ODK {rname} healthcheck changed")
        if "/data" not in mount_targets(redis):
            fail(errors, f"ODK {rname} persistence target changed")

    enketo = services["enketo"]
    eenv = env_map(enketo)
    if enketo.get("image") != "ghcr.io/enketo/enketo:7.6.1":
        fail(errors, "ODK Enketo image pin changed")
    if eenv.get("DOMAIN") != "${SERVICE_FQDN_NGINX}" or eenv.get("HTTPS_PORT") != "443":
        fail(errors, "ODK Enketo canonical-domain wiring changed")
    for target in (
        "/srv/src/enketo/packages/enketo-express/config/config.json.template",
        "/opt/odk/start-enketo.sh",
        "/etc/secrets",
    ):
        if target not in mount_targets(enketo):
            fail(errors, f"ODK Enketo required mount missing: {target}")

    service = services["service"]
    senv = env_map(service)
    if service.get("image") != "ghcr.io/getodk/central-service:v2026.2.4":
        fail(errors, "ODK Central service image pin changed")
    expected_service_env = {
        "DOMAIN": "${SERVICE_FQDN_NGINX}",
        "PGHOST": "postgres14",
        "PGDATABASE": "odk",
        "PGUSER": "odk",
        "PGPASSWORD": "${SERVICE_PASSWORD_64_POSTGRES}",
    }
    for key, expected_value in expected_service_env.items():
        if senv.get(key) != expected_value:
            fail(errors, f"ODK service {key} regression: {senv.get(key)!r}")
    if "8383" not in " ".join(str(x) for x in ((service.get("healthcheck") or {}).get("test") or [])):
        fail(errors, "ODK Central service healthcheck lost backend port 8383")

    admin = services["admin-init"]
    aenv = env_map(admin)
    if admin.get("image") != "ghcr.io/getodk/central-service:v2026.2.4":
        fail(errors, "ODK admin-init image diverged from Central service release")
    if admin.get("working_dir") != "/usr/odk":
        fail(errors, "ODK admin-init working directory changed")
    if aenv.get("PGPASSWORD") != "${SERVICE_PASSWORD_64_POSTGRES}":
        fail(errors, "ODK admin-init lost shared PostgreSQL credential identity")
    if aenv.get("ODK_ADMIN_PASSWORD") != "${SERVICE_PASSWORD_64_ODKADMIN}":
        fail(errors, "ODK initial admin magic credential changed")
    acmd = "\n".join(str(x) for x in (admin.get("command") or []))
    for token in ("/tmp/odk-central-admin-task.js", "createUser", "promoteUser", "any_admin", "user_exists"):
        if token not in acmd:
            fail(errors, f"ODK admin-init lost accepted idempotence/task token: {token}")
    adep = (admin.get("depends_on") or {}).get("service") or {}
    if not isinstance(adep, dict) or adep.get("condition") != "service_healthy":
        fail(errors, "ODK admin-init no longer waits for healthy Central service")
    if str(admin.get("restart")) != "no" or admin.get("exclude_from_hc") is not True:
        fail(errors, "ODK admin-init lost one-shot lifecycle semantics")

    nginx = services["nginx"]
    nenv = env_map(nginx)
    if nginx.get("image") != "ghcr.io/getodk/central-nginx:v2026.2.4":
        fail(errors, "ODK Nginx image pin changed")
    if "SERVICE_URL_NGINX_80" not in nenv:
        fail(errors, "ODK Nginx lost Coolify port-80 route declaration")
    if nenv.get("DOMAIN") != "${SERVICE_FQDN_NGINX}" or nenv.get("SSL_TYPE") != "upstream":
        fail(errors, "ODK Nginx canonical-domain/upstream-TLS wiring changed")
    for target in ("/usr/share/odk/nginx/odk.conf.template", "/usr/share/odk/nginx/client-config.json.template"):
        if target not in mount_targets(nginx):
            fail(errors, f"ODK Nginx required upstream runtime template missing: {target}")
    ndep = (nginx.get("depends_on") or {}).get("admin-init") or {}
    if not isinstance(ndep, dict) or ndep.get("condition") != "service_completed_successfully":
        fail(errors, "ODK Nginx lost admin-init successful-completion gate")

    for name in expected:
        if has_host_ports(services[name]):
            fail(errors, f"ODK service {name} unexpectedly publishes a host port")
    if doc.get("networks"):
        fail(errors, "ODK golden unexpectedly introduced a custom network")
    expected_volumes = {
        "postgres14", "postgres96_legacy", "postgres14_upgrade", "secrets",
        "enketo_redis_main", "enketo_redis_cache",
    }
    if set(doc.get("volumes") or {}) != expected_volumes:
        fail(errors, f"ODK persistence volume set changed: {sorted(doc.get('volumes') or {})}")

def check_frappe(path: Path, errors: list[str]) -> None:
    raw, doc = load(path)
    services = doc["services"]
    expected = {
        "db", "redis-cache", "redis-queue", "configurator", "site-bootstrap",
        "migrator", "backend", "websocket", "queue-short", "queue-long",
        "scheduler", "frontend",
    }
    if not require_service_set("Frappe", services, expected, errors):
        return
    expected_sha = "01a534d234516f3c5572d7c0b940cfa3cc6d43f5f58df6ef34487dfbf8b3838f"
    actual_sha = sha256_file(path)
    if actual_sha != expected_sha:
        fail(errors, f"Frappe Golden fixture bytes changed: expected SHA-256 {expected_sha}, got {actual_sha}")
    if "Frappe Framework regression fixture / golden case — NOT a generic Coolify skeleton." not in raw:
        fail(errors, "Frappe golden lost explicit non-generic-skeleton warning")

    db = services["db"]
    denv = env_map(db)
    root_magic = "${SERVICE_PASSWORD_64_FRAPPEDBROOT:?Coolify must generate SERVICE_PASSWORD_64_FRAPPEDBROOT}"
    if denv.get("MYSQL_ROOT_PASSWORD") != root_magic:
        fail(errors, "Frappe DB root Magic Variable identity changed")
    if db.get("ports"):
        fail(errors, "Frappe MariaDB unexpectedly publishes a host port")
    if "--innodb_initialized" not in str((db.get("healthcheck") or {}).get("test") or ""):
        fail(errors, "Frappe MariaDB healthcheck lost innodb_initialized readiness")

    bootstrap = services["site-bootstrap"]
    benv = env_map(bootstrap)
    expected_env = {
        "FRAPPE_SITE_NAME": "${SERVICE_FQDN_FRONTEND:?Coolify must generate SERVICE_FQDN_FRONTEND}",
        "FRAPPE_PUBLIC_FQDN": "${SERVICE_FQDN_FRONTEND:?Coolify must generate SERVICE_FQDN_FRONTEND}",
        "FRAPPE_PUBLIC_URL": "${SERVICE_URL_FRONTEND:?Coolify must generate SERVICE_URL_FRONTEND}",
        "DB_ROOT_PASSWORD": root_magic,
        "ADMIN_PASSWORD": "${SERVICE_PASSWORD_64_FRAPPEADMIN:?Coolify must generate SERVICE_PASSWORD_64_FRAPPEADMIN}",
    }
    for key, expected_value in expected_env.items():
        if benv.get(key) != expected_value:
            fail(errors, f"Frappe site-bootstrap {key} regression: {benv.get(key)!r}")
    bcmd = "\n".join(str(x) for x in (bootstrap.get("command") or []))
    for token in ("bench new-site", "--admin-password", "--db-root-password", "enable-scheduler", "list-apps", "Existing site detected; creation skipped"):
        if token not in bcmd:
            fail(errors, f"Frappe site-bootstrap lost accepted lifecycle token: {token}")
    if "--install-app erpnext" in bcmd or "install-app erpnext" in bcmd:
        fail(errors, "Frappe golden must remain Frappe-only and not auto-install ERPNext")
    if str(bootstrap.get("restart")) != "no" or bootstrap.get("exclude_from_hc") is not True:
        fail(errors, "Frappe site-bootstrap lost one-shot lifecycle semantics")
    cdep = (bootstrap.get("depends_on") or {}).get("configurator") or {}
    if not isinstance(cdep, dict) or cdep.get("condition") != "service_completed_successfully":
        fail(errors, "Frappe site-bootstrap no longer gates on configurator successful completion")

    migrator = services["migrator"]
    mcmd = "\n".join(str(x) for x in (migrator.get("command") or []))
    if "bench --site all migrate" not in mcmd:
        fail(errors, "Frappe migrator lost upstream migrate command")
    mdep = (migrator.get("depends_on") or {}).get("site-bootstrap") or {}
    if not isinstance(mdep, dict) or mdep.get("condition") != "service_completed_successfully":
        fail(errors, "Frappe migrator lost bootstrap successful-completion gate")
    if migrator.get("exclude_from_hc") is not True:
        fail(errors, "Frappe migrator lost one-shot Coolify health exclusion")

    frontend = services["frontend"]
    fenv = env_map(frontend)
    for key in ("SERVICE_URL_FRONTEND", "SERVICE_URL_FRONTEND_8080", "SERVICE_FQDN_FRONTEND"):
        if key not in fenv:
            fail(errors, f"Frappe frontend lost Coolify public identity/routing variable {key}")
    if fenv.get("BACKEND") != "backend:8000" or fenv.get("SOCKETIO") != "websocket:9000":
        fail(errors, "Frappe semantic frontend backend/socketio routing changed")
    fhc = str((frontend.get("healthcheck") or {}).get("test") or "")
    if "api/method/ping" not in fhc or "socket.io" not in fhc:
        fail(errors, "Frappe frontend healthcheck lost HTTP + Socket.IO coverage")

    backend = services["backend"]
    if "api/method/ping" not in str((backend.get("healthcheck") or {}).get("test") or ""):
        fail(errors, "Frappe backend healthcheck lost application ping")
    websocket = services["websocket"]
    if "socketio.js" not in str(websocket.get("command") or ""):
        fail(errors, "Frappe websocket command changed")
    if "socket.io" not in str((websocket.get("healthcheck") or {}).get("test") or ""):
        fail(errors, "Frappe websocket healthcheck lost Socket.IO handshake")
    if services["queue-short"].get("command") != ["bench", "worker", "--queue", "short,default"]:
        fail(errors, "Frappe queue-short command changed")
    if services["queue-long"].get("command") != ["bench", "worker", "--queue", "long,default,short"]:
        fail(errors, "Frappe queue-long command changed")
    if services["scheduler"].get("command") != ["bench", "schedule"]:
        fail(errors, "Frappe scheduler command changed")

    for name in expected:
        if has_host_ports(services[name]):
            fail(errors, f"Frappe service {name} unexpectedly publishes a host port")
    if doc.get("networks"):
        fail(errors, "Frappe golden unexpectedly introduced a custom network")
    if set(doc.get("volumes") or {}) != {"db-data", "redis-queue-data", "sites"}:
        fail(errors, f"Frappe persistence volume set changed: {sorted(doc.get('volumes') or {})}")


def check_erpnext(path: Path, errors: list[str]) -> None:
    raw, doc = load(path)
    services = doc["services"]
    expected = {
        "db", "redis-cache", "redis-queue", "configurator", "site-bootstrap",
        "migrator", "backend", "websocket", "queue-short", "queue-long",
        "scheduler", "frontend",
    }
    if not require_service_set("ERPNext", services, expected, errors):
        return

    expected_sha = "64660809aba082409a41e20006d0d24dbc913928a30f07590ba873171ee2a7cb"
    actual_sha = sha256_file(path)
    if actual_sha != expected_sha:
        fail(errors, f"ERPNext Golden fixture bytes changed: expected SHA-256 {expected_sha}, got {actual_sha}")

    image = str(services["backend"].get("image") or "")
    expected_image = "frappe/erpnext:v16.33.0@sha256:493cecf82c92c828bf0d0c57df60694e07dc61671e374ac93a070d1cc86df1bd"
    if image != expected_image:
        fail(errors, f"ERPNext accepted image pin changed: {image!r}")
    if services["backend"].get("platform") != "linux/amd64":
        fail(errors, "ERPNext accepted CPU platform changed")

    db = services["db"]
    root_magic = "${SERVICE_PASSWORD_64_ERPNEXTDBROOT:?Coolify must generate SERVICE_PASSWORD_64_ERPNEXTDBROOT}"
    if env_map(db).get("MYSQL_ROOT_PASSWORD") != root_magic:
        fail(errors, "ERPNext DB root Magic Variable identity changed")
    if db.get("image") != "mariadb:11.8.9@sha256:2439dcd7d14010ecd1ff7a4e1c5abe8e208c34fe35290744deeeaac3569043c3":
        fail(errors, "ERPNext MariaDB pin changed")
    if "--innodb_initialized" not in str((db.get("healthcheck") or {}).get("test") or ""):
        fail(errors, "ERPNext MariaDB healthcheck lost innodb_initialized readiness")

    for redis_name in ("redis-cache", "redis-queue"):
        redis = services[redis_name]
        if redis.get("image") != "redis:8.6.6-alpine@sha256:75934ddb37bfaebe3b4082ba673cac39f66495244134f33dd0a502ce03cdcd36":
            fail(errors, f"ERPNext {redis_name} pin changed")
        if "redis-cli" not in str((redis.get("healthcheck") or {}).get("test") or ""):
            fail(errors, f"ERPNext {redis_name} lost redis-cli readiness")

    bootstrap = services["site-bootstrap"]
    benv = env_map(bootstrap)
    expected_env = {
        "SITE": "${SERVICE_FQDN_FRONTEND:?Coolify must provide SERVICE_FQDN_FRONTEND}",
        "FRAPPE_PUBLIC_URL": "${SERVICE_URL_FRONTEND:?Coolify must provide SERVICE_URL_FRONTEND}",
        "FRAPPE_PUBLIC_FQDN": "${SERVICE_FQDN_FRONTEND:?Coolify must provide SERVICE_FQDN_FRONTEND}",
        "DB_ROOT_PASSWORD": root_magic,
        "ADMIN_USERNAME": "${ERPNEXT_ADMIN_USERNAME:-Administrator}",
        "ADMIN_PASSWORD": "${SERVICE_PASSWORD_64_ERPNEXTADMIN:?Coolify must generate SERVICE_PASSWORD_64_ERPNEXTADMIN}",
        "ALLOW_EXISTING_FRAPPE_SITE_CONVERSION": "${ALLOW_EXISTING_FRAPPE_SITE_CONVERSION:-false}",
    }
    for key, expected_value in expected_env.items():
        if benv.get(key) != expected_value:
            fail(errors, f"ERPNext site-bootstrap {key} regression: {benv.get(key)!r}")

    bcmd = "\n".join(str(x) for x in (bootstrap.get("command") or []))
    required_tokens = (
        "bench new-site", "--install-app erpnext", "--admin-password",
        "--db-root-password", "list-apps", "partial site directory exists",
        "list-apps failed; refusing to guess", "site exists without ERPNext",
        "ALLOW_EXISTING_FRAPPE_SITE_CONVERSION", "enable-scheduler",
        "frappe=1", "erpnext=1", "ERPNext site verified",
    )
    for token in required_tokens:
        if token not in bcmd:
            fail(errors, f"ERPNext site-bootstrap lost required activation/state token: {token}")

    if "sed 's#^sites/##" in bcmd:
        fail(errors, "ERPNext Golden reintroduced the RC1 fragile sed site-name transformation")
    if '${other_site#sites/}' not in bcmd or '${other_site%/site_config.json}' not in bcmd:
        fail(errors, "ERPNext Golden lost accepted shell parameter-expansion site normalization")

    for unrelated in ("crm", "helpdesk", "lms", "hrms"):
        if re.search(rf"(?:--install-app|install-app)\s+{re.escape(unrelated)}(?:\s|;|$)", bcmd, re.I):
            fail(errors, f"ERPNext Golden gained unrelated sibling app activation: {unrelated}")

    if str(bootstrap.get("restart")) != "no" or bootstrap.get("exclude_from_hc") is not True:
        fail(errors, "ERPNext site-bootstrap lost one-shot lifecycle semantics")
    cdep = (bootstrap.get("depends_on") or {}).get("configurator") or {}
    if not isinstance(cdep, dict) or cdep.get("condition") != "service_completed_successfully":
        fail(errors, "ERPNext bootstrap no longer gates on configurator successful completion")

    migrator = services["migrator"]
    if migrator.get("exclude_from_hc") is not True:
        fail(errors, "ERPNext migrator lost one-shot Coolify health exclusion")
    if "bench --site all migrate" not in "\n".join(str(x) for x in (migrator.get("command") or [])):
        fail(errors, "ERPNext migrator lost supported migrate path")
    mdep = (migrator.get("depends_on") or {}).get("site-bootstrap") or {}
    if not isinstance(mdep, dict) or mdep.get("condition") != "service_completed_successfully":
        fail(errors, "ERPNext migrator lost bootstrap successful-completion gate")

    if services["configurator"].get("exclude_from_hc") is not True:
        fail(errors, "ERPNext configurator lost one-shot Coolify health exclusion")

    frontend = services["frontend"]
    fenv = env_map(frontend)
    for key in ("SERVICE_URL_FRONTEND", "SERVICE_URL_FRONTEND_8080", "SERVICE_FQDN_FRONTEND"):
        if key not in fenv:
            fail(errors, f"ERPNext frontend lost Coolify public identity/routing variable {key}")
    if fenv.get("BACKEND") != "backend:8000" or fenv.get("SOCKETIO") != "websocket:9000":
        fail(errors, "ERPNext semantic Frappe frontend backend/socketio routing changed")

    long_running = (
        "backend", "websocket", "queue-short", "queue-long", "scheduler",
        "frontend", "db", "redis-cache", "redis-queue",
    )
    for name in long_running:
        if not services[name].get("healthcheck"):
            fail(errors, f"ERPNext long-running service lost healthcheck: {name}")

    if services["queue-short"].get("command") != ["bench", "worker", "--queue", "short,default"]:
        fail(errors, "ERPNext queue-short command changed")
    if services["queue-long"].get("command") != ["bench", "worker", "--queue", "long,default,short"]:
        fail(errors, "ERPNext queue-long command changed")
    if services["scheduler"].get("command") != ["bench", "schedule"]:
        fail(errors, "ERPNext scheduler command changed")

    for name in expected:
        if has_host_ports(services[name]):
            fail(errors, f"ERPNext service {name} unexpectedly publishes a host port")
        for mount in services[name].get("volumes") or []:
            if "/var/run/docker.sock" in str(mount):
                fail(errors, f"ERPNext service {name} unexpectedly mounts Docker socket")

    if doc.get("networks"):
        fail(errors, "ERPNext Golden unexpectedly introduced a custom network")
    if set(doc.get("volumes") or {}) != {"db-data", "redis-queue-data", "sites"}:
        fail(errors, f"ERPNext persistence volume set changed: {sorted(doc.get('volumes') or {})}")

    for name, svc in services.items():
        image_low = str(svc.get("image") or "").lower()
        if any(token in image_low for token in ("certbot", "letsencrypt", "traefik")):
            fail(errors, f"ERPNext service {name} introduced a second TLS/ACME edge: {image_low}")




def check_mem0(path: Path, errors: list[str]) -> None:
    raw, doc = load(path)
    services = doc["services"]
    expected = {"mem0", "dashboard", "postgres"}
    if not require_service_set("Mem0", services, expected, errors):
        return

    expected_sha = "b2f2b6442a49275f692e5bd586a20f6d35a109538df56e2f82055ccd86b1fcc7"
    if sha256_file(path) != expected_sha:
        fail(errors, f"Mem0 Golden must preserve exact accepted RC1 bytes: {sha256_file(path)}")

    api = services["mem0"]
    aenv = env_map(api)
    db = services["postgres"]
    denv = env_map(db)
    dashboard = services["dashboard"]
    fenv = env_map(dashboard)

    if aenv.get("POSTGRES_PASSWORD") != "${SERVICE_PASSWORD_64_MEM0DB:?Coolify must generate the Mem0 PostgreSQL password}":
        fail(errors, "Mem0 API lost accepted SERVICE_PASSWORD_64_MEM0DB identity")
    if denv.get("POSTGRES_PASSWORD") != "${SERVICE_PASSWORD_64_MEM0DB:?Coolify must generate the Mem0 PostgreSQL password}":
        fail(errors, "Mem0 PostgreSQL lost shared SERVICE_PASSWORD_64_MEM0DB identity")
    if aenv.get("JWT_SECRET") != "${SERVICE_PASSWORD_64_MEM0JWT:?Coolify must generate the Mem0 JWT secret}":
        fail(errors, "Mem0 JWT lost SERVICE_PASSWORD_64_MEM0JWT identity")
    if aenv.get("AUTH_DISABLED") != "false":
        fail(errors, "Mem0 Golden must keep AUTH_DISABLED=false")
    if "SERVICE_URL_MEM0" not in aenv:
        fail(errors, "Mem0 API lost SERVICE_URL_MEM0")
    if "SERVICE_URL_DASHBOARD" not in fenv:
        fail(errors, "Mem0 dashboard lost SERVICE_URL_DASHBOARD")
    if fenv.get("NEXT_PUBLIC_API_URL") != "${SERVICE_URL_MEM0:?Coolify must generate the Mem0 API public URL}":
        fail(errors, "Mem0 dashboard browser API URL no longer uses public SERVICE_URL_MEM0")
    if aenv.get("DASHBOARD_URL") != "${SERVICE_URL_DASHBOARD:?Coolify must generate the dashboard public URL}":
        fail(errors, "Mem0 API CORS/dashboard origin no longer uses SERVICE_URL_DASHBOARD")

    if str((api.get("build") or {}).get("context")) != "https://github.com/mem0ai/mem0.git#dc82354e143c2581d505d581a00286d6ef8c3605":
        fail(errors, "Mem0 API immutable source snapshot changed")
    if str((dashboard.get("build") or {}).get("context")) != "https://github.com/mem0ai/mem0.git#dc82354e143c2581d505d581a00286d6ef8c3605:server/dashboard":
        fail(errors, "Mem0 dashboard immutable source snapshot changed")
    expected_pg = "pgvector/pgvector:0.8.6-pg17@sha256:cf134a767f474095eeba57e0117be8e568e011a63f33fbf252f14c9b760f8e6f"
    if db.get("image") != expected_pg:
        fail(errors, f"Mem0 pgvector pin changed: {db.get('image')!r}")

    cmd = " ".join(str(x) for x in (api.get("command") or []))
    if "alembic upgrade head" not in cmd or "uvicorn main:app" not in cmd or "&&" not in cmd:
        fail(errors, "Mem0 accepted migration-owned API startup semantics changed")

    volumes = set(doc.get("volumes") or {})
    if volumes != {"mem0_postgres", "mem0_history"}:
        fail(errors, f"Mem0 persistence volume set changed: {sorted(volumes)}")
    if "/app/history" not in mount_targets(api):
        fail(errors, "Mem0 API lost /app/history persistence")
    if "/var/lib/postgresql/data" not in mount_targets(db):
        fail(errors, "Mem0 PostgreSQL lost PGDATA persistence")

    forbidden_names = {"redis", "mongodb", "mongo", "mariadb", "solr", "nginx", "worker", "scheduler", "migrator", "admin-init", "bootstrap"}
    for name, svc in services.items():
        low = name.lower() + " " + str(svc.get("image") or "").lower()
        if any(tok in low for tok in forbidden_names):
            fail(errors, f"Mem0 Golden introduced speculative infrastructure token in service {name}: {low}")
        if has_host_ports(svc):
            fail(errors, f"Mem0 service {name} unexpectedly publishes a host port")
        for mount in svc.get("volumes") or []:
            if "/var/run/docker.sock" in str(mount):
                fail(errors, f"Mem0 service {name} unexpectedly mounts Docker socket")
    if doc.get("networks"):
        fail(errors, "Mem0 Golden unexpectedly introduced a custom network")

    health_text = " ".join(str((services[n].get("healthcheck") or {}).get("test") or "") for n in expected).lower()
    for external in ("api.openai.com", "api.anthropic.com", "generativelanguage.googleapis.com"):
        if external in health_text:
            fail(errors, f"Mem0 Golden healthcheck unexpectedly depends on external provider: {external}")



def check_overleaf(path: Path, errors: list[str]) -> None:
    raw, doc = load(path)
    services = doc["services"]
    expected = {"overleaf", "adminbootstrap", "mongo", "redis"}
    if not require_service_set("Overleaf CE", services, expected, errors):
        return

    expected_sha = "b8cb9425523d38088f7069c70d762ab24fbf07232d736f607572e5b191621585"
    actual_sha = sha256_file(path)
    if actual_sha != expected_sha:
        fail(errors, f"Overleaf Golden must preserve exact accepted RC4 bytes: {actual_sha}")

    exact_image = "sharelatex/sharelatex:6.2.2@sha256:cfdeecb4e55a7ae76f0244b86d1b896580bc7137b82733b886a97575fba19d43"
    app = services["overleaf"]
    aenv = env_map(app)
    if app.get("image") != exact_image or app.get("platform") != "linux/amd64":
        fail(errors, "Overleaf CE accepted application image/digest or amd64 boundary changed")
    if "SERVICE_URL_OVERLEAF" not in aenv:
        fail(errors, "Overleaf CE Golden lost SERVICE_URL_OVERLEAF")
    if aenv.get("OVERLEAF_SITE_URL") != "${SERVICE_URL_OVERLEAF}":
        fail(errors, f"Overleaf canonical URL wiring changed: {aenv.get('OVERLEAF_SITE_URL')!r}")
    if aenv.get("OVERLEAF_INVITE_TOKEN_SECRET") != "${SERVICE_REALBASE64_32_OVERLEAFINVITE:?Coolify must generate SERVICE_REALBASE64_32_OVERLEAFINVITE}":
        fail(errors, "Overleaf invite-token Magic Variable identity/format changed")
    for key, val in {
        "OVERLEAF_MONGO_URL": "mongodb://mongo/sharelatex",
        "OVERLEAF_REDIS_HOST": "redis",
        "OVERLEAF_REDIS_PORT": "6379",
        "REDIS_HOST": "redis",
        "REDIS_PORT": "6379",
        "OVERLEAF_BEHIND_PROXY": "true",
        "OVERLEAF_SECURE_COOKIE": "true",
    }.items():
        if aenv.get(key) != val:
            fail(errors, f"Overleaf CE {key} regression: {aenv.get(key)!r}")
    if "TRUSTED_PROXY_IPS" not in aenv:
        fail(errors, "Overleaf CE Golden lost explicit trusted-proxy configuration")
    if "/var/lib/overleaf" not in mount_targets(app):
        fail(errors, "Overleaf CE Golden lost /var/lib/overleaf persistence")
    ahc = " ".join(str(x) for x in ((app.get("healthcheck") or {}).get("test") or []))
    if "127.0.0.1:3000/status" not in ahc:
        fail(errors, "Overleaf CE healthcheck lost local web readiness endpoint")

    # Overleaf 5+ rejects any environment-variable name containing SHARELATEX.
    # The string remains legitimate in the image name and Mongo database value.
    for name, svc in services.items():
        for key in env_map(svc):
            if "SHARELATEX" in key.upper():
                fail(errors, f"Overleaf service {name} reintroduced rejected SHARELATEX environment name: {key}")
    if "SERVICE_URL_SHARELATEX" in raw:
        fail(errors, "Overleaf Golden reintroduced SERVICE_URL_SHARELATEX; accepted RC4 requires SERVICE_URL_OVERLEAF")

    bootstrap = services["adminbootstrap"]
    benv = env_map(bootstrap)
    if bootstrap.get("image") != exact_image or bootstrap.get("platform") != "linux/amd64":
        fail(errors, "Overleaf adminbootstrap image/digest or amd64 boundary changed")
    if str(bootstrap.get("restart")) != "no" or bootstrap.get("exclude_from_hc") is not True:
        fail(errors, "Overleaf adminbootstrap lost one-shot/exclude_from_hc lifecycle")
    dep = (bootstrap.get("depends_on") or {}).get("overleaf") or {}
    if not isinstance(dep, dict) or dep.get("condition") != "service_healthy":
        fail(errors, "Overleaf adminbootstrap must wait for healthy Overleaf")
    if benv.get("OVERLEAF_ADMIN_EMAIL") != "${OVERLEAF_ADMIN_EMAIL:?Set OVERLEAF_ADMIN_EMAIL before the first deployment}":
        fail(errors, "Overleaf operator-provided admin identity contract changed")
    if benv.get("OVERLEAF_ADMIN_PASSWORD") != "${SERVICE_PASSWORD_64_OVERLEAFADMIN:?Coolify must generate SERVICE_PASSWORD_64_OVERLEAFADMIN}":
        fail(errors, "Overleaf generated admin-password Magic Variable identity changed")
    if benv.get("OVERLEAF_INVITE_TOKEN_SECRET") != "${SERVICE_REALBASE64_32_OVERLEAFINVITE:?Coolify must generate SERVICE_REALBASE64_32_OVERLEAFINVITE}":
        fail(errors, "Overleaf bootstrap lost shared invite-token secret identity")
    bcmd = "\n".join(str(x) for x in (bootstrap.get("command") or []))
    for token in (
        "AuthenticationManager.validateEmail", "AuthenticationManager.validatePassword",
        "UserRegistrationHandler.promises.registerNewUser", "existingAdmin", "existingUser",
        "preserving its existing password", "already exists with a different email",
        "already exists but is not an admin", "hashedPassword",
        "Admin bootstrap post-condition failed", "$$set:",
    ):
        if token not in bcmd:
            fail(errors, f"Overleaf adminbootstrap lost accepted fail-closed/post-condition token: {token}")
    if re.search(r"(?<!\$)\$set\s*:", bcmd):
        fail(errors, "Overleaf adminbootstrap reintroduced unescaped $set Compose interpolation risk")

    mongo = services["mongo"]
    if mongo.get("image") != "mongo:8.0.29":
        fail(errors, "Overleaf Mongo image pin changed")
    if "--replSet overleaf" not in str(mongo.get("command") or ""):
        fail(errors, "Overleaf Mongo single-member replica-set semantics changed")
    mt = mount_targets(mongo)
    if "/data/db" not in mt or "/docker-entrypoint-initdb.d/mongodb-init-replica-set.js" not in mt:
        fail(errors, "Overleaf Mongo persistence/native init mounts changed")
    managed = [m for m in (mongo.get("volumes") or []) if isinstance(m, dict) and m.get("target") == "/docker-entrypoint-initdb.d/mongodb-init-replica-set.js"]
    if len(managed) != 1:
        fail(errors, "Overleaf Mongo native replica-set initializer mount missing/duplicated")
    else:
        m = managed[0]
        content = str(m.get("content") or "")
        if m.get("is_directory") is not False or not all(tok in content for tok in ("rs.initiate", "_id: 'overleaf'", "mongo:27017")):
            fail(errors, "Overleaf Mongo native replica-set initializer semantics changed")
    if "mongo:127.0.0.1" not in [str(x) for x in (mongo.get("extra_hosts") or [])]:
        fail(errors, "Overleaf Mongo upstream bootstrap host mapping changed")

    redis = services["redis"]
    if redis.get("image") != "redis:7.4.11":
        fail(errors, "Overleaf Redis image pin changed")
    rcmd = " ".join(str(x) for x in (redis.get("command") or []))
    if "redis-server" not in rcmd or "--appendonly" not in rcmd or "yes" not in rcmd:
        fail(errors, "Overleaf Redis AOF durability semantics changed")
    if "/data" not in mount_targets(redis):
        fail(errors, "Overleaf Redis AOF data persistence changed")

    if set(doc.get("volumes") or {}) != {"overleaf-data", "mongo-data", "redis-data"}:
        fail(errors, f"Overleaf persistence volume set changed: {sorted(doc.get('volumes') or {})}")
    if doc.get("networks"):
        fail(errors, "Overleaf Golden unexpectedly introduced a custom network")
    if "mongo-init" in services:
        fail(errors, "Overleaf Golden invented mongo-init instead of native Mongo entrypoint initialization")

    forbidden = ("/var/run/docker.sock", "SANDBOXED_COMPILES", "DOCKER_RUNNER", "sandboxed-compiles", "sibling compile")
    for token in forbidden:
        if token.lower() in raw.lower():
            fail(errors, f"Overleaf CE Golden introduced Server-Pro/privileged compile artifact: {token}")
    for name, svc in services.items():
        if has_host_ports(svc):
            fail(errors, f"Overleaf service {name} unexpectedly publishes a host port")



def check_netbox(path: Path, errors: list[str]) -> None:
    raw, doc = load(path)
    services = doc["services"]
    expected = {"netbox", "netbox-worker", "postgres", "redis", "redis-cache"}
    if not require_service_set("NetBox", services, expected, errors):
        return

    expected_sha = "e4be06751d206704a2e9460ac2926d92833b39a71266cf1bd5a8a788da319804"
    actual_sha = sha256_file(path)
    if actual_sha != expected_sha:
        fail(errors, f"NetBox Golden must preserve exact runtime-accepted RC2 bytes: {actual_sha}")

    exact_image = "ghcr.io/netbox-community/netbox:v4.6.9-5.0.2@sha256:b1639229a0cf67052a2d53d7f7df004c840f49c9959a321bf310b6373df7240c"
    web = services["netbox"]
    worker = services["netbox-worker"]
    if web.get("image") != exact_image or worker.get("image") != exact_image:
        fail(errors, "NetBox accepted web/worker image or digest changed")

    wenv = env_map(web)
    for key, value in {
        "DB_HOST": "postgres",
        "REDIS_HOST": "redis",
        "REDIS_DATABASE": "0",
        "REDIS_CACHE_HOST": "redis-cache",
        "REDIS_CACHE_DATABASE": "1",
        "SKIP_SUPERUSER": "false",
    }.items():
        if wenv.get(key) != value:
            fail(errors, f"NetBox web {key} regression: {wenv.get(key)!r}")
    for key in ("SERVICE_URL_NETBOX_8080",):
        if key not in wenv:
            fail(errors, f"NetBox Golden lost public routing declaration {key}")
    if wenv.get("DB_PASSWORD") != "${SERVICE_PASSWORD_64_NETBOXDB:?Coolify must generate SERVICE_PASSWORD_64_NETBOXDB}":
        fail(errors, "NetBox DB Magic Variable identity changed")
    if wenv.get("REDIS_PASSWORD") != "${SERVICE_PASSWORD_64_NETBOXREDIS:?Coolify must generate SERVICE_PASSWORD_64_NETBOXREDIS}":
        fail(errors, "NetBox tasks Valkey credential identity changed")
    if wenv.get("REDIS_CACHE_PASSWORD") != "${SERVICE_PASSWORD_64_NETBOXCACHE:?Coolify must generate SERVICE_PASSWORD_64_NETBOXCACHE}":
        fail(errors, "NetBox cache Valkey credential identity changed")
    if wenv.get("SUPERUSER_PASSWORD") != "${SERVICE_PASSWORD_64_NETBOXADMIN:?Coolify must generate SERVICE_PASSWORD_64_NETBOXADMIN}":
        fail(errors, "NetBox native superuser credential identity changed")

    hc = " ".join(str(x) for x in ((web.get("healthcheck") or {}).get("test") or []))
    if "http://localhost:8080/login/" not in hc or "127.0.0.1:8080/login/" in hc:
        fail(errors, "NetBox Golden must preserve accepted localhost healthcheck Host semantics")

    targets = mount_targets(web)
    if "/etc/netbox/config" in targets:
        fail(errors, "NetBox Golden must not mask image-baked /etc/netbox/config with a whole-directory mount")
    if "/etc/netbox/config/zz_coolify.py" not in targets:
        fail(errors, "NetBox Golden lost minimal zz_coolify.py managed override")
    cfg_mounts = [m for m in (web.get("volumes") or []) if isinstance(m, dict) and str(m.get("target") or "").startswith("/etc/netbox/config/")]
    if len(cfg_mounts) != 1 or cfg_mounts[0].get("target") != "/etc/netbox/config/zz_coolify.py":
        fail(errors, "NetBox Golden must keep exactly one managed config override under /etc/netbox/config")

    wcmd = " ".join(str(x) for x in (worker.get("command") or []))
    if "/opt/netbox/netbox/manage.py" not in wcmd or "rqworker" not in wcmd:
        fail(errors, "NetBox Golden lost native RQ worker command")
    if has_host_ports(worker):
        fail(errors, "NetBox worker unexpectedly publishes a host port")

    tasks = services["redis"]
    cache = services["redis-cache"]
    tasks_cmd = " ".join(str(x) for x in (tasks.get("command") or []))
    cache_cmd = " ".join(str(x) for x in (cache.get("command") or []))
    if "--appendonly yes" not in tasks_cmd:
        fail(errors, "NetBox tasks Valkey lost AOF durability")
    if "--appendonly" in cache_cmd:
        fail(errors, "NetBox cache Valkey unexpectedly gained AOF; preserve disposable cache semantics")
    if env_map(tasks).get("REDIS_PASSWORD") != "${SERVICE_PASSWORD_64_NETBOXREDIS:?Coolify must generate SERVICE_PASSWORD_64_NETBOXREDIS}":
        fail(errors, "NetBox tasks Valkey producer credential no longer matches consumers")
    if env_map(cache).get("REDIS_PASSWORD") != "${SERVICE_PASSWORD_64_NETBOXCACHE:?Coolify must generate SERVICE_PASSWORD_64_NETBOXCACHE}":
        fail(errors, "NetBox cache Valkey producer credential no longer matches consumers")
    if env_map(tasks).get("REDIS_PASSWORD") == env_map(cache).get("REDIS_PASSWORD"):
        fail(errors, "NetBox tasks/cache credentials were accidentally merged")

    for name in ("postgres", "redis", "redis-cache"):
        if has_host_ports(services[name]):
            fail(errors, f"NetBox private infrastructure unexpectedly publishes a host port: {name}")

    for forbidden in ("adminbootstrap", "migrator", "mongo", "mariadb", "socketio", "celery", "pgvector"):
        if forbidden in services:
            fail(errors, f"NetBox Golden imported unrelated infrastructure: {forbidden}")


def check_baserow(path: Path, errors: list[str]) -> None:
    raw, doc = load(path)
    services = doc["services"]
    expected = {
        "baserow-media-permissions", "baserow-db", "baserow-redis",
        "baserow-backend", "baserow-web-frontend", "baserow-celery",
        "baserow-celery-export", "baserow-celery-beat", "baserow",
    }
    if not require_service_set("Baserow", services, expected, errors):
        return

    expected_sha = "143c3a94952b16e85638d87bd50fa29a49fa756b7c12cba097fe32383c4312f6"
    actual_sha = sha256_file(path)
    if actual_sha != expected_sha:
        fail(errors, f"Baserow Golden must preserve exact runtime-accepted RC5 bytes: {actual_sha}")

    if services["baserow-db"].get("image") != "pgvector/pgvector:0.8.1-pg15":
        fail(errors, "Baserow PostgreSQL/pgvector image changed")
    if services["baserow-redis"].get("image") != "redis:6.2.23-alpine":
        fail(errors, "Baserow Redis image changed")
    if services["baserow-backend"].get("image") != "baserow/backend:2.3.3":
        fail(errors, "Baserow backend image changed")
    if services["baserow-web-frontend"].get("image") != "baserow/web-frontend:2.3.3":
        fail(errors, "Baserow web frontend image changed")
    if services["baserow"].get("image") != "caddy:2.11.4":
        fail(errors, "Baserow semantic Caddy image changed")

    benv = env_map(services["baserow-backend"])
    expected_backend_env = {
        "DATABASE_HOST": "baserow-db",
        "DATABASE_PORT": "5432",
        "DATABASE_NAME": "baserow",
        "DATABASE_USER": "baserow",
        "REDIS_HOST": "baserow-redis",
        "REDIS_PORT": "6379",
        "REDIS_PROTOCOL": "redis",
        "PRIVATE_BACKEND_URL": "http://baserow-backend:8000",
        "MIGRATE_ON_STARTUP": "false",
    }
    for key, value in expected_backend_env.items():
        if benv.get(key) != value:
            fail(errors, f"Baserow backend {key} regression: {benv.get(key)!r}")
    if benv.get("SECRET_KEY") != "${SERVICE_PASSWORD_64_BASEROWSECRET:?Coolify must generate SERVICE_PASSWORD_64_BASEROWSECRET}":
        fail(errors, "Baserow SECRET_KEY Magic Variable identity changed")
    if benv.get("BASEROW_JWT_SIGNING_KEY") != "${SERVICE_PASSWORD_64_BASEROWJWT:?Coolify must generate SERVICE_PASSWORD_64_BASEROWJWT}":
        fail(errors, "Baserow JWT Magic Variable identity changed")
    if benv.get("DATABASE_PASSWORD") != "${SERVICE_PASSWORD_64_BASEROWDB:?Coolify must generate SERVICE_PASSWORD_64_BASEROWDB}":
        fail(errors, "Baserow DB Magic Variable identity changed")
    if benv.get("REDIS_PASSWORD") != "${SERVICE_PASSWORD_64_BASEROWREDIS:?Coolify must generate SERVICE_PASSWORD_64_BASEROWREDIS}":
        fail(errors, "Baserow Redis Magic Variable identity changed")
    if benv.get("BASEROW_PUBLIC_URL") != "${SERVICE_URL_BASEROW:?Coolify must generate SERVICE_URL_BASEROW for the public baserow gateway}":
        fail(errors, "Baserow canonical public URL wiring changed")

    bcmd = "\n".join(str(x) for x in (services["baserow-backend"].get("command") or []))
    for token in (
        "wait_for_db", "locked_migrate", "PasswordProviderHandler.get()",
        "PasswordAuthProviderModel.objects.order_by", "Refusing automatic deletion; manual review required.",
        "gunicorn",
    ):
        if token not in bcmd:
            fail(errors, f"Baserow accepted 2.3.3 lifecycle/singleton safeguard lost token: {token}")

    role_commands = {
        "baserow-celery": "celery-worker",
        "baserow-celery-export": "celery-exportworker",
        "baserow-celery-beat": "celery-beat",
    }
    for name, token in role_commands.items():
        svc = services[name]
        if svc.get("image") != "baserow/backend:2.3.3":
            fail(errors, f"Baserow {name} image changed")
        cmd = "\n".join(str(x) for x in (svc.get("command") or []))
        if token not in cmd or "baserow-backend:8000/api/_health/" not in cmd:
            fail(errors, f"Baserow {name} lost accepted worker command/readiness gate")
        if not svc.get("healthcheck"):
            fail(errors, f"Baserow {name} lost healthcheck")

    front = services["baserow-web-frontend"]
    fenv = env_map(front)
    if fenv.get("BASEROW_PUBLIC_URL") != benv.get("BASEROW_PUBLIC_URL"):
        fail(errors, "Baserow frontend/backend canonical public origins diverged")
    fhc = " ".join(str(x) for x in ((front.get("healthcheck") or {}).get("test") or []))
    if "http://localhost:3000/_health/" not in fhc:
        fail(errors, "Baserow frontend lost native local health path")

    gateway = services["baserow"]
    genv = env_map(gateway)
    if "SERVICE_URL_BASEROW" not in genv:
        fail(errors, "Baserow Golden lost single Coolify public routing identity")
    if genv.get("BASEROW_PUBLIC_URL") != "${SERVICE_URL_BASEROW:?Coolify must generate SERVICE_URL_BASEROW for the public baserow gateway}":
        fail(errors, "Baserow Caddy canonical public URL changed")
    ghc = " ".join(str(x) for x in ((gateway.get("healthcheck") or {}).get("test") or []))
    if "127.0.0.1/__coolify_gateway_health" not in ghc:
        fail(errors, "Baserow Golden lost dedicated Host/path-safe gateway health endpoint")
    if re.search(r"http://127\.0\.0\.1/(?:\s|$)", ghc):
        fail(errors, "Baserow Golden regressed to host-sensitive root-path healthcheck")

    caddy_mounts = [m for m in (gateway.get("volumes") or []) if isinstance(m, dict) and m.get("target") == "/etc/caddy/Caddyfile"]
    if len(caddy_mounts) != 1:
        fail(errors, "Baserow semantic Caddy managed file missing or duplicated")
    else:
        content = str(caddy_mounts[0].get("content") or "")
        for token in ("/__coolify_gateway_health", "/api/*", "/ws/*", "/mcp/*", "/assistant/*", "/static/*", "/media/*", "PRIVATE_WEB_FRONTEND_URL", "PRIVATE_BACKEND_URL"):
            if token not in content:
                fail(errors, f"Baserow semantic Caddy lost route/responsibility token: {token}")

    for name in ("baserow-db", "baserow-redis", "baserow-backend", "baserow-web-frontend", "baserow-celery", "baserow-celery-export", "baserow-celery-beat"):
        if has_host_ports(services[name]):
            fail(errors, f"Baserow private/internal service unexpectedly publishes a host port: {name}")
    if has_host_ports(gateway):
        fail(errors, "Baserow semantic gateway should be routed by Coolify, not a host-published port")

    if set(doc.get("volumes") or {}) != {"baserow-postgres", "baserow-media"}:
        fail(errors, f"Baserow persistence volume set changed: {sorted(doc.get('volumes') or {})}")

    redis_cmd = " ".join(str(x) for x in (services["baserow-redis"].get("command") or []))
    if "--appendonly" in redis_cmd:
        fail(errors, "Baserow Golden imported NetBox-style Redis AOF without Baserow-specific accepted cause")



def check_openspp(path: Path, errors: list[str]) -> None:
    raw, doc = load(path)
    services = doc["services"]
    expected = {"openspp", "odoo", "queue-worker", "db", "backup"}
    if not require_service_set("OpenSPP", services, expected, errors):
        return

    expected_sha = "f00a8755fa2be8e8b1f50970978ae1b57c1877093c2a35108edf35a675d4587b"
    actual_sha = sha256_file(path)
    if actual_sha != expected_sha:
        fail(errors, f"OpenSPP Golden must preserve exact operator-accepted RC8 bytes: {actual_sha}")

    gateway = services["openspp"]
    if gateway.get("image") != "nginx:1.30.4-alpine":
        fail(errors, "OpenSPP semantic gateway image changed")
    if has_host_ports(gateway):
        fail(errors, "OpenSPP semantic gateway should be routed by Coolify, not a host-published port")
    genv = env_map(gateway)
    if "SERVICE_URL_OPENSPP_8080" not in genv:
        fail(errors, "OpenSPP gateway lost Coolify public route identity")
    gmounts = [m for m in (gateway.get("volumes") or []) if isinstance(m, dict) and "content" in m]
    nginx_mounts = [m for m in gmounts if m.get("target") == "/etc/nginx/openspp-rc8.conf.template"]
    if len(nginx_mounts) != 1:
        fail(errors, "OpenSPP Golden lost unique RC8 managed Nginx template identity")
    else:
        content = str(nginx_mounts[0].get("content") or "")
        if "$$remote_addr" in content or "$$host" in content or "$$http_upgrade" in content:
            fail(errors, "OpenSPP managed Nginx file regressed to Compose-style $$ native-variable escaping")
        for token in (
            "$remote_addr", "$host", "$http_upgrade",
            "return 302 /web/login?db=openspp",
            "location = /web/database/selector",
            "location ^~ /web/database",
            "proxy_set_header X-Forwarded-Proto https",
            "proxy_set_header X-Forwarded-Port 443",
            "server odoo:8069", "server odoo:8072",
        ):
            if token not in content:
                fail(errors, f"OpenSPP semantic gateway lost accepted token: {token}")
    gcmd = "\n".join(str(x) for x in (gateway.get("command") or []))
    for token in ("envsubst", "OPENSPP_RATE_LIMIT", "OPENSPP_RATE_BURST", "OPENSPP_CLIENT_MAX_BODY_SIZE", "OPENSPP_PROXY_TIMEOUT", "nginx -t -c /tmp/openspp-rc8.conf"):
        if token not in gcmd:
            fail(errors, f"OpenSPP gateway render/validation command lost token: {token}")

    for app_name in ("odoo", "queue-worker"):
        app = services[app_name]
        if app.get("image") != "openspp-coolify:2026.08":
            fail(errors, f"OpenSPP {app_name} local image identity changed")
        if str(app.get("pull_policy")) != "never":
            fail(errors, f"OpenSPP {app_name} lost current-Coolify local-build pull guard")
        build = app.get("build") or {}
        if not isinstance(build, dict) or build.get("context") != "https://github.com/OpenSPP/OpenSPP2.git#208d97582791b369b562cdfcb3e41766a2be710f":
            fail(errors, f"OpenSPP {app_name} source commit pin changed")
        if build.get("target") != "production":
            fail(errors, f"OpenSPP {app_name} production build target changed")

    odoo = services["odoo"]
    oenv = env_map(odoo)
    expected_oenv = {
        "DB_HOST": "db",
        "DB_USER": "odoo",
        "DB_NAME": "openspp",
        "DB_FILTER": "^openspp$",
        "LIST_DB": "False",
        "PROXY_MODE": "True",
        "ODOO_INIT_MODULES": "${ODOO_INIT_MODULES:-spp_starter_sp_mis}",
    }
    for key, expected_value in expected_oenv.items():
        if oenv.get(key) != expected_value:
            fail(errors, f"OpenSPP Odoo {key} regression: {oenv.get(key)!r}")
    if oenv.get("DB_PASSWORD") != "${SERVICE_PASSWORD_64_OPENSPPDBODOO:?OpenSPP database password was not generated}":
        fail(errors, "OpenSPP Odoo runtime DB credential identity changed")
    if oenv.get("ODOO_ADMIN_PASSWD") != "${SERVICE_PASSWORD_64_OPENSPPMASTER:?OpenSPP admin/master password was not generated}":
        fail(errors, "OpenSPP Odoo admin/master credential identity changed")
    ohc = " ".join(str(x) for x in ((odoo.get("healthcheck") or {}).get("test") or []))
    if "/web/health" not in ohc or "spp_starter_sp_mis" not in ohc or "state='installed'" not in ohc:
        fail(errors, "OpenSPP Odoo health lost activation-aware SP-MIS readiness")
    if "/var/lib/odoo" not in mount_targets(odoo):
        fail(errors, "OpenSPP Odoo filestore persistence target changed")
    compat_targets = {
        "/mnt/extra-addons/openspp/spp_user_roles/views/user.xml",
        "/mnt/extra-addons/openspp/spp_area/views/user.xml",
    }
    if not compat_targets.issubset(set(mount_targets(odoo))):
        fail(errors, "OpenSPP Odoo dependency-scoped compatibility overlays changed/missing")

    worker = services["queue-worker"]
    wenv = env_map(worker)
    if wenv.get("DB_USER") != "odoo" or wenv.get("DB_PASSWORD") != oenv.get("DB_PASSWORD"):
        fail(errors, "OpenSPP queue worker no longer shares the intentional Odoo runtime DB role")
    deps = worker.get("depends_on") or {}
    odep = deps.get("odoo") or {}
    if not isinstance(odep, dict) or odep.get("condition") != "service_healthy":
        fail(errors, "OpenSPP queue worker lost activation-aware Odoo readiness gate")
    whc = " ".join(str(x) for x in ((worker.get("healthcheck") or {}).get("test") or []))
    if "job_worker_healthcheck.py" not in whc:
        fail(errors, "OpenSPP queue worker health command changed")
    if not compat_targets.issubset(set(mount_targets(worker))):
        fail(errors, "OpenSPP queue worker lost matching dependency compatibility overlays")

    db = services["db"]
    if db.get("image") != "postgis/postgis:18-3.6-alpine":
        fail(errors, "OpenSPP PostgreSQL/PostGIS image changed")
    if has_host_ports(db):
        fail(errors, "OpenSPP database unexpectedly publishes a host port")
    denv = env_map(db)
    if denv.get("POSTGRES_USER") != "openspp_admin" or denv.get("POSTGRES_DB") != "openspp":
        fail(errors, "OpenSPP bootstrap/admin DB role or database name changed")
    if denv.get("POSTGRES_PASSWORD") != "${SERVICE_PASSWORD_64_OPENSPPDBADMIN:?OpenSPP database admin password was not generated}":
        fail(errors, "OpenSPP DB admin credential identity changed")
    if denv.get("ODOO_DB_PASSWORD") != "${SERVICE_PASSWORD_64_OPENSPPDBODOO:?OpenSPP database password was not generated}":
        fail(errors, "OpenSPP DB role-creation credential identity changed")
    init_mounts = [m for m in (db.get("volumes") or []) if isinstance(m, dict) and m.get("target") == "/docker-entrypoint-initdb.d/20-openspp-roles.sh"]
    if len(init_mounts) != 1:
        fail(errors, "OpenSPP intentional DB-role bootstrap script missing")
    else:
        init = str(init_mounts[0].get("content") or "")
        for token in ("CREATE ROLE odoo", "NOSUPERUSER", "NOCREATEDB", "CREATE EXTENSION IF NOT EXISTS postgis"):
            if token not in init:
                fail(errors, f"OpenSPP DB-role bootstrap lost token: {token}")

    backup = services["backup"]
    if backup.get("image") != "postgis/postgis:18-3.6-alpine":
        fail(errors, "OpenSPP backup image changed")
    benv = env_map(backup)
    if benv.get("PGUSER") != "odoo" or benv.get("PGPASSWORD") != oenv.get("DB_PASSWORD"):
        fail(errors, "OpenSPP backup no longer uses the runtime DB role")
    btargets = set(mount_targets(backup))
    for target in ("/backups", "/var/lib/odoo", "/backup.sh", "/backup-entrypoint.sh"):
        if target not in btargets:
            fail(errors, f"OpenSPP coherent DB+filestore backup target missing: {target}")
    bmounts = [m for m in (backup.get("volumes") or []) if isinstance(m, dict) and m.get("target") == "/backup.sh"]
    if len(bmounts) != 1:
        fail(errors, "OpenSPP backup script managed file missing")
    else:
        bscript = str(bmounts[0].get("content") or "")
        if "pg_dump" not in bscript or "filestore.tar.gz" not in bscript or "SHA256SUMS" not in bscript:
            fail(errors, "OpenSPP backup lost PostgreSQL + filestore recovery-set capture")

    if set(doc.get("volumes") or {}) != {"postgres_data", "odoo_data", "backup_data"}:
        fail(errors, f"OpenSPP persistence volume set changed: {sorted(doc.get('volumes') or {})}")


def architecture_capabilities(doc: dict) -> set[str]:
    """Heuristic architecture classes for advisory comparison, never hard rejection."""
    features: set[str] = set()
    services = doc.get("services") or {}
    for name, svc in services.items():
        if not isinstance(svc, dict):
            continue
        image = str(svc.get("image") or "").lower()
        low_name = name.lower()
        hay = f"{low_name} {image}"
        if "postgres" in hay or "postgis" in hay:
            features.add("db:postgres")
        if "mysql" in hay or "mariadb" in hay:
            features.add("db:mysql")
        if "mongo" in hay:
            features.add("db:mongo")
        if "redis" in hay or "valkey" in hay:
            features.add("cache:redis-compatible")
        if "clickhouse" in hay:
            features.add("db:clickhouse")
        if any(x in hay for x in ("solr", "elasticsearch", "opensearch")):
            features.add("search")
        if any(x in hay for x in ("rabbitmq", "kafka", "nats", "pulsar")):
            features.add("broker/queue")
        if any(x in hay for x in ("minio", "garage", "seaweedfs")):
            features.add("object-store")
        if any(x in low_name for x in ("worker", "celery")):
            features.add("worker")
        if any(x in low_name for x in ("beat", "scheduler", "cron")):
            features.add("scheduler")
        if any(x in low_name for x in ("gateway", "proxy")) or "nginx" in hay or "caddy" in hay:
            features.add("gateway/proxy")
        if any(x in low_name for x in ("init", "migrate", "bootstrap", "setup")):
            features.add("init/migration-service")
    if doc.get("volumes"):
        features.add("persistence")
    return features


def fingerprint(doc: dict) -> set[str]:
    features = set(architecture_capabilities(doc))
    services = doc.get("services") or {}
    features.add(f"services:{len(services)}")
    public_count = 0
    for svc in services.values():
        if not isinstance(svc, dict):
            continue
        env = env_map(svc)
        if any(k.startswith(("SERVICE_URL_", "SERVICE_FQDN_")) for k in env):
            public_count += 1
    features.add(f"public-magic:{public_count}")
    return features

def similarity(a: set[str], b: set[str]) -> float:
    if not a and not b:
        return 1.0
    return len(a & b) / max(1, len(a | b))


def report_candidate(candidate: Path, goldens: list[tuple[str, Path]], upstream: Path | None = None) -> None:
    _, cand_doc = load(candidate)
    cand = fingerprint(cand_doc)
    scores = []
    for label, path in goldens:
        _, gdoc = load(path)
        scores.append((similarity(cand, fingerprint(gdoc)), label))
    scores.sort(reverse=True)
    print("INFO: candidate architecture fingerprint:", ", ".join(sorted(cand)))
    for score, label in scores:
        print(f"INFO: candidate similarity to {label}: {score:.2f}")
    if scores and scores[0][0] >= 0.60:
        upstream_matches = False
        if upstream is not None:
            _, up_doc = load(upstream)
            upstream_matches = (
                architecture_capabilities(cand_doc) == architecture_capabilities(up_doc)
                and len(cand_doc.get("services") or {}) == len(up_doc.get("services") or {})
            )
        if upstream_matches:
            print("INFO: high Golden similarity is informational because candidate capabilities/service count match the supplied upstream baseline; current-upstream provenance outranks Golden resemblance.")
        else:
            print("REVIEW REQUIRED: candidate resembles a bundled golden case. Similarity is not contamination evidence; document current-upstream justification for the shared topology before borrowing any mechanism.")


def report_upstream_delta(candidate: Path, upstream: Path) -> None:
    _, cand_doc = load(candidate)
    _, up_doc = load(upstream)
    cand_services = set((cand_doc.get("services") or {}).keys())
    up_services = set((up_doc.get("services") or {}).keys())
    cand_caps = architecture_capabilities(cand_doc)
    up_caps = architecture_capabilities(up_doc)

    print(f"INFO: upstream service count: {len(up_services)}; candidate service count: {len(cand_services)} (count is descriptive only)")
    print("INFO: upstream architecture capabilities:", ", ".join(sorted(up_caps)) or "none detected")
    print("INFO: candidate architecture capabilities:", ", ".join(sorted(cand_caps)) or "none detected")

    added_names = sorted(cand_services - up_services)
    removed_names = sorted(up_services - cand_services)
    if added_names:
        print("INFO: candidate-only service names:", ", ".join(added_names))
    if removed_names:
        print("INFO: upstream-only service names:", ", ".join(removed_names))

    added_caps = sorted(cand_caps - up_caps)
    removed_caps = sorted(up_caps - cand_caps)
    if added_caps:
        print("REVIEW REQUIRED: candidate introduces architecture capabilities not detected in upstream: " + ", ".join(added_caps) + ". Each needs current-upstream or explicit Coolify-operational provenance; absence upstream is not automatic failure.")
    if removed_caps:
        print("REVIEW REQUIRED: candidate omits/replaces architecture capabilities detected in upstream: " + ", ".join(removed_caps) + ". Document which Coolify/platform/application responsibility replaces them; do not simplify by count alone.")
    if not added_caps and not removed_caps:
        print("INFO: no architecture-capability delta detected by the advisory heuristic; still inspect service semantics and upstream provenance manually.")

def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("skill_dir", nargs="?", type=Path, default=Path(__file__).resolve().parents[1])
    ap.add_argument("--candidate", type=Path, help="Optional candidate Compose for advisory architecture-similarity reporting.")
    ap.add_argument("--upstream", type=Path, help="Optional upstream Compose baseline for advisory capability-delta reporting; requires --candidate.")
    args = ap.parse_args()
    if args.upstream and not args.candidate:
        ap.error("--upstream requires --candidate")
    root = args.skill_dir.resolve()
    paths = [
        ("KoboToolbox", root / "assets/kobotoolbox-v19.3-golden.yml"),
        ("CKAN", root / "assets/ckan-v1.0.8-golden.yml"),
        ("OpenMRS", root / "assets/openmrs-3.7.1-v1.0.0-golden.yml"),
        ("OpenEMR", root / "assets/openemr-8.3.0-v1.0.0-golden.yml"),
        ("ODK Central", root / "assets/odk-central-v2026.2.4-v1.0.0-golden.yml"),
        ("Frappe Framework", root / "assets/frappe-framework-v16.32.0-v1.0.0-golden.yml"),
        ("ERPNext", root / "assets/erpnext-v16.33.0-v1.0.0-golden.yml"),
        ("Mem0", root / "assets/mem0-v2.0.19-v1.0.0-golden.yml"),
        ("Overleaf CE", root / "assets/overleaf-ce-6.2.2-v1.0.0-golden.yml"),
        ("NetBox", root / "assets/netbox-4.6.9-v1.0.0-golden.yml"),
        ("Baserow", root / "assets/baserow-2.3.3-v1.0.0-golden.yml"),
        ("OpenSPP", root / "assets/openspp-2026.08-v1.0.0-golden.yml"),
    ]
    errors: list[str] = []
    try:
        check_kobo(paths[0][1], errors)
        check_ckan(paths[1][1], errors)
        check_openmrs(paths[2][1], errors)
        check_openemr(paths[3][1], errors)
        check_odk(paths[4][1], errors)
        check_frappe(paths[5][1], errors)
        check_erpnext(paths[6][1], errors)
        check_mem0(paths[7][1], errors)
        check_overleaf(paths[8][1], errors)
        check_netbox(paths[9][1], errors)
        check_baserow(paths[10][1], errors)
        check_openspp(paths[11][1], errors)
    except Exception as exc:
        errors.append(str(exc))

    if errors:
        for item in errors:
            print("ERROR:", item)
        return 1
    print("Golden regression invariants: OK")
    print("Golden cases checked: 12")
    if args.candidate:
        report_candidate(args.candidate, paths, args.upstream)
        if args.upstream:
            report_upstream_delta(args.candidate, args.upstream)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
````

<!-- END PORTABLE RESOURCE: scripts/validate_golden_cases.py -->

<!-- BEGIN PORTABLE RESOURCE: scripts/validate_portable.py -->
<!-- SOURCE SHA256: e88fdf0a9f9cc63196f882272baa7b6c76f8d470a5a150da7635889007adf4d2 -->
<!-- EMBEDDED SHA256: 6f03f0567ca4f93429826c574efc2976f5bd2b05191f2a18e09f522da72a0688 -->

## Portable resource: `scripts/validate_portable.py`

````python
#!/usr/bin/env python3
"""Validate portable structure, hashes, and optional synchronization to a root."""

from __future__ import annotations

import argparse
import hashlib
import re
from pathlib import Path

from build_portable import discover_resources, render_payload


def sha256_text(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).hexdigest()


def validate_fences(text: str) -> list[str]:
    errors: list[str] = []
    in_fence = False
    fence_char = ""
    fence_len = 0
    opened_at = 0

    for lineno, line in enumerate(text.splitlines(), 1):
        stripped = line.lstrip()
        match = re.match(r"(`{3,}|~{3,})", stripped)
        if not match:
            continue
        token = match.group(1)
        char = token[0]
        length = len(token)
        if not in_fence:
            in_fence = True
            fence_char = char
            fence_len = length
            opened_at = lineno
        elif char == fence_char and length >= fence_len:
            in_fence = False
            fence_char = ""
            fence_len = 0
            opened_at = 0
    if in_fence:
        errors.append(f"unclosed Markdown fence opened at line {opened_at}")
    return errors


RESOURCE_RE = re.compile(
    r"<!-- BEGIN PORTABLE RESOURCE: (?P<rel>[^>]+) -->\n"
    r"<!-- SOURCE SHA256: (?P<source>[0-9a-f]{64}) -->\n"
    r"<!-- EMBEDDED SHA256: (?P<embedded>[0-9a-f]{64}) -->\n"
    r"(?P<payload>.*?)"
    r"<!-- END PORTABLE RESOURCE: (?P=rel) -->",
    re.S,
)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("portable", type=Path)
    parser.add_argument("--root", type=Path, help="Structured skill root to verify source synchronization.")
    args = parser.parse_args()

    text = args.portable.read_text(encoding="utf-8")
    errors: list[str] = []

    if not text.startswith("---\nname: coolify-architect\n"):
        errors.append("portable file must begin with the Agent Skill YAML frontmatter")
    if "\n# coolify-architect\n" not in text[:2500]:
        errors.append("main skill heading not found near the beginning")
    errors.extend(validate_fences(text))

    matches = list(RESOURCE_RE.finditer(text))
    begin_count = len(re.findall(r"^<!-- BEGIN PORTABLE RESOURCE: [^>]+ -->$", text, re.M))
    end_count = len(re.findall(r"^<!-- END PORTABLE RESOURCE: [^>]+ -->$", text, re.M))
    if begin_count != end_count or len(matches) != begin_count:
        errors.append("portable resource markers/hashes are malformed or unmatched")

    rels: list[str] = []
    for m in matches:
        rel = m.group("rel")
        rels.append(rel)
        if sha256_text(m.group("payload")) != m.group("embedded"):
            errors.append(f"embedded payload hash mismatch: {rel}")

    if len(rels) != len(set(rels)):
        errors.append("duplicate portable resource entries found")

    if args.root:
        root = args.root.resolve()
        expected_paths = discover_resources(root)
        expected = [p.relative_to(root).as_posix() for p in expected_paths]
        if rels != expected:
            missing = [r for r in expected if r not in rels]
            extra = [r for r in rels if r not in expected]
            if missing:
                errors.append("portable missing structured resources: " + ", ".join(missing))
            if extra:
                errors.append("portable has unexpected resources: " + ", ".join(extra))
            if not missing and not extra:
                errors.append("portable resource order differs from structured discovery order")

        by_rel = {m.group("rel"): m for m in matches}
        for path in expected_paths:
            rel = path.relative_to(root).as_posix()
            m = by_rel.get(rel)
            if not m:
                continue
            source = path.read_text(encoding="utf-8")
            if sha256_text(source) != m.group("source"):
                errors.append(f"source hash mismatch: {rel}")
            expected_payload = render_payload(rel, source)
            if sha256_text(expected_payload) != m.group("embedded"):
                errors.append(f"rendered payload does not match current structured source: {rel}")

    if errors:
        for error in errors:
            print(f"ERROR: {error}")
        return 1

    print("Portable structure: OK")
    print(f"Resources embedded: {len(matches)}")
    print(f"Lines: {len(text.splitlines())}")
    if args.root:
        print("Structured/portable synchronization: OK")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
````

<!-- END PORTABLE RESOURCE: scripts/validate_portable.py -->

<!-- BEGIN PORTABLE RESOURCE: scripts/validate_skill.py -->
<!-- SOURCE SHA256: 6843859c3d56b447d3158e17c0688e769b227fafe86c858c157d41775c88200b -->
<!-- EMBEDDED SHA256: 98fa136b8f1edd439ed56fde13c1bac4ca5479a2b7ab21d29f4ac676c7670b51 -->

## Portable resource: `scripts/validate_skill.py`

````python
#!/usr/bin/env python3
"""Structural/reference integrity validator for the structured coolify-architect skill."""

from __future__ import annotations

import argparse
import hashlib
import re
import sys
from pathlib import Path

try:
    import yaml
except Exception as exc:
    print(f"ERROR: PyYAML is required: {exc}", file=sys.stderr)
    raise SystemExit(2)

NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")

REQUIRED = [
    "SKILL.md", "README.md", "CHANGELOG.md", "LICENSE", "agents/openai.yaml",
    "references/source-priority.md", "references/architecture-discovery.md",
    "references/coolify-rules.md", "references/networking-and-domains.md",
    "references/production-readiness.md", "references/anti-patterns.md",
    "references/kobotoolbox-case-study.md", "references/ckan-case-study.md",
    "references/openmrs-case-study.md", "references/openemr-case-study.md",
    "references/odk-central-case-study.md", "references/frappe-framework-case-study.md", "references/erpnext-case-study.md", "references/mem0-case-study.md", "references/overleaf-ce-case-study.md", "references/netbox-case-study.md", "references/rc5-vs-rc6-netbox-regression-analysis.md", "references/baserow-case-study.md", "references/baserow-profile-evidence-matrix.md", "references/eleven-benchmark-audit.md", "references/openspp-case-study.md", "references/openspp-rc1-to-rc8-causal-ledger.md", "references/openspp-skill-learning-delta.md", "references/openspp-golden-sha256.txt", "references/twelve-benchmark-audit.md", "references/baserow-validated-alternative-external-postgres-rc2.yml", "references/baserow-reference-all-in-one-rc8.yml", "references/five-benchmark-audit.md", "references/six-benchmark-audit.md", "references/seven-benchmark-audit.md", "references/eight-benchmark-audit.md", "references/nine-benchmark-audit.md", "references/ten-benchmark-audit.md",
    "references/cross-benchmark-lessons.md", "references/golden-regression-cases.md", "references/evaluation-prompts.md",
    "references/sources.md", "references/official-coolify-template-corpus.md",
    "references/official-template-taxonomy.md",
    "scripts/audit_compose.py", "scripts/validate_compose.sh",
    "scripts/validate_embedded.py", "scripts/validate_skill.py",
    "scripts/select_reference_templates.py", "scripts/build_portable.py",
    "scripts/validate_portable.py", "scripts/validate_golden_cases.py", "scripts/test_magic_variables.py", "scripts/test_embedded_validation.py", "scripts/test_regression_learning.py", "scripts/test_profile_selection.py", "scripts/test_openspp_learning.py",
    "assets/kobotoolbox-v19.3-golden.yml", "assets/ckan-v1.0.8-golden.yml",
    "assets/openmrs-3.7.1-v1.0.0-golden.yml", "assets/openemr-8.3.0-v1.0.0-golden.yml",
    "assets/odk-central-v2026.2.4-v1.0.0-golden.yml",
    "assets/frappe-framework-v16.32.0-v1.0.0-golden.yml",
    "assets/erpnext-v16.33.0-v1.0.0-golden.yml",
    "assets/mem0-v2.0.19-v1.0.0-golden.yml",
    "assets/overleaf-ce-6.2.2-v1.0.0-golden.yml",
    "assets/netbox-4.6.9-v1.0.0-golden.yml",
    "assets/baserow-2.3.3-v1.0.0-golden.yml",
    "assets/openspp-2026.08-v1.0.0-golden.yml",
]

GOLDENS = (
    "assets/kobotoolbox-v19.3-golden.yml",
    "assets/ckan-v1.0.8-golden.yml",
    "assets/openmrs-3.7.1-v1.0.0-golden.yml",
    "assets/openemr-8.3.0-v1.0.0-golden.yml",
    "assets/odk-central-v2026.2.4-v1.0.0-golden.yml",
    "assets/frappe-framework-v16.32.0-v1.0.0-golden.yml",
    "assets/erpnext-v16.33.0-v1.0.0-golden.yml",
    "assets/mem0-v2.0.19-v1.0.0-golden.yml",
    "assets/overleaf-ce-6.2.2-v1.0.0-golden.yml",
    "assets/netbox-4.6.9-v1.0.0-golden.yml",
    "assets/baserow-2.3.3-v1.0.0-golden.yml",
    "assets/openspp-2026.08-v1.0.0-golden.yml",
)

TOKEN_REF_RE = re.compile(r"`((?:references|assets|scripts)/[^`]+)`")


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("skill_dir", type=Path)
    args = ap.parse_args()
    skill = args.skill_dir.resolve()
    path = skill / "SKILL.md"
    errors: list[str] = []
    reviews: list[str] = []

    for rel in REQUIRED:
        if not (skill / rel).is_file():
            errors.append(f"required resource missing: {rel}")

    if not path.exists():
        print("ERROR: SKILL.md missing")
        return 1

    text = path.read_text(encoding="utf-8")
    if not text.startswith("---\n"):
        errors.append("SKILL.md must start with YAML frontmatter")
    else:
        parts = text.split("---", 2)
        if len(parts) < 3:
            errors.append("frontmatter is not closed")
        else:
            try:
                fm = yaml.safe_load(parts[1]) or {}
            except Exception as exc:
                fm = {}
                errors.append(f"frontmatter YAML parse failed: {exc}")
            name = str(fm.get("name") or "")
            desc = str(fm.get("description") or "")
            if not name:
                errors.append("frontmatter.name is required")
            elif not NAME_RE.fullmatch(name):
                errors.append("frontmatter.name must be kebab-case")
            elif name != skill.name:
                errors.append(f"frontmatter.name '{name}' must match folder '{skill.name}'")
            if not desc:
                errors.append("frontmatter.description is required")
            elif len(desc) > 1024:
                errors.append("frontmatter.description exceeds 1024 characters")

    if "# coolify-architect" not in text.lower():
        errors.append("expected H1 title not found")

    line_count = len(text.splitlines())
    if line_count > 800:
        errors.append(f"SKILL.md is {line_count} lines; keep entrypoint concise (<800 for the twelve-Golden entrypoint)")

    for directory in ("scripts", "references", "assets"):
        d = skill / directory
        if d.exists():
            nested_dirs = [p for p in d.rglob("*") if p.is_dir() and p.name != "__pycache__"]
            if nested_dirs:
                errors.append(f"{directory}/ contains nested directories; keep resources flat for portability")

    # Golden fixtures must parse and contain services.
    for rel in GOLDENS:
        p = skill / rel
        if not p.exists():
            continue
        try:
            doc = yaml.safe_load(p.read_text(encoding="utf-8"))
        except Exception as exc:
            errors.append(f"golden YAML parse failed {rel}: {exc}")
            continue
        if not isinstance(doc, dict) or not isinstance(doc.get("services"), dict) or not doc["services"]:
            errors.append(f"golden fixture has no services: {rel}")

    openmrs = skill / "assets/openmrs-3.7.1-v1.0.0-golden.yml"
    if openmrs.exists() and "OpenMRS regression fixture / golden case — NOT a generic Coolify skeleton." not in openmrs.read_text(encoding="utf-8"):
        errors.append("OpenMRS golden fixture missing explicit NOT-a-generic-skeleton warning")

    openemr = skill / "assets/openemr-8.3.0-v1.0.0-golden.yml"
    if openemr.exists() and "OpenEMR regression fixture / golden case — NOT a generic Coolify skeleton." not in openemr.read_text(encoding="utf-8"):
        errors.append("OpenEMR golden fixture missing explicit NOT-a-generic-skeleton warning")

    odk = skill / "assets/odk-central-v2026.2.4-v1.0.0-golden.yml"
    if odk.exists() and "ODK Central regression fixture / golden case — NOT a generic Coolify skeleton." not in odk.read_text(encoding="utf-8"):
        errors.append("ODK Central golden fixture missing explicit NOT-a-generic-skeleton warning")

    frappe = skill / "assets/frappe-framework-v16.32.0-v1.0.0-golden.yml"
    if frappe.exists():
        ftext = frappe.read_text(encoding="utf-8")
        if "Frappe Framework regression fixture / golden case — NOT a generic Coolify skeleton." not in ftext:
            errors.append("Frappe golden fixture missing explicit NOT-a-generic-skeleton warning")

    erpnext = skill / "assets/erpnext-v16.33.0-v1.0.0-golden.yml"
    if erpnext.exists():
        actual = hashlib.sha256(erpnext.read_bytes()).hexdigest()
        expected = "64660809aba082409a41e20006d0d24dbc913928a30f07590ba873171ee2a7cb"
        if actual != expected:
            errors.append(f"ERPNext golden fixture must preserve exact accepted RC5 bytes: {actual}")

    regress = skill / "references/golden-regression-cases.md"
    if regress.exists():
        rt = regress.read_text(encoding="utf-8")
        for token in ("kobotoolbox-v19.3-golden.yml", "ckan-v1.0.8-golden.yml", "openmrs-3.7.1-v1.0.0-golden.yml", "openemr-8.3.0-v1.0.0-golden.yml", "odk-central-v2026.2.4-v1.0.0-golden.yml", "frappe-framework-v16.32.0-v1.0.0-golden.yml", "erpnext-v16.33.0-v1.0.0-golden.yml", "mem0-v2.0.19-v1.0.0-golden.yml", "overleaf-ce-6.2.2-v1.0.0-golden.yml", "netbox-4.6.9-v1.0.0-golden.yml", "baserow-2.3.3-v1.0.0-golden.yml", "openspp-2026.08-v1.0.0-golden.yml"):
            if token not in rt:
                errors.append(f"golden regression reference missing token: {token}")
        if "Frappe #6 / ERPNext #7 relationship" not in rt:
            errors.append("golden-regression-cases.md must identify the distinct Frappe #6 / ERPNext #7 relationship")
        if "Sibling Product Delta Gate" not in rt:
            errors.append("golden-regression-cases.md must include the Sibling Product Delta Gate")
        if "token blacklist" not in rt.lower() and "blacklist" not in rt.lower():
            reviews.append("golden-regression-cases.md should explicitly explain why token blacklists are not the anti-contamination model")

    # Ensure portable-visible Markdown resource references resolve.
    for md in [skill / "SKILL.md", skill / "README.md", *(skill / "references").glob("*.md")]:
        if not md.exists():
            continue
        mtext = md.read_text(encoding="utf-8")
        for rel in TOKEN_REF_RE.findall(mtext):
            # Strip command args/fragments only if clearly not part of path.
            rel_clean = rel.split(" ", 1)[0].rstrip(".,;:")
            if not (skill / rel_clean).exists():
                errors.append(f"broken cross-reference in {md.relative_to(skill)}: {rel_clean}")

    # Stale pre-fifth-benchmark language would make the portable edition contradictory.
    stale_patterns = (
        "two runtime golden/regression cases",
        "three runtime golden/regression cases",
        "the next benchmark is OpenMRS",
        "the next benchmark is OpenEMR",
        "OpenMRS benchmark guard",
        "OpenEMR benchmark guard",
        "using the three golden cases in this Skill",
        "Golden cases checked: 2",
        "Golden cases checked: 3",
        "four runtime golden/regression cases",
        "confirmed across all four benchmarks",
        "using the four golden cases in this Skill",
        "using all four golden cases",
        "Golden cases checked: 4",
    )
    for p in [skill / "SKILL.md", skill / "README.md", *(skill / "references").glob("*.md")]:
        if not p.exists():
            continue
        low = p.read_text(encoding="utf-8").lower()
        for stale in stale_patterns:
            if stale.lower() in low:
                errors.append(f"stale benchmark-count wording in {p.relative_to(skill)}: {stale}")


    # Current corpus-facing docs should identify twelve Goldens. Historical audits and changelog entries are exempt.
    current_count_targets = [
        skill / "SKILL.md",
        skill / "README.md",
        skill / "references/cross-benchmark-lessons.md",
        skill / "references/golden-regression-cases.md",
        skill / "references/twelve-benchmark-audit.md",
    ]
    for p in current_count_targets:
        if not p.exists():
            continue
        low = p.read_text(encoding="utf-8").lower()
        if any(x in low for x in (
            "six runtime golden", "six-golden corpus", "seven runtime golden", "seven-golden corpus",
            "eight runtime golden", "eight-golden corpus", "across the eight cases", "among the eight cases",
            "eleven runtime golden", "eleven-golden corpus"
        )):
            errors.append(f"stale pre-twelve-Golden current-corpus wording in {p.relative_to(skill)}")

    # Current docs must not contradict Frappe Golden #6. The historical five-benchmark audit is intentionally exempt.
    frappe_stale = ("not golden case #6", "not golden #6", "frappe remains partial", "partial frappe framework runtime case", "frappe partial-runtime")
    for p in [skill / "SKILL.md", skill / "README.md", *(skill / "references").glob("*.md")]:
        if not p.exists() or p.name == "five-benchmark-audit.md":
            continue
        low = p.read_text(encoding="utf-8").lower()
        for stale in frappe_stale:
            if stale in low:
                errors.append(f"stale Frappe pre-promotion wording in {p.relative_to(skill)}: {stale}")

    # Current docs must not describe ERPNext as partial/non-golden.
    erpnext_stale = ("erpnext remains partial", "erpnext is not golden", "not golden case #7")
    for p in [skill / "SKILL.md", skill / "README.md", *(skill / "references").glob("*.md")]:
        if not p.exists() or p.name in {"five-benchmark-audit.md", "six-benchmark-audit.md"}:
            continue
        low = p.read_text(encoding="utf-8").lower()
        for stale in erpnext_stale:
            if stale in low:
                errors.append(f"stale ERPNext pre-promotion wording in {p.relative_to(skill)}: {stale}")

    # Current docs must not describe Mem0 as untested/candidate after Golden #8 promotion.
    mem0_stale = ("mem0 is not golden", "mem0 remains candidate", "mem0 runtime not tested", "mem0 rc1 awaiting deployment")
    for p in [skill / "SKILL.md", skill / "README.md", *(skill / "references").glob("*.md")]:
        if not p.exists() or p.name in {"five-benchmark-audit.md", "six-benchmark-audit.md", "seven-benchmark-audit.md"}:
            continue
        low = p.read_text(encoding="utf-8").lower()
        for stale in mem0_stale:
            if stale in low:
                errors.append(f"stale Mem0 pre-promotion wording in {p.relative_to(skill)}: {stale}")

    # Current docs must not describe Overleaf CE as an untested/non-golden candidate after Golden #9 promotion.
    overleaf_stale = ("overleaf is not golden", "overleaf remains candidate", "overleaf runtime not tested", "overleaf rc4 awaiting deployment")
    for p in [skill / "SKILL.md", skill / "README.md", *(skill / "references").glob("*.md")]:
        if not p.exists() or p.name in {"five-benchmark-audit.md", "six-benchmark-audit.md", "seven-benchmark-audit.md", "eight-benchmark-audit.md"}:
            continue
        low = p.read_text(encoding="utf-8").lower()
        for stale in overleaf_stale:
            if stale in low:
                errors.append(f"stale Overleaf pre-promotion wording in {p.relative_to(skill)}: {stale}")

    # Current docs must not describe NetBox as non-golden after Golden #10 promotion.
    netbox_stale = ("netbox is not golden", "netbox remains candidate", "netbox runtime not tested", "netbox rc2 awaiting deployment")
    for p in [skill / "SKILL.md", skill / "README.md", *(skill / "references").glob("*.md")]:
        if not p.exists() or p.name in {"five-benchmark-audit.md", "six-benchmark-audit.md", "seven-benchmark-audit.md", "eight-benchmark-audit.md", "nine-benchmark-audit.md"}:
            continue
        low = p.read_text(encoding="utf-8").lower()
        for stale in netbox_stale:
            if stale in low:
                errors.append(f"stale NetBox pre-promotion wording in {p.relative_to(skill)}: {stale}")


    # Current docs must distinguish Baserow Golden #11 from its validated alternative/reference profiles.
    baserow_stale = ("baserow is not golden", "baserow remains candidate", "baserow runtime not tested")
    for pth in [skill / "SKILL.md", skill / "README.md", *(skill / "references").glob("*.md")]:
        if not pth.exists() or pth.name in {"five-benchmark-audit.md", "six-benchmark-audit.md", "seven-benchmark-audit.md", "eight-benchmark-audit.md", "nine-benchmark-audit.md", "ten-benchmark-audit.md"}:
            continue
        low = pth.read_text(encoding="utf-8").lower()
        for stale in baserow_stale:
            if stale in low:
                errors.append(f"stale Baserow pre-promotion wording in {pth.relative_to(skill)}: {stale}")

    # Current corpus docs must not describe OpenSPP as non-Golden after runtime acceptance.
    openspp_current = [
        skill / "SKILL.md",
        skill / "README.md",
        skill / "references/cross-benchmark-lessons.md",
        skill / "references/golden-regression-cases.md",
        skill / "references/openspp-case-study.md",
        skill / "references/twelve-benchmark-audit.md",
    ]
    for pth in openspp_current:
        if not pth.exists():
            continue
        low = pth.read_text(encoding="utf-8").lower()
        for stale in ("openspp is not golden", "openspp remains candidate", "openspp runtime not tested"):
            if stale in low:
                errors.append(f"stale OpenSPP pre-promotion wording in {pth.relative_to(skill)}: {stale}")

    # Golden #12 promotion must never rewrite the eleven earlier runtime oracles.
    legacy_hashes = {
        "assets/frappe-framework-v16.32.0-v1.0.0-golden.yml": "01a534d234516f3c5572d7c0b940cfa3cc6d43f5f58df6ef34487dfbf8b3838f",
        "assets/ckan-v1.0.8-golden.yml": "5495852f0a4dc45670a74de7faf73dab6a9cec40f7705639d324483beeaff3d9",
        "assets/erpnext-v16.33.0-v1.0.0-golden.yml": "64660809aba082409a41e20006d0d24dbc913928a30f07590ba873171ee2a7cb",
        "assets/openmrs-3.7.1-v1.0.0-golden.yml": "6c03683f5d64290acba97e968ea5d55aee4164eba95963bc02ae832810bf90a3",
        "assets/kobotoolbox-v19.3-golden.yml": "937f1e7d4dd3ecc0712a37736ca370fa759582f17fc23de15d7afd0bd4de6c3a",
        "assets/mem0-v2.0.19-v1.0.0-golden.yml": "b2f2b6442a49275f692e5bd586a20f6d35a109538df56e2f82055ccd86b1fcc7",
        "assets/openemr-8.3.0-v1.0.0-golden.yml": "e029e268153ce1ce42e69baca2dbeda898c609dc16e8b543a257e863683ca990",
        "assets/odk-central-v2026.2.4-v1.0.0-golden.yml": "e93e59d7f2ea4ebb8eaec2c7bd8ecd7d06d223a53ee57f23b3636d021eb88768",
        "assets/overleaf-ce-6.2.2-v1.0.0-golden.yml": "b8cb9425523d38088f7069c70d762ab24fbf07232d736f607572e5b191621585",
    }
    for rel, expected in legacy_hashes.items():
        gp = skill / rel
        if gp.exists():
            actual = hashlib.sha256(gp.read_bytes()).hexdigest()
            if actual != expected:
                errors.append(f"pre-OpenSPP Golden fixture changed unexpectedly: {rel}: {actual}")

    overleaf = skill / "assets/overleaf-ce-6.2.2-v1.0.0-golden.yml"
    if overleaf.exists():
        actual = hashlib.sha256(overleaf.read_bytes()).hexdigest()
        expected = "b8cb9425523d38088f7069c70d762ab24fbf07232d736f607572e5b191621585"
        if actual != expected:
            errors.append(f"Overleaf golden fixture must preserve exact accepted RC4 bytes: {actual}")

    netbox = skill / "assets/netbox-4.6.9-v1.0.0-golden.yml"
    if netbox.exists():
        actual = hashlib.sha256(netbox.read_bytes()).hexdigest()
        expected = "e4be06751d206704a2e9460ac2926d92833b39a71266cf1bd5a8a788da319804"
        if actual != expected:
            errors.append(f"NetBox golden fixture must preserve exact runtime-accepted RC2 bytes: {actual}")

    baserow = skill / "assets/baserow-2.3.3-v1.0.0-golden.yml"
    if baserow.exists():
        actual = hashlib.sha256(baserow.read_bytes()).hexdigest()
        expected = "143c3a94952b16e85638d87bd50fa29a49fa756b7c12cba097fe32383c4312f6"
        if actual != expected:
            errors.append(f"Baserow golden fixture must preserve exact runtime-accepted RC5 bytes: {actual}")

    openspp = skill / "assets/openspp-2026.08-v1.0.0-golden.yml"
    if openspp.exists():
        actual = hashlib.sha256(openspp.read_bytes()).hexdigest()
        expected = "f00a8755fa2be8e8b1f50970978ae1b57c1877093c2a35108edf35a675d4587b"
        if actual != expected:
            errors.append(f"OpenSPP golden fixture must preserve exact operator-accepted RC8 bytes: {actual}")

    openspp_hash_record = skill / "references/openspp-golden-sha256.txt"
    if openspp_hash_record.exists():
        record = openspp_hash_record.read_text(encoding="utf-8")
        if "f00a8755fa2be8e8b1f50970978ae1b57c1877093c2a35108edf35a675d4587b" not in record:
            errors.append("OpenSPP exact Golden SHA-256 record is missing or inconsistent")

    # If release portable copies are present, require byte identity. Source sync is
    # checked separately by validate_portable.py after regeneration.
    pmd = skill / "coolify-architect-portable.SKILL.md"
    ptxt = skill / "coolify-architect-portable.txt"
    if pmd.exists() != ptxt.exists():
        errors.append("portable release copies must be present together (.SKILL.md and .txt)")
    if pmd.exists() and ptxt.exists() and pmd.read_bytes() != ptxt.read_bytes():
        errors.append("portable .SKILL.md and .txt are not byte-identical")

    print(f"SKILL.md lines: {line_count}")
    print(f"Required resources: {len(REQUIRED)}")
    print(f"Golden fixtures declared: {len(GOLDENS)}")
    for item in reviews:
        print("REVIEW REQUIRED:", item)
    if errors:
        for item in errors:
            print("ERROR:", item)
        return 1
    print("Skill structure and references: OK")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
````

<!-- END PORTABLE RESOURCE: scripts/validate_skill.py -->

<!-- BEGIN PORTABLE RESOURCE: assets/baserow-2.3.3-v1.0.0-golden.yml -->
<!-- SOURCE SHA256: 143c3a94952b16e85638d87bd50fa29a49fa756b7c12cba097fe32383c4312f6 -->
<!-- EMBEDDED SHA256: f023a72ad552dacc91e30f6b4ffbbd80560c8b6d12bb4bb8f31e5b6759d22de9 -->

## Portable resource: `assets/baserow-2.3.3-v1.0.0-golden.yml`

````yaml
# Baserow Coolify V1.0.0-RC5 candidate — distributed profile + single Coolify-managed public route + proxy-safe healthcheck
# Upstream: Baserow 2.3.3
#
# IMPORTANT PUBLIC-DOMAIN CONTRACT
# - Exactly one Coolify-managed public URL is declared: SERVICE_URL_BASEROW.
# - The public service is named `baserow` and is the internal Caddy semantic gateway on port 80.
# - BASEROW_PUBLIC_URL is derived from that same URL in backend, frontend and gateway.
# - To use a custom hostname, REPLACE the `baserow` service domain in Coolify Domains; do not add a second domain.
# - After changing the domain, redeploy so SERVICE_URL_BASEROW reaches all containers.
# - Do not attach public domains to backend, web-frontend, workers, PostgreSQL or Redis.
# - RC5 fixes RC4's gateway healthcheck: never probe `/` with Host=127.0.0.1, because Baserow treats unknown hosts as Builder published-site domains.
#
# Optional Baserow variables below use YAML null values intentionally. Docker Compose
# will pass them through only when the operator defines them, preserving Baserow's own
# defaults instead of replacing defaults with empty strings.

x-baserow-backend-environment: &baserow-backend-environment
  SECRET_KEY: ${SERVICE_PASSWORD_64_BASEROWSECRET:?Coolify must generate SERVICE_PASSWORD_64_BASEROWSECRET}
  BASEROW_JWT_SIGNING_KEY: ${SERVICE_PASSWORD_64_BASEROWJWT:?Coolify must generate SERVICE_PASSWORD_64_BASEROWJWT}
  DATABASE_PASSWORD: ${SERVICE_PASSWORD_64_BASEROWDB:?Coolify must generate SERVICE_PASSWORD_64_BASEROWDB}
  REDIS_PASSWORD: ${SERVICE_PASSWORD_64_BASEROWREDIS:?Coolify must generate SERVICE_PASSWORD_64_BASEROWREDIS}
  BASEROW_PUBLIC_URL: ${SERVICE_URL_BASEROW:?Coolify must generate SERVICE_URL_BASEROW for the public baserow gateway}
  DATABASE_USER: baserow
  DATABASE_NAME: baserow
  DATABASE_HOST: baserow-db
  DATABASE_PORT: "5432"
  REDIS_HOST: baserow-redis
  REDIS_PORT: "6379"
  REDIS_PROTOCOL: redis
  PRIVATE_BACKEND_URL: http://baserow-backend:8000
  MEDIA_ROOT: /baserow/media
  BASEROW_BACKEND_BIND_ADDRESS: 0.0.0.0
  BASEROW_BACKEND_PORT: "8000"
  BASEROW_ENABLE_SECURE_PROXY_SSL_HEADER: "yes"
  MIGRATE_ON_STARTUP: "false"
  SYNC_TEMPLATES_ON_STARTUP: "true"
  BASEROW_EXTRA_PUBLIC_URLS:
  DATABASE_OPTIONS:
  DATABASE_URL:
  BASEROW_CONN_MAX_AGE:
  DATABASE_READ_1_USER:
  DATABASE_READ_1_NAME:
  DATABASE_READ_1_HOST:
  DATABASE_READ_1_PORT:
  DATABASE_READ_1_PASSWORD:
  DATABASE_READ_1_OPTIONS:
  DATABASE_READ_1_URL:
  DATABASE_READ_2_USER:
  DATABASE_READ_2_NAME:
  DATABASE_READ_2_HOST:
  DATABASE_READ_2_PORT:
  DATABASE_READ_2_PASSWORD:
  DATABASE_READ_2_OPTIONS:
  DATABASE_READ_2_URL:
  REDIS_URL:
  REDIS_USER:
  REDIS_SSL_CERT_REQS:
  REDIS_SSL_CA_CERTS:
  EMAIL_SMTP:
  EMAIL_SMTP_HOST:
  EMAIL_SMTP_PORT:
  EMAIL_SMTP_USE_TLS:
  EMAIL_SMTP_USE_SSL:
  EMAIL_SMTP_USER:
  EMAIL_SMTP_PASSWORD:
  EMAIL_SMTP_SSL_CERTFILE_PATH:
  EMAIL_SMTP_SSL_KEYFILE_PATH:
  FROM_EMAIL:
  AWS_ACCESS_KEY_ID:
  AWS_SECRET_ACCESS_KEY:
  AWS_STORAGE_BUCKET_NAME:
  AWS_S3_REGION_NAME:
  AWS_S3_ENDPOINT_URL:
  AWS_S3_CUSTOM_DOMAIN:
  BASEROW_AMOUNT_OF_WORKERS:
  BASEROW_AMOUNT_OF_GUNICORN_WORKERS:
  BASEROW_CELERY_BEAT_STARTUP_DELAY:
  BASEROW_CELERY_BEAT_DEBUG_LEVEL:
  BASEROW_ROW_PAGE_SIZE_LIMIT:
  BASEROW_DEPENDANT_ROWS_REALTIME_UPDATE_LIMIT:
  BASEROW_INTEGRATION_LOCAL_BASEROW_PAGE_SIZE_LIMIT:
  BASEROW_FORMULA_RANGE_MAX_ITEMS:
  BASEROW_INTEGRATION_LOCAL_BASEROW_BATCH_OPERATION_SIZE_LIMIT:
  BASEROW_INTEGRATION_ALLOW_SMTP_SERVICE_TO_USE_INSTANCE_SETTINGS:
  BATCH_ROWS_SIZE_LIMIT:
  INITIAL_TABLE_DATA_LIMIT:
  BASEROW_FILE_UPLOAD_SIZE_LIMIT_MB:
  BASEROW_FILE_UPLOAD_ACTIVE_CONTENT_POLICY:
  BASEROW_OPENAI_UPLOADED_FILE_SIZE_LIMIT_MB:
  BASEROW_UNIQUE_ROW_VALUES_SIZE_LIMIT:
  BASEROW_MAX_FIELD_TEXT_LENGTH:
  BASEROW_REALTIME_REPLAY_MAX_EVENTS:
  BASEROW_AUTOMATION_HISTORY_PAGE_SIZE_LIMIT:
  BASEROW_AUTOMATION_WORKFLOW_RATE_LIMIT_MAX_RUNS:
  BASEROW_AUTOMATION_WORKFLOW_RATE_LIMITS:
  BASEROW_AUTOMATION_WORKFLOW_RATE_LIMIT_CACHE_EXPIRY_SECONDS:
  BASEROW_AUTOMATION_WORKFLOW_HISTORY_RATE_LIMIT_CACHE_EXPIRY_SECONDS:
  BASEROW_AUTOMATION_WORKFLOW_ERROR_LIMITS:
  BASEROW_AUTOMATION_WORKFLOW_MAX_CONSECUTIVE_ERRORS:
  BASEROW_AUTOMATION_WORKFLOW_TIMEOUT_HOURS:
  BASEROW_AUTOMATION_WORKFLOW_HISTORY_MAX_DAYS:
  BASEROW_AUTOMATION_WORKFLOW_HISTORY_MAX_ENTRIES:
  BASEROW_AUTOMATION_WORKFLOW_HISTORY_MIN_RETENTION_DAYS:
  BASEROW_AUTOMATION_WORKFLOW_HISTORY_CLEANUP_INTERVAL_MINUTES:
  BASEROW_EXTRA_ALLOWED_HOSTS:
  ADDITIONAL_APPS:
  BASEROW_PLUGIN_GIT_REPOS:
  BASEROW_PLUGIN_URLS:
  BASEROW_SYNC_TEMPLATES_PATTERN:
  DONT_UPDATE_FORMULAS_AFTER_MIGRATION:
  BASEROW_TRIGGER_SYNC_TEMPLATES_AFTER_MIGRATION:
  BASEROW_SYNC_TEMPLATES_TIME_LIMIT:
  BASEROW_BACKEND_DEBUG:
  BASEROW_BACKEND_LOG_LEVEL:
  BASEROW_DJANGO_REQUEST_LOG_LEVEL:
  FEATURE_FLAGS:
  BASEROW_PRESENCE_VISIBLE_USERS:
  BASEROW_ENABLE_OTEL:
  BASEROW_DEPLOYMENT_ENV:
  OTEL_EXPORTER_OTLP_ENDPOINT:
  OTEL_RESOURCE_ATTRIBUTES:
  POSTHOG_PROJECT_API_KEY:
  POSTHOG_HOST:
  PUBLIC_BACKEND_URL:
  PUBLIC_WEB_FRONTEND_URL:
  BASEROW_EMBEDDED_SHARE_URL:
  MEDIA_URL:
  BASEROW_AIRTABLE_IMPORT_SOFT_TIME_LIMIT:
  HOURS_UNTIL_TRASH_PERMANENTLY_DELETED:
  OLD_ACTION_CLEANUP_INTERVAL_MINUTES:
  MINUTES_UNTIL_ACTION_CLEANED_UP:
  BASEROW_GROUP_STORAGE_USAGE_QUEUE:
  DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS:
  BASEROW_WAIT_INSTEAD_OF_409_CONFLICT_ERROR:
  BASEROW_DISABLE_MODEL_CACHE:
  BASEROW_PLUGIN_DIR:
  BASEROW_JOB_EXPIRATION_TIME_LIMIT:
  BASEROW_JOB_CLEANUP_INTERVAL_MINUTES:
  BASEROW_ROW_HISTORY_CLEANUP_INTERVAL_MINUTES:
  BASEROW_ROW_HISTORY_RETENTION_DAYS:
  BASEROW_USER_LOG_ENTRY_CLEANUP_INTERVAL_MINUTES:
  BASEROW_USER_LOG_ENTRY_RETENTION_DAYS:
  BASEROW_IMPORT_EXPORT_RESOURCE_CLEANUP_INTERVAL_MINUTES:
  BASEROW_IMPORT_EXPORT_RESOURCE_REMOVAL_AFTER_DAYS:
  BASEROW_IMPORT_EXPORT_TABLE_ROWS_COUNT_LIMIT:
  BASEROW_MAX_ROW_REPORT_ERROR_COUNT:
  BASEROW_JOB_SOFT_TIME_LIMIT:
  BASEROW_FRONTEND_JOBS_POLLING_TIMEOUT_MS:
  BASEROW_INITIAL_CREATE_SYNC_TABLE_DATA_LIMIT:
  BASEROW_MAX_SNAPSHOTS_PER_GROUP:
  BASEROW_SNAPSHOT_EXPIRATION_TIME_DAYS:
  BASEROW_WEBHOOKS_ALLOW_PRIVATE_ADDRESS:
  BASEROW_WEBHOOKS_IP_BLACKLIST:
  BASEROW_WEBHOOKS_IP_WHITELIST:
  BASEROW_WEBHOOKS_URL_REGEX_BLACKLIST:
  BASEROW_WEBHOOKS_URL_CHECK_TIMEOUT_SECS:
  BASEROW_WEBHOOKS_MAX_CONSECUTIVE_TRIGGER_FAILURES:
  BASEROW_WEBHOOKS_MAX_RETRIES_PER_CALL:
  BASEROW_WEBHOOKS_MAX_PER_TABLE:
  BASEROW_WEBHOOKS_MAX_CALL_LOG_ENTRIES:
  BASEROW_WEBHOOKS_REQUEST_TIMEOUT_SECONDS:
  BASEROW_INTEGRATIONS_ALLOW_PRIVATE_ADDRESS:
  BASEROW_INTEGRATIONS_PERIODIC_MINUTE_MIN:
  BASEROW_DATA_SYNC_ALLOW_PRIVATE_ADDRESS:
  BASEROW_SSO_ALLOW_PRIVATE_ADDRESS:
  BASEROW_ENTERPRISE_AUDIT_LOG_CLEANUP_INTERVAL_MINUTES:
  BASEROW_ENTERPRISE_AUDIT_LOG_RETENTION_DAYS:
  BASEROW_ALLOW_MULTIPLE_SSO_PROVIDERS_FOR_SAME_ACCOUNT:
  BASEROW_SEAT_USAGE_JOB_CRONTAB:
  BASEROW_PERIODIC_FIELD_UPDATE_CRONTAB:
  BASEROW_PERIODIC_FIELD_UPDATE_UNUSED_WORKSPACE_INTERVAL_MIN:
  BASEROW_PERIODIC_FIELD_UPDATE_TIMEOUT_MINUTES:
  BASEROW_PERIODIC_FIELD_UPDATE_QUEUE_NAME:
  BASEROW_PERIODIC_FIELD_UPDATE_BATCH_COUNT:
  BASEROW_MAX_CONCURRENT_USER_REQUESTS:
  BASEROW_CONCURRENT_USER_REQUESTS_THROTTLE_TIMEOUT:
  BASEROW_THROTTLE_BLACKLIST_TTL_SECONDS:
  BASEROW_CACHE_TTL_SECONDS:
  BASEROW_THROTTLE_IP_ENABLED:
  BASEROW_SEND_VERIFY_EMAIL_RATE_LIMIT:
  BASEROW_LOGIN_ACTION_LOG_LIMIT:
  BASEROW_OSS_ONLY:
  OTEL_TRACES_SAMPLER:
  OTEL_TRACES_SAMPLER_ARG:
  OTEL_PER_MODULE_SAMPLER_OVERRIDES:
  BASEROW_CACHALOT_ENABLED:
  BASEROW_CACHALOT_MODE:
  BASEROW_CACHALOT_ONLY_CACHABLE_TABLES:
  BASEROW_CACHALOT_UNCACHABLE_TABLES:
  BASEROW_CACHALOT_TIMEOUT:
  BASEROW_BUILDER_PUBLICLY_USED_PROPERTIES_CACHE_TTL_SECONDS:
  BASEROW_BUILDER_DISPATCH_ACTION_CACHE_TTL_SECONDS:
  BASEROW_AUTO_INDEX_VIEW_ENABLED:
  BASEROW_PERSONAL_VIEW_LOWEST_ROLE_ALLOWED:
  BASEROW_DISABLE_LOCKED_MIGRATIONS:
  BASEROW_USE_PG_FULLTEXT_SEARCH:
  BASEROW_PG_FULLTEXT_SEARCH_UPDATE_DATA_THROTTLE_SECONDS:
  BASEROW_BUILDER_DOMAINS:
  SENTRY_DSN:
  SENTRY_BACKEND_DSN:
  SENTRY_TRACES_SAMPLE_RATE:
  SENTRY_MONITOR_BEAT_TASKS:
  SENTRY_EXCLUDE_BEAT_TASKS:
  OPENAI_API_KEY:
  GROQ_API_KEY:
  BASEROW_OPENAI_API_KEY:
  BASEROW_OPENAI_ORGANIZATION:
  BASEROW_OPENAI_MODELS:
  BASEROW_OPENAI_BASE_URL:
  BASEROW_OPENROUTER_API_KEY:
  BASEROW_OPENROUTER_ORGANIZATION:
  BASEROW_OPENROUTER_MODELS:
  BASEROW_ANTHROPIC_API_KEY:
  BASEROW_ANTHROPIC_MODELS:
  BASEROW_MISTRAL_API_KEY:
  BASEROW_MISTRAL_MODELS:
  BASEROW_OLLAMA_HOST:
  BASEROW_OLLAMA_MODELS:
  BASEROW_AI_FIELD_MAX_CONCURRENT_GENERATIONS:
  BASEROW_SERVE_FILES_THROUGH_BACKEND:
  BASEROW_SERVE_FILES_THROUGH_BACKEND_PERMISSION:
  BASEROW_SERVE_FILES_THROUGH_BACKEND_EXPIRE_SECONDS:
  BASEROW_ICAL_VIEW_MAX_EVENTS:
  BASEROW_ACCESS_TOKEN_LIFETIME_MINUTES:
  BASEROW_REFRESH_TOKEN_LIFETIME_HOURS:
  BASEROW_PREVENT_POSTGRESQL_DATA_SYNC_CONNECTION_TO_DATABASE:
  BASEROW_POSTGRESQL_DATA_SYNC_BLACKLIST:
  BASEROW_TWO_WAY_SYNC_MAX_CONSECUTIVE_FAILURES:
  BASEROW_TWO_WAY_SYNC_MAX_RETRIES:
  BASEROW_ASGI_HTTP_MAX_CONCURRENCY:
  BASEROW_MAX_WEBHOOK_CALLS_IN_QUEUE_PER_WEBHOOK:
  BASEROW_MAX_HEALTHY_CELERY_QUEUE_SIZE:
  BASEROW_ENTERPRISE_PERIODIC_DATA_SYNC_CHECK_INTERVAL_MINUTES:
  BASEROW_ENTERPRISE_MAX_PERIODIC_DATA_SYNC_CONSECUTIVE_ERRORS:
  BASEROW_USE_LOCAL_CACHE:
  BASEROW_WEBHOOKS_BATCH_LIMIT:
  BASEROW_WEBHOOK_ROWS_ENTER_VIEW_BATCH_SIZE:
  BASEROW_DEADLOCK_INITIAL_BACKOFF:
  BASEROW_DEADLOCK_MAX_RETRIES:
  BASEROW_PREMIUM_GROUPED_AGGREGATE_SERVICE_MAX_SERIES:
  BASEROW_PREMIUM_GROUPED_AGGREGATE_SERVICE_MAX_AGG_BUCKETS:
  BASEROW_ENTERPRISE_CODE_RUNNER_DEFAULT_TYPE:
  BASEROW_ENTERPRISE_CODE_RUNNER_WASMTIME_EXECUTABLE:
  BASEROW_ENTERPRISE_CODE_RUNNER_QUICKJS_WASM_PATH:
  BASEROW_ENTERPRISE_CODE_RUNNER_TIMEOUT_SECONDS:
  BASEROW_ENTERPRISE_CODE_RUNNER_MEMORY_LIMIT_BYTES:
  BASEROW_ENTERPRISE_CODE_RUNNER_FUEL_LIMIT:
  BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL:
  BASEROW_ENTERPRISE_ASSISTANT_LLM_TEMPERATURE:
  BASEROW_EMBEDDINGS_API_URL:
  BASEROW_OAUTH_BACKEND_URL:
  BASEROW_TOTP_ISSUER_NAME:
  BASEROW_ENABLE_CAPTCHA:
  BASEROW_CAPTCHA_PROVIDER:
  BASEROW_CLOUDFLARE_TURNSTILE_SITE_KEY:
  BASEROW_CLOUDFLARE_TURNSTILE_SECRET_KEY:

x-baserow-frontend-environment: &baserow-frontend-environment
  BASEROW_PUBLIC_URL: ${SERVICE_URL_BASEROW:?Coolify must generate SERVICE_URL_BASEROW for the public baserow gateway}
  PRIVATE_BACKEND_URL: http://baserow-backend:8000
  BASEROW_WEBFRONTEND_BIND_ADDRESS: 0.0.0.0
  BASEROW_WEBFRONTEND_PORT: "3000"
  MEDIA_URL:
  BASEROW_EXTRA_PUBLIC_URLS:
  PUBLIC_BACKEND_URL:
  PUBLIC_WEB_FRONTEND_URL:
  BASEROW_EMBEDDED_SHARE_URL:
  BASEROW_DISABLE_PUBLIC_URL_CHECK:
  INITIAL_TABLE_DATA_LIMIT:
  DOWNLOAD_FILE_VIA_XHR:
  BASEROW_DISABLE_GOOGLE_DOCS_FILE_PREVIEW:
  BASEROW_DISABLE_SUPPORT:
  BASEROW_EXTRA_CLIENT_SCRIPT_URLS:
  HOURS_UNTIL_TRASH_PERMANENTLY_DELETED:
  DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS:
  FEATURE_FLAGS:
  BASEROW_PRESENCE_VISIBLE_USERS:
  ADDITIONAL_MODULES:
  BASEROW_MAX_IMPORT_FILE_SIZE_MB:
  BASEROW_MAX_SNAPSHOTS_PER_GROUP:
  BASEROW_ENABLE_OTEL:
  BASEROW_DEPLOYMENT_ENV:
  BASEROW_OSS_ONLY:
  BASEROW_USE_PG_FULLTEXT_SEARCH:
  BASEROW_PG_FULLTEXT_SEARCH_UPDATE_DATA_THROTTLE_SECONDS:
  POSTHOG_PROJECT_API_KEY:
  POSTHOG_HOST:
  BASEROW_UNIQUE_ROW_VALUES_SIZE_LIMIT:
  BASEROW_MAX_FIELD_TEXT_LENGTH:
  BASEROW_ROW_PAGE_SIZE_LIMIT:
  BASEROW_DEPENDANT_ROWS_REALTIME_UPDATE_LIMIT:
  BASEROW_INTEGRATION_LOCAL_BASEROW_PAGE_SIZE_LIMIT:
  BASEROW_REALTIME_REPLAY_MAX_EVENTS:
  BASEROW_FORMULA_RANGE_MAX_ITEMS:
  BASEROW_INTEGRATION_LOCAL_BASEROW_BATCH_OPERATION_SIZE_LIMIT:
  BASEROW_INTEGRATION_ALLOW_SMTP_SERVICE_TO_USE_INSTANCE_SETTINGS:
  BASEROW_BUILDER_DOMAINS:
  BASEROW_FRONTEND_SAME_SITE_COOKIE:
  BASEROW_FRONTEND_COOKIE_PREFIX:
  SENTRY_DSN:
  SENTRY_TRACES_SAMPLE_RATE:
  SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE:
  BASEROW_PREMIUM_GROUPED_AGGREGATE_SERVICE_MAX_SERIES:
  BASEROW_PREMIUM_GROUPED_AGGREGATE_SERVICE_MAX_AGG_BUCKETS:
  BASEROW_AUTOMATION_HISTORY_PAGE_SIZE_LIMIT:
  BASEROW_AUTOMATION_WORKFLOW_RATE_LIMIT_MAX_RUNS:
  BASEROW_AUTOMATION_WORKFLOW_RATE_LIMITS:
  BASEROW_AUTOMATION_WORKFLOW_RATE_LIMIT_CACHE_EXPIRY_SECONDS:
  BASEROW_AUTOMATION_WORKFLOW_HISTORY_RATE_LIMIT_CACHE_EXPIRY_SECONDS:
  BASEROW_AUTOMATION_WORKFLOW_ERROR_LIMITS:
  BASEROW_AUTOMATION_WORKFLOW_MAX_CONSECUTIVE_ERRORS:
  BASEROW_AUTOMATION_WORKFLOW_TIMEOUT_HOURS:
  BASEROW_AUTOMATION_WORKFLOW_HISTORY_MAX_DAYS:
  BASEROW_AUTOMATION_WORKFLOW_HISTORY_MAX_ENTRIES:
  BASEROW_AUTOMATION_WORKFLOW_HISTORY_MIN_RETENTION_DAYS:
  BASEROW_AUTOMATION_WORKFLOW_HISTORY_CLEANUP_INTERVAL_MINUTES:
  BASEROW_INTEGRATIONS_PERIODIC_MINUTE_MIN:
  BASEROW_ENTERPRISE_CODE_RUNNER_DEFAULT_TYPE:
  BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL:

services:
  baserow-media-permissions:
    image: bash:4.4
    restart: "no"
    exclude_from_hc: true
    command: ["chown", "9999:9999", "-R", "/baserow/media"]
    volumes:
      - baserow-media:/baserow/media

  baserow-db:
    image: pgvector/pgvector:0.8.1-pg15
    restart: unless-stopped
    environment:
      POSTGRES_DB: baserow
      POSTGRES_USER: baserow
      POSTGRES_PASSWORD: ${SERVICE_PASSWORD_64_BASEROWDB:?Coolify must generate SERVICE_PASSWORD_64_BASEROWDB}
    volumes:
      - baserow-postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 20
      start_period: 10s

  baserow-redis:
    image: redis:6.2.23-alpine
    restart: unless-stopped
    environment:
      REDIS_PASSWORD: ${SERVICE_PASSWORD_64_BASEROWREDIS:?Coolify must generate SERVICE_PASSWORD_64_BASEROWREDIS}
      REDISCLI_AUTH: ${SERVICE_PASSWORD_64_BASEROWREDIS:?Coolify must generate SERVICE_PASSWORD_64_BASEROWREDIS}
    command: ["redis-server", "--requirepass", "${SERVICE_PASSWORD_64_BASEROWREDIS:?Coolify must generate SERVICE_PASSWORD_64_BASEROWREDIS}"]
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep -q PONG"]
      interval: 10s
      timeout: 5s
      retries: 20
      start_period: 5s

  baserow-backend:
    image: baserow/backend:2.3.3
    restart: unless-stopped
    environment:
      <<: *baserow-backend-environment
    command:
      - bash
      - |
          set -euo pipefail
          echo "[COOLIFY-STARTUP] Waiting for PostgreSQL using Baserow's native probe."
          /baserow/backend/docker/docker-entrypoint.sh wait_for_db

          echo "[COOLIFY-STARTUP] Applying Baserow native locked migrations before public runtime."
          /baserow/backend/docker/docker-entrypoint.sh manage locked_migrate

          echo "[COOLIFY-STARTUP] Materializing the native singleton password provider before concurrent requests."
          /baserow/backend/docker/docker-entrypoint.sh manage shell -c '
          from baserow.core.auth_provider.handler import PasswordProviderHandler
          from baserow.core.auth_provider.models import PasswordAuthProviderModel

          providers = list(PasswordAuthProviderModel.objects.order_by("pk"))
          if len(providers) == 0:
              provider = PasswordProviderHandler.get()
              print(f"Created password auth provider id={provider.pk} before Gunicorn startup.")
          elif len(providers) == 1:
              print(f"Password auth provider id={providers[0].pk} already valid.")
          else:
              raise RuntimeError(
                  f"Expected one password auth provider before public runtime, found {len(providers)}. "
                  "Refusing automatic deletion; manual review required."
              )
          '

          echo "[COOLIFY-STARTUP] Starting Baserow ASGI backend."
          exec /baserow/backend/docker/docker-entrypoint.sh gunicorn
    expose:
      - "8000"
    volumes:
      - baserow-media:/baserow/media
    depends_on:
      baserow-db:
        condition: service_healthy
      baserow-redis:
        condition: service_healthy
      baserow-media-permissions:
        condition: service_completed_successfully
    healthcheck:
      test: ["CMD-SHELL", "/baserow/backend/docker/docker-entrypoint.sh backend-healthcheck"]
      interval: 15s
      timeout: 10s
      retries: 20
      start_period: 15m

  baserow-web-frontend:
    image: baserow/web-frontend:2.3.3
    restart: unless-stopped
    environment:
      <<: *baserow-frontend-environment
    expose:
      - "3000"
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O /dev/null http://localhost:3000/_health/ || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 30
      start_period: 20s

  baserow-celery:
    image: baserow/backend:2.3.3
    restart: unless-stopped
    environment:
      <<: *baserow-backend-environment
    command:
      - bash
      - |
          set -e
          echo "[CELERY] Waiting for the migrated Baserow backend."
          until curl -fsS http://baserow-backend:8000/api/_health/ >/dev/null; do sleep 3; done
          exec /baserow/backend/docker/docker-entrypoint.sh celery-worker
    volumes:
      - baserow-media:/baserow/media
    healthcheck:
      test: ["CMD-SHELL", "/baserow/backend/docker/docker-entrypoint.sh celery-worker-healthcheck"]
      interval: 30s
      timeout: 10s
      retries: 10
      start_period: 2m

  baserow-celery-export:
    image: baserow/backend:2.3.3
    restart: unless-stopped
    environment:
      <<: *baserow-backend-environment
    command:
      - bash
      - |
          set -e
          echo "[EXPORT-WORKER] Waiting for the migrated Baserow backend."
          until curl -fsS http://baserow-backend:8000/api/_health/ >/dev/null; do sleep 3; done
          exec /baserow/backend/docker/docker-entrypoint.sh celery-exportworker
    volumes:
      - baserow-media:/baserow/media
    healthcheck:
      test: ["CMD-SHELL", "/baserow/backend/docker/docker-entrypoint.sh celery-exportworker-healthcheck"]
      interval: 30s
      timeout: 10s
      retries: 10
      start_period: 2m

  baserow-celery-beat:
    image: baserow/backend:2.3.3
    restart: unless-stopped
    environment:
      <<: *baserow-backend-environment
    command:
      - bash
      - |
          set -e
          echo "[BEAT] Waiting for the migrated Baserow backend."
          until curl -fsS http://baserow-backend:8000/api/_health/ >/dev/null; do sleep 3; done
          exec /baserow/backend/docker/docker-entrypoint.sh celery-beat
    stop_signal: SIGQUIT
    volumes:
      - baserow-media:/baserow/media
    healthcheck:
      test: ["CMD-SHELL", "/baserow/backend/docker/docker-entrypoint.sh celery-beat-healthcheck"]
      interval: 30s
      timeout: 10s
      retries: 10
      start_period: 2m

  baserow:
    image: caddy:2.11.4
    restart: unless-stopped
    environment:
      BASEROW_CADDY_ADDRESSES: ":80"
      SERVICE_URL_BASEROW:
      BASEROW_PUBLIC_URL: ${SERVICE_URL_BASEROW:?Coolify must generate SERVICE_URL_BASEROW for the public baserow gateway}
      BASEROW_EXTRA_PUBLIC_URLS:
      PRIVATE_BACKEND_URL: http://baserow-backend:8000
      PRIVATE_WEB_FRONTEND_URL: http://baserow-web-frontend:3000
      MEDIA_URL:
      MEDIA_ROOT: /baserow/media
      STATIC_ROOT:
      BASEROW_CADDY_GLOBAL_CONF:
      BASEROW_CADDY_BACKEND_EXTRA_ROUTES:
    expose:
      - "80"
    volumes:
      - baserow-media:/baserow/media:ro
      - type: bind
        source: ./.coolify/baserow/Caddyfile
        target: /etc/caddy/Caddyfile
        read_only: true
        is_directory: false
        content: |
          {
              # Coolify owns public HTTPS/TLS. Caddy remains only the Baserow semantic gateway.
              auto_https off
              {$BASEROW_CADDY_GLOBAL_CONF}
          }

          (baserow_media_files) {
              handle_path /media/* {
                  @downloads {
                      query dl=*
                  }
                  header @downloads Content-Disposition "attachment; filename={query.dl}"
                  header X-Content-Type-Options "nosniff"
                  header Content-Security-Policy "sandbox; default-src 'none'; script-src 'none'; object-src 'none'; base-uri 'none'"
                  header {
                      Access-Control-Allow-Origin {$BASEROW_PUBLIC_URL}
                      Access-Control-Allow-Methods "GET, HEAD, OPTIONS"
                      Access-Control-Allow-Headers "*"
                      Access-Control-Expose-Headers "Content-Length, Content-Type"
                  }
                  file_server {
                      root {$MEDIA_ROOT:/baserow/media/}
                  }
              }
          }

          {$BASEROW_CADDY_ADDRESSES} {
              # Local-only liveness endpoint for the Coolify/Docker healthcheck.
              # It must bypass Baserow's host-sensitive routing: probing `/` with
              # Host=127.0.0.1 makes the web frontend interpret 127.0.0.1 as a
              # Builder published-site hostname and return `Site not found`.
              @coolify_gateway_health path /__coolify_gateway_health
              respond @coolify_gateway_health "ok" 200

              @is_baserow_tool {
                  expression `
                      "{$BASEROW_PUBLIC_URL}".contains({http.request.host}) ||
                      "{$BASEROW_EXTRA_PUBLIC_URLS}".split(",")
                          .filter(u, u != "" && u.contains({http.request.host}))
                          .size() > 0
                  `
              }

              @is_baserow_media {
                  path /media/*
                  expression `
                      "{$MEDIA_URL:}".startsWith("http://" + {http.request.host} + "/") ||
                      "{$MEDIA_URL:}".startsWith("https://" + {http.request.host} + "/") ||
                      "{$MEDIA_URL:}".startsWith("http://" + {http.request.host} + ":") ||
                      "{$MEDIA_URL:}".startsWith("https://" + {http.request.host} + ":")
                  `
              }

              handle @is_baserow_media {
                  import baserow_media_files
              }

              handle @is_baserow_tool {
                  @backend_routes path /api/* /ws/* /mcp/* /assistant/* {$BASEROW_CADDY_BACKEND_EXTRA_ROUTES:}
                  handle @backend_routes {
                      reverse_proxy {$PRIVATE_BACKEND_URL} {
                          # The only ingress to this Caddy service is Coolify's HTTPS edge.
                          header_up Host {http.request.host}
                          header_up X-Forwarded-Host {http.request.host}
                          header_up X-Forwarded-Proto https
                      }
                  }

                  import baserow_media_files

                  handle_path /static/* {
                      file_server {
                          root {$STATIC_ROOT:/baserow/static/}
                      }
                  }
              }

              reverse_proxy {$PRIVATE_WEB_FRONTEND_URL} {
                  header_up Host {http.request.host}
                  header_up X-Forwarded-Host {http.request.host}
                  header_up X-Forwarded-Proto https
              }
          }
    depends_on:
      baserow-backend:
        condition: service_healthy
      baserow-web-frontend:
        condition: service_healthy
    healthcheck:
      # Check Caddy itself plus both public-facing upstreams WITHOUT using `/` on
      # 127.0.0.1. The root route is intentionally host-sensitive in Baserow.
      test:
        - CMD-SHELL
        - >-
          wget -q -O /dev/null http://127.0.0.1/__coolify_gateway_health &&
          wget -q -O /dev/null http://baserow-backend:8000/api/_health/ &&
          wget -q -O /dev/null http://baserow-web-frontend:3000/_health/
      interval: 10s
      timeout: 10s
      retries: 30
      start_period: 60s

volumes:
  baserow-postgres:
  baserow-media:
````

<!-- END PORTABLE RESOURCE: assets/baserow-2.3.3-v1.0.0-golden.yml -->

<!-- BEGIN PORTABLE RESOURCE: assets/ckan-v1.0.8-golden.yml -->
<!-- SOURCE SHA256: 5495852f0a4dc45670a74de7faf73dab6a9cec40f7705639d324483beeaff3d9 -->
<!-- EMBEDDED SHA256: 85067c5406f208fa3dc73e3f30931d613ccd86c745c71a23b8692c32f16e1677 -->

## Portable resource: `assets/ckan-v1.0.8-golden.yml`

````yaml
# documentation: https://docs.ckan.org/en/2.12/
# slogan: CKAN open data management system
# category: data
# tags: ckan, open-data, catalog, datastore, solr
# port: 5000
# Coolify template revision: 1.0.8
# Runtime-tested CKAN 2.12 / Coolify compose. Upgrade in place from the validated fresh-resource deployment.

services:
  ckan:
    image: ckan/ckan-base:2.12.0
    platform: linux/amd64
    depends_on:
      db:
        condition: service_healthy
      solr:
        condition: service_healthy
      redis:
        condition: service_healthy
      datapusher:
        condition: service_healthy
    environment:
      - SERVICE_URL_CKAN_5000
      - SERVICE_FQDN_CKAN
      - CKAN_SITE_URL=https://${SERVICE_FQDN_CKAN}
      - CKAN_SITE_ID=${CKAN_SITE_ID:-default}
      - CKAN_SQLALCHEMY_URL=postgresql://ckan:${SERVICE_PASSWORD_64_CKANDB:?Coolify must generate SERVICE_PASSWORD_64_CKANDB}@db/ckan
      - CKAN_DATASTORE_WRITE_URL=postgresql://ckan:${SERVICE_PASSWORD_64_CKANDB:?Coolify must generate SERVICE_PASSWORD_64_CKANDB}@db/datastore
      - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${SERVICE_PASSWORD_64_DATASTORERO:?Coolify must generate SERVICE_PASSWORD_64_DATASTORERO}@db/datastore
      - CKAN_SOLR_URL=http://solr:8983/solr/ckan
      - CKAN_REDIS_URL=redis://redis:6379/1
      - CKAN_DATAPUSHER_URL=http://datapusher:8800
      - CKAN__DATAPUSHER__CALLBACK_URL_BASE=http://ckan:5000
      - CKAN_STORAGE_PATH=/var/lib/ckan
      - CKAN__PLUGINS=image_view text_view datatables_view datastore datapusher envvars
      - CKAN___SECRET_KEY=${SERVICE_HEX_64_CKANSECRET:?Coolify must generate SERVICE_HEX_64_CKANSECRET}
      - CKAN_SYSADMIN_NAME=ckan_admin
      - CKAN_SYSADMIN_PASSWORD=${SERVICE_PASSWORD_64_CKANSYSADMIN:?Coolify must generate SERVICE_PASSWORD_64_CKANSYSADMIN}
      - CKAN_SYSADMIN_EMAIL=${CKAN_SYSADMIN_EMAIL:?Set CKAN_SYSADMIN_EMAIL before deploying}
      - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB:-100}
      - EXTRA_UWSGI_OPTS=${EXTRA_UWSGI_OPTS:-}
      - CKAN_SMTP_SERVER=${CKAN_SMTP_SERVER:-}
      - CKAN_SMTP_STARTTLS=${CKAN_SMTP_STARTTLS:-True}
      - CKAN_SMTP_USER=${CKAN_SMTP_USER:-}
      - CKAN_SMTP_PASSWORD=${CKAN_SMTP_PASSWORD:-}
      - CKAN_SMTP_MAIL_FROM=${CKAN_SMTP_MAIL_FROM:-}
      - TZ=${TZ:-UTC}
    expose:
      - "5000"
    volumes:
      - ckan_storage:/var/lib/ckan
      - type: bind
        source: ./01_setup_datapusher.sh
        target: /docker-entrypoint.d/01_setup_datapusher.sh
        is_directory: false
        content: |
          #!/bin/bash
          # This file is sourced by /srv/app/start_ckan.sh. Do not use
          # `set -u`, `set -euo pipefail`, or other global shell-option changes
          # here because they would leak into the upstream parent shell.

          if [[ "${CKAN__PLUGINS:-}" == *"datapusher"* ]]; then
              token_file="${CKAN_STORAGE_PATH}/.datapusher_api_token"

              if [[ -n "${CKAN__DATAPUSHER__API_TOKEN:-}" ]]; then
                  token="${CKAN__DATAPUSHER__API_TOKEN}"
              else
                  # Match upstream ckan-docker behavior: create a fresh token on
                  # CKAN startup. Persist a copy only so the worker container can
                  # load the same token before starting its RQ worker.
                  token="$(ckan -c "${CKAN_INI}" user token add "${CKAN_SYSADMIN_NAME:-ckan_admin}" datapusher | tail -n 1 | tr -d '\t\r\n')"
              fi

              if [[ -z "${token:-}" ]]; then
                  echo "Failed to obtain CKAN DataPusher API token" >&2
                  return 1
              fi

              umask 077
              printf '%s' "${token}" > "${token_file}"
              ckan config-tool "${CKAN_INI}" "ckan.datapusher.api_token=${token}"
          fi
    restart: unless-stopped
    stop_grace_period: 30s
    # A fresh CKAN instance can spend several minutes running database
    # migrations, DataStore permissions and bootstrap hooks before uWSGI
    # begins serving port 5000. Keep the health gate strict, but give the
    # first boot enough time so Compose does not abort while CKAN is healthying.
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:5000/api/action/status_show >/dev/null"]
      interval: 30s
      timeout: 10s
      retries: 10
      start_period: 300s
    logging:
      options:
        max-size: "20m"
        max-file: "5"

  ckan-worker:
    image: ckan/ckan-base:2.12.0
    platform: linux/amd64
    # Do not gate worker creation on CKAN health. A first CKAN boot can run
    # migrations/bootstrap for minutes and Coolify/Docker Compose may abort the
    # dependent start before CKAN later becomes healthy. Start the container in
    # dependency order, then let the worker perform its own readiness wait.
    depends_on:
      ckan:
        condition: service_started
      redis:
        condition: service_healthy
    environment:
      - CKAN_SITE_URL=https://${SERVICE_FQDN_CKAN}
      - CKAN_SITE_ID=${CKAN_SITE_ID:-default}
      - CKAN_SQLALCHEMY_URL=postgresql://ckan:${SERVICE_PASSWORD_64_CKANDB:?Coolify must generate SERVICE_PASSWORD_64_CKANDB}@db/ckan
      - CKAN_DATASTORE_WRITE_URL=postgresql://ckan:${SERVICE_PASSWORD_64_CKANDB:?Coolify must generate SERVICE_PASSWORD_64_CKANDB}@db/datastore
      - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${SERVICE_PASSWORD_64_DATASTORERO:?Coolify must generate SERVICE_PASSWORD_64_DATASTORERO}@db/datastore
      - CKAN_SOLR_URL=http://solr:8983/solr/ckan
      - CKAN_REDIS_URL=redis://redis:6379/1
      - CKAN_DATAPUSHER_URL=http://datapusher:8800
      - CKAN__DATAPUSHER__CALLBACK_URL_BASE=http://ckan:5000
      - CKAN_STORAGE_PATH=/var/lib/ckan
      - CKAN__PLUGINS=image_view text_view datatables_view datastore datapusher envvars
      - CKAN___SECRET_KEY=${SERVICE_HEX_64_CKANSECRET:?Coolify must generate SERVICE_HEX_64_CKANSECRET}
      - TZ=${TZ:-UTC}
    volumes:
      - ckan_storage:/var/lib/ckan
    command:
      - /bin/bash
      - -ec
      - |
        echo "[worker] Waiting for CKAN API readiness..."
        until curl -fsS http://ckan:5000/api/action/status_show >/dev/null 2>&1; do
            sleep 5
        done

        token_file="$${CKAN_STORAGE_PATH}/.datapusher_api_token"
        attempts=0
        until [[ -s "$${token_file}" ]]; do
            attempts=$$((attempts + 1))
            if [[ "$${attempts}" -ge 60 ]]; then
                echo "[worker] DataPusher token file was not created after 120 seconds" >&2
                exit 1
            fi
            sleep 2
        done

        token="$$(cat "$${token_file}")"
        ckan config-tool "$${CKAN_INI}" "ckan.datapusher.api_token=$${token}"
        echo "[worker] CKAN ready; starting CKAN background worker..."
        # Readiness marker avoids brittle /proc/cmdline matching. After this
        # point the shell is replaced by the long-running CKAN RQ worker.
        touch /tmp/ckan-worker-ready
        exec ckan -c "$${CKAN_INI}" jobs worker
    restart: unless-stopped
    stop_grace_period: 60s
    healthcheck:
      # The dedicated container has only one long-running purpose. The marker
      # is created only after CKAN API readiness and DataPusher token setup,
      # immediately before `exec ckan ... jobs worker`. If that PID exits, the
      # container exits and restart: unless-stopped handles recovery.
      test: ["CMD-SHELL", "test -f /tmp/ckan-worker-ready && kill -0 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 600s
    logging:
      options:
        max-size: "20m"
        max-file: "5"

  datapusher:
    image: ckan/ckan-base-datapusher:0.0.21@sha256:84d11924549f44bcc1419256811d156893d837f90885b24f81f1753733b9d6ef
    platform: linux/amd64
    expose:
      - "8800"
    restart: unless-stopped
    stop_grace_period: 30s
    healthcheck:
      test: ["CMD", "wget", "-qO", "/dev/null", "http://127.0.0.1:8800"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 20s
    logging:
      options:
        max-size: "20m"
        max-file: "5"

  db:
    image: postgres:16.14-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=${SERVICE_PASSWORD_64_POSTGRES:?Coolify must generate SERVICE_PASSWORD_64_POSTGRES}
      - POSTGRES_DB=postgres
      - CKAN_DB_USER=ckan
      - CKAN_DB_PASSWORD=${SERVICE_PASSWORD_64_CKANDB:?Coolify must generate SERVICE_PASSWORD_64_CKANDB}
      - CKAN_DB=ckan
      - DATASTORE_READONLY_USER=datastore_ro
      - DATASTORE_READONLY_PASSWORD=${SERVICE_PASSWORD_64_DATASTORERO:?Coolify must generate SERVICE_PASSWORD_64_DATASTORERO}
      - DATASTORE_DB=datastore
    expose:
      - "5432"
    volumes:
      - ckan_pg_data:/var/lib/postgresql/data
      - type: bind
        source: ./10_create_ckandb.sh
        target: /docker-entrypoint-initdb.d/10_create_ckandb.sh
        is_directory: false
        content: |
          #!/bin/bash
          set -e

          psql -v ON_ERROR_STOP=1 --username "${POSTGRES_USER}" <<-EOSQL
              CREATE ROLE "${CKAN_DB_USER}" NOSUPERUSER CREATEDB CREATEROLE LOGIN PASSWORD '${CKAN_DB_PASSWORD}';
              CREATE DATABASE "${CKAN_DB}" OWNER "${CKAN_DB_USER}" ENCODING 'utf-8';
          EOSQL
      - type: bind
        source: ./20_create_datastore.sh
        target: /docker-entrypoint-initdb.d/20_create_datastore.sh
        is_directory: false
        content: |
          #!/bin/bash
          set -e

          psql -v ON_ERROR_STOP=1 --username "${POSTGRES_USER}" <<-EOSQL
              CREATE ROLE "${DATASTORE_READONLY_USER}" NOSUPERUSER NOCREATEDB NOCREATEROLE LOGIN PASSWORD '${DATASTORE_READONLY_PASSWORD}';
              CREATE DATABASE "${DATASTORE_DB}" OWNER "${CKAN_DB_USER}" ENCODING 'utf-8';
          EOSQL
    restart: unless-stopped
    stop_grace_period: 60s
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 10s
    logging:
      options:
        max-size: "20m"
        max-file: "5"

  solr:
    image: ckan/ckan-solr:2.12-solr9.9@sha256:c8208fdf5effd2bf692169d73cc53a1bdfe07988412810ace26cdb7a7b01373f
    platform: linux/amd64
    expose:
      - "8983"
    volumes:
      - solr_data:/var/solr
    restart: unless-stopped
    stop_grace_period: 30s
    healthcheck:
      test: ["CMD", "wget", "-qO", "/dev/null", "http://127.0.0.1:8983/solr/"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
    logging:
      options:
        max-size: "20m"
        max-file: "5"

  redis:
    image: redis:7.2.16-alpine
    command: ["redis-server", "--appendonly", "yes", "--appendfsync", "everysec"]
    expose:
      - "6379"
    volumes:
      - redis_data:/data
    restart: unless-stopped
    stop_grace_period: 30s
    healthcheck:
      test: ["CMD", "redis-cli", "PING"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 5s
    logging:
      options:
        max-size: "20m"
        max-file: "5"

volumes:
  ckan_storage:
  ckan_pg_data:
  solr_data:
  redis_data:
````

<!-- END PORTABLE RESOURCE: assets/ckan-v1.0.8-golden.yml -->

<!-- BEGIN PORTABLE RESOURCE: assets/erpnext-v16.33.0-v1.0.0-golden.yml -->
<!-- SOURCE SHA256: 64660809aba082409a41e20006d0d24dbc913928a30f07590ba873171ee2a7cb -->
<!-- EMBEDDED SHA256: e11d6b48ea970ae42c2ab8ed2332bda51c8b0692eea0873c9b242aa8a34c4f76 -->

## Portable resource: `assets/erpnext-v16.33.0-v1.0.0-golden.yml`

````yaml
# ERPNext Coolify V1.0.0-RC5 candidate
#
# Adaptation provenance:
# - Runtime topology follows the current frappe_docker production Compose files.
# - ERPNext is the installed site app; there is no second ERPNext backend.
# - Coolify owns the public proxy/TLS route. The semantic Frappe Nginx frontend remains.
# - No host ports, public database, public Redis, Docker socket, or second ACME stack.

x-erpnext-image: &erpnext_image
  image: frappe/erpnext:v16.33.0@sha256:493cecf82c92c828bf0d0c57df60694e07dc61671e374ac93a070d1cc86df1bd
  pull_policy: always
  platform: linux/amd64

services:
  db:
    image: mariadb:11.8.9@sha256:2439dcd7d14010ecd1ff7a4e1c5abe8e208c34fe35290744deeeaac3569043c3
    restart: unless-stopped
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      - --skip-character-set-client-handshake
    environment:
      MYSQL_ROOT_PASSWORD: ${SERVICE_PASSWORD_64_ERPNEXTDBROOT:?Coolify must generate SERVICE_PASSWORD_64_ERPNEXTDBROOT}
      MARIADB_AUTO_UPGRADE: "1"
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      start_period: 5s
      interval: 5s
      timeout: 5s
      retries: 5
    volumes:
      - db-data:/var/lib/mysql

  redis-cache:
    image: redis:8.6.6-alpine@sha256:75934ddb37bfaebe3b4082ba673cac39f66495244134f33dd0a502ce03cdcd36
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 10

  redis-queue:
    image: redis:8.6.6-alpine@sha256:75934ddb37bfaebe3b4082ba673cac39f66495244134f33dd0a502ce03cdcd36
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 10
    volumes:
      - redis-queue-data:/data

  configurator:
    <<: *erpnext_image
    restart: on-failure:5
    exclude_from_hc: true
    entrypoint:
      - bash
      - -c
    command:
      - >-
        set -eu;
        ls -1 apps > sites/apps.txt;
        bench set-config -g db_host "$${DB_HOST}";
        bench set-config -gp db_port "$${DB_PORT}";
        bench set-config -g redis_cache "redis://$${REDIS_CACHE}";
        bench set-config -g redis_queue "redis://$${REDIS_QUEUE}";
        bench set-config -g redis_socketio "redis://$${REDIS_QUEUE}";
        bench set-config -gp socketio_port "$${SOCKETIO_PORT}";
        bench set-config -g chromium_path /usr/bin/chromium-headless-shell;
    environment:
      DB_HOST: db
      DB_PORT: "3306"
      REDIS_CACHE: redis-cache:6379
      REDIS_QUEUE: redis-queue:6379
      SOCKETIO_PORT: "9000"
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    depends_on:
      db:
        condition: service_healthy
      redis-cache:
        condition: service_healthy
      redis-queue:
        condition: service_healthy

  site-bootstrap:
    <<: *erpnext_image
    restart: "no"
    exclude_from_hc: true
    entrypoint:
      - bash
      - -c
    command:
      - >-
        set -euo pipefail;
        : "$${SITE:?SITE is required}";
        : "$${FRAPPE_PUBLIC_URL:?FRAPPE_PUBLIC_URL is required}";
        : "$${FRAPPE_PUBLIC_FQDN:?FRAPPE_PUBLIC_FQDN is required}";
        : "$${DB_ROOT_PASSWORD:?DB_ROOT_PASSWORD is required}";
        : "$${ADMIN_USERNAME:?ADMIN_USERNAME is required}";
        : "$${ADMIN_PASSWORD:?ADMIN_PASSWORD is required}";
        if [[ "$${ADMIN_USERNAME}" != "Administrator" ]]; then
          echo "[site-bootstrap] ERPNext/Frappe bootstrap username is fixed to Administrator; set ERPNEXT_ADMIN_USERNAME=Administrator" >&2;
          exit 1;
        fi;
        if [[ "$${FRAPPE_PUBLIC_URL}" != "https://$${FRAPPE_PUBLIC_FQDN}" ]]; then
          echo "[site-bootstrap] expected SERVICE_URL_FRONTEND to be https://SERVICE_FQDN_FRONTEND for this HTTPS root-domain profile" >&2;
          echo "[site-bootstrap] URL=$${FRAPPE_PUBLIC_URL} FQDN=$${FRAPPE_PUBLIC_FQDN}" >&2;
          exit 1;
        fi;
        start="$$(date +%s)";
        until [[ -n "$$(grep -hs ^ sites/common_site_config.json | jq -r '.db_host // empty')" ]] &&
          [[ -n "$$(grep -hs ^ sites/common_site_config.json | jq -r '.redis_cache // empty')" ]] &&
          [[ -n "$$(grep -hs ^ sites/common_site_config.json | jq -r '.redis_queue // empty')" ]];
        do
          echo "[site-bootstrap] waiting for configured Frappe services";
          sleep 5;
          if (( $$(date +%s) - start > 600 )); then
            echo "[site-bootstrap] common_site_config.json did not become ready" >&2;
            exit 1;
          fi;
        done;
        if [[ -e "sites/$${SITE}" && ! -e "sites/$${SITE}/site_config.json" ]]; then
          echo "[site-bootstrap] partial site directory exists: $${SITE}; refusing automatic repair" >&2;
          exit 1;
        fi;
        if [[ ! -e "sites/$${SITE}/site_config.json" ]]; then
          other_site="$$(find sites -mindepth 2 -maxdepth 2 -name site_config.json -print 2>/dev/null | head -n 1)";
          if [[ -n "$${other_site}" ]]; then
            other_site="$${other_site#sites/}";
            other_site="$${other_site%/site_config.json}";
          fi;
          if [[ -n "$${other_site}" ]]; then
            echo "[site-bootstrap] another site already exists ($${other_site}); this RC2 contract permits one initial site" >&2;
            exit 1;
          fi;
          echo "[site-bootstrap] creating ERPNext site $${SITE}";
          bench new-site "$${SITE}" --mariadb-user-host-login-scope='%' --admin-password "$${ADMIN_PASSWORD}" --db-root-username root --db-root-password "$${DB_ROOT_PASSWORD}" --install-app erpnext --set-default;
        else
          echo "[site-bootstrap] site exists; inspecting installed apps";
          if ! app_list="$$(bench --site "$${SITE}" list-apps 2>&1)"; then
            echo "$${app_list}" >&2;
            echo "[site-bootstrap] list-apps failed; refusing to guess whether ERPNext installation is partial" >&2;
            exit 1;
          fi;
          if printf '%s\n' "$${app_list}" | awk '$$1 == "erpnext" { found=1 } END { exit(found ? 0 : 1) }'; then
            echo "[site-bootstrap] ERPNext already installed; safe reconciliation only";
          elif [[ "$${ALLOW_EXISTING_FRAPPE_SITE_CONVERSION}" == "true" ]]; then
            echo "[site-bootstrap] explicit conversion enabled; installing ERPNext into existing site";
            bench --site "$${SITE}" install-app erpnext;
          else
            echo "[site-bootstrap] site exists without ERPNext; set ALLOW_EXISTING_FRAPPE_SITE_CONVERSION=true only after review" >&2;
            exit 1;
          fi;
        fi;
        bench --site "$${SITE}" set-config host_name "$${FRAPPE_PUBLIC_URL}";
        bench --site "$${SITE}" enable-scheduler;
        final_apps="$$(bench --site "$${SITE}" list-apps)";
        printf '%s\n' "$${final_apps}";
        printf '%s\n' "$${final_apps}" | awk '$$1 == "frappe" { frappe=1 } $$1 == "erpnext" { erpnext=1 } END { exit(frappe && erpnext ? 0 : 1) }';
        echo "[site-bootstrap] ERPNext site verified: $${SITE}";
    environment:
      SITE: ${SERVICE_FQDN_FRONTEND:?Coolify must provide SERVICE_FQDN_FRONTEND}
      FRAPPE_PUBLIC_URL: ${SERVICE_URL_FRONTEND:?Coolify must provide SERVICE_URL_FRONTEND}
      FRAPPE_PUBLIC_FQDN: ${SERVICE_FQDN_FRONTEND:?Coolify must provide SERVICE_FQDN_FRONTEND}
      DB_ROOT_PASSWORD: ${SERVICE_PASSWORD_64_ERPNEXTDBROOT:?Coolify must generate SERVICE_PASSWORD_64_ERPNEXTDBROOT}
      ADMIN_USERNAME: ${ERPNEXT_ADMIN_USERNAME:-Administrator}
      ADMIN_PASSWORD: ${SERVICE_PASSWORD_64_ERPNEXTADMIN:?Coolify must generate SERVICE_PASSWORD_64_ERPNEXTADMIN}
      ALLOW_EXISTING_FRAPPE_SITE_CONVERSION: ${ALLOW_EXISTING_FRAPPE_SITE_CONVERSION:-false}
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    depends_on:
      configurator:
        condition: service_completed_successfully
      db:
        condition: service_healthy
      redis-cache:
        condition: service_healthy
      redis-queue:
        condition: service_healthy

  migrator:
    <<: *erpnext_image
    restart: on-failure:5
    exclude_from_hc: true
    entrypoint:
      - bash
      - -c
    command:
      - >-
        set -euo pipefail;
        if [[ "$${MIGRATE_SITES}" != "true" ]]; then
          echo "[migrator] MIGRATE_SITES is not true; skipping";
          exit 0;
        fi;
        if [[ -z "$$(find sites -mindepth 2 -maxdepth 2 -name site_config.json -print -quit 2>/dev/null)" ]]; then
          echo "[migrator] no sites found; skipping";
          exit 0;
        fi;
        echo "[migrator] migrating all installed Frappe and ERPNext sites";
        bench --site all migrate;
    environment:
      MIGRATE_SITES: ${MIGRATE_SITES:-true}
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    depends_on:
      site-bootstrap:
        condition: service_completed_successfully

  backend:
    <<: *erpnext_image
    restart: unless-stopped
    environment:
      GUNICORN_THREADS: ${GUNICORN_THREADS:-4}
      GUNICORN_WORKERS: ${GUNICORN_WORKERS:-2}
      GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-120}
      FRAPPE_SITE_NAME: ${SERVICE_FQDN_FRONTEND:?Coolify must provide SERVICE_FQDN_FRONTEND}
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS -H \"Host: $${FRAPPE_SITE_NAME}\" http://127.0.0.1:8000/api/method/ping | grep -q pong"]
      start_period: 30s
      interval: 10s
      timeout: 10s
      retries: 12
    depends_on:
      migrator:
        condition: service_completed_successfully

  websocket:
    <<: *erpnext_image
    restart: unless-stopped
    command:
      - node
      - /home/frappe/frappe-bench/apps/frappe/socketio.js
    environment:
      FRAPPE_PUBLIC_URL: ${SERVICE_URL_FRONTEND:?Coolify must provide SERVICE_URL_FRONTEND}
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS -H \"Origin: $${FRAPPE_PUBLIC_URL}\" 'http://127.0.0.1:9000/socket.io/?EIO=4&transport=polling' | grep -q 'sid'"]
      start_period: 30s
      interval: 10s
      timeout: 10s
      retries: 12
    depends_on:
      migrator:
        condition: service_completed_successfully

  queue-short:
    <<: *erpnext_image
    restart: unless-stopped
    command:
      - bench
      - worker
      - --queue
      - short,default
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    healthcheck:
      test:
        - CMD-SHELL
        - >-
          test -f /home/frappe/frappe-bench/sites/common_site_config.json &&
          tr '\0' ' ' < /proc/1/cmdline | grep -q 'worker' &&
          tr '\0' ' ' < /proc/1/cmdline | grep -q -- '--queue short,default' &&
          /home/frappe/frappe-bench/env/bin/python -c "import socket; s=socket.create_connection(('redis-queue',6379),3); s.close()"
      start_period: 30s
      interval: 30s
      timeout: 5s
      retries: 3
    depends_on:
      migrator:
        condition: service_completed_successfully

  queue-long:
    <<: *erpnext_image
    restart: unless-stopped
    command:
      - bench
      - worker
      - --queue
      - long,default,short
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    healthcheck:
      test:
        - CMD-SHELL
        - >-
          test -f /home/frappe/frappe-bench/sites/common_site_config.json &&
          tr '\0' ' ' < /proc/1/cmdline | grep -q 'worker' &&
          tr '\0' ' ' < /proc/1/cmdline | grep -q -- '--queue long,default,short' &&
          /home/frappe/frappe-bench/env/bin/python -c "import socket; s=socket.create_connection(('redis-queue',6379),3); s.close()"
      start_period: 30s
      interval: 30s
      timeout: 5s
      retries: 3
    depends_on:
      migrator:
        condition: service_completed_successfully

  scheduler:
    <<: *erpnext_image
    restart: unless-stopped
    command:
      - bench
      - schedule
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    healthcheck:
      test:
        - CMD-SHELL
        - >-
          test -f /home/frappe/frappe-bench/sites/common_site_config.json &&
          tr '\0' ' ' < /proc/1/cmdline | grep -q 'schedule' &&
          /home/frappe/frappe-bench/env/bin/python -c "import socket; s=socket.create_connection(('redis-queue',6379),3); s.close()"
      start_period: 30s
      interval: 30s
      timeout: 5s
      retries: 3
    depends_on:
      migrator:
        condition: service_completed_successfully

  frontend:
    <<: *erpnext_image
    restart: unless-stopped
    command:
      - nginx-entrypoint.sh
    environment:
      - SERVICE_URL_FRONTEND_8080
      - SERVICE_URL_FRONTEND
      - SERVICE_FQDN_FRONTEND
      - BACKEND=backend:8000
      - SOCKETIO=websocket:9000
      - FRAPPE_SITE_NAME=${SERVICE_FQDN_FRONTEND:?Coolify must provide SERVICE_FQDN_FRONTEND}
      - FRAPPE_SITE_NAME_HEADER=${FRAPPE_SITE_NAME_HEADER:-$$host}
      - UPSTREAM_REAL_IP_ADDRESS=${UPSTREAM_REAL_IP_ADDRESS:-127.0.0.1}
      - UPSTREAM_REAL_IP_HEADER=${UPSTREAM_REAL_IP_HEADER:-X-Forwarded-For}
      - UPSTREAM_REAL_IP_RECURSIVE=${UPSTREAM_REAL_IP_RECURSIVE:-off}
      - PROXY_READ_TIMEOUT=${PROXY_READ_TIMEOUT:-120}
      - CLIENT_MAX_BODY_SIZE=${CLIENT_MAX_BODY_SIZE:-50m}
    expose:
      - "8080"
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS -H \"Host: $${FRAPPE_SITE_NAME}\" http://127.0.0.1:8080/api/method/ping | grep -q pong"]
      start_period: 30s
      interval: 10s
      timeout: 10s
      retries: 12
    depends_on:
      backend:
        condition: service_healthy
      websocket:
        condition: service_healthy

volumes:
  sites:
  db-data:
  redis-queue-data:
````

<!-- END PORTABLE RESOURCE: assets/erpnext-v16.33.0-v1.0.0-golden.yml -->

<!-- BEGIN PORTABLE RESOURCE: assets/frappe-framework-v16.32.0-v1.0.0-golden.yml -->
<!-- SOURCE SHA256: 01a534d234516f3c5572d7c0b940cfa3cc6d43f5f58df6ef34487dfbf8b3838f -->
<!-- EMBEDDED SHA256: 647ca58ad8e1057609898d822e45cf872ee3f0d78e28f16b11e66b4b624e2a29 -->

## Portable resource: `assets/frappe-framework-v16.32.0-v1.0.0-golden.yml`

````yaml
# Frappe Framework regression fixture / golden case — NOT a generic Coolify skeleton.
# Runtime-accepted RC3 executable behavior preserved as Golden / Regression Case #6.
# Frappe Framework Coolify V1.0.0-RC3 accepted regression baseline.
# Upstream topology: frappe/frappe_docker main compose + MariaDB + Redis + migrator overrides.
# The pinned frappe/erpnext image contains both Frappe and ERPNext code; this template creates a Frappe-only site.

x-frappe-image: &frappe-image frappe/erpnext:v16.33.0@sha256:493cecf82c92c828bf0d0c57df60694e07dc61671e374ac93a070d1cc86df1bd
x-frappe-restart: &frappe-restart unless-stopped

services:
  db:
    image: mariadb:11.8.8@sha256:24e76fcec8c003a0362d0dd53f4806e7e79458d7fdeaf47437760e19496f5a9c
    restart: unless-stopped
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      - --skip-character-set-client-handshake
    environment:
      MYSQL_ROOT_PASSWORD: ${SERVICE_PASSWORD_64_FRAPPEDBROOT:?Coolify must generate SERVICE_PASSWORD_64_FRAPPEDBROOT}
      MARIADB_AUTO_UPGRADE: "1"
    volumes:
      - db-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      start_period: 20s
      interval: 10s
      timeout: 5s
      retries: 12

  redis-cache:
    image: redis:8.6.3-alpine@sha256:becdda6c7f4b3fb42e42fd7f120bbf5c54c4caaaf16f26da24e4563d2c1f0576
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      start_period: 5s
      interval: 10s
      timeout: 5s
      retries: 10

  redis-queue:
    image: redis:8.6.3-alpine@sha256:becdda6c7f4b3fb42e42fd7f120bbf5c54c4caaaf16f26da24e4563d2c1f0576
    restart: unless-stopped
    volumes:
      - redis-queue-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      start_period: 5s
      interval: 10s
      timeout: 5s
      retries: 10

  configurator:
    image: *frappe-image
    platform: linux/amd64
    entrypoint: ["bash", "-c"]
    command:
      - |
        set -e
        echo "[configurator] Writing current upstream common-site configuration"
        ls -1 apps > sites/apps.txt
        bench set-config -g db_host "$$DB_HOST"
        bench set-config -gp db_port "$$DB_PORT"
        bench set-config -g redis_cache "redis://$$REDIS_CACHE"
        bench set-config -g redis_queue "redis://$$REDIS_QUEUE"
        bench set-config -g redis_socketio "redis://$$REDIS_QUEUE"
        bench set-config -gp socketio_port "$$SOCKETIO_PORT"
        bench set-config -g chromium_path /usr/bin/chromium-headless-shell
        echo "[configurator] Complete"
    environment:
      DB_HOST: db
      DB_PORT: "3306"
      REDIS_CACHE: redis-cache:6379
      REDIS_QUEUE: redis-queue:6379
      SOCKETIO_PORT: "9000"
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    depends_on:
      db:
        condition: service_healthy
      redis-cache:
        condition: service_healthy
      redis-queue:
        condition: service_healthy
    restart: on-failure:5
    exclude_from_hc: true

  site-bootstrap:
    image: *frappe-image
    platform: linux/amd64
    entrypoint: ["bash", "-c"]
    command:
      - |
        set -euo pipefail

        : "$${FRAPPE_SITE_NAME:?FRAPPE_SITE_NAME is required}"
        : "$${FRAPPE_PUBLIC_URL:?FRAPPE_PUBLIC_URL is required}"
        : "$${FRAPPE_PUBLIC_FQDN:?FRAPPE_PUBLIC_FQDN is required}"
        : "$${DB_ROOT_PASSWORD:?DB_ROOT_PASSWORD is required}"
        : "$${ADMIN_PASSWORD:?ADMIN_PASSWORD is required}"

        if [ "$$FRAPPE_PUBLIC_URL" != "https://$$FRAPPE_PUBLIC_FQDN" ]; then
          echo "[site-bootstrap] Refusing bootstrap: expected canonical SERVICE_URL_FRONTEND to equal https://SERVICE_FQDN_FRONTEND for this root-domain HTTPS profile." >&2
          echo "[site-bootstrap] URL=$$FRAPPE_PUBLIC_URL FQDN=$$FRAPPE_PUBLIC_FQDN" >&2
          exit 1
        fi

        site="$$FRAPPE_SITE_NAME"
        echo "[site-bootstrap] Target site: $$site"
        echo "[site-bootstrap] Coolify public FQDN: $$FRAPPE_PUBLIC_FQDN"
        echo "[site-bootstrap] Coolify public URL: $$FRAPPE_PUBLIC_URL"

        until jq -e '.db_host and .db_port and .redis_cache and .redis_queue and .socketio_port' sites/common_site_config.json >/dev/null 2>&1; do
          echo "[site-bootstrap] Waiting for configurator output"
          sleep 2
        done

        if [ -f "sites/$$site/site_config.json" ]; then
          echo "[site-bootstrap] Existing site detected; creation skipped"
        else
          other_sites="$$(find sites -mindepth 2 -maxdepth 2 -name site_config.json -printf '%h\n' 2>/dev/null | sed 's#^sites/##' || true)"
          if [ -n "$$other_sites" ]; then
            echo "[site-bootstrap] Refusing to create a second implicit site. Existing site(s):" >&2
            printf '%s\n' "$$other_sites" >&2
            echo "[site-bootstrap] FRAPPE_SITE_NAME is persistent deployment identity; do not change it after initialization." >&2
            exit 1
          fi
          if [ -e "sites/$$site" ]; then
            echo "[site-bootstrap] Partial site directory exists without site_config.json: sites/$$site" >&2
            echo "[site-bootstrap] Refusing destructive or implicit recovery; inspect the failed bootstrap first." >&2
            exit 1
          fi

          echo "[site-bootstrap] Creating Frappe-only site"
          bench new-site "$$site" \
            --mariadb-user-host-login-scope='%' \
            --admin-password "$$ADMIN_PASSWORD" \
            --db-root-username root \
            --db-root-password "$$DB_ROOT_PASSWORD" \
            --set-default

          echo "[site-bootstrap] Setting canonical public URL from Coolify SERVICE_URL"
          bench --site "$$site" set-config host_name "$$FRAPPE_PUBLIC_URL"

          echo "[site-bootstrap] Enabling scheduler for newly created site"
          bench --site "$$site" enable-scheduler
          echo "[site-bootstrap] Site creation complete"
        fi

        current_host="$$(jq -r '.host_name // empty' "sites/$$site/site_config.json")"
        if [ "$$current_host" != "$$FRAPPE_PUBLIC_URL" ]; then
          echo "[site-bootstrap] Updating host_name to current Coolify public URL"
          bench --site "$$site" set-config host_name "$$FRAPPE_PUBLIC_URL"
        fi

        echo "[site-bootstrap] Installed applications:"
        bench --site "$$site" list-apps
        echo "[site-bootstrap] Scheduler status:"
        bench --site "$$site" scheduler status
        echo "[site-bootstrap] Complete"
    environment:
      FRAPPE_SITE_NAME: ${SERVICE_FQDN_FRONTEND:?Coolify must generate SERVICE_FQDN_FRONTEND}
      FRAPPE_PUBLIC_FQDN: ${SERVICE_FQDN_FRONTEND:?Coolify must generate SERVICE_FQDN_FRONTEND}
      FRAPPE_PUBLIC_URL: ${SERVICE_URL_FRONTEND:?Coolify must generate SERVICE_URL_FRONTEND}
      DB_ROOT_PASSWORD: ${SERVICE_PASSWORD_64_FRAPPEDBROOT:?Coolify must generate SERVICE_PASSWORD_64_FRAPPEDBROOT}
      ADMIN_PASSWORD: ${SERVICE_PASSWORD_64_FRAPPEADMIN:?Coolify must generate SERVICE_PASSWORD_64_FRAPPEADMIN}
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    depends_on:
      configurator:
        condition: service_completed_successfully
      db:
        condition: service_healthy
      redis-cache:
        condition: service_healthy
      redis-queue:
        condition: service_healthy
    restart: "no"
    exclude_from_hc: true

  migrator:
    image: *frappe-image
    platform: linux/amd64
    entrypoint: ["bash", "-c"]
    command:
      - |
        set -e
        if [ "$$MIGRATE_SITES" != "true" ]; then
          echo "[migrator] Migration disabled"
          exit 0
        fi
        if [ -z "$$(find sites -mindepth 2 -maxdepth 2 -name site_config.json -print -quit 2>/dev/null)" ]; then
          echo "[migrator] No sites found; skipping migration"
          exit 0
        fi
        echo "[migrator] Migrating all sites"
        bench --site all migrate
        echo "[migrator] Complete"
    environment:
      MIGRATE_SITES: ${MIGRATE_SITES:-true}
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    depends_on:
      site-bootstrap:
        condition: service_completed_successfully
    restart: on-failure:5
    exclude_from_hc: true

  backend:
    image: *frappe-image
    platform: linux/amd64
    restart: *frappe-restart
    environment:
      GUNICORN_THREADS: ${GUNICORN_THREADS:-4}
      GUNICORN_WORKERS: ${GUNICORN_WORKERS:-2}
      GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-120}
      FRAPPE_SITE_NAME: ${SERVICE_FQDN_FRONTEND:?Coolify must generate SERVICE_FQDN_FRONTEND}
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    depends_on:
      migrator:
        condition: service_completed_successfully
    healthcheck:
      test:
        - CMD-SHELL
        - >-
          curl -fsS -H "X-Frappe-Site-Name: $$FRAPPE_SITE_NAME"
          http://127.0.0.1:8000/api/method/ping | grep -q pong
      start_period: 90s
      interval: 15s
      timeout: 10s
      retries: 12

  websocket:
    image: *frappe-image
    platform: linux/amd64
    command: ["node", "/home/frappe/frappe-bench/apps/frappe/socketio.js"]
    restart: *frappe-restart
    environment:
      FRAPPE_PUBLIC_URL: ${SERVICE_URL_FRONTEND:?Coolify must generate SERVICE_URL_FRONTEND}
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    depends_on:
      migrator:
        condition: service_completed_successfully
    healthcheck:
      test:
        - CMD-SHELL
        - >-
          curl -fsS -H "Origin: $$FRAPPE_PUBLIC_URL"
          'http://127.0.0.1:9000/socket.io/?EIO=4&transport=polling' | grep -q sid
      start_period: 30s
      interval: 15s
      timeout: 10s
      retries: 12

  queue-short:
    image: *frappe-image
    platform: linux/amd64
    command: ["bench", "worker", "--queue", "short,default"]
    restart: *frappe-restart
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    depends_on:
      migrator:
        condition: service_completed_successfully

  queue-long:
    image: *frappe-image
    platform: linux/amd64
    command: ["bench", "worker", "--queue", "long,default,short"]
    restart: *frappe-restart
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    depends_on:
      migrator:
        condition: service_completed_successfully

  scheduler:
    image: *frappe-image
    platform: linux/amd64
    command: ["bench", "schedule"]
    restart: *frappe-restart
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    depends_on:
      migrator:
        condition: service_completed_successfully

  frontend:
    image: *frappe-image
    platform: linux/amd64
    command: ["nginx-entrypoint.sh"]
    restart: *frappe-restart
    environment:
      - BACKEND=backend:8000
      - SOCKETIO=websocket:9000
      - UPSTREAM_REAL_IP_ADDRESS=${UPSTREAM_REAL_IP_ADDRESS:-127.0.0.1}
      - UPSTREAM_REAL_IP_HEADER=${UPSTREAM_REAL_IP_HEADER:-X-Forwarded-For}
      - UPSTREAM_REAL_IP_RECURSIVE=${UPSTREAM_REAL_IP_RECURSIVE:-off}
      - PROXY_READ_TIMEOUT=${PROXY_READ_TIMEOUT:-120}
      - CLIENT_MAX_BODY_SIZE=${CLIENT_MAX_BODY_SIZE:-50m}
      - FRAPPE_PUBLIC_FQDN=${SERVICE_FQDN_FRONTEND:?Coolify must generate SERVICE_FQDN_FRONTEND}
      # Port-scoped magic variable declares Coolify proxy routing to internal Nginx :8080.
      - SERVICE_URL_FRONTEND_8080
      # Generic URL is the browser-facing canonical origin; do not feed the internal target port into Frappe host_name.
      - SERVICE_URL_FRONTEND
      - SERVICE_FQDN_FRONTEND
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    depends_on:
      backend:
        condition: service_healthy
      websocket:
        condition: service_healthy
    healthcheck:
      test:
        - CMD-SHELL
        - >-
          curl -fsS -H "Host: $$FRAPPE_PUBLIC_FQDN" http://127.0.0.1:8080/api/method/ping | grep -q pong
          && curl -fsS -H "Host: $$FRAPPE_PUBLIC_FQDN" 'http://127.0.0.1:8080/socket.io/?EIO=4&transport=polling' | grep -q sid
      start_period: 30s
      interval: 15s
      timeout: 10s
      retries: 12

volumes:
  db-data:
  redis-queue-data:
  sites:
````

<!-- END PORTABLE RESOURCE: assets/frappe-framework-v16.32.0-v1.0.0-golden.yml -->

<!-- BEGIN PORTABLE RESOURCE: assets/kobotoolbox-v19.3-golden.yml -->
<!-- SOURCE SHA256: 937f1e7d4dd3ecc0712a37736ca370fa759582f17fc23de15d7afd0bd4de6c3a -->
<!-- EMBEDDED SHA256: a2d39ad56c51dcc2d5cc656cbce57fbceeacfa8d91295c61d2e251dfb9069dfd -->

## Portable resource: `assets/kobotoolbox-v19.3-golden.yml`

````yaml
# KoboToolbox for Coolify — Compose version 19.3
# NOTE: KOBO_PUBLIC_DOMAIN defines the parent domain used by KF, KC and Enketo.
# REQUIRED: Keep x-kobo-public-hosts synchronized when KOBO_PUBLIC_DOMAIN changes.
# SAFETY: super_admin is used because /admin/ is reserved by Django.
x-logging: &default-logging
  driver: json-file
  options:
    max-size: 100m
    max-file: '5'
x-kpi-environment: &kpi-environment
  DJANGO_SETTINGS_MODULE: kobo.settings.prod
  DJANGO_DEBUG: ${DEBUG:-False}
  TEMPLATE_DEBUG: ${DEBUG:-False}
  DJANGO_SECRET_KEY: ${SERVICE_HEX_64_DJANGO}
  PUBLIC_REQUEST_SCHEME: https
  PUBLIC_DOMAIN_NAME: ${KOBO_PUBLIC_DOMAIN:-kobo.example.org}
  INTERNAL_DOMAIN_NAME: internal
  KOBOFORM_PUBLIC_SUBDOMAIN: kf
  KOBOCAT_PUBLIC_SUBDOMAIN: kc
  ENKETO_EXPRESS_PUBLIC_SUBDOMAIN: ee
  KOBOFORM_URL: https://kf.${KOBO_PUBLIC_DOMAIN:-kobo.example.org}
  KOBOCAT_URL: https://kc.${KOBO_PUBLIC_DOMAIN:-kobo.example.org}
  ENKETO_URL: https://ee.${KOBO_PUBLIC_DOMAIN:-kobo.example.org}
  KOBOFORM_INTERNAL_URL: http://kf
  KOBOCAT_INTERNAL_URL: http://kc
  ENKETO_INTERNAL_URL: http://ee
  DJANGO_ALLOWED_HOSTS: >-
    kf.${KOBO_PUBLIC_DOMAIN:-kobo.example.org}
    kc.${KOBO_PUBLIC_DOMAIN:-kobo.example.org}
    ee.${KOBO_PUBLIC_DOMAIN:-kobo.example.org}
    .${KOBO_PUBLIC_DOMAIN:-kobo.example.org}
    kpi kf kc ee localhost 127.0.0.1
  SESSION_COOKIE_DOMAIN: .${KOBO_PUBLIC_DOMAIN:-kobo.example.org}
  DJANGO_SESSION_COOKIE_AGE: ${DJANGO_SESSION_COOKIE_AGE:-604800}
  KPI_PREFIX: /
  USE_X_FORWARDED_HOST: 'True'
  SECURE_PROXY_SSL_HEADER: HTTP_X_FORWARDED_PROTO,https
  # SAFETY: Do not use "admin"; /admin/ is reserved by Django.
  KOBO_SUPERUSER_USERNAME: super_admin
  KOBO_LEGACY_SUPERUSER_USERNAME: admin
  KOBO_SUPERUSER_EMAIL: ${KOBO_SUPERUSER_EMAIL:-admin@example.invalid}
  KOBO_SUPERUSER_PASSWORD: ${SERVICE_PASSWORD_64_SUPERUSER}
  ACCOUNT_EMAIL_VERIFICATION: ${ACCOUNT_EMAIL_VERIFICATION:-none}
  ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS: ${ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS:-3}
  POSTGRES_HOST: postgres
  POSTGRES_PORT: '5432'
  POSTGRES_USER: ${SERVICE_USER_POSTGRES}
  POSTGRES_PASSWORD: ${SERVICE_PASSWORD_64_POSTGRES}
  KPI_POSTGRES_DB: koboform
  KC_POSTGRES_DB: kobocat
  DATABASE_URL: postgis://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_64_POSTGRES}@postgres:5432/koboform
  KPI_DATABASE_URL: postgis://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_64_POSTGRES}@postgres:5432/koboform
  KC_DATABASE_URL: postgis://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_64_POSTGRES}@postgres:5432/kobocat
  KOBO_MONGO_HOST: mongo
  KOBO_MONGO_PORT: '27017'
  KOBO_MONGO_USERNAME: ${SERVICE_USER_MONGO}
  KOBO_MONGO_PASSWORD: ${SERVICE_PASSWORD_64_MONGO}
  MONGO_DB_NAME: formhub
  MONGO_DB_URL: mongodb://${SERVICE_USER_MONGO}:${SERVICE_PASSWORD_64_MONGO}@mongo:27017/formhub?authSource=formhub
  REDIS_PASSWORD: ${SERVICE_PASSWORD_64_REDIS}
  REDIS_SESSION_URL: redis://:${SERVICE_PASSWORD_64_REDIS}@redis-cache:6380/2
  CACHE_URL: redis://:${SERVICE_PASSWORD_64_REDIS}@redis-cache:6380/5
  CELERY_BROKER_URL: redis://:${SERVICE_PASSWORD_64_REDIS}@redis-main:6379/1
  ENKETO_REDIS_MAIN_URL: redis://:${SERVICE_PASSWORD_64_REDIS}@redis-main:6379/0
  ENKETO_API_KEY: ${SERVICE_PASSWORD_64_ENKETOAPI}
  CELERY_AUTOSCALE_MIN: ${CELERY_AUTOSCALE_MIN:-1}
  CELERY_AUTOSCALE_MAX: ${CELERY_AUTOSCALE_MAX:-3}
  UWSGI_MAX_REQUESTS: ${UWSGI_MAX_REQUESTS:-512}
  UWSGI_WORKERS_COUNT: ${UWSGI_WORKERS_COUNT:-2}
  UWSGI_CHEAPER_WORKERS_COUNT: ${UWSGI_CHEAPER_WORKERS_COUNT:-1}
  UWSGI_CHEAPER_RSS_LIMIT_SOFT: ${UWSGI_CHEAPER_RSS_LIMIT_SOFT:-134217728}
  UWSGI_HARAKIRI: ${UWSGI_HARAKIRI:-120}
  UWSGI_WORKER_RELOAD_MERCY: ${UWSGI_WORKER_RELOAD_MERCY:-120}
  WSGI: uWSGI
  EMAIL_BACKEND: ${EMAIL_BACKEND:-django.core.mail.backends.filebased.EmailBackend}
  EMAIL_FILE_PATH: /srv/src/kpi/emails
  EMAIL_HOST: ${SMTP_HOST:-}
  EMAIL_PORT: ${SMTP_PORT:-587}
  EMAIL_HOST_USER: ${SMTP_USER:-}
  EMAIL_HOST_PASSWORD: ${SMTP_PASSWORD:-}
  EMAIL_USE_TLS: ${SMTP_USE_TLS:-True}
  DEFAULT_FROM_EMAIL: ${DEFAULT_FROM_EMAIL:-no-reply@example.invalid}
  CONSTANCE_SUPPORT_EMAIL: ${DEFAULT_FROM_EMAIL:-no-reply@example.invalid}
  GOOGLE_API_KEY: ${GOOGLE_API_KEY:-}
  GOOGLE_ANALYTICS_TOKEN: ${GOOGLE_UA:-}
  SENTRY_DSN: ${SENTRY_DSN:-}
  SENTRY_JS_DSN: ${SENTRY_JS_DSN:-}
  STRIPE_ENABLED: ${KOBO_STRIPE_ENABLED:-False}
  STRIPE_LIVE_MODE: 'False'
  CONSTANCE_USAGE_LIMIT_ENFORCEMENT: 'False'
x-kpi-volumes: &kpi-volumes
  - kpi-logs:/srv/logs
  - kpi-media:/srv/src/kpi/media
  - kobocat-media:/srv/src/kobocat/media
  - kpi-emails:/srv/src/kpi/emails
# NOTE: Public Kobo hostnames loop back through Coolify via Docker host-gateway.
x-kobo-public-hosts: &kobo-public-hosts
  kf.kobo.example.org: host-gateway
  kc.kobo.example.org: host-gateway
  ee.kobo.example.org: host-gateway
services:
  postgres:
    image: postgis/postgis:14-3.2
    restart: unless-stopped
    stop_grace_period: 5m
    environment:
      POSTGRES_USER: ${SERVICE_USER_POSTGRES}
      POSTGRES_PASSWORD: ${SERVICE_PASSWORD_64_POSTGRES}
      POSTGRES_DB: koboform
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
    - postgres-data:/var/lib/postgresql/data
    - type: bind
      source: ./.coolify/20-kobo-postgres-init.sh
      target: /docker-entrypoint-initdb.d/20-kobo-postgres-init.sh
      read_only: true
      content: "#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGPASSWORD=\"$POSTGRES_PASSWORD\"\n\nif ! psql \\\n  --username \"$POSTGRES_USER\" \\\n  --dbname postgres \\\n  --tuples-only \\\n  --no-align \\\n  --command \"SELECT 1 FROM pg_database WHERE datname='kobocat'\" \\\n  | grep -q '^1$'; then\n  createdb \\\n    --username \"$POSTGRES_USER\" \\\n    --owner \"$POSTGRES_USER\" \\\n    kobocat\nfi\n\nfor database in koboform kobocat; do\n  psql \\\n    --username \"$POSTGRES_USER\" \\\n    --dbname \"$database\" \\\n    --set ON_ERROR_STOP=1 <<'SQL'\nCREATE EXTENSION IF NOT EXISTS postgis;\nCREATE EXTENSION IF NOT EXISTS postgis_topology;\nCREATE EXTENSION IF NOT EXISTS fuzzystrmatch;\nCREATE EXTENSION IF NOT EXISTS postgis_tiger_geocoder;\nSQL\ndone\n"
    healthcheck:
      test:
      - CMD-SHELL
      - pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}
      interval: 5s
      timeout: 20s
      retries: 30
      start_period: 20s
    logging: *default-logging
  mongo:
    image: mongo:8.0
    restart: unless-stopped
    stop_grace_period: 5m
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${SERVICE_USER_MONGOROOT}
      MONGO_INITDB_ROOT_PASSWORD: ${SERVICE_PASSWORD_64_MONGOROOT}
      MONGO_INITDB_DATABASE: formhub
      KOBO_MONGO_USERNAME: ${SERVICE_USER_MONGO}
      KOBO_MONGO_PASSWORD: ${SERVICE_PASSWORD_64_MONGO}
    volumes:
    - mongo-data:/data/db
    - mongo-config:/data/configdb
    - type: bind
      source: ./.coolify/20-kobo-mongo-init.js
      target: /docker-entrypoint-initdb.d/20-kobo-mongo-init.js
      read_only: true
      content: "const databaseName = process.env.MONGO_INITDB_DATABASE || \"formhub\";\nconst appUsername = process.env.KOBO_MONGO_USERNAME;\nconst appPassword = process.env.KOBO_MONGO_PASSWORD;\n\nif (!appUsername || !appPassword) {\n  throw new Error(\"KOBO_MONGO_USERNAME / KOBO_MONGO_PASSWORD are required\");\n}\n\nconst targetDb = db.getSiblingDB(databaseName);\n\ntargetDb.createUser({\n  user: appUsername,\n  pwd: appPassword,\n  roles: [{ role: \"readWrite\", db: databaseName }]\n});\n\nconst instances = targetDb.getCollection(\"instances\");\n\ninstances.createIndex({ _userform_id: 1, _id: -1 });\ninstances.createIndex({ _userform_id: 1, _submission_time: -1 });\ninstances.createIndex(\n  { \"meta/rootUuid\": 1, _id: 1 },\n  { name: \"rootUuid_id_idx\" }\n);\ninstances.createIndex(\n  { _id: 1, \"meta/rootUuid\": 1 },\n  { name: \"id_rootUuid_idx\" }\n);\ninstances.createIndex({ _userform_id: 1, _uuid: 1 });\n"
    healthcheck:
      test:
      - CMD-SHELL
      - 'mongosh --quiet --host 127.0.0.1 --username "$$MONGO_INITDB_ROOT_USERNAME" --password "$$MONGO_INITDB_ROOT_PASSWORD" --authenticationDatabase admin --eval "quit(db.adminCommand({ping:1}).ok ? 0 : 2)"'
      interval: 5s
      timeout: 20s
      retries: 30
      start_period: 30s
    logging: *default-logging
  redis-main:
    image: redis:7.2
    restart: unless-stopped
    stop_grace_period: 2m30s
    sysctls:
      net.core.somaxconn: '2048'
    environment:
      REDIS_PASSWORD: ${SERVICE_PASSWORD_64_REDIS}
      REDISCLI_AUTH: ${SERVICE_PASSWORD_64_REDIS}
    command:
    - sh
    - -ec
    - "exec redis-server \\\n  --port 6379 \\\n  --bind 0.0.0.0 \\\n  --protected-mode yes \\\n  --requirepass \"$$REDIS_PASSWORD\" \\\n  --databases 16 \\\n  --save 300 1 \\\n  --stop-writes-on-bgsave-error yes \\\n  --rdbcompression yes \\\n  --rdbchecksum yes \\\n  --dbfilename enketo-main.rdb \\\n  --dir /data \\\n  --appendonly no\n"
    volumes:
    - redis-main-data:/data
    healthcheck:
      test:
      - CMD-SHELL
      - redis-cli -p 6379 ping | grep -q PONG
      interval: 5s
      timeout: 10s
      retries: 20
      start_period: 10s
    logging: *default-logging
  redis-cache:
    image: redis:7.2
    restart: unless-stopped
    stop_grace_period: 2m30s
    sysctls:
      net.core.somaxconn: '2048'
    environment:
      REDIS_PASSWORD: ${SERVICE_PASSWORD_64_REDIS}
      REDISCLI_AUTH: ${SERVICE_PASSWORD_64_REDIS}
      REDIS_CACHE_MAX_MEMORY_MB: ${REDIS_CACHE_MAX_MEMORY_MB:-512}
    command:
    - sh
    - -ec
    - "exec redis-server \\\n  --port 6380 \\\n  --bind 0.0.0.0 \\\n  --protected-mode yes \\\n  --requirepass \"$$REDIS_PASSWORD\" \\\n  --databases 16 \\\n  --save 3600 1 \\\n  --stop-writes-on-bgsave-error yes \\\n  --rdbcompression yes \\\n  --rdbchecksum yes \\\n  --dbfilename enketo-cache.rdb \\\n  --dir /data \\\n  --appendonly no \\\n  --maxmemory \"$${REDIS_CACHE_MAX_MEMORY_MB}mb\" \\\n  --maxmemory-policy volatile-ttl\n"
    volumes:
    - redis-cache-data:/data
    healthcheck:
      test:
      - CMD-SHELL
      - redis-cli -p 6380 ping | grep -q PONG
      interval: 5s
      timeout: 10s
      retries: 20
      start_period: 10s
    logging: *default-logging
  enketo-express:
    image: kobotoolbox/enketo-express-extra-widgets:7.6.3
    init: true
    restart: unless-stopped
    extra_hosts: *kobo-public-hosts
    environment:
      PORT: '8005'
      ENKETO_PORT: '8005'
      ENKETO_APP_NAME: ${ENKETO_APP_NAME:-Enketo Express for KoboToolbox}
      ENKETO_OFFLINE_ENABLED: 'true'
      ENKETO_LINKED_FORM_AND_DATA_SERVER_NAME: KoboToolbox
      ENKETO_LINKED_FORM_AND_DATA_SERVER_SERVER_URL: ''
      ENKETO_LINKED_FORM_AND_DATA_SERVER_API_KEY: ${SERVICE_PASSWORD_64_ENKETOAPI}
      ENKETO_ENCRYPTION_KEY: ${SERVICE_HEX_64_ENKETOCRYPT}
      ENKETO_LESS_SECURE_ENCRYPTION_KEY: ${SERVICE_HEX_64_ENKETOLESS}
      ENKETO_SUPPORT_EMAIL: ${DEFAULT_FROM_EMAIL:-no-reply@example.invalid}
      ENKETO_DEFAULT_THEME: kobo
      ENKETO_IP_FILTERING_ALLOWPRIVATEIPADDRESS: 'true'
      ENKETO_IP_FILTERING_ALLOWMETAIPADDRESS: 'false'
      ENKETO_REDIS_MAIN_URL: redis://:${SERVICE_PASSWORD_64_REDIS}@redis-main:6379/0
      ENKETO_REDIS_CACHE_URL: redis://:${SERVICE_PASSWORD_64_REDIS}@redis-cache:6380/0
      ENKETO_GOOGLE_API_KEY: ${GOOGLE_API_KEY:-}
      ENKETO_PAYLOAD_LIMIT: 1mb
      ENKETO_TEXT_FIELD_CHARACTER_LIMIT: '1000000'
      ENKETO_NO_SNIFF: 'true'
      ENKETO_WIDGETS_0: note
      ENKETO_WIDGETS_1: select-desktop
      ENKETO_WIDGETS_2: select-mobile
      ENKETO_WIDGETS_3: autocomplete
      ENKETO_WIDGETS_4: geo
      ENKETO_WIDGETS_5: textarea
      ENKETO_WIDGETS_6: url
      ENKETO_WIDGETS_7: table
      ENKETO_WIDGETS_8: radio
      ENKETO_WIDGETS_9: date
      ENKETO_WIDGETS_10: time
      ENKETO_WIDGETS_11: datetime
      ENKETO_WIDGETS_12: select-media
      ENKETO_WIDGETS_13: file
      ENKETO_WIDGETS_14: draw
      ENKETO_WIDGETS_15: rank
      ENKETO_WIDGETS_16: likert
      ENKETO_WIDGETS_17: range
      ENKETO_WIDGETS_18: columns
      ENKETO_WIDGETS_19: image-view
      ENKETO_WIDGETS_20: comment
      ENKETO_WIDGETS_21: image-map
      ENKETO_WIDGETS_22: date-native
      ENKETO_WIDGETS_23: date-native-ios
      ENKETO_WIDGETS_24: date-mobile
      ENKETO_WIDGETS_25: text-max
      ENKETO_WIDGETS_26: text-print
      ENKETO_WIDGETS_27: rating
      ENKETO_WIDGETS_28: thousands-sep
      ENKETO_WIDGETS_29: integer
      ENKETO_WIDGETS_30: decimal
      ENKETO_WIDGETS_31: ../../../node_modules/enketo-image-customization-widget/image-customization
      ENKETO_WIDGETS_32: ../../../node_modules/enketo-literacy-test-widget/literacywidget
    depends_on:
      redis-main:
        condition: service_healthy
      redis-cache:
        condition: service_healthy
    expose:
    - '8005'
    healthcheck:
      test:
      - CMD-SHELL
      - "node -e \" const http=require('http'); const req=http.get('http://127.0.0.1:8005/',r=>{\n  process.exit(r.statusCode < 500 ? 0 : 1)\n}); req.on('error',()=>process.exit(1)); req.setTimeout(5000,()=>{req.destroy();process.exit(1)}); \""
      interval: 10s
      timeout: 10s
      retries: 20
      start_period: 60s
    logging: *default-logging
  kpi:
    image: kobotoolbox/kpi:2.026.30c
    init: true
    restart: unless-stopped
    extra_hosts: *kobo-public-hosts
    stop_grace_period: 2m
    sysctls:
      net.core.somaxconn: '2048'
    environment: *kpi-environment
    volumes:
    - kpi-static:/srv/static
    - kpi-logs:/srv/logs
    - kpi-media:/srv/src/kpi/media
    - kobocat-media:/srv/src/kobocat/media
    - kpi-emails:/srv/src/kpi/emails
    - type: bind
      source: ./.coolify/wait_for_postgres.bash
      target: /srv/init/wait_for_postgres.bash
      read_only: true
      content: "#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGPASSWORD=\"$POSTGRES_PASSWORD\"\n\nuntil pg_isready \\\n  -h \"$POSTGRES_HOST\" \\\n  -p \"$POSTGRES_PORT\" \\\n  -U \"$POSTGRES_USER\" \\\n  -d \"$KPI_POSTGRES_DB\" >/dev/null 2>&1; do\n  sleep 2\ndone\n\nfor database in \"$KPI_POSTGRES_DB\" \"$KC_POSTGRES_DB\"; do\n  until psql \\\n    -h \"$POSTGRES_HOST\" \\\n    -p \"$POSTGRES_PORT\" \\\n    -U \"$POSTGRES_USER\" \\\n    -d \"$database\" \\\n    -tAc \"SELECT 1\" 2>/dev/null | grep -q '^1$'; do\n    sleep 2\n  done\ndone\n"
    - type: bind
      source: ./.coolify/wait_for_mongo.bash
      target: /srv/init/wait_for_mongo.bash
      read_only: true
      content: "#!/usr/bin/env bash\nset -euo pipefail\n\npython - <<'PY'\nimport os\nimport time\nfrom pymongo import MongoClient\n\nurl = os.environ[\"MONGO_DB_URL\"]\n\nfor attempt in range(90):\n    try:\n        client = MongoClient(url, serverSelectionTimeoutMS=3000)\n        client.admin.command(\"ping\")\n        break\n    except Exception:\n        if attempt == 89:\n            raise\n        time.sleep(2)\nPY\n"
    - type: bind
      source: ./.coolify/kpi-entrypoint.bash
      target: /srv/init/kpi-entrypoint.bash
      read_only: true
      content: |
        #!/usr/bin/env bash
        set -euo pipefail

        cd "${KPI_SRC_DIR:-/srv/src/kpi}"

        python - <<'PY'
        import os
        import django
        from django.db import connections
        from django.db.utils import OperationalError, ProgrammingError

        os.environ.setdefault("DJANGO_SETTINGS_MODULE", "kobo.settings.prod")
        django.setup()

        from django.contrib.auth import get_user_model

        User = get_user_model()
        legacy = os.environ.get("KOBO_LEGACY_SUPERUSER_USERNAME", "admin")
        target = os.environ.get("KOBO_SUPERUSER_USERNAME", "super_admin")

        if target == "admin":
            raise SystemExit(
                'KOBO_SUPERUSER_USERNAME="admin" is unsafe: /admin/ is reserved by Django.'
            )

        table = User._meta.db_table
        renamed = []

        for alias in ("default", "kobocat"):
            try:
                tables = connections[alias].introspection.table_names()
            except (OperationalError, ProgrammingError) as exc:
                print(f"[{alias}] database not ready for user migration ({exc}); skipping.")
                continue

            if table not in tables:
                continue

            legacy_qs = User.objects.using(alias).filter(username=legacy)
            target_qs = User.objects.using(alias).filter(username=target)

            legacy_user = legacy_qs.first()
            target_user = target_qs.first()

            if legacy_user and target_user and legacy_user.pk != target_user.pk:
                raise SystemExit(
                    f"[{alias}] both {legacy!r} (id={legacy_user.pk}) "
                    f"and {target!r} (id={target_user.pk}) exist. Refusing an unsafe automatic merge."
                )

            if legacy_user and not target_user:
                updated = legacy_qs.update(username=target)
                print(
                    f"[{alias}] renamed {legacy!r} -> {target!r} "
                    f"(id={legacy_user.pk}, rows={updated})."
                )
                renamed.append(alias)
            elif target_user:
                continue

        if renamed:
            print("Legacy superuser migration completed for: " + ", ".join(renamed))
        PY

        exec /bin/bash /srv/src/kpi/docker/entrypoint.sh
    command:
    - /bin/bash
    - /srv/init/kpi-entrypoint.bash
    depends_on:
      postgres:
        condition: service_healthy
      mongo:
        condition: service_healthy
      redis-main:
        condition: service_healthy
      redis-cache:
        condition: service_healthy
      enketo-express:
        condition: service_healthy
    expose:
    - '8000'
    healthcheck:
      test:
      - CMD
      - python
      - -c
      - import socket; s=socket.create_connection(('127.0.0.1',8000),5); s.close()
      interval: 10s
      timeout: 10s
      retries: 30
      start_period: 5m
    logging: *default-logging
  worker:
    image: kobotoolbox/kpi:2.026.30c
    init: true
    restart: unless-stopped
    extra_hosts: *kobo-public-hosts
    stop_grace_period: 2m
    environment: *kpi-environment
    volumes: *kpi-volumes
    command:
    - bash
    - /srv/src/kpi/docker/entrypoint_celery_kpi_worker.bash
    depends_on:
      kpi:
        condition: service_healthy
    healthcheck:
      test:
      - CMD-SHELL
      - test -s /tmp/celery_kpi_worker.pid && pgrep -F /tmp/celery_kpi_worker.pid -f celery >/dev/null
      interval: 15s
      timeout: 10s
      retries: 8
      start_period: 60s
    logging: *default-logging
  worker-low-priority:
    image: kobotoolbox/kpi:2.026.30c
    init: true
    restart: unless-stopped
    extra_hosts: *kobo-public-hosts
    stop_grace_period: 2m
    environment: *kpi-environment
    volumes: *kpi-volumes
    command:
    - bash
    - /srv/src/kpi/docker/entrypoint_celery_kpi_worker_low_priority.bash
    depends_on:
      kpi:
        condition: service_healthy
    healthcheck:
      test:
      - CMD-SHELL
      - test -s /tmp/celery_kpi_worker_low_priority.pid && pgrep -F /tmp/celery_kpi_worker_low_priority.pid -f celery >/dev/null
      interval: 15s
      timeout: 10s
      retries: 8
      start_period: 60s
    logging: *default-logging
  worker-long-running-tasks:
    image: kobotoolbox/kpi:2.026.30c
    init: true
    restart: unless-stopped
    extra_hosts: *kobo-public-hosts
    stop_grace_period: 2m
    environment: *kpi-environment
    volumes: *kpi-volumes
    command:
    - bash
    - /srv/src/kpi/docker/entrypoint_celery_kpi_worker_long_running_tasks.bash
    depends_on:
      kpi:
        condition: service_healthy
    healthcheck:
      test:
      - CMD-SHELL
      - test -s /tmp/celery_kpi_worker_long_running_tasks.pid && pgrep -F /tmp/celery_kpi_worker_long_running_tasks.pid -f celery >/dev/null
      interval: 15s
      timeout: 10s
      retries: 8
      start_period: 60s
    logging: *default-logging
  worker-kobocat:
    image: kobotoolbox/kpi:2.026.30c
    init: true
    restart: unless-stopped
    extra_hosts: *kobo-public-hosts
    stop_grace_period: 2m
    environment: *kpi-environment
    volumes: *kpi-volumes
    command:
    - bash
    - /srv/src/kpi/docker/entrypoint_celery_kobocat_worker.bash
    depends_on:
      kpi:
        condition: service_healthy
    healthcheck:
      test:
      - CMD-SHELL
      - test -s /tmp/celery_kobocat_worker.pid && pgrep -F /tmp/celery_kobocat_worker.pid -f celery >/dev/null
      interval: 15s
      timeout: 10s
      retries: 8
      start_period: 60s
    logging: *default-logging
  beat:
    image: kobotoolbox/kpi:2.026.30c
    init: true
    restart: unless-stopped
    extra_hosts: *kobo-public-hosts
    stop_grace_period: 2m
    environment: *kpi-environment
    volumes: *kpi-volumes
    command:
    - bash
    - /srv/src/kpi/docker/entrypoint_celery_beat.bash
    depends_on:
      kpi:
        condition: service_healthy
    healthcheck:
      test:
      - CMD-SHELL
      - test -s /tmp/celery_beat.pid && pgrep -F /tmp/celery_beat.pid -f celery >/dev/null
      interval: 15s
      timeout: 10s
      retries: 8
      start_period: 60s
    logging: *default-logging
  kf:
    image: nginx:1.27-alpine
    restart: unless-stopped
    environment:
      SERVICE_URL_KF_80: /
    expose:
    - '80'
    volumes:
    - kpi-static:/srv/www/kpi:ro
    - kpi-media:/srv/kpi_media:ro
    - kobocat-media:/media:ro
    - type: bind
      source: ./.coolify/kf.conf
      target: /etc/nginx/conf.d/default.conf
      read_only: true
      content: "map $http_x_forwarded_proto $kobo_forwarded_proto {\n    default $http_x_forwarded_proto;\n    \"\"      $scheme;\n}\n\nmap $arg_format $schema_v2_file {\n    default /schema_v2.yaml;\n    json    /schema_v2.json;\n}\n\nmap $arg_format $schema_openrosa_file {\n    default /schema_openrosa.yaml;\n    json    /schema_openrosa.json;\n}\n\nmap $arg_format $schema_type {\n    default application/yaml;\n    json    application/json;\n}\n\nserver {\n    listen 80 default_server;\n    listen [::]:80 default_server;\n    server_name _;\n\n    client_max_body_size 100M;\n    large_client_header_buffers 8 16k;\n\n    location ~ ^/forms/(.*) {\n        return 301 /$1;\n    }\n\n    # NOTE: OpenRosa API is served by KC.\n    location /api/v1 {\n        return 404;\n    }\n\n    location /static {\n        alias /srv/www/kpi;\n    }\n\n    location /media/__public {\n        alias /srv/kpi_media/__public;\n    }\n\n    location /protected/ {\n        internal;\n        alias /media/;\n    }\n\n    location ~ ^/protected-s3/(.*)$ {\n        internal;\n        resolver 8.8.8.8 8.8.4.4 valid=300s;\n        resolver_timeout 10s;\n        proxy_pass_request_body off;\n        proxy_pass_request_headers off;\n        proxy_buffering off;\n        proxy_hide_header x-amz-delete-marker;\n        proxy_hide_header x-amz-id-2;\n        proxy_hide_header x-amz-request-id;\n        proxy_hide_header x-amz-version-id;\n        proxy_pass $1;\n    }\n\n    location ~ ^/api/(v2|openrosa)/schema$ {\n        return 301 /api/$1/schema/$is_args$args;\n    }\n\n    location = /api/v2/schema/ {\n        root /srv/www/kpi/openapi;\n        try_files $schema_v2_file =404;\n        add_header Content-Type $schema_type always;\n    }\n\n    location = /api/openrosa/schema/ {\n        root /srv/www/kpi/openapi;\n        try_files $schema_openrosa_file =404;\n        add_header Content-Type $schema_type always;\n    }\n\n    location / {\n        resolver 127.0.0.11 ipv6=off valid=1s;\n        set $kpi_upstream \"kpi:8000\";\n\n        uwsgi_read_timeout 130s;\n        uwsgi_send_timeout 130s;\n        uwsgi_pass $kpi_upstream;\n\n        include /etc/nginx/uwsgi_params;\n\n        uwsgi_param HTTP_HOST $host;\n        uwsgi_param HTTP_X_REAL_IP $remote_addr;\n        uwsgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;\n        uwsgi_param HTTP_X_FORWARDED_HOST $host;\n        uwsgi_param HTTP_X_FORWARDED_PROTO $kobo_forwarded_proto;\n\n        uwsgi_buffers 8 16k;\n        uwsgi_buffer_size 16k;\n        uwsgi_force_ranges on;\n    }\n}\n"
    healthcheck:
      test:
      - CMD
      - nginx
      - -t
      interval: 15s
      timeout: 10s
      retries: 5
      start_period: 15s
    logging: *default-logging
  kc:
    image: nginx:1.27-alpine
    restart: unless-stopped
    environment:
      SERVICE_URL_KC_80: /
      KF_PUBLIC_URL: https://kf.${KOBO_PUBLIC_DOMAIN:-kobo.example.org}
      # NOTE: Expand only KF_PUBLIC_URL; preserve nginx runtime variables.
      NGINX_ENVSUBST_FILTER: ^KF_PUBLIC_URL$
    expose:
    - '80'
    volumes:
    - kpi-static:/srv/www/kpi:ro
    - kobocat-media:/media:ro
    - type: bind
      source: ./.coolify/kc.conf.template
      target: /etc/nginx/templates/default.conf.template
      read_only: true
      content: "map $http_x_forwarded_proto $kobo_forwarded_proto {\n    default $http_x_forwarded_proto;\n    \"\"      $scheme;\n}\n\nserver {\n    listen 80 default_server;\n    listen [::]:80 default_server;\n    server_name _;\n\n    client_max_body_size 100M;\n    large_client_header_buffers 8 16k;\n\n    location /static {\n        alias /srv/www/kpi;\n    }\n\n    location /protected/ {\n        internal;\n        alias /media/;\n    }\n\n    location ~ ^/protected-s3/(.*)$ {\n        internal;\n        resolver 8.8.8.8 8.8.4.4 valid=300s;\n        resolver_timeout 10s;\n        proxy_pass_request_body off;\n        proxy_pass_request_headers off;\n        proxy_buffering off;\n        proxy_hide_header x-amz-delete-marker;\n        proxy_hide_header x-amz-id-2;\n        proxy_hide_header x-amz-request-id;\n        proxy_hide_header x-amz-version-id;\n        proxy_pass $1;\n    }\n\n    location ~ ^/.+/bulk-submission(-form)?$ {\n        include /etc/nginx/includes/kpi-uwsgi.conf;\n    }\n\n    location ~ ^/.+/(exports|forms)/.+/ {\n        include /etc/nginx/includes/kpi-uwsgi.conf;\n    }\n\n    location ~ ^/.+/(formList|submission|xformsManifest|xformsMedia) {\n        include /etc/nginx/includes/kpi-uwsgi.conf;\n    }\n\n    location ~ ^/(attachment|media)/(.*)$ {\n        include /etc/nginx/includes/kpi-uwsgi.conf;\n    }\n\n    location ~ ^/(formList|formUpload|submission|upload)$ {\n        include /etc/nginx/includes/kpi-uwsgi.conf;\n    }\n\n    location ~ ^/view/(downloadSubmission|submissionList)$ {\n        include /etc/nginx/includes/kpi-uwsgi.conf;\n    }\n\n    location ~ ^/(forms|xformsManifest|xformsMedia)/([0-9]+) {\n        include /etc/nginx/includes/kpi-uwsgi.conf;\n    }\n\n    location /api/v1 {\n        include /etc/nginx/includes/kpi-uwsgi.conf;\n    }\n\n    location /legacy/ {\n        include /etc/nginx/includes/kpi-uwsgi.conf;\n    }\n\n    # NOTE: Redirect KC root to the public KF URL.\n    location = / {\n        return 302 ${KF_PUBLIC_URL};\n    }\n\n    location / {\n        return 404;\n    }\n}\n"
    - type: bind
      source: ./.coolify/kc-kpi-uwsgi.conf
      target: /etc/nginx/includes/kpi-uwsgi.conf
      read_only: true
      content: 'resolver 127.0.0.11 ipv6=off valid=1s;

        set $kpi_upstream "kpi:8000";


        uwsgi_read_timeout 130s;

        uwsgi_send_timeout 130s;

        uwsgi_pass $kpi_upstream;


        include /etc/nginx/uwsgi_params;


        uwsgi_param HTTP_HOST $host;

        uwsgi_param HTTP_X_REAL_IP $remote_addr;

        uwsgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;

        uwsgi_param HTTP_X_FORWARDED_HOST $host;

        uwsgi_param HTTP_X_FORWARDED_PROTO $kobo_forwarded_proto;


        uwsgi_buffers 8 16k;

        uwsgi_buffer_size 16k;

        uwsgi_force_ranges on;

        '
    healthcheck:
      test:
      - CMD
      - nginx
      - -t
      interval: 15s
      timeout: 10s
      retries: 5
      start_period: 15s
    logging: *default-logging
  ee:
    image: nginx:1.27-alpine
    restart: unless-stopped
    environment:
      SERVICE_URL_EE_80: /
    expose:
    - '80'
    volumes:
    - type: bind
      source: ./.coolify/ee.conf
      target: /etc/nginx/conf.d/default.conf
      read_only: true
      content: "map $http_x_forwarded_proto $kobo_forwarded_proto {\n    default $http_x_forwarded_proto;\n    \"\"      $scheme;\n}\n\nserver {\n    listen 80 default_server;\n    listen [::]:80 default_server;\n    server_name _;\n\n    client_max_body_size 100M;\n    large_client_header_buffers 8 16k;\n\n    resolver 127.0.0.11 ipv6=off valid=1s;\n    set $enketo_upstream \"enketo-express:8005\";\n\n    add_header X-Content-Type-Options nosniff always;\n\n    location / {\n        proxy_http_version 1.1;\n        proxy_pass http://$enketo_upstream;\n        proxy_redirect off;\n\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Host $host;\n        proxy_set_header X-Forwarded-Proto $kobo_forwarded_proto;\n    }\n}\n"
    healthcheck:
      test:
      - CMD
      - nginx
      - -t
      interval: 15s
      timeout: 10s
      retries: 5
      start_period: 15s
    logging: *default-logging
# NOTE: Named volumes persist across normal redeployments.
volumes:
  postgres-data: {}
  mongo-data: {}
  mongo-config: {}
  redis-main-data: {}
  redis-cache-data: {}
  kpi-static: {}
  kpi-media: {}
  kobocat-media: {}
  kpi-logs: {}
  kpi-emails: {}
````

<!-- END PORTABLE RESOURCE: assets/kobotoolbox-v19.3-golden.yml -->

<!-- BEGIN PORTABLE RESOURCE: assets/mem0-v2.0.19-v1.0.0-golden.yml -->
<!-- SOURCE SHA256: b2f2b6442a49275f692e5bd586a20f6d35a109538df56e2f82055ccd86b1fcc7 -->
<!-- EMBEDDED SHA256: 46942f5d58c0df10aa643e1eb7ad67a9b1cf14dce1d0a640660cb1756de016ed -->

## Portable resource: `assets/mem0-v2.0.19-v1.0.0-golden.yml`

````yaml
# Mem0 Coolify V1.0.0-RC1 candidate
# Upstream source snapshot: mem0ai/mem0 v2.0.19 @ dc82354e143c2581d505d581a00286d6ef8c3605
# This is a runtime candidate for Coolify Docker Compose Empty, not a production-ready declaration.

name: mem0-coolify

services:
  mem0:
    build:
      # Upstream currently does not publish a current semver-tagged self-hosted server image.
      # Pin the Git source snapshot instead of consuming the stale mutable DockerHub `latest` image.
      context: https://github.com/mem0ai/mem0.git#dc82354e143c2581d505d581a00286d6ef8c3605
      dockerfile: server/dev.Dockerfile
    restart: unless-stopped
    expose:
      - "8000"
    environment:
      # Coolify public endpoint. The bare service URL uses the service's single exposed port.
      - SERVICE_URL_MEM0

      # Baseline provider: current upstream defaults both LLM and embedder to OpenAI.
      # External provider credentials are operator-supplied; Coolify must never fabricate them.
      - OPENAI_API_KEY=${OPENAI_API_KEY:?Set a valid OpenAI API key for the baseline Mem0 provider}
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
      - GOOGLE_API_KEY=${GOOGLE_API_KEY:-}

      # PostgreSQL/pgvector memory store. One logical DB password = one exact Magic Variable identity.
      - POSTGRES_HOST=postgres
      - POSTGRES_PORT=5432
      - POSTGRES_DB=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=${SERVICE_PASSWORD_64_MEM0DB:?Coolify must generate the Mem0 PostgreSQL password}
      - POSTGRES_COLLECTION_NAME=${POSTGRES_COLLECTION_NAME:-memories}

      # Application/auth DB is a second PostgreSQL database in the same PostgreSQL cluster.
      - APP_DB_NAME=mem0_app

      # Auth remains enabled. JWT signing state must remain stable across redeploys.
      - JWT_SECRET=${SERVICE_PASSWORD_64_MEM0JWT:?Coolify must generate the Mem0 JWT secret}
      - AUTH_DISABLED=false

      # Exact browser origin accepted by FastAPI CORS.
      - DASHBOARD_URL=${SERVICE_URL_DASHBOARD:?Coolify must generate the dashboard public URL}

      # Preserve current upstream defaults unless the operator deliberately changes them.
      - MEM0_DEFAULT_LLM_MODEL=${MEM0_DEFAULT_LLM_MODEL:-gpt-5-mini}
      - MEM0_DEFAULT_EMBEDDER_MODEL=${MEM0_DEFAULT_EMBEDDER_MODEL:-text-embedding-3-small}
      - MEM0_TELEMETRY=${MEM0_TELEMETRY:-true}
      - REQUEST_LOG_RETENTION_DAYS=${REQUEST_LOG_RETENTION_DAYS:-30}
      - HISTORY_DB_PATH=/app/history/history.db
      - PYTHONDONTWRITEBYTECODE=1
      - PYTHONUNBUFFERED=1
      - PYTHONPATH=
    volumes:
      - mem0_history:/app/history
    depends_on:
      postgres:
        condition: service_healthy
    # Preserve upstream automatic Alembic migration semantics, but remove dev-only hot reload
    # and the upstream bind-mount-time `pip install mem0ai` workaround.
    command:
      - sh
      - -c
      - >-
        alembic upgrade head &&
        exec uvicorn main:app --host 0.0.0.0 --port 8000
    healthcheck:
      # /auth/setup-status is provider-independent and proves FastAPI + mem0_app DB readiness.
      test:
        - CMD
        - python
        - -c
        - >-
          import json,urllib.request;
          r=urllib.request.urlopen('http://127.0.0.1:8000/auth/setup-status',timeout=4);
          assert r.status==200;
          d=json.load(r);
          assert 'needsSetup' in d
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 30s

  postgres:
    # Current upstream `pg17` resolves to pgvector 0.8.6 on PostgreSQL 17 today.
    # RC1 pins the multi-platform index digest to avoid floating `pg17` behavior.
    image: pgvector/pgvector:0.8.6-pg17@sha256:cf134a767f474095eeba57e0117be8e568e011a63f33fbf252f14c9b760f8e6f
    restart: unless-stopped
    shm_size: "128mb"
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=${SERVICE_PASSWORD_64_MEM0DB:?Coolify must generate the Mem0 PostgreSQL password}
      # Upstream init-db.sh creates mem0_app in addition to the default postgres DB.
      # Using the official Postgres entrypoint's POSTGRES_DB primitive creates the same second DB
      # without requiring a repository-local init script in Docker Compose Empty.
      - POSTGRES_DB=mem0_app
    volumes:
      - mem0_postgres:/var/lib/postgresql/data
    healthcheck:
      test:
        - CMD-SHELL
        - pg_isready -q -d "$${POSTGRES_DB}" -U "$${POSTGRES_USER}"
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 10s

  dashboard:
    build:
      # Same immutable upstream snapshot, but dashboard Dockerfile expects server/dashboard as context.
      context: https://github.com/mem0ai/mem0.git#dc82354e143c2581d505d581a00286d6ef8c3605:server/dashboard
      dockerfile: Dockerfile
    restart: unless-stopped
    expose:
      - "3000"
    environment:
      - SERVICE_URL_DASHBOARD
      # The upstream entrypoint deliberately replaces NEXT_PUBLIC_* build placeholders at runtime.
      - NEXT_PUBLIC_API_URL=${SERVICE_URL_MEM0:?Coolify must generate the Mem0 API public URL}
      - API_INTERNAL_URL=http://mem0:8000
      - NEXT_PUBLIC_INSTANCE_NAME=${MEM0_INSTANCE_NAME:-Mem0}
    depends_on:
      mem0:
        condition: service_healthy
    healthcheck:
      # Preserve the upstream dashboard probe.
      test:
        - CMD
        - wget
        - -qO-
        - http://127.0.0.1:3000/api/health
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 20s

volumes:
  mem0_postgres:
  mem0_history:
````

<!-- END PORTABLE RESOURCE: assets/mem0-v2.0.19-v1.0.0-golden.yml -->

<!-- BEGIN PORTABLE RESOURCE: assets/netbox-4.6.9-v1.0.0-golden.yml -->
<!-- SOURCE SHA256: e4be06751d206704a2e9460ac2926d92833b39a71266cf1bd5a8a788da319804 -->
<!-- EMBEDDED SHA256: 09b9b5371c69f885f6fb9854d623938a0caec7ccd062d64e875c3362b0eea849 -->

## Portable resource: `assets/netbox-4.6.9-v1.0.0-golden.yml`

````yaml
# documentation: https://github.com/netbox-community/netbox-docker
# slogan: NetBox is an open source IPAM and DCIM platform.
# category: monitoring
# tags: netbox,dcim,ipam,network,infrastructure
# port: 8080
# NOTE: NetBox Coolify V1.0.0-RC2 candidate. Runtime fix: use upstream-compatible localhost healthcheck host.

services:
  netbox:
    # Upstream netbox-docker 5.0.2 publishes this exact NetBox 4.6.9 image.
    # Manifest digest captured 2026-09-01 from the upstream GHCR package.
    image: ghcr.io/netbox-community/netbox:v4.6.9-5.0.2@sha256:b1639229a0cf67052a2d53d7f7df004c840f49c9959a321bf310b6373df7240c
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      redis-cache:
        condition: service_healthy
    user: "netbox:root"
    environment:
      # Coolify public routing: the port-qualified variable declares the proxy target.
      # The base URL/FQDN are the canonical browser origin/hostname consumed by NetBox.
      - SERVICE_URL_NETBOX_8080
      - ALLOWED_HOSTS=${SERVICE_FQDN_NETBOX:?Coolify must generate SERVICE_FQDN_NETBOX}
      - CSRF_TRUSTED_ORIGINS=${SERVICE_URL_NETBOX:?Coolify must generate SERVICE_URL_NETBOX}
      - CORS_ORIGIN_ALLOW_ALL=false

      # PostgreSQL
      - DB_HOST=postgres
      - DB_NAME=netbox
      - DB_USER=netbox
      - DB_PASSWORD=${SERVICE_PASSWORD_64_NETBOXDB:?Coolify must generate SERVICE_PASSWORD_64_NETBOXDB}

      # Valkey/Redis tasks queue: separate credential and durable AOF-backed store.
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_DATABASE=0
      - REDIS_PASSWORD=${SERVICE_PASSWORD_64_NETBOXREDIS:?Coolify must generate SERVICE_PASSWORD_64_NETBOXREDIS}
      - REDIS_SSL=false
      - REDIS_INSECURE_SKIP_TLS_VERIFY=false

      # Valkey/Redis cache: distinct store, distinct credential, disposable semantics.
      - REDIS_CACHE_HOST=redis-cache
      - REDIS_CACHE_PORT=6379
      - REDIS_CACHE_DATABASE=1
      - REDIS_CACHE_PASSWORD=${SERVICE_PASSWORD_64_NETBOXCACHE:?Coolify must generate SERVICE_PASSWORD_64_NETBOXCACHE}
      - REDIS_CACHE_SSL=false
      - REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY=false

      # Durable application secrets. Symbol-bearing 64-char generators match NetBox's
      # >=50 character, diverse-character guidance and must remain stable on redeploy.
      - SECRET_KEY=${SERVICE_PASSWORDWITHSYMBOLS_64_NETBOXSECRET:?Coolify must generate SERVICE_PASSWORDWITHSYMBOLS_64_NETBOXSECRET}
      - API_TOKEN_PEPPER_1=${SERVICE_PASSWORDWITHSYMBOLS_64_NETBOXTOKENPEPPER:?Coolify must generate SERVICE_PASSWORDWITHSYMBOLS_64_NETBOXTOKENPEPPER}

      # Native netbox-docker first-admin bootstrap. super_user.py is idempotent:
      # it creates the account only when the username does not already exist.
      - SKIP_SUPERUSER=false
      - SUPERUSER_NAME=${NETBOX_SUPERUSER_NAME:-admin}
      - SUPERUSER_EMAIL=${NETBOX_SUPERUSER_EMAIL:-admin@example.com}
      - SUPERUSER_PASSWORD=${SERVICE_PASSWORD_64_NETBOXADMIN:?Coolify must generate SERVICE_PASSWORD_64_NETBOXADMIN}

      # Current netbox-docker baseline settings retained where they are runtime-relevant.
      - GRANIAN_BACKPRESSURE=${GRANIAN_BACKPRESSURE:-4}
      - GRANIAN_WORKERS=${GRANIAN_WORKERS:-4}
      - GRAPHQL_ENABLED=${GRAPHQL_ENABLED:-true}
      - MEDIA_ROOT=/opt/netbox/netbox/media
      - METRICS_ENABLED=${METRICS_ENABLED:-false}
      - RELEASE_CHECK_URL=${RELEASE_CHECK_URL:-https://api.github.com/repos/netbox-community/netbox/releases}
      - WEBHOOKS_ENABLED=${WEBHOOKS_ENABLED:-true}
      - TIME_ZONE=${TIME_ZONE:-UTC}

      # Optional external SMTP configuration. These are operator/provider supplied,
      # not synthetic Coolify credentials.
      - EMAIL_SERVER=${EMAIL_SERVER:-localhost}
      - EMAIL_PORT=${EMAIL_PORT:-25}
      - EMAIL_USERNAME=${EMAIL_USERNAME:-}
      - EMAIL_PASSWORD=${EMAIL_PASSWORD:-}
      - EMAIL_FROM=${EMAIL_FROM:-netbox@localhost}
      - EMAIL_TIMEOUT=${EMAIL_TIMEOUT:-5}
      - EMAIL_USE_SSL=${EMAIL_USE_SSL:-false}
      - EMAIL_USE_TLS=${EMAIL_USE_TLS:-false}
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:8080/login/ >/dev/null || exit 1"]
      start_period: 90s
      timeout: 3s
      interval: 15s
      retries: 5
    volumes: &netbox-volumes
      # The full /etc/netbox/config bundle is already baked into the upstream image.
      # This single managed file augments, rather than masks, that bundle.
      - type: bind
        source: ./zz-coolify.py
        target: /etc/netbox/config/zz_coolify.py
        read_only: true
        is_directory: false
        content: |
          # Coolify terminates public TLS. Mark authentication/CSRF cookies secure.
          # These NetBox settings are not exposed by netbox-docker 5.0.2 as env mappings.
          CSRF_COOKIE_SECURE = True
          SESSION_COOKIE_SECURE = True
      - netbox-media-files:/opt/netbox/netbox/media:rw
      - netbox-reports-files:/opt/netbox/netbox/reports:rw
      - netbox-scripts-files:/opt/netbox/netbox/scripts:rw

  netbox-worker:
    image: ghcr.io/netbox-community/netbox:v4.6.9-5.0.2@sha256:b1639229a0cf67052a2d53d7f7df004c840f49c9959a321bf310b6373df7240c
    user: "netbox:root"
    depends_on:
      netbox:
        condition: service_healthy
    command:
      - /opt/netbox/venv/bin/python
      - /opt/netbox/netbox/manage.py
      - rqworker
    environment:
      # Worker needs application state/queue configuration, but no public Coolify URL
      # declaration. This avoids binding NETBOX public-domain magic to the worker service.
      - DB_HOST=postgres
      - DB_NAME=netbox
      - DB_USER=netbox
      - DB_PASSWORD=${SERVICE_PASSWORD_64_NETBOXDB:?Coolify must generate SERVICE_PASSWORD_64_NETBOXDB}
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_DATABASE=0
      - REDIS_PASSWORD=${SERVICE_PASSWORD_64_NETBOXREDIS:?Coolify must generate SERVICE_PASSWORD_64_NETBOXREDIS}
      - REDIS_SSL=false
      - REDIS_INSECURE_SKIP_TLS_VERIFY=false
      - REDIS_CACHE_HOST=redis-cache
      - REDIS_CACHE_PORT=6379
      - REDIS_CACHE_DATABASE=1
      - REDIS_CACHE_PASSWORD=${SERVICE_PASSWORD_64_NETBOXCACHE:?Coolify must generate SERVICE_PASSWORD_64_NETBOXCACHE}
      - REDIS_CACHE_SSL=false
      - REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY=false
      - SECRET_KEY=${SERVICE_PASSWORDWITHSYMBOLS_64_NETBOXSECRET:?Coolify must generate SERVICE_PASSWORDWITHSYMBOLS_64_NETBOXSECRET}
      - API_TOKEN_PEPPER_1=${SERVICE_PASSWORDWITHSYMBOLS_64_NETBOXTOKENPEPPER:?Coolify must generate SERVICE_PASSWORDWITHSYMBOLS_64_NETBOXTOKENPEPPER}
      - MEDIA_ROOT=/opt/netbox/netbox/media
      - TIME_ZONE=${TIME_ZONE:-UTC}
      - EMAIL_SERVER=${EMAIL_SERVER:-localhost}
      - EMAIL_PORT=${EMAIL_PORT:-25}
      - EMAIL_USERNAME=${EMAIL_USERNAME:-}
      - EMAIL_PASSWORD=${EMAIL_PASSWORD:-}
      - EMAIL_FROM=${EMAIL_FROM:-netbox@localhost}
      - EMAIL_TIMEOUT=${EMAIL_TIMEOUT:-5}
      - EMAIL_USE_SSL=${EMAIL_USE_SSL:-false}
      - EMAIL_USE_TLS=${EMAIL_USE_TLS:-false}
    volumes: *netbox-volumes
    healthcheck:
      # Liveness only. Runtime acceptance separately requires a real NetBox job.
      test: ["CMD-SHELL", "ps -aux | grep -v grep | grep -q rqworker || exit 1"]
      start_period: 20s
      timeout: 3s
      interval: 15s
      retries: 5

  postgres:
    # Preserved from netbox-docker 5.0.2. NetBox 4.6 supports PostgreSQL 14+;
    # upstream intentionally selects PostgreSQL 18.
    image: docker.io/postgres:18-alpine
    environment:
      - POSTGRES_DB=netbox
      - POSTGRES_USER=netbox
      - POSTGRES_PASSWORD=${SERVICE_PASSWORD_64_NETBOXDB:?Coolify must generate SERVICE_PASSWORD_64_NETBOXDB}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -q -t 2 -d \"$$POSTGRES_DB\" -U \"$$POSTGRES_USER\""]
      start_period: 20s
      timeout: 30s
      interval: 10s
      retries: 5
    volumes:
      # PostgreSQL 18 upstream layout: preserve /var/lib/postgresql exactly.
      - netbox-postgres:/var/lib/postgresql

  redis:
    # tasks queue: trusted infrastructure; private, authenticated, AOF enabled.
    image: docker.io/valkey/valkey:9.1-alpine
    environment:
      - REDIS_PASSWORD=${SERVICE_PASSWORD_64_NETBOXREDIS:?Coolify must generate SERVICE_PASSWORD_64_NETBOXREDIS}
    command:
      - sh
      - -c
      - exec valkey-server --appendonly yes --requirepass "$$REDIS_PASSWORD"
    healthcheck:
      test: ["CMD-SHELL", "[ \"$$(valkey-cli --pass \"$$REDIS_PASSWORD\" ping 2>/dev/null)\" = \"PONG\" ]"]
      start_period: 5s
      timeout: 3s
      interval: 1s
      retries: 5
    volumes:
      - netbox-redis-data:/data

  redis-cache:
    # cache: private and authenticated, but deliberately no AOF (upstream semantics).
    image: docker.io/valkey/valkey:9.1-alpine
    environment:
      - REDIS_PASSWORD=${SERVICE_PASSWORD_64_NETBOXCACHE:?Coolify must generate SERVICE_PASSWORD_64_NETBOXCACHE}
    command:
      - sh
      - -c
      - exec valkey-server --requirepass "$$REDIS_PASSWORD"
    healthcheck:
      test: ["CMD-SHELL", "[ \"$$(valkey-cli --pass \"$$REDIS_PASSWORD\" ping 2>/dev/null)\" = \"PONG\" ]"]
      start_period: 5s
      timeout: 3s
      interval: 1s
      retries: 5
    volumes:
      - netbox-redis-cache-data:/data

volumes:
  netbox-media-files:
  netbox-postgres:
  netbox-redis-cache-data:
  netbox-redis-data:
  netbox-reports-files:
  netbox-scripts-files:
````

<!-- END PORTABLE RESOURCE: assets/netbox-4.6.9-v1.0.0-golden.yml -->

<!-- BEGIN PORTABLE RESOURCE: assets/odk-central-v2026.2.4-v1.0.0-golden.yml -->
<!-- SOURCE SHA256: e93e59d7f2ea4ebb8eaec2c7bd8ecd7d06d223a53ee57f23b3636d021eb88768 -->
<!-- EMBEDDED SHA256: b5d0b27f146e696805c0e0037d826e719ca7dbb6fa199edd896de1dc0d4aeb24 -->

## Portable resource: `assets/odk-central-v2026.2.4-v1.0.0-golden.yml`

````yaml
# ODK Central regression fixture / golden case — NOT a generic Coolify skeleton.
# Executable content preserved from the Coolify-validated v1.0.0-rc6 candidate.
# documentation: https://docs.getodk.org/central-install/
# source: https://github.com/getodk/central/tree/v2026.2.4
# slogan: ODK Central — self-hosted data collection server
# category: data-collection
# tags: odk,forms,surveys,data-collection,enketo
# port: 80
#
# ODK Central v2026.2.4 — Coolify One-Click candidate v1.0.0-rc6
# Fresh-install baseline. The upstream PostgreSQL 9.6 -> 14 helper is retained for
# lifecycle compatibility, but migration of an existing non-Coolify Central install
# requires a separate data/volume migration plan.

services:
  postgres14:
    image: postgres:14.23
    shm_size: 512m
    entrypoint: ["bash"]
    command: ["/usr/local/share/odk/start-postgres.sh"]
    environment:
      PGDATA: /var/lib/odk/postgresql/14/data
      POSTGRES_USER: odk
      POSTGRES_PASSWORD: ${SERVICE_PASSWORD_64_POSTGRES}
      POSTGRES_DB: odk
    volumes:
      - postgres14:/var/lib/odk/postgresql/14
      - type: bind
        source: ./.coolify/odk/start-postgres.sh
        target: /usr/local/share/odk/start-postgres.sh
        read_only: true
        is_directory: false
        content: |
          #!/bin/bash -eu
          set -o pipefail
          shopt -s inherit_errexit

          flag_upgradeCompletedOk="$PGDATA/../.postgres14-upgrade-successful"

          logPrefix="$(basename "$0")"
          log() {
            echo "$(TZ=GMT date) [$logPrefix] $*"
          }

          if ! [[ -f "$flag_upgradeCompletedOk" ]]; then
            log "Waiting for upgrade to complete..."
            while ! [[ -f "$flag_upgradeCompletedOk" ]]; do sleep 1; done
            log "Upgrade complete."
          fi

          log "Starting postgres..."
          exec docker-entrypoint.sh postgres "$@"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\" || exit 1"]
      interval: 5s
      timeout: 5s
      retries: 30
      start_period: 30s
    restart: always

  postgres:
    # Upstream one-shot bootstrap/upgrade helper. On a fresh install it detects
    # that there is no PostgreSQL 9.6 cluster and writes the PG14 success marker.
    image: tianon/postgres-upgrade:9.6-to-14
    platform: linux/amd64
    entrypoint: ["bash"]
    command: ["/usr/local/share/odk/upgrade-postgres.sh"]
    environment:
      PGUSER: odk
      PGDATAOLD: /var/lib/postgresql/data
      POSTGRES_INITDB_ARGS: -U odk
      POSTGRES_PASSWORD: ${SERVICE_PASSWORD_64_POSTGRES}
      POSTGRES_DB: odk
    volumes:
      - postgres96_legacy:/var/lib/postgresql/data
      - postgres14:/var/lib/postgresql/14
      - postgres14_upgrade:/postgres14-upgrade
      - type: bind
        source: ./.coolify/odk/upgrade-postgres.sh
        target: /usr/local/share/odk/upgrade-postgres.sh
        read_only: true
        is_directory: false
        content: |
          #!/bin/bash -eu
          set -o pipefail
          shopt -s inherit_errexit

          flag_upgradeCompletedOk="$PGDATANEW/../.postgres14-upgrade-successful"
          flag_deleteOldData_name="delete-old-data"
          flag_deleteOldData_internal="/postgres14-upgrade/$flag_deleteOldData_name"
          flag_oldDataDeleted="/postgres14-upgrade/old-data-deleted"

          logPrefix="$(basename "$0")"
          log() {
            echo "$(TZ=GMT date) [$logPrefix] $*"
          }

          log "Checking for existing upgrade marker file..."
          if [[ -f "$flag_upgradeCompletedOk" ]]; then
            log "Upgrade has been run previously."

            if [[ -f "$flag_deleteOldData_internal" ]]; then
              log "Deleting old data..."
              rm "$flag_deleteOldData_internal"
              rm -rf /var/lib/postgresql/data/*
              touch "$flag_oldDataDeleted"
              log "Old data deleted."
            elif [[ -f "$PGDATAOLD/PG_VERSION" ]]; then
              log "!!!"
              log "!!! WARNING: you still have old data from PostgreSQL 9.6"
              log "!!!"
              log "!!! This is taking up disk space: $(du -hs "$PGDATAOLD" 2>/dev/null | cut -f1)B"
              log "!!!"
              log "!!! Continue with the instructions at https://docs.getodk.org/central-upgrade/"
              log "!!!"
            fi
          else
            if [[ -f "$flag_deleteOldData_internal" ]]; then
              log "!!!"
              log "!!! ERROR: Deletion request file created, but upgrade has not yet run!"
              log "!!!"
              log "!!! Please email support@getodk.org for assistance."
              log "!!!"
              exit 1
            fi

            if ! [[ -f "$PGDATAOLD/PG_VERSION" ]]; then
              log "No old data found."
            elif [[ -f "$PGDATANEW/PG_VERSION" ]]; then
              log "!!!"
              log "!!! ERROR: New data found, but upgrade not flagged as complete."
              log "!!!"
              log "!!! Please email support@getodk.org for assistance."
              log "!!!"
              exit 1
            else (
              log "Upgrade not run previously; upgrading now..."

              log "From: $PGDATAOLD"
              log "  To: $PGDATANEW"

              if ! docker-upgrade pg_upgrade; then
                log "!!!"
                log "!!! pg_upgrade FAILED; dumping log files..."
                log "!!!"
                tail -n+1 pg_upgrade_*.log || log "No pg_upgrade log files found ¯\\_(ツ)_/¯"
                log "!!!"
                log "!!! pg_upgrade FAILED; check above for clues."
                log "!!!"
                exit 1
              fi

              cp "$PGDATAOLD/pg_hba.conf" "$PGDATANEW/pg_hba.conf"

              log "Starting postgres server for maintenance..."
              gosu postgres pg_ctl -D "$PGDATANEW" -l logfile start

              log "Updating extensions..."
              psql -f update_extensions.sql

              log "Regenerating optimizer statistics..."
              /usr/lib/postgresql/14/bin/vacuumdb --all --analyze-in-stages

              log "Stopping postgres server..."
              gosu postgres pg_ctl -D "$PGDATANEW" -m smart stop

              log "Upgrade complete."
            ) > >(tee --append "/postgres14-upgrade/upgrade-postgres.log" >&2) 2>&1
            fi
            touch "$flag_upgradeCompletedOk"
            touch "/postgres14-upgrade/upgrade-successful"
          fi

          log "Complete."
    restart: "no"
    exclude_from_hc: true

  secrets:
    # ODK owns the Enketo secret lifecycle and exact 64/32/128-byte formats.
    image: node:24.16.0-slim
    command: ["bash", "/opt/odk/generate-secrets.sh"]
    volumes:
      - secrets:/etc/secrets
      - type: bind
        source: ./.coolify/odk/generate-secrets.sh
        target: /opt/odk/generate-secrets.sh
        read_only: true
        is_directory: false
        content: |
          #!/bin/bash -eu
          set -o pipefail
          shopt -s inherit_errexit

          if [ ! -f /etc/secrets/enketo-secret ]; then
            head -c1024 /dev/urandom | LC_ALL=C tr -dc '[:alnum:]' | head -c64  > /etc/secrets/enketo-secret
          fi

          if [ ! -f /etc/secrets/enketo-less-secret ]; then
            head -c512  /dev/urandom | LC_ALL=C tr -dc '[:alnum:]' | head -c32  > /etc/secrets/enketo-less-secret
          fi

          if [ ! -f /etc/secrets/enketo-api-key ]; then
            head -c2048 /dev/urandom | LC_ALL=C tr -dc '[:alnum:]' | head -c128 > /etc/secrets/enketo-api-key
          fi
    restart: "no"
    exclude_from_hc: true

  pyxform:
    image: ghcr.io/getodk/pyxform-http:v4.5.0
    healthcheck:
      test: ["CMD", "python", "-c", "import socket; s=socket.create_connection(('127.0.0.1',80),3); s.close()"]
      interval: 10s
      timeout: 5s
      retries: 20
      start_period: 20s
    restart: always

  enketo_redis_main:
    image: redis:8.6.4
    command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
    volumes:
      - enketo_redis_main:/data
      - type: bind
        source: ./.coolify/odk/redis-enketo-main.conf
        target: /usr/local/etc/redis/redis.conf
        read_only: true
        is_directory: false
        content: |
          # Redis configuration for Enketo's main database instance
          port 6379
          bind 0.0.0.0
          timeout 0
          tcp-keepalive 0
          loglevel notice
          databases 16
          save 300 1
          stop-writes-on-bgsave-error yes
          rdbcompression yes
          rdbchecksum yes
          dbfilename enketo-main.rdb
          slave-serve-stale-data yes
          slave-read-only yes
          repl-disable-tcp-nodelay no
          slave-priority 100
          appendonly no
          lua-time-limit 5000
          slowlog-log-slower-than 10000
          slowlog-max-len 128
          notify-keyspace-events ""
          hash-max-ziplist-entries 512
          hash-max-ziplist-value 64
          list-max-ziplist-entries 512
          list-max-ziplist-value 64
          set-max-intset-entries 512
          zset-max-ziplist-entries 128
          zset-max-ziplist-value 64
          activerehashing yes
          client-output-buffer-limit normal 0 0 0
          client-output-buffer-limit slave 256mb 64mb 60
          client-output-buffer-limit pubsub 32mb 8mb 60
          hz 10
          aof-rewrite-incremental-fsync yes
    healthcheck:
      test: ["CMD", "redis-cli", "-p", "6379", "ping"]
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 5s
    restart: always

  enketo_redis_cache:
    image: redis:8.6.4
    command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
    volumes:
      - enketo_redis_cache:/data
      - type: bind
        source: ./.coolify/odk/redis-enketo-cache.conf
        target: /usr/local/etc/redis/redis.conf
        read_only: true
        is_directory: false
        content: |
          # Redis configuration for Enketo's XSLT cache
          port 6380
          bind 0.0.0.0
          timeout 0
          tcp-keepalive 0
          loglevel notice
          databases 16
          save 3600 1
          stop-writes-on-bgsave-error yes
          rdbcompression yes
          rdbchecksum yes
          dbfilename enketo-cache.rdb
          slave-serve-stale-data yes
          slave-read-only yes
          repl-disable-tcp-nodelay no
          slave-priority 100
          appendonly no
          lua-time-limit 5000
          slowlog-log-slower-than 10000
          slowlog-max-len 128
          notify-keyspace-events ""
          hash-max-ziplist-entries 512
          hash-max-ziplist-value 64
          list-max-ziplist-entries 512
          list-max-ziplist-value 64
          set-max-intset-entries 512
          zset-max-ziplist-entries 128
          zset-max-ziplist-value 64
          activerehashing yes
          client-output-buffer-limit normal 0 0 0
          client-output-buffer-limit slave 256mb 64mb 60
          client-output-buffer-limit pubsub 32mb 8mb 60
          hz 10
          aof-rewrite-incremental-fsync yes
    healthcheck:
      test: ["CMD", "redis-cli", "-p", "6380", "ping"]
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 5s
    restart: always

  enketo:
    image: ghcr.io/enketo/enketo:7.6.1
    working_dir: /srv/src/enketo/packages/enketo-express
    command:
      - bash
      - -ec
      - |
          mkdir -p /scripts
          cp /opt/odk/envsub.awk /scripts/envsub.awk
          chmod 0755 /scripts/envsub.awk
          cp config/config.json.template config/config.json
          exec bash /opt/odk/start-enketo.sh
    environment:
      ENKETO_SRC_DIR: /srv/src/enketo/packages/enketo-express
      DOMAIN: ${SERVICE_FQDN_NGINX}
      SUPPORT_EMAIL: ${ODK_ADMIN_EMAIL:?}
      HTTPS_PORT: "443"
    volumes:
      - secrets:/etc/secrets
      - type: bind
        source: ./.coolify/odk/envsub.awk
        target: /opt/odk/envsub.awk
        read_only: true
        is_directory: false
        content: |
          #!/usr/bin/mawk -f
          BEGIN {
            errorCount = 0;
          }
          {
            while(match($0, /\$\{[A-Z_][A-Z_0-9]*\}/) > 0) {
              k = substr($0, RSTART+2, RLENGTH-3);
              if(k in ENVIRON) {
                v = ENVIRON[k];
              } else {
                print "ERR: var not defined on line " NR ": ${" k "}" > "/dev/stderr";
                ++errorCount;
                v = "!!!VALUE-MISSING: " k "!!!"
              }
              gsub("\\$\\{" k "\\}", v);
            }
            print $0;
          }
          END {
            if(errorCount > 0) {
              print "" > "/dev/stderr";
              print errorCount " error(s) found." > "/dev/stderr";
              exit 1;
            }
          }
      - type: bind
        source: ./.coolify/odk/enketo-config.json.template
        target: /srv/src/enketo/packages/enketo-express/config/config.json.template
        read_only: true
        is_directory: false
        content: |
          {
              "app name": "Enketo",
              "base path": "-",
              "encryption key": "${SECRET}",
              "id length": 31,
              "less secure encryption key": "${LESS_SECRET}",
              "linked form and data server": {
                  "api key": "${API_KEY}",
                  "authentication": {
                      "type": "cookie",
                      "url": "${BASE_URL}/login?next={RETURNURL}"
                  },
                  "name": "ODK Central",
                  "server url": "${DOMAIN}"
              },
              "logo": {
                  "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
                  "href": ""
              },
              "offline enabled": true,
              "payload limit": "1mb",
              "port": "8005",
              "query parameter to pass to submission": "st",
              "redis": {
                  "main": {
                      "host": "enketo_redis_main",
                      "port": "6379"
                  },
                  "cache": {
                      "host": "enketo_redis_cache",
                      "port": "6380"
                  }
              },
              "support": {
                  "email": "support@getodk.org"
              },
              "text field character limit": 1000000,
              "exclude non-relevant": true,
              "hide powered by": true
          }
      - type: bind
        source: ./.coolify/odk/start-enketo.sh
        target: /opt/odk/start-enketo.sh
        read_only: true
        is_directory: false
        content: |
          #!/bin/bash -eu
          set -o pipefail
          shopt -s inherit_errexit

          log() { echo >&2 "[$(basename "$0")] $*"; }

          assert_size() {
            local f="$1"
            local expectedSize="$2"

            if ! [[ -f "$f" ]]; then
              log "!!! File not found: $f"
              exit 1
            fi

            actualSize="$(stat -c "%s" "$f")"
            if ! [[ "$actualSize" = "$expectedSize" ]]; then
              log "!!!"
              log "!!! Unexpected file size:"
              log "!!!   file: $f"
              log "!!!   expected: $expectedSize b"
              log "!!!   actual:   $actualSize b"
              log "!!!"
              exit 1
            fi
          }
          if [[ "${ENKETO_SECRETS-}" = "danger-insecure" ]]; then
            log "Skipping secrets check."
          else
            log "Checking secrets exist..."
            assert_size /etc/secrets/enketo-secret       64
            assert_size /etc/secrets/enketo-less-secret  32
            assert_size /etc/secrets/enketo-api-key     128
          fi

          CONFIG_PATH=${ENKETO_SRC_DIR}/config/config.json
          log "Generating enketo configuration..."

          BASE_URL=$( [ "${HTTPS_PORT}" = 443 ] && echo https://"${DOMAIN}" || echo https://"${DOMAIN}":"${HTTPS_PORT}" ) \
          SECRET=$(cat /etc/secrets/enketo-secret) \
          LESS_SECRET=$(cat /etc/secrets/enketo-less-secret) \
          API_KEY=$(cat /etc/secrets/enketo-api-key) \
          /scripts/envsub.awk \
              < "$CONFIG_PATH.template" \
              > "$CONFIG_PATH"

          log "Starting enketo..."
          exec yarn workspace enketo-express start
    depends_on:
      secrets:
        condition: service_completed_successfully
      enketo_redis_main:
        condition: service_healthy
      enketo_redis_cache:
        condition: service_healthy
    healthcheck:
      test:
        - CMD
        - node
        - -e
        - >-
          const net=require('net');const s=net.connect(8005,'127.0.0.1',()=>{s.end();process.exit(0)});s.on('error',()=>process.exit(1));setTimeout(()=>process.exit(1),3000)
      interval: 10s
      timeout: 5s
      retries: 20
      start_period: 30s
    restart: always

  mail:
    # Preserve ODK's upstream local SMTP fallback so a fresh One-Click install
    # does not require an external provider. Production operators may override
    # ODK_EMAIL_* below with a dedicated SMTP service.
    image: registry.gitlab.com/egos-tech/smtp:1.2.8
    environment:
      MAILNAME: ${SERVICE_FQDN_NGINX}
      DKIM_KEY_PATH: /etc/exim4/dkim.key.temp
    volumes:
      - type: bind
        source: ./.coolify/odk/mail-rsa.private
        target: /etc/exim4/dkim.key.temp
        read_only: true
        is_directory: false
        content: ""
    restart: always

  service:
    image: ghcr.io/getodk/central-service:v2026.2.4
    command: ["./start-odk.sh"]
    environment:
      DOMAIN: ${SERVICE_FQDN_NGINX}
      SYSADMIN_EMAIL: ${ODK_ADMIN_EMAIL:?}
      HTTPS_PORT: "443"
      NODE_OPTIONS: ${ODK_NODE_OPTIONS:-}

      PGHOST: postgres14
      PGDATABASE: odk
      PGUSER: odk
      PGPASSWORD: ${SERVICE_PASSWORD_64_POSTGRES}
      PGAPPNAME: odkcentral
      DB_POOL_SIZE: ${DB_POOL_SIZE:-10}

      EMAIL_FROM: ${ODK_EMAIL_FROM:-no-reply@${SERVICE_FQDN_NGINX}}
      EMAIL_HOST: ${ODK_EMAIL_HOST:-mail}
      EMAIL_PORT: ${ODK_EMAIL_PORT:-25}
      EMAIL_SECURE: ${ODK_EMAIL_SECURE:-false}
      EMAIL_IGNORE_TLS: ${ODK_EMAIL_IGNORE_TLS:-true}
      EMAIL_USER: ${ODK_EMAIL_USER:-}
      EMAIL_PASSWORD: ${ODK_EMAIL_PASSWORD:-}

      OIDC_ENABLED: ${OIDC_ENABLED:-false}
      OIDC_ISSUER_URL: ${OIDC_ISSUER_URL:-}
      OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-}
      OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-}

      SENTRY_ORG_SUBDOMAIN: o130137
      SENTRY_KEY: 3cf75f54983e473da6bd07daddf0d2ee
      SENTRY_PROJECT: "1298632"
      SENTRY_TRACE_RATE: "0.1"

      S3_SERVER: ${S3_SERVER:-}
      S3_ACCESS_KEY: ${S3_ACCESS_KEY:-}
      S3_SECRET_KEY: ${S3_SECRET_KEY:-}
      S3_BUCKET_NAME: ${S3_BUCKET_NAME:-}

      SESSION_LIFETIME: ${SESSION_LIFETIME:-86400}
    volumes:
      - secrets:/etc/secrets
    depends_on:
      secrets:
        condition: service_completed_successfully
      postgres14:
        condition: service_healthy
      mail:
        condition: service_started
      pyxform:
        condition: service_healthy
      enketo:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "nc -z 127.0.0.1 8383 || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 30
      start_period: 120s
    restart: always
    logging:
      driver: local

  admin-init:
    # One-shot, idempotent first-admin bootstrap using ODK's own backend tasks.
    # It creates/promotes only when missing; redeploys never reset an existing password.
    # Runtime Bash dollars are doubled here because Compose interpolates command strings.
    image: ghcr.io/getodk/central-service:v2026.2.4
    working_dir: /usr/odk
    environment:
      PGHOST: postgres14
      PGDATABASE: odk
      PGUSER: odk
      PGPASSWORD: ${SERVICE_PASSWORD_64_POSTGRES}
      PGAPPNAME: odkcentral-admin-init
      ODK_ADMIN_EMAIL: ${ODK_ADMIN_EMAIL:?}
      ODK_ADMIN_PASSWORD: ${SERVICE_PASSWORD_64_ODKADMIN}
      OIDC_ENABLED: ${OIDC_ENABLED:-false}
    command:
      - bash
      - -ec
      - |
          echo "Checking ODK Central administrator bootstrap..."

          if [[ "$$OIDC_ENABLED" != "true" ]]; then
            if [[ -z "$$ODK_ADMIN_PASSWORD" || $${#ODK_ADMIN_PASSWORD} -lt 10 ]]; then
              echo "ERROR: generated ODK administrator password is missing or too short." >&2
              exit 1
            fi
          fi

          until pg_isready -h "$$PGHOST" -U "$$PGUSER" -d "$$PGDATABASE" >/dev/null 2>&1; do
            sleep 2
          done

          any_admin="$$(
            printf '%s\n' "SELECT 1 FROM users u JOIN assignments a ON a.\"actorId\" = u.\"actorId\" JOIN roles r ON r.id = a.\"roleId\" WHERE r.system = 'admin' LIMIT 1;" \
              | psql -X -qAt -h "$$PGHOST" -U "$$PGUSER" -d "$$PGDATABASE" 2>/dev/null \
              || true
          )"

          if [[ "$$any_admin" = "1" ]]; then
            echo "An ODK Central administrator already exists; bootstrap is intentionally skipped."
            exit 0
          fi

          user_exists="$$(
            printf '%s\n' "SELECT 1 FROM users WHERE email = :'admin_email' LIMIT 1;" \
              | psql -X -qAt -h "$$PGHOST" -U "$$PGUSER" -d "$$PGDATABASE" \
                  -v admin_email="$$ODK_ADMIN_EMAIL" 2>/dev/null \
              || true
          )"

          trap 'rm -f /tmp/odk-central-admin-task.js' EXIT

          cat > /tmp/odk-central-admin-task.js <<'NODE'
          const { createUser, promoteUser } = require('/usr/odk/lib/task/account');

          const action = process.argv[2];
          const email = process.env.ODK_ADMIN_EMAIL;
          const password = process.env.OIDC_ENABLED === 'true' ? null : process.env.ODK_ADMIN_PASSWORD;

          let operation;
          if (action === 'create') {
            operation = createUser(email, password);
          } else if (action === 'promote') {
            operation = promoteUser(email);
          } else {
            console.error('Unknown admin bootstrap action:', action);
            process.exit(2);
          }

          operation
            .then(() => process.exit(0))
            .catch((err) => { console.error(err); process.exit(1); });
          NODE

          if [[ "$$user_exists" != "1" ]]; then
            echo "Creating initial ODK Central user: $$ODK_ADMIN_EMAIL"
            node /tmp/odk-central-admin-task.js create
          else
            echo "Initial ODK Central user already exists; leaving its password unchanged."
          fi

          # We already proved above that the installation has no administrator.
          # At this point the target user exists (newly created or pre-existing),
          # so promote it through ODK's own account task rather than editing role tables.
          echo "Promoting initial ODK Central user to administrator..."
          node /tmp/odk-central-admin-task.js promote

          echo "ODK Central administrator bootstrap complete."
    depends_on:
      service:
        condition: service_healthy
    restart: "no"
    exclude_from_hc: true

  nginx:
    # ODK's Nginx remains application infrastructure; Coolify only terminates TLS.
    image: ghcr.io/getodk/central-nginx:v2026.2.4
    environment:
      - SERVICE_URL_NGINX_80
      - DOMAIN=${SERVICE_FQDN_NGINX}
      - CERTBOT_EMAIL=${ODK_ADMIN_EMAIL:?}
      - SSL_TYPE=upstream
      - SENTRY_ORG_SUBDOMAIN=o130137
      - SENTRY_KEY=3cf75f54983e473da6bd07daddf0d2ee
      - SENTRY_PROJECT=1298632
      - OIDC_ENABLED=${OIDC_ENABLED:-false}
      - SENTRY_DSN_FRONTEND=${SENTRY_DSN_FRONTEND:-}
    volumes:
      # Upstream central-nginx expects these two templates as runtime mounts.
      - type: bind
        source: ./.coolify/odk/odk.conf.template
        target: /usr/share/odk/nginx/odk.conf.template
        read_only: true
        is_directory: false
        content: |
          server {
            listen 443 default_server ssl;
            server_tokens off;
          
            ssl_certificate /etc/nginx/ssl/nginx.default.crt;
            ssl_certificate_key /etc/nginx/ssl/nginx.default.key;
          
            return 421;
          }
          
          types {
            application/manifest+json webmanifest;
          }
          
          map "$request_method::$uri$is_args$args" $cache_strategy {
            # general
            ~^(GET|HEAD)::/client-config\.json$ "revalidate";
            ~^(GET|HEAD)::/robots\.txt$         "revalidate";
            ~^(GET|HEAD)::/version\.txt$        "revalidate";
          
            # central-backend
            ~^(GET|HEAD)::/v1/                 "passthrough";
          
            # central-frontend - unversioned
            ~^(GET|HEAD)::/$                            "revalidate";
            ~^(GET|HEAD)::/index\.html$                 "revalidate";
            ~^(GET|HEAD)::/android-chrome-192x192\.png$ "revalidate";
            ~^(GET|HEAD)::/android-chrome-512x512\.png$ "revalidate";
            ~^(GET|HEAD)::/apple-touch-icon\.png$       "revalidate";
            ~^(GET|HEAD)::/blank\.html$                 "revalidate";
            ~^(GET|HEAD)::/favicon\.ico$                "revalidate";
            ~^(GET|HEAD)::/favicon-16x16\.png$          "revalidate";
            ~^(GET|HEAD)::/favicon-32x32\.png$          "revalidate";
            ~^(GET|HEAD)::/fonts/.*\?\w+                "immutable";
            ~^(GET|HEAD)::/fonts/                       "revalidate";
            ~^(GET|HEAD)::/site\.webmanifest$           "revalidate";
          
            # central-frontend - versioned
            ~^(GET|HEAD)::/assets/ "immutable";
          
            # enketo
            ~^(GET|HEAD)::/-(/x)?/css/                 "revalidate";
            ~^(GET|HEAD)::/-(/x)?/fonts/.*\?v=         "immutable";
            ~^(GET|HEAD)::/-(/x)?/fonts/               "revalidate";
            ~^(GET|HEAD)::/-(/x)?/images/              "revalidate";
            ~^(GET|HEAD)::/-(/x)?/js/build/chunks/     "immutable";
            ~^(GET|HEAD)::/-(/x)?/js/build/            "revalidate";
            ~^(GET|HEAD)::/-(/x)?/locales/             "revalidate";
            ~^(GET|HEAD)::/-/x/[a-zA-Z0-9]+            "revalidate";
            ~^(GET|HEAD)::/-/x/offline-app-worker\.js$ "revalidate";
          
            default "single-use";
          }
          map $cache_strategy $cache_header_cache_control {
            "immutable"  "max-age=31536000";
            "revalidate" "no-cache";
            "passthrough"  "";
            default      "no-store";
          }
          map $cache_strategy $cache_header_pragma {
            "immutable"  "";
            "revalidate" "no-cache";
            "passthrough"  "";
            default      "no-cache";
          }
          map $cache_strategy $cache_header_vary {
            "immutable"  "Accept-Encoding";
            "revalidate" "Accept-Encoding";
            "passthrough"  "";
            default      "*";
          }
          
          map $args $qp_deliminator {
            ~.+ "&";
            default "?";
          }
          
          map $arg_st $redirect_non_single_prefix {
            ~.+     "${is_args}${args}${qp_deliminator}single=false";
            default "/new${is_args}${args}";
          }
          
          map $arg_st $redirect_single_prefix {
            ~.+     "${is_args}${args}";
            default "/new${is_args}${args}${qp_deliminator}single=true";
          }
          
          # Note: using $request_uri here remains safe while percent-encodings are not
          # normalised in frontend URLs.  Tracked at https://github.com/getodk/central/issues/1532
          map $request_uri $spa_name {
            # form routes: approximately /f/... and /projects/.../forms/...
            ~^/(?:f/[^/]+(?:/.*)?|projects/\d+/forms/[^/]+/(?:(?:draft/)?(?:preview|submissions/new(?:/offline)?)|submissions/[^/]+/edit)(?:/)?)(?:\?.*)?$
              form-wrapper;
            default
              root-app;
          }
          map $spa_name $spa_html {
            form-wrapper "/apps/forms/index.html";
            default      "/index.html";
          }
          map $spa_name $spa_csp {
            # Use 'none' per directive instead of falling back to default-src to make CSP violation reports more specific
          
            form-wrapper
              "default-src 'report-sample' 'none'; connect-src 'self' https:; font-src 'self' data:; form-action 'self'; frame-ancestors 'self'; frame-src 'self' https://getodk.github.io/central/; img-src blob: data: https:; manifest-src 'self'; media-src blob:; object-src 'none'; script-src 'report-sample' 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; worker-src 'report-sample' blob: data:; report-uri /csp-report";
          
            default
              "default-src 'report-sample' 'none'; connect-src 'self' ${SENTRY_DSN_FRONTEND_ROOT} https://translate.google.com https://translate.googleapis.com; font-src 'self'; form-action 'self'; frame-ancestors 'none'; frame-src 'self' https://getodk.github.io/central/; img-src data: https:; manifest-src 'self'; media-src 'none'; object-src 'none'; script-src 'report-sample' 'self'; style-src 'report-sample' 'self'; style-src-attr 'unsafe-inline'; worker-src 'report-sample' blob:; report-uri /csp-report";
          }
          
          map $upstream_http_content_security_policy $central_backend_csp {
            # pass through any Content-Security-Policy received from upstream services (central-backend, enketo)
            ""      "default-src 'report-sample' 'none'; form-action 'none'; frame-ancestors 'none'; img-src http://${DOMAIN}/favicon.ico; report-uri /csp-report";
            default $upstream_http_content_security_policy;
          }
          
          server {
            listen 443 ssl;
            http2 on;
            server_name ${DOMAIN};
          
            ssl_certificate /etc/${SSL_TYPE}/live/${CERT_DOMAIN}/fullchain.pem;
            ssl_certificate_key /etc/${SSL_TYPE}/live/${CERT_DOMAIN}/privkey.pem;
            ssl_trusted_certificate /etc/${SSL_TYPE}/live/${CERT_DOMAIN}/fullchain.pem;
          
            ssl_protocols TLSv1.2 TLSv1.3;
            ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
            ssl_prefer_server_ciphers off;
          
            ssl_dhparam /etc/dh/nginx.pem;
          
            server_tokens off;
          
            add_header Content-Security-Policy "default-src 'report-sample' 'none'; connect-src https://translate.google.com https://translate.googleapis.com; img-src https://translate.google.com; report-uri /csp-report" always;
            include /usr/share/odk/nginx/common-headers.conf;
          
            client_max_body_size 100m;
          
            gzip on;
            gzip_comp_level 6;
            gzip_vary on;
            gzip_min_length 1024;
            gzip_http_version 1.1;
            gzip_types text/plain text/css application/json application/geo+json application/x-javascript application/javascript text/xml application/xml text/csv image/svg+xml;
            gzip_proxied any;
          
            # Enketo Configuration.
            # Enketo express is traditionally served at /- but with the introduction of ODK Web Forms
            # we want old Enketo URLs redirected to a Central frontend page which dynamically decides
            # whether to show a WebForm or an iframed Enketo.
            #
            # Following are the locations that serve a Form and these are redirected to the frontend:
            location ~ "^/-/single/(?<enketoId>[a-zA-Z0-9]+)$" {
              # Form fill link, public
              # If 'st' query parameter is not present, redirect to protected route with single only
              # end-of-form behavior (/new?single=true)
              return 301 "/f/$enketoId$redirect_single_prefix";
            }
            location ~ "^/-/preview/(?<enketoId>[a-zA-Z0-9]+)$" {
              # preview link
              return 301 "/f/$enketoId/preview$is_args$args";
            }
            # The negative look ahead patterns in the following regex are for the Enketo endpoints which are
            # similar to the new submission endpoint i.e. /-/:enketoId but these are not enketoId, therefore
            # we don't want them to be redirected to central-frontend
            location ~ "^/-/(?!thanks$|connection$|login$|logout$|api$|preview$)(?<enketoId>[a-zA-Z0-9]+)$" {
              # Form fill link (non-public), or Draft
              # If 'st' query parameter is present, add ?single=false for public access
              return 301 "/f/$enketoId$redirect_non_single_prefix";
            }
            # To read single submission cookies
            location = /-/single/check-submitted {
              try_files $uri @blank.html;
            }
          
            # For that iframe to work, we'll need another path prefix (enketo-passthrough) under which we can
            # reach Enketo — this one will not be intercepted.
            location ~ ^/(?:-|enketo-passthrough)(?:/|$) {
              if ($args ~* "(^|&|;)(x|%78|%58)?(f|%66|%46)(o|%6f|%4f)(r|%72|%52)(m|%6d|%4d)(=[^;&]*)?(&|;|$)" ) {
                return 400;
              }
          
              rewrite ^/enketo-passthrough(/.*)?$ /-$1 break;
              proxy_pass http://enketo:8005;
              proxy_redirect off;
              proxy_set_header Host $host;
              proxy_hide_header Vary;
              proxy_hide_header Cache-Control;
          
              # More lax CSP for enketo-express:
              # Google Maps API: https://developers.google.com/maps/documentation/javascript/content-security-policy
              # Use 'none' per directive instead of falling back to default-src to make CSP violation reports more specific
              proxy_hide_header Content-Security-Policy;
              proxy_hide_header Content-Security-Policy-Report-Only;
              add_header Content-Security-Policy "default-src 'report-sample' 'none'; connect-src 'self' blob: https://maps.googleapis.com/ https://maps.google.com/ https://maps.gstatic.com/mapfiles/ https://fonts.gstatic.com/ https://fonts.googleapis.com/ https://translate.google.com https://translate.googleapis.com; font-src 'self' https://fonts.gstatic.com/; form-action 'self'; frame-ancestors 'self'; frame-src 'none'; img-src data: blob: jr: 'self' https://maps.google.com/maps/ https://maps.gstatic.com/mapfiles/ https://maps.googleapis.com/maps/ https://tile.openstreetmap.org/ https://translate.google.com; manifest-src 'none'; media-src blob: jr: 'self'; object-src 'none'; script-src 'report-sample' 'unsafe-inline' 'self' https://maps.googleapis.com/maps/api/js/ https://maps.google.com/maps/ https://maps.google.com/maps-api-v3/api/js/; style-src 'unsafe-inline' 'self' https://fonts.googleapis.com/css; style-src-attr 'unsafe-inline'; report-uri /csp-report" always;
          
              include /usr/share/odk/nginx/common-headers.conf;
            }
            # End of Enketo Configuration.
          
            # central-backend
            location ~ ^/v\d {
              proxy_hide_header Content-Security-Policy;
              add_header Content-Security-Policy $central_backend_csp always;
          
              include /usr/share/odk/nginx/common-headers.conf;
          
              proxy_set_header X-Forwarded-Proto $scheme;
              proxy_pass http://service:8383;
              proxy_redirect off;
          
              # buffer requests, but not responses, so streaming out works.
              proxy_request_buffering on;
              proxy_buffering off;
              proxy_read_timeout 2m;
            }
          
            location @blank.html {
              root /usr/share/nginx/html;
              try_files /blank.html =404;
          
              add_header Content-Security-Policy "default-src 'report-sample' 'none'; connect-src https://translate.google.com https://translate.googleapis.com; form-action 'self'; frame-ancestors 'self'; img-src http://${DOMAIN}/favicon.ico https://translate.google.com; report-uri /csp-report" always;
              include /usr/share/odk/nginx/common-headers.conf;
            }
            location = /blank.html {
              try_files $uri @blank.html;
            }
          
            # central-frontend
            location / {
              root /usr/share/nginx/html;
              try_files $uri $uri/ $spa_html;
          
              add_header Content-Security-Policy "$spa_csp" always;
          
              include /usr/share/odk/nginx/common-headers.conf;
            }
          
            location /csp-report {
              proxy_pass https://${SENTRY_ORG_SUBDOMAIN}.ingest.sentry.io/api/${SENTRY_PROJECT}/security/?sentry_key=${SENTRY_KEY};
              proxy_ssl_server_name on;
            }
          }
      - type: bind
        source: ./.coolify/odk/client-config.json.template
        target: /usr/share/odk/nginx/client-config.json.template
        read_only: true
        is_directory: false
        content: |
          {
            "oidcEnabled": ${OIDC_ENABLED},
            "sentryDsn": "${SENTRY_DSN_FRONTEND}"
          }

    depends_on:
      service:
        condition: service_healthy
      enketo:
        condition: service_healthy
      admin-init:
        condition: service_completed_successfully
    healthcheck:
      test: ["CMD-SHELL", "nc -z localhost 80 || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 20
      start_period: 20s
    restart: always
    logging:
      driver: local
      options:
        max-file: "30"

volumes:
  # Authoritative ODK Central application database.
  postgres14:
  # Preserved only for compatibility with the upstream 9.6 -> 14 helper.
  postgres96_legacy:
  # Upgrade logs/control state; the PG14 success marker itself lives in postgres14.
  postgres14_upgrade:
  # Enketo encryption/API keys; preserve across every normal redeploy and restore.
  secrets:
  # Enketo's authoritative Redis state; preserve for existing Web Form links.
  enketo_redis_main:
  # Enketo XSLT cache; upstream persists it and full-system restore should include it.
  enketo_redis_cache:
````

<!-- END PORTABLE RESOURCE: assets/odk-central-v2026.2.4-v1.0.0-golden.yml -->

<!-- BEGIN PORTABLE RESOURCE: assets/openemr-8.3.0-v1.0.0-golden.yml -->
<!-- SOURCE SHA256: e029e268153ce1ce42e69baca2dbeda898c609dc16e8b543a257e863683ca990 -->
<!-- EMBEDDED SHA256: 5859dd1dc455835620129b165adc138b148f6f17393d19e864aa6ac8c3977dd3 -->

## Portable resource: `assets/openemr-8.3.0-v1.0.0-golden.yml`

````yaml
# OpenEMR regression fixture / golden case — NOT a generic Coolify skeleton.
# OpenEMR 8.3.0 / Coolify accepted regression fixture v1.0.0
# Upstream baseline: openemr/openemr rel-830 docker/production/docker-compose.yml
services:
  mysql:
    image: mariadb:11.8.8@sha256:d9f7eb2637296652f24b484afd5d246f759f49f5babcadc6a9e344c9acb75fbf
    restart: always
    command:
      - mariadbd
      - --character-set-server=utf8mb4
    environment:
      MYSQL_ROOT_PASSWORD: ${SERVICE_PASSWORD_64_OPENEMRDBROOT}
    volumes:
      - databasevolume:/var/lib/mysql
    healthcheck:
      test:
        - CMD
        - /usr/local/bin/healthcheck.sh
        - --su-mysql
        - --connect
        - --innodb_initialized
      start_period: 1m
      start_interval: 10s
      interval: 1m
      timeout: 5s
      retries: 3

  openemr:
    image: openemr/openemr:8.3.0-2026-08-29
    restart: always
    environment:
      SERVICE_URL_OPENEMR_80: ${SERVICE_URL_OPENEMR_80}
      MYSQL_HOST: mysql
      MYSQL_ROOT_PASS: ${SERVICE_PASSWORD_64_OPENEMRDBROOT}
      MYSQL_USER: openemr
      MYSQL_PASS: ${SERVICE_PASSWORD_64_OPENEMRDB}
      OE_USER: admin
      OE_PASS: ${SERVICE_PASSWORD_64_OPENEMRADMIN}
    volumes:
      - logvolume01:/var/log
      - sitevolume:/var/www/localhost/htdocs/openemr/sites
    depends_on:
      mysql:
        condition: service_healthy
    healthcheck:
      test:
        - CMD
        - /usr/bin/curl
        - --fail
        - --insecure
        - --location
        - --show-error
        - --silent
        - https://localhost/meta/health/readyz
      start_period: 3m
      start_interval: 10s
      interval: 1m
      timeout: 5s
      retries: 3

volumes:
  logvolume01: {}
  sitevolume: {}
  databasevolume: {}
````

<!-- END PORTABLE RESOURCE: assets/openemr-8.3.0-v1.0.0-golden.yml -->

<!-- BEGIN PORTABLE RESOURCE: assets/openmrs-3.7.1-v1.0.0-golden.yml -->
<!-- SOURCE SHA256: 6c03683f5d64290acba97e968ea5d55aee4164eba95963bc02ae832810bf90a3 -->
<!-- EMBEDDED SHA256: 714de8c5fd88b554c70cc1ebf606f220d9ff485b1160528a070f569428617e63 -->

## Portable resource: `assets/openmrs-3.7.1-v1.0.0-golden.yml`

````yaml
# OpenMRS regression fixture / golden case — NOT a generic Coolify skeleton.
# Coolify template revision: 1.0.0
# OpenMRS Reference Application / O3 3.7.1
# This fixture preserves the executable Compose that passed the OpenMRS Coolify benchmark.
# The benchmark operator subsequently confirmed the final acceptance run succeeded.
# Upstream topology preserved: gateway + frontend + backend + MariaDB.
# MYSQL, MYSQLROOT and ADMIN are the exact magic-variable identifiers validated in this
# deployment; they are not a universal naming rule for other applications.

x-logging: &default-logging
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

services:
  gateway:
    image: openmrs/openmrs-reference-application-3-gateway:3.7.1
    restart: unless-stopped
    depends_on:
      frontend:
        condition: service_started
      backend:
        condition: service_started
    environment:
      - SERVICE_URL_GATEWAY_80
      - 'FRAME_ANCESTORS=${OPENMRS_FRAME_ANCESTORS:-}'
    expose:
      - "80"
    healthcheck:
      test:
        - CMD-SHELL
        - >-
          wget -q -O /dev/null http://127.0.0.1/openmrs/health/started &&
          wget -q -O /dev/null http://127.0.0.1/openmrs/spa/home || exit 1
      interval: 15s
      timeout: 10s
      retries: 10
      start_period: 15m
    logging: *default-logging

  frontend:
    image: openmrs/openmrs-reference-application-3-frontend:3.7.1
    restart: unless-stopped
    depends_on:
      backend:
        condition: service_started
    environment:
      - SPA_PATH=/openmrs/spa
      - API_URL=/openmrs
      - SPA_CONFIG_URLS=/openmrs/spa/config-core_demo.json
      - 'SPA_DEFAULT_LOCALE=${OPENMRS_SPA_DEFAULT_LOCALE:-en_GB}'
    expose:
      - "80"
    healthcheck:
      test:
        - CMD-SHELL
        - wget -q -O /dev/null http://127.0.0.1/ || exit 1
      interval: 15s
      timeout: 5s
      retries: 10
      start_period: 30s
    logging: *default-logging

  backend:
    image: openmrs/openmrs-reference-application-3-backend:3.7.1
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      - 'OMRS_MODULE_WEB_ADMIN=${OPENMRS_MODULE_WEB_ADMIN:-true}'
      - OMRS_AUTO_UPDATE_DATABASE=true
      - OMRS_CREATE_TABLES=true
      - OMRS_INSTALL_METHOD=auto
      - OMRS_DB_HOSTNAME=db
      - OMRS_DB_NAME=openmrs
      - 'OMRS_DB_USERNAME=${SERVICE_USER_MYSQL}'
      - 'OMRS_DB_PASSWORD=${SERVICE_PASSWORD_64_MYSQL}'
      # OpenMRS' bootstrap username is upstream-defined as "admin".
      - 'OMRS_ADMIN_USER_PASSWORD=${SERVICE_PASSWORD_64_ADMIN}'
      - OMRS_ADMIN_PASSWORD_LOCKED=false
    # OpenMRS may start mutating a fresh schema before rejecting a bad admin password.
    # Fail before /openmrs/startup.sh if a credential is unexpectedly empty/invalid.
    command:
      - /bin/bash
      - -ec
      - |
        password="$${OMRS_ADMIN_USER_PASSWORD:-}"
        fail() { echo "OpenMRS credential preflight failed: $$1" >&2; exit 64; }
        [[ -n "$${OMRS_DB_USERNAME:-}" ]] || fail "database username is empty"
        [[ -n "$${OMRS_DB_PASSWORD:-}" ]] || fail "database password is empty"
        [[ -n "$$password" ]] || fail "admin password is empty"
        [[ $${#password} -ge 8 ]] || fail "admin password must contain at least 8 characters"
        [[ "$$password" =~ [A-Z] ]] || fail "admin password requires an uppercase letter"
        [[ "$$password" =~ [a-z] ]] || fail "admin password requires a lowercase letter"
        [[ "$$password" =~ [0-9] ]] || fail "admin password requires a digit"
        [[ "$$password" != "admin" && "$$password" != "test" && "$$password" != "Admin123" ]] || fail "admin password must not be a default password"
        exec /openmrs/startup.sh
    expose:
      - "8080"
    volumes:
      - openmrs-data:/openmrs/data
    healthcheck:
      test:
        - CMD-SHELL
        - curl -fsS http://127.0.0.1:8080/openmrs/health/started >/dev/null || exit 1
      interval: 15s
      timeout: 10s
      retries: 10
      start_period: 15m
    logging: *default-logging

  db:
    image: mariadb:10.11.7
    restart: unless-stopped
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
    environment:
      - MYSQL_DATABASE=openmrs
      - 'MYSQL_USER=${SERVICE_USER_MYSQL}'
      - 'MYSQL_PASSWORD=${SERVICE_PASSWORD_64_MYSQL}'
      - 'MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_64_MYSQLROOT}'
    volumes:
      - db-data:/var/lib/mysql
    healthcheck:
      test:
        - CMD
        - healthcheck.sh
        - --connect
        - --innodb_initialized
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 30s
    logging: *default-logging

volumes:
  openmrs-data:
  db-data:
````

<!-- END PORTABLE RESOURCE: assets/openmrs-3.7.1-v1.0.0-golden.yml -->

<!-- BEGIN PORTABLE RESOURCE: assets/openspp-2026.08-v1.0.0-golden.yml -->
<!-- SOURCE SHA256: f00a8755fa2be8e8b1f50970978ae1b57c1877093c2a35108edf35a675d4587b -->
<!-- EMBEDDED SHA256: a0a33402aa3851ea12003ea8a3c1df1d03ab96e8cebd95ef05f23d5f158ada97 -->

## Portable resource: `assets/openspp-2026.08-v1.0.0-golden.yml`

````yaml
# documentation: https://docs.openspp.org/ops_guide/deployment/production-hardening
# slogan: Production-hardened OpenSPP V2 single-node deployment for Coolify.
# category: government
# tags: openspp,social-protection,registry,odoo,postgis
# port: 8080
#
# OpenSPP V2 2026.08 / Odoo 19 / PostgreSQL 18 + PostGIS 3.6
# Coolify candidate: v1.0.0-rc8
#
# NOTE: Coolify owns Internet TLS and hostname routing. The `openspp` Nginx service
# remains as an application-semantic gateway for HTTP/WebSocket routing, hardening,
# rate limiting, secure cookie flags, and database-manager blocking.
# RC8 keeps the unique Coolify-managed Nginx file introduced after RC6, but fixes
# the RC7 escaping bug: managed-file `content:` is written literally by Coolify, so
# Nginx variables must use a single `$` (not Compose `$$`). A small, explicitly
# whitelisted envsubst render step substitutes only the four operator-tunable gateway
# values and leaves native Nginx variables untouched.

x-default-logging: &default-logging
  driver: json-file
  options:
    max-size: "${LOG_MAX_SIZE:-5m}"
    max-file: "${LOG_MAX_FILES:-3}"

x-openspp-environment: &openspp-environment
  DB_HOST: db
  DB_PORT: "5432"
  DB_USER: odoo
  DB_PASSWORD: ${SERVICE_PASSWORD_64_OPENSPPDBODOO:?OpenSPP database password was not generated}
  DB_NAME: openspp
  DB_SSLMODE: prefer
  DB_FILTER: "^openspp$"
  LIST_DB: "False"
  ODOO_ADMIN_PASSWD: ${SERVICE_PASSWORD_64_OPENSPPMASTER:?OpenSPP admin/master password was not generated}
  PROXY_MODE: "True"
  LOG_LEVEL: ${LOG_LEVEL:-warn}

# Compatibility shim for OpenSPP 2026.08 with the current OCA/server-backend 19.0
# dependency fetched by the upstream Dockerfile. OCA renamed res.users.role_ids to
# res.users.user_role_ids; the released OpenSPP XML still targets role_ids.
x-spp-user-roles-compat: &spp_user_roles_compat
  type: bind
  source: ./coolify/openspp/compat/spp_user_roles_user.xml
  is_directory: false
  target: /mnt/extra-addons/openspp/spp_user_roles/views/user.xml
  read_only: true
  content: |
    <odoo>
        <record id="view_res_users_form_inherit_spp_user_roles" model="ir.ui.view">
            <field name="name">res.users.form.inherit.spp.user.roles</field>
            <field name="model">res.users</field>
            <field name="inherit_id" ref="base_user_role.view_res_users_form_inherit" />
            <field name="arch" type="xml">
                <xpath
                    expr="//field[@name='role_line_ids']/list/field[@name='role_id']"
                    position="after"
                >
                    <field name="role_type" invisible="1" />
                </xpath>
            </field>
        </record>

        <record id="view_res_users_tree_inherit_spp_user_roles" model="ir.ui.view">
            <field name="name">res.users.list.inherit.spp.user.roles</field>
            <field name="model">res.users</field>
            <field name="inherit_id" ref="base_user_role.view_res_users_tree_inherit" />
            <field name="priority">99</field>
            <field name="arch" type="xml">
                <xpath expr="//field[@name='user_role_ids']" position="replace">
                    <field name="role_ids_stored" widget="many2many_tags" optional="show" />
                </xpath>
            </field>
        </record>

        <record id="view_res_users_tree_hide_role_spp_user_roles" model="ir.ui.view">
            <field name="name">res.users.list.hide.role</field>
            <field name="model">res.users</field>
            <field name="inherit_id" ref="base.view_users_tree" />
            <field name="arch" type="xml">
                <xpath expr="//field[@name='role']" position="attributes">
                    <attribute name="column_invisible">1</attribute>
                </xpath>
            </field>
        </record>
    </odoo>

x-spp-area-compat: &spp_area_compat
  type: bind
  source: ./coolify/openspp/compat/spp_area_user.xml
  is_directory: false
  target: /mnt/extra-addons/openspp/spp_area/views/user.xml
  read_only: true
  content: |
    <odoo>
        <record id="view_res_users_form_inherit_spp_area" model="ir.ui.view">
            <field name="name">res.users.form.inherit.spp.area</field>
            <field name="model">res.users</field>
            <field name="inherit_id" ref="base_user_role.view_res_users_form_inherit" />
            <field name="arch" type="xml">
                <xpath
                    expr="//field[@name='role_line_ids']/list/field[@name='role_id']"
                    position="after"
                >
                    <field
                        name="local_area_ids"
                        widget="many2many_tags"
                        options="{'no_open':True,'no_create':True,'no_edit':True}"
                        readonly="role_type != 'local'"
                        invisible="role_type != 'local'"
                    />
                </xpath>
                <xpath expr="//field[@name='user_role_ids']" position="before">
                    <group colspan="4" col="4">
                        <field
                            name="center_area_ids"
                            options="{'no_open':True,'no_create':True,'no_edit':True}"
                            readonly="1"
                            widget="many2many_tags"
                        />
                    </group>
                </xpath>
                <xpath
                    expr="//field[@name='role_line_ids']/list/field[@name='role_id']"
                    position="attributes"
                >
                    <attribute name="options">{'no_open':True,'no_create':True,'no_edit':True}</attribute>
                    <attribute name="readonly">local_area_ids</attribute>
                    <attribute name="force_save">True</attribute>
                </xpath>
            </field>
        </record>
    </odoo>


services:
  openspp:
    image: nginx:1.30.4-alpine
    depends_on:
      odoo:
        condition: service_healthy
    environment:
    - SERVICE_URL_OPENSPP_8080
    - OPENSPP_RATE_LIMIT=${OPENSPP_RATE_LIMIT:-50r/m}
    - OPENSPP_RATE_BURST=${OPENSPP_RATE_BURST:-100}
    - OPENSPP_CLIENT_MAX_BODY_SIZE=${OPENSPP_CLIENT_MAX_BODY_SIZE:-100m}
    - OPENSPP_PROXY_TIMEOUT=${OPENSPP_PROXY_TIMEOUT:-720s}
    expose:
    - "8080"
    volumes:
    - type: bind
      source: ./coolify/openspp/rc8/nginx/openspp.conf.template
      is_directory: false
      target: /etc/nginx/openspp-rc8.conf.template
      read_only: true
      content: |
        user nginx;
        worker_processes auto;
        pid /run/nginx.pid;
        error_log /dev/stderr warn;
        
        events {
            worker_connections 1024;
        }
        
        http {
            include /etc/nginx/mime.types;
            default_type application/octet-stream;
        
            log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                            '$status $body_bytes_sent "$http_referer" '
                            '"$http_user_agent"';
        
            map $http_upgrade $connection_upgrade {
                default upgrade;
                ''      close;
            }
        
            # Coolify is the only intended upstream proxy. Trust private/container ranges
            # so rate limiting keys on the real client rather than the proxy address.
            set_real_ip_from 10.0.0.0/8;
            set_real_ip_from 172.16.0.0/12;
            set_real_ip_from 192.168.0.0/16;
            set_real_ip_from fc00::/7;
            real_ip_header X-Forwarded-For;
            real_ip_recursive on;
        
            limit_req_zone $binary_remote_addr zone=openspp_general:10m rate=${OPENSPP_RATE_LIMIT};
        
            client_body_temp_path /var/cache/nginx/client_temp;
            proxy_temp_path /var/cache/nginx/proxy_temp;
            fastcgi_temp_path /var/cache/nginx/fastcgi_temp;
            uwsgi_temp_path /var/cache/nginx/uwsgi_temp;
            scgi_temp_path /var/cache/nginx/scgi_temp;
        
            upstream odoo_http {
                server odoo:8069;
                keepalive 32;
            }
        
            upstream odoo_gevent {
                server odoo:8072;
                keepalive 16;
            }
        
            server {
                listen 8080;
                server_name _;
                server_tokens off;
                absolute_redirect off;
                port_in_redirect off;
        
                access_log /dev/stdout main;
        
                client_max_body_size ${OPENSPP_CLIENT_MAX_BODY_SIZE};
                gzip on;
                gzip_types text/css text/scss text/plain text/xml application/xml application/json application/javascript;
                limit_req_status 429;
        
                add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
                add_header X-Content-Type-Options "nosniff" always;
                add_header X-Frame-Options "DENY" always;
                add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
                add_header Referrer-Policy "strict-origin-when-cross-origin" always;
                add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
                add_header X-XSS-Protection "1; mode=block" always;
        
                proxy_hide_header Server;
                proxy_hide_header X-Powered-By;
                proxy_cookie_flags session_id secure httponly samesite=lax;
        
                location = /__coolify_health {
                    access_log off;
                    add_header Content-Type text/plain;
                    return 200 "ok\n";
                }
        
                # Single-database production entry. Explicit db selection establishes
                # Odoo session.db without exposing the database selector/manager.
                location = / {
                    return 302 /web/login?db=openspp;
                }
        
                location = /web/database/selector {
                    return 302 /web/login?db=openspp;
                }
        
                location ^~ /web/database {
                    return 404;
                }
        
                location /websocket {
                    proxy_pass http://odoo_gevent;
                    proxy_http_version 1.1;
                    proxy_set_header Upgrade $http_upgrade;
                    proxy_set_header Connection $connection_upgrade;
                    proxy_set_header Host $host;
                    proxy_set_header X-Real-IP $remote_addr;
                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                    proxy_set_header X-Forwarded-Host $host;
                    proxy_set_header X-Forwarded-Proto https;
                    proxy_set_header X-Forwarded-Port 443;
                    proxy_read_timeout ${OPENSPP_PROXY_TIMEOUT};
                    proxy_send_timeout ${OPENSPP_PROXY_TIMEOUT};
                }
        
                location / {
                    limit_req zone=openspp_general burst=${OPENSPP_RATE_BURST} nodelay;
                    proxy_pass http://odoo_http;
                    proxy_http_version 1.1;
                    proxy_set_header Host $host;
                    proxy_set_header X-Real-IP $remote_addr;
                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                    proxy_set_header X-Forwarded-Host $host;
                    proxy_set_header X-Forwarded-Proto https;
                    proxy_set_header X-Forwarded-Port 443;
                    proxy_connect_timeout 60s;
                    proxy_read_timeout ${OPENSPP_PROXY_TIMEOUT};
                    proxy_send_timeout ${OPENSPP_PROXY_TIMEOUT};
                    proxy_buffering on;
                    proxy_buffer_size 16k;
                    proxy_buffers 16 64k;
                    proxy_busy_buffers_size 128k;
                }
            }
        }
    restart: unless-stopped
    read_only: true
    tmpfs:
    - /tmp:size=10M
    - /var/cache/nginx:size=50M
    - /run:size=5M
    cap_drop:
    - ALL
    cap_add:
    - CHOWN
    - SETGID
    - SETUID
    security_opt:
    - no-new-privileges:true
    logging: *default-logging
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/web/health >/dev/null || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s
    deploy:
      resources:
        limits:
          cpus: "${GATEWAY_CPU_LIMIT:-1}"
          memory: ${GATEWAY_MEMORY_LIMIT:-256M}
        reservations:
          memory: ${GATEWAY_MEMORY_RESERVATION:-64M}

    entrypoint:
    - /bin/sh
    - -ec
    command:
    - |
      envsubst '$${OPENSPP_RATE_LIMIT} $${OPENSPP_RATE_BURST} $${OPENSPP_CLIENT_MAX_BODY_SIZE} $${OPENSPP_PROXY_TIMEOUT}'         < /etc/nginx/openspp-rc8.conf.template         > /tmp/openspp-rc8.conf
      nginx -t -c /tmp/openspp-rc8.conf
      exec nginx -c /tmp/openspp-rc8.conf -g 'daemon off;'
  db:
    image: postgis/postgis:18-3.6-alpine
    environment:
      POSTGRES_USER: openspp_admin
      POSTGRES_PASSWORD: ${SERVICE_PASSWORD_64_OPENSPPDBADMIN:?OpenSPP database admin password was not generated}
      POSTGRES_DB: openspp
      POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
      ODOO_DB_PASSWORD: ${SERVICE_PASSWORD_64_OPENSPPDBODOO:?OpenSPP database password was not generated}
    volumes:
    - postgres_data:/var/lib/postgresql
    - type: bind
      source: ./coolify/openspp/initdb/20-openspp-roles.sh
      is_directory: false
      target: /docker-entrypoint-initdb.d/20-openspp-roles.sh
      read_only: true
      content: |
        #!/bin/sh
        set -eu

        : "${POSTGRES_USER:?POSTGRES_USER is required}"
        : "${POSTGRES_DB:?POSTGRES_DB is required}"
        : "${ODOO_DB_PASSWORD:?ODOO_DB_PASSWORD is required}"

        psql -v ON_ERROR_STOP=1 \
          --username "$POSTGRES_USER" \
          --dbname postgres \
          --set=odoo_password="$ODOO_DB_PASSWORD" <<'SQL'
        SELECT format(
            'CREATE ROLE odoo LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION',
            :'odoo_password'
        )
        WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'odoo')
        \gexec
        SQL

        psql -v ON_ERROR_STOP=1 \
          --username "$POSTGRES_USER" \
          --dbname "$POSTGRES_DB" <<'SQL'
        CREATE EXTENSION IF NOT EXISTS postgis;
        GRANT CONNECT, CREATE, TEMPORARY ON DATABASE openspp TO odoo;
        GRANT USAGE, CREATE ON SCHEMA public TO odoo;
        SQL
    restart: unless-stopped
    read_only: true
    tmpfs:
    - /tmp:size=256M
    - /run:size=5M
    cap_drop:
    - ALL
    cap_add:
    - CHOWN
    - DAC_OVERRIDE
    - FOWNER
    - SETGID
    - SETUID
    security_opt:
    - no-new-privileges:true
    logging: *default-logging
    healthcheck:
      test:
      - CMD-SHELL
      - >-
        pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}" &&
        test "$$(psql -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}" -tAc
        "SELECT (EXISTS (SELECT 1 FROM pg_roles WHERE rolname='odoo' AND NOT rolsuper AND NOT rolcreatedb AND NOT rolcreaterole) AND EXISTS (SELECT 1 FROM pg_extension WHERE extname='postgis'))::int")" = "1"
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 20s
    deploy:
      resources:
        limits:
          cpus: "${DB_CPU_LIMIT:-2}"
          memory: ${DB_MEMORY_LIMIT:-4G}
        reservations:
          memory: ${DB_MEMORY_RESERVATION:-2G}

  odoo:
    image: openspp-coolify:2026.08
    # Coolify runs `docker compose pull` before `up --build`; this image exists only locally.
    pull_policy: never
    build:
      context: "https://github.com/OpenSPP/OpenSPP2.git#208d97582791b369b562cdfcb3e41766a2be710f"
      dockerfile: docker/Dockerfile
      target: production
    depends_on:
      db:
        condition: service_healthy
    environment:
      <<: *openspp-environment
      ODOO_WORKERS: ${ODOO_WORKERS:-2}
      ODOO_CRON_THREADS: ${ODOO_CRON_THREADS:-1}
      ODOO_MEMORY_SOFT: ${ODOO_MEMORY_SOFT:-2147483648}
      ODOO_MEMORY_HARD: ${ODOO_MEMORY_HARD:-2684354560}
      ODOO_DB_MAXCONN: ${ODOO_DB_MAXCONN:-128}
      ODOO_LIMIT_REQUEST: ${ODOO_LIMIT_REQUEST:-8192}
      ODOO_TIME_CPU: ${ODOO_TIME_CPU:-600}
      ODOO_TIME_REAL: ${ODOO_TIME_REAL:-1200}
      ODOO_INIT_MODULES: ${ODOO_INIT_MODULES:-spp_starter_sp_mis}
      ODOO_UPDATE_MODULES: ${ODOO_UPDATE_MODULES:-}
      # Current upstream sets DB_FILTER but its 2026.08 template does not render it;
      # the native entrypoint supports ODOO_EXTRA_ARGS, so enforce the exact filter.
      ODOO_EXTRA_ARGS: "--db-filter=^openspp$"
    volumes:
    - odoo_data:/var/lib/odoo
    - *spp_user_roles_compat
    - *spp_area_compat
    restart: unless-stopped
    tmpfs:
    - /tmp:size=256M
    - /run:size=5M
    cap_drop:
    - ALL
    cap_add:
    - CHOWN
    - FOWNER
    - SETGID
    - SETUID
    security_opt:
    - no-new-privileges:true
    logging: *default-logging
    healthcheck:
      test:
      - CMD-SHELL
      - >-
        curl -fsS http://localhost:8069/web/health >/dev/null &&
        test "$$(PGPASSWORD="$${DB_PASSWORD}" psql -h "$${DB_HOST}" -p "$${DB_PORT}" -U "$${DB_USER}" -d "$${DB_NAME}" -tAc
        "SELECT CASE WHEN EXISTS (SELECT 1 FROM ir_module_module WHERE name='spp_starter_sp_mis' AND state='installed') THEN 1 ELSE 0 END")" = "1"
      interval: 15s
      timeout: 10s
      start_period: 300s
      retries: 20
    stop_grace_period: 60s
    deploy:
      resources:
        limits:
          cpus: "${ODOO_CPU_LIMIT:-2}"
          memory: ${ODOO_MEMORY_LIMIT:-4G}
        reservations:
          memory: ${ODOO_MEMORY_RESERVATION:-2G}

  queue-worker:
    image: openspp-coolify:2026.08
    # Coolify runs `docker compose pull` before `up --build`; this image exists only locally.
    pull_policy: never
    build:
      context: "https://github.com/OpenSPP/OpenSPP2.git#208d97582791b369b562cdfcb3e41766a2be710f"
      dockerfile: docker/Dockerfile
      target: production
    depends_on:
      db:
        condition: service_healthy
      odoo:
        condition: service_healthy
    environment:
      <<: *openspp-environment
      ODOO_WORKERS: "0"
      ODOO_CRON_THREADS: "0"
      ODOO_MEMORY_SOFT: ${QUEUE_ODOO_MEMORY_SOFT:-2147483648}
      ODOO_MEMORY_HARD: ${QUEUE_ODOO_MEMORY_HARD:-2684354560}
      ODOO_DB_MAXCONN: ${QUEUE_ODOO_DB_MAXCONN:-32}
      ODOO_LIMIT_REQUEST: ${QUEUE_ODOO_LIMIT_REQUEST:-8192}
      ODOO_TIME_CPU: ${QUEUE_ODOO_TIME_CPU:-600}
      ODOO_TIME_REAL: ${QUEUE_ODOO_TIME_REAL:-1200}
    command:
    - python
    - /mnt/extra-addons/odoo-job-worker/job_worker_runner.py
    - -c
    - /etc/odoo/odoo.conf
    volumes:
    - odoo_data:/var/lib/odoo
    - *spp_user_roles_compat
    - *spp_area_compat
    restart: unless-stopped
    tmpfs:
    - /tmp:size=256M
    - /run:size=5M
    cap_drop:
    - ALL
    cap_add:
    - CHOWN
    - FOWNER
    - SETGID
    - SETUID
    security_opt:
    - no-new-privileges:true
    logging: *default-logging
    healthcheck:
      test:
      - CMD-SHELL
      - >-
        python /mnt/extra-addons/odoo-job-worker/job_worker_healthcheck.py
      interval: 30s
      timeout: 10s
      start_period: 60s
      retries: 3
    stop_grace_period: 60s
    deploy:
      resources:
        limits:
          cpus: "${QUEUE_CPU_LIMIT:-1}"
          memory: ${QUEUE_MEMORY_LIMIT:-2G}
        reservations:
          memory: ${QUEUE_MEMORY_RESERVATION:-1G}

  backup:
    image: postgis/postgis:18-3.6-alpine
    depends_on:
      db:
        condition: service_healthy
    environment:
      PGHOST: db
      PGPORT: "5432"
      PGDATABASE: openspp
      PGUSER: odoo
      PGPASSWORD: ${SERVICE_PASSWORD_64_OPENSPPDBODOO:?OpenSPP database password was not generated}
      BACKUP_SCHEDULE: ${BACKUP_SCHEDULE:-0 2 * * *}
      BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-7}
      BACKUP_KEEP_WEEKS: ${BACKUP_KEEP_WEEKS:-4}
      BACKUP_KEEP_MONTHS: ${BACKUP_KEEP_MONTHS:-6}
      OPENSPP_VERSION: "2026.08"
      TZ: ${TZ:-UTC}
    entrypoint:
    - /backup-entrypoint.sh
    volumes:
    - backup_data:/backups
    - odoo_data:/var/lib/odoo:ro
    - type: bind
      source: ./coolify/openspp/backup/backup.sh
      is_directory: false
      target: /backup.sh
      read_only: true
      content: |
        #!/bin/sh
        set -eu

        umask 077
        : "${PGHOST:?PGHOST is required}"
        : "${PGPORT:?PGPORT is required}"
        : "${PGDATABASE:?PGDATABASE is required}"
        : "${PGUSER:?PGUSER is required}"
        : "${PGPASSWORD:?PGPASSWORD is required}"

        now="$(date -u +%Y%m%dT%H%M%SZ)"
        daily_root=/backups/daily
        weekly_root=/backups/weekly
        monthly_root=/backups/monthly
        set_dir="${daily_root}/${PGDATABASE}_${now}"
        filestore="/var/lib/odoo/filestore/${PGDATABASE}"

        mkdir -p "$set_dir" "$weekly_root" "$monthly_root"

        echo "[backup] creating PostgreSQL/PostGIS dump: $set_dir/database.dump"
        pg_dump --format=custom --compress=6 --file="$set_dir/database.dump"

        if [ -d "$filestore" ]; then
            echo "[backup] creating filestore archive"
            tar -C /var/lib/odoo -czf "$set_dir/filestore.tar.gz" "filestore/${PGDATABASE}"
        else
            echo "[backup] filestore directory is absent; recording an explicit marker"
            : > "$set_dir/filestore.absent"
        fi

        cat > "$set_dir/manifest.txt" <<EOF
        openspp_version=${OPENSPP_VERSION}
        database=${PGDATABASE}
        created_utc=${now}
        backup_mode=online
        note=For a coherent pre-upgrade recovery point, stop public/app/queue writers before invoking /backup.sh manually.
        EOF

        (
            cd "$set_dir"
            sha256sum database.dump manifest.txt > SHA256SUMS
            if [ -f filestore.tar.gz ]; then
                sha256sum filestore.tar.gz >> SHA256SUMS
            else
                sha256sum filestore.absent >> SHA256SUMS
            fi
        )

        day_of_week="$(date -u +%u)"
        day_of_month="$(date -u +%d)"
        if [ "$day_of_week" = "7" ]; then
            cp -a "$set_dir" "$weekly_root/"
        fi
        if [ "$day_of_month" = "01" ]; then
            cp -a "$set_dir" "$monthly_root/"
        fi

        find "$daily_root" -type d -name "${PGDATABASE}_*" -mtime "+${BACKUP_KEEP_DAYS}" -prune -exec rm -rf {} \; 2>/dev/null || true
        weekly_days="$((BACKUP_KEEP_WEEKS * 7))"
        monthly_days="$((BACKUP_KEEP_MONTHS * 31))"
        find "$weekly_root" -type d -name "${PGDATABASE}_*" -mtime "+${weekly_days}" -prune -exec rm -rf {} \; 2>/dev/null || true
        find "$monthly_root" -type d -name "${PGDATABASE}_*" -mtime "+${monthly_days}" -prune -exec rm -rf {} \; 2>/dev/null || true

        ln -sfn "$set_dir" /backups/latest
        echo "[backup] complete: $set_dir"
    - type: bind
      source: ./coolify/openspp/backup/backup-entrypoint.sh
      is_directory: false
      target: /backup-entrypoint.sh
      read_only: true
      content: |
        #!/bin/sh
        set -eu

        : "${BACKUP_SCHEDULE:?BACKUP_SCHEDULE is required}"
        : "${PGPASSWORD:?PGPASSWORD is required}"

        cat > /etc/profile.d/openspp-backup-env.sh <<EOF
        export PGHOST='${PGHOST}'
        export PGPORT='${PGPORT}'
        export PGDATABASE='${PGDATABASE}'
        export PGUSER='${PGUSER}'
        export PGPASSWORD='${PGPASSWORD}'
        export BACKUP_KEEP_DAYS='${BACKUP_KEEP_DAYS}'
        export BACKUP_KEEP_WEEKS='${BACKUP_KEEP_WEEKS}'
        export BACKUP_KEEP_MONTHS='${BACKUP_KEEP_MONTHS}'
        export OPENSPP_VERSION='${OPENSPP_VERSION}'
        export TZ='${TZ}'
        EOF
        chmod 600 /etc/profile.d/openspp-backup-env.sh

        printf '%s %s\n' \
          "$BACKUP_SCHEDULE" \
          '. /etc/profile.d/openspp-backup-env.sh; /backup.sh >>/proc/1/fd/1 2>>/proc/1/fd/2' \
          > /etc/crontabs/root

        echo "[backup] schedule: $BACKUP_SCHEDULE"
        exec crond -f -l 8
    restart: unless-stopped
    read_only: true
    tmpfs:
    - /tmp:size=256M
    - /run:size=5M
    - /etc/crontabs:size=1M
    - /etc/profile.d:size=1M
    cap_drop:
    - ALL
    cap_add:
    - CHOWN
    - DAC_OVERRIDE
    - FOWNER
    - SETGID
    - SETUID
    security_opt:
    - no-new-privileges:true
    logging: *default-logging
    healthcheck:
      test:
      - CMD-SHELL
      - >-
        test -s /etc/crontabs/root &&
        pg_isready -h "$${PGHOST}" -p "$${PGPORT}" -U "$${PGUSER}" -d "$${PGDATABASE}"
      interval: 60s
      timeout: 10s
      retries: 3
      start_period: 20s
    deploy:
      resources:
        limits:
          cpus: "${BACKUP_CPU_LIMIT:-0.5}"
          memory: ${BACKUP_MEMORY_LIMIT:-512M}
        reservations:
          memory: ${BACKUP_MEMORY_RESERVATION:-128M}

volumes:
  postgres_data:
  odoo_data:
  backup_data:
````

<!-- END PORTABLE RESOURCE: assets/openspp-2026.08-v1.0.0-golden.yml -->

<!-- BEGIN PORTABLE RESOURCE: assets/overleaf-ce-6.2.2-v1.0.0-golden.yml -->
<!-- SOURCE SHA256: b8cb9425523d38088f7069c70d762ab24fbf07232d736f607572e5b191621585 -->
<!-- EMBEDDED SHA256: 39c8ab7c0353fb6793558897f6d57f7223f9e98f119bca72f1fb519428f3e79c -->

## Portable resource: `assets/overleaf-ce-6.2.2-v1.0.0-golden.yml`

````yaml
# documentation: https://docs.overleaf.com/on-premises
# slogan: Self-hosted collaborative LaTeX editor — Community Edition
# category: productivity
# tags: overleaf,latex,collaboration,editor,community-edition
# port: 80
#
# Overleaf Community Edition Coolify V1.0.0-RC4 candidate
# Static candidate only. Runtime acceptance is required before any production-ready/Golden claim.

services:
  # REQUIRED: service name must not contain "sharelatex". Coolify derives
  # SERVICE_* environment names from the Compose service name, and Overleaf 5+
  # refuses startup when any environment variable name contains SHARELATEX.
  overleaf:
    image: sharelatex/sharelatex:6.2.2@sha256:cfdeecb4e55a7ae76f0244b86d1b896580bc7137b82733b886a97575fba19d43
    platform: linux/amd64
    restart: always
    depends_on:
      mongo:
        condition: service_healthy
      redis:
        condition: service_healthy
    expose:
      - "80"
    stop_grace_period: 60s
    volumes:
      - overleaf-data:/var/lib/overleaf
    environment:
      # Coolify public route: browser/canonical HTTPS origin -> overleaf:80.
      - SERVICE_URL_OVERLEAF
      - OVERLEAF_SITE_URL=${SERVICE_URL_OVERLEAF}

      # Community Edition runtime identity and private dependencies.
      - OVERLEAF_APP_NAME=${OVERLEAF_APP_NAME:-Overleaf Community Edition}
      - OVERLEAF_MONGO_URL=mongodb://mongo/sharelatex
      - OVERLEAF_REDIS_HOST=redis
      - OVERLEAF_REDIS_PORT=6379
      - REDIS_HOST=redis
      - REDIS_PORT=6379

      # Current upstream CE defaults retained from Toolkit variables.env.
      - ENABLED_LINKED_FILE_TYPES=project_file,project_output_file
      - ENABLE_CONVERSIONS=true
      - EMAIL_CONFIRMATION_DISABLED=${EMAIL_CONFIRMATION_DISABLED:-true}

      # Coolify terminates TLS. Overleaf must trust only the private proxy path,
      # not arbitrary Internet clients supplying X-Forwarded-* headers.
      - OVERLEAF_BEHIND_PROXY=true
      - OVERLEAF_SECURE_COOKIE=true
      - TRUSTED_PROXY_IPS=${TRUSTED_PROXY_IPS:-loopback,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7}

      # Toolkit bin/init generates this secret. REALBASE64_32 matches
      # `openssl rand -base64 32` semantics (32 random bytes, Base64 encoded).
      - OVERLEAF_INVITE_TOKEN_SECRET=${SERVICE_REALBASE64_32_OVERLEAFINVITE:?Coolify must generate SERVICE_REALBASE64_32_OVERLEAFINVITE}
    healthcheck:
      test:
        - CMD-SHELL
        - curl -fsS http://127.0.0.1:3000/status >/dev/null
      interval: 10s
      timeout: 10s
      retries: 30
      start_period: 60s


  # One-shot first-admin bootstrap for Coolify One-Click installs.
  # The login identity is the email address. Change OVERLEAF_ADMIN_EMAIL before
  # the first deployment. Coolify generates the 64-character bootstrap password.
  # Existing admin credentials are never overwritten on a normal redeploy.
  adminbootstrap:
    image: sharelatex/sharelatex:6.2.2@sha256:cfdeecb4e55a7ae76f0244b86d1b896580bc7137b82733b886a97575fba19d43
    platform: linux/amd64
    restart: "no"
    exclude_from_hc: true
    depends_on:
      overleaf:
        condition: service_healthy
    entrypoint:
      - /bin/bash
      - -ce
    command:
      - |
        cd /overleaf/services/web
        exec node --input-type=module <<'NODE'
        import crypto from 'node:crypto'
        import { db } from './app/src/infrastructure/mongodb.mjs'
        import AuthenticationManager from './app/src/Features/Authentication/AuthenticationManager.mjs'
        import UserRegistrationHandler from './app/src/Features/User/UserRegistrationHandler.mjs'
        import { User } from './app/src/models/User.mjs'

        const email = (process.env.OVERLEAF_ADMIN_EMAIL || '').trim().toLowerCase()
        const password = process.env.OVERLEAF_ADMIN_PASSWORD || ''

        if (!email) {
          throw new Error('OVERLEAF_ADMIN_EMAIL is required')
        }
        if (!password) {
          throw new Error('OVERLEAF_ADMIN_PASSWORD is empty')
        }

        const emailError = AuthenticationManager.validateEmail(email)
        if (emailError) {
          throw emailError
        }
        const passwordError = AuthenticationManager.validatePassword(password, email)
        if (passwordError) {
          throw passwordError
        }

        const existingAdmin = await db.users.findOne(
          { isAdmin: true },
          { projection: { _id: 1, email: 1, isAdmin: 1 } }
        )

        if (existingAdmin) {
          if (existingAdmin.email === email) {
            console.log('Overleaf admin already exists for ' + email + '; preserving its existing password.')
            process.exit(0)
          }
          throw new Error(
            'An Overleaf admin already exists with a different email (' +
              existingAdmin.email +
              '). Refusing to create or replace another first admin.'
          )
        }

        const existingUser = await db.users.findOne(
          { email },
          { projection: { _id: 1, email: 1, isAdmin: 1, hashedPassword: 1 } }
        )

        if (existingUser) {
          throw new Error(
            'User ' + email +
              ' already exists but is not an admin. Refusing to overwrite an existing account password automatically.'
          )
        }

        const user = await UserRegistrationHandler.promises.registerNewUser({
          email,
          password,
          analyticsId: crypto.randomUUID(),
        })

        const domain = email.split('@')[1]
        const reversedHostname = domain.split('').reverse().join('')

        await User.updateOne(
          { _id: user._id },
          {
            $$set: {
              isAdmin: true,
              emails: [{ email, reversedHostname }],
            },
          }
        ).exec()

        const createdAdmin = await db.users.findOne(
          { _id: user._id },
          { projection: { email: 1, isAdmin: 1, hashedPassword: 1 } }
        )

        if (!createdAdmin || createdAdmin.isAdmin !== true || !createdAdmin.hashedPassword) {
          throw new Error('Admin bootstrap post-condition failed')
        }

        console.log('Successfully created Overleaf administrator ' + email + '.')
        console.log('Login with OVERLEAF_ADMIN_EMAIL and SERVICE_PASSWORD_64_OVERLEAFADMIN from Coolify.')
        NODE
    environment:
      # Operator-visible login identity. Override before first deployment.
      - OVERLEAF_ADMIN_EMAIL=${OVERLEAF_ADMIN_EMAIL:?Set OVERLEAF_ADMIN_EMAIL before the first deployment}
      # Bootstrap-only alias. The generated Magic Variable is the persistent
      # credential identity shown by Coolify.
      - OVERLEAF_ADMIN_PASSWORD=${SERVICE_PASSWORD_64_OVERLEAFADMIN:?Coolify must generate SERVICE_PASSWORD_64_OVERLEAFADMIN}
      - OVERLEAF_SITE_URL=${SERVICE_URL_OVERLEAF}
      - OVERLEAF_MONGO_URL=mongodb://mongo/sharelatex
      - OVERLEAF_REDIS_HOST=redis
      - OVERLEAF_REDIS_PORT=6379
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - OVERLEAF_INVITE_TOKEN_SECRET=${SERVICE_REALBASE64_32_OVERLEAFINVITE:?Coolify must generate SERVICE_REALBASE64_32_OVERLEAFINVITE}

  mongo:
    image: mongo:8.0.29
    restart: always
    command: "--replSet overleaf"
    expose:
      - "27017"
    volumes:
      - mongo-data:/data/db
      # Mirrors the current upstream overleaf/overleaf replica-set initializer.
      # Coolify creates this file on the deployment host; it is not a sidecar.
      - type: bind
        source: ./mongodb-init-replica-set.js
        target: /docker-entrypoint-initdb.d/mongodb-init-replica-set.js
        read_only: true
        content: |
          /* eslint-disable no-undef */

          rs.initiate({ _id: 'overleaf', members: [{ _id: 0, host: 'mongo:27017' }] })
        is_directory: false
    environment:
      - MONGO_INITDB_DATABASE=sharelatex
    extra_hosts:
      # Required by the upstream automatic replica-set bootstrap while executing
      # the init script inside the mongo container itself.
      - "mongo:127.0.0.1"
    healthcheck:
      test:
        - CMD-SHELL
        - echo 'db.stats().ok' | mongosh localhost:27017/test --quiet
      interval: 10s
      timeout: 10s
      retries: 10
      start_period: 20s

  redis:
    image: redis:7.4.11
    restart: always
    command:
      - redis-server
      - --appendonly
      - "yes"
    expose:
      - "6379"
    volumes:
      - redis-data:/data
    healthcheck:
      test:
        - CMD
        - redis-cli
        - ping
      interval: 10s
      timeout: 5s
      retries: 20

volumes:
  overleaf-data:
  mongo-data:
  redis-data:
````

<!-- END PORTABLE RESOURCE: assets/overleaf-ce-6.2.2-v1.0.0-golden.yml -->
