apply drops install_command / build_command / is_static — a static monorepo app gets built and RUN as its root package.json, and boots the wrong workspace #63

Closed
opened 2026-07-14 23:04:50 +00:00 by dan-claude-bot · 0 comments
dan-claude-bot commented 2026-07-14 23:04:50 +00:00 (Migrated from github.com)

What happened

heavy-duty/incubator's landing resource, freshly created by apply on the prod box, crash-loops booting the wrong workspace. The logs are not landing serving files — they are the core API server starting inside landing's container:

> npm run start -w apps/core
> node dist/main.js
Error: Missing required env var: DATABASE_URL
    at loadConfig (file:///app/apps/core/dist/platform/config.js:37:19)
npm error workspace core

Landing is a static Astro site in an npm-workspace monorepo. It has no env_template and no database — correctly. But because three build fields were dropped on create, Coolify auto-detected the build from the repo-root package.json, whose scripts are scoped to the server:

"build": "npm run build -ws --if-present",     // builds every workspace
"start": "npm run start -w apps/core"          // boots the core API server

So nixpacks installed at root, built every workspace, and Coolify ran the start commandnpm run start -w apps/core — which demands DATABASE_URL and dies. The publish_directory cast did set (/apps/landing-site/dist) is inert, because nothing told Coolify to serve it instead of running a server.

The three fields cast cannot express

The working resource on the hand-built box and the cast-created one differ in exactly three settings — every one of which cast has no manifest field for:

Field hand-built box (works) cast-created (crash-loops)
is_static (Static site) enabled disabled
install_command npm ci (empty → nixpacks default)
build_command npm run build -w apps/landing-site (empty → nixpacks default)
publish_directory /apps/landing-site/dist /apps/landing-site/dist

With static on Coolify serves publish_directory and never runs a start command; with a workspace-scoped build/install it builds only landing instead of the whole monorepo (and never touches the server that needs env). cast set the one field it models and had no way to set the three that make this app what it is.

cast already knows — draft says so out loud

This is not a newly-discovered gap. draft.ts (which reverse-engineers a manifest from a live instance) has a list titled, verbatim:

// Real settings, present on the live object, that the manifest has no field
// for. Each one changes what the application IS, and each would be silently
// absent from a rebuild.

install_command, build_command, and start_command are in that list (draft.ts:418-420). So a draft of the working box would have flagged all three as unmappable — and then they were lost when the manifest was authored by hand. is_static is worse: it is not even in the list, so draft is silent about it entirely. The comment's own prediction — "silently absent from a rebuild" — is precisely what happened the first time a rebuild actually ran.

Why the diff gate reported parity anyway

The manifest was verified against the hand-built box via a field diff, and it came back clean. It came back clean because is_static / install_command / build_command are not in cast's diffed field set — the same write-only-field blind spot as #50 (source-commit toggle) and #51 (backup schedules). A field cast neither creates nor diffs reads as zero drift forever, so a manifest can be "verified at parity" while missing the settings that keep the app alive. This is now the third instance of that class; it may be worth a tracking meta-issue for "settings on the live object that apply neither writes nor diffs."

Suggested shape

Add to the manifest's build block (nixpacks/static/dockerfile packs):

build:
  pack: nixpacks
  base_directory: /
  install_command: npm ci
  build_command: npm run build -w apps/landing-site
  publish_directory: /apps/landing-site/dist
  static: true      # → is_static; serve publish_directory, run no start command
  • apply writes install_command, build_command, is_static on create, and diffs them (so their drift stops being invisible).
  • Consider inferring static: true when a nixpacks app declares a publish_directory — you do not publish a directory from a long-running service — but an explicit field is safer than a heuristic, and Coolify keeps them independent.
  • draft should map these three out of its NO_HOME list into real emitted fields, and learn is_static.
  • Validation: static: true with no publish_directory is almost certainly a mistake (nothing to serve) — warn or error.

Immediate operator remedy (no code, unblocks prod now)

On the prod landing application, Advanced tab: enable Is it a static site?, set install npm ci, set build npm run build -w apps/landing-site (publish directory is already correct), redeploy. This reproduces the working box's config by hand until the manifest can carry it.


Filed off the heavy-duty/incubator prod migration. Field values above are from a live side-by-side of the two boxes' landing applications.

## What happened `heavy-duty/incubator`'s `landing` resource, freshly created by `apply` on the prod box, **crash-loops booting the wrong workspace**. The logs are not landing serving files — they are the *core API server* starting inside landing's container: ``` > npm run start -w apps/core > node dist/main.js Error: Missing required env var: DATABASE_URL at loadConfig (file:///app/apps/core/dist/platform/config.js:37:19) npm error workspace core ``` Landing is a static Astro site in an npm-workspace monorepo. It has no `env_template` and no database — correctly. But because three build fields were dropped on create, Coolify auto-detected the build from the **repo-root `package.json`**, whose scripts are scoped to the server: ```json "build": "npm run build -ws --if-present", // builds every workspace "start": "npm run start -w apps/core" // boots the core API server ``` So nixpacks installed at root, built every workspace, and Coolify **ran the start command** — `npm run start -w apps/core` — which demands `DATABASE_URL` and dies. The `publish_directory` cast *did* set (`/apps/landing-site/dist`) is inert, because nothing told Coolify to serve it instead of running a server. ## The three fields cast cannot express The working resource on the hand-built box and the cast-created one differ in exactly three settings — every one of which cast has no manifest field for: | Field | hand-built box (works) | cast-created (crash-loops) | |---|---|---| | `is_static` (Static site) | **enabled** | disabled | | `install_command` | `npm ci` | *(empty → nixpacks default)* | | `build_command` | `npm run build -w apps/landing-site` | *(empty → nixpacks default)* | | `publish_directory` | `/apps/landing-site/dist` | `/apps/landing-site/dist` ✓ | With static **on** Coolify serves `publish_directory` and never runs a start command; with a workspace-scoped **build/install** it builds only landing instead of the whole monorepo (and never touches the server that needs env). cast set the one field it models and had no way to set the three that make this app *what it is*. ## cast already knows — `draft` says so out loud This is not a newly-discovered gap. `draft.ts` (which reverse-engineers a manifest from a live instance) has a list titled, verbatim: ``` // Real settings, present on the live object, that the manifest has no field // for. Each one changes what the application IS, and each would be silently // absent from a rebuild. ``` `install_command`, `build_command`, and `start_command` are **in that list** (`draft.ts:418-420`). So a `draft` of the working box would have flagged all three as unmappable — and then they were lost when the manifest was authored by hand. `is_static` is worse: it is not even in the list, so `draft` is silent about it entirely. The comment's own prediction — *"silently absent from a rebuild"* — is precisely what happened the first time a rebuild actually ran. ## Why the diff gate reported parity anyway The manifest was verified against the hand-built box via a field diff, and it came back clean. It came back clean because `is_static` / `install_command` / `build_command` are not in cast's diffed field set — the same write-only-field blind spot as #50 (source-commit toggle) and #51 (backup schedules). A field cast neither creates nor diffs reads as **zero drift forever**, so a manifest can be "verified at parity" while missing the settings that keep the app alive. This is now the third instance of that class; it may be worth a tracking meta-issue for "settings on the live object that apply neither writes nor diffs." ## Suggested shape Add to the manifest's `build` block (nixpacks/static/dockerfile packs): ```yaml build: pack: nixpacks base_directory: / install_command: npm ci build_command: npm run build -w apps/landing-site publish_directory: /apps/landing-site/dist static: true # → is_static; serve publish_directory, run no start command ``` - `apply` writes `install_command`, `build_command`, `is_static` on create, and **diffs** them (so their drift stops being invisible). - Consider inferring `static: true` when a nixpacks app declares a `publish_directory` — you do not publish a directory from a long-running service — but an explicit field is safer than a heuristic, and Coolify keeps them independent. - `draft` should map these three out of its `NO_HOME` list into real emitted fields, and learn `is_static`. - Validation: `static: true` with no `publish_directory` is almost certainly a mistake (nothing to serve) — warn or error. ## Immediate operator remedy (no code, unblocks prod now) On the prod `landing` application, Advanced tab: enable **Is it a static site?**, set install `npm ci`, set build `npm run build -w apps/landing-site` (publish directory is already correct), redeploy. This reproduces the working box's config by hand until the manifest can carry it. --- Filed off the `heavy-duty/incubator` prod migration. Field values above are from a live side-by-side of the two boxes' `landing` applications.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/cast#63
No description provided.