Skip to content

Postgres self-host path is missing the pg driver: CLI install has no pg, boot fails fast #14510

Description

@hotlong

Found while verifying every command in the self-hosting story for a www.objectos.ai how-to (site issue #92). Filed unassigned, not fixed there.

What I observed

pg is an optional peer dependency of @objectstack/driver-sql, so nothing installs it automatically:

packages/drivers/driver-sql/package.json
  peerDependencies:     { "mysql2": "^3.0.0", "pg": "^8.0.0", "tedious": "^18.0.0" }
  peerDependenciesMeta: { mysql2: {optional:true}, pg: {optional:true}, tedious: {optional:true} }
  optionalDependencies: { "better-sqlite3": "^13.0.3" }

npm 7+ auto-installs peer deps but skips those marked optional, so a plain install of the CLI has no pg. Verified empirically against the published 17.2.0, not just the source:

  • npx @objectstack/cli@17.2.0 produced a tree containing better-sqlite3 and mongodb at top level, and no pg.
  • Published @objectstack/cli@17.2.0 package.json: no pg in dependencies, optionalDependencies is {better-sqlite3} only, and there is no postinstall hook that could add one.

Booting that install with a Postgres URL fails at startup:

✗ datasource 'default': connect failed — Knex: run
$ npm install pg --save
Cannot find module 'pg'
... (declared boot-critical by the host — it is the platform's primary datasource
and every object without an explicit binding routes to it ⇒ fail-fast per ADR-0062 D5)

The failure is loud and the message names the fix, which is good. The problem is which documented paths walk into it.

Why this matters beyond one missing package

The scaffolder's default production stack targets Postgres, and nothing in that stack installs pg:

  • npm create objectstack@latest writes a docker-compose.yml whose db service is postgres:17 and whose app gets OS_DATABASE_URL: postgres://objectstack:...@db:5432/myapp.
  • The generated package.json dependencies are the six @objectstack/* packages — no pg.
  • The generated Dockerfile build stage runs npm ci + npx os build, then the runtime stage copies only dist/objectstack.json into ghcr.io/objectstack-ai/objectstack:17.2.0. So even a pg added to the app's package.json would not reach the runtime layer.
  • docker/Dockerfile builds that image with npm install -g @objectstack/cli@${OS_CLI_VERSION} and nothing else.

So the documented docker run ... -e OS_DATABASE_URL="postgres://..." in docker/README.md, and docker compose up -d on a freshly scaffolded project, both appear to land on an image with no pg.

I could not run Docker to confirm the image end to end — no Docker daemon in my container. That last step is inference from the Dockerfile plus the reproduced npm-tree behaviour, not an observed container boot. Someone with a daemon should confirm before choosing a fix.

Docs angle

content/docs/deployment/self-hosting.mdx is careful about exactly this class of problem for Turso — "its driver is an optional package: npm install @objectstack/driver-turso, otherwise the boot fails loudly" — but the same table offers postgres://… and mysql://… with no equivalent note, and the bare-Node recipe is npm install -g @objectstack/cli followed by a Postgres EnvironmentFile.

Repro

npm create objectstack@latest selfhost-test    # no pg in dependencies
cd selfhost-test && npx os build               # ✓ dist/objectstack.json
OS_DATABASE_URL=postgres://user@host:5432/db \
OS_AUTH_SECRET=$(openssl rand -hex 32) \
OS_SECRET_KEY=$(openssl rand -hex 32) \
npx os start                                   # ✗ Cannot find module 'pg'
pnpm add pg && npx os start                    # ✓ boots: Driver: SqlDriver(pg)

After pnpm add pg the app boots cleanly and migrates 68 tables into an empty Postgres 16 database, so the driver itself is fine — this is purely about what gets installed.

Possible directions (not a recommendation — needs a maintainer call)

  1. Bake the common drivers (pg, mysql2) into the official runtime image, since that image exists precisely to run production deployments and its own README advertises the Postgres invocation.
  2. Add pg to the scaffolder's generated package.json when the generated compose file targets Postgres, and copy the driver into the runtime stage.
  3. Docs-only: give Postgres and MySQL the same "optional package, install it" callout Turso already has, in the env-var table and the bare-Node recipe.

These are not exclusive; 1 and 3 address different audiences (image users vs bare-Node users).


Triage — confirmed, split, and graded

Re-measured at origin/main 4a37870:

  • driver-sql/package.jsonpg, mysql2, tedious are optional peers; better-sqlite3 is a real optional dependency. Exactly as quoted.
  • content/docs/deployment/self-hosting.mdx:35 — the OS_DATABASE_URL row gives Turso the optional-package warning verbatim as you quote it, gives MySQL three dialect caveats and MongoDB a tenancy caveat, and gives postgres://… and mysql://… no optional-package note at all. The asymmetry is visible in one line. The same page advertises postgres:// invocations at :74, :93, :153, :202.

Direction 3 is split out as #14551 and is dispatchable now. It is owed under every one of your three directions — even with the image and scaffolder fixed, the bare-Node reader still needs it, which is the audience separation you drew yourself. It should not wait behind the packaging call.

Directions 1 and 2 stay here. Both add runtime dependencies to something the project ships — the human floor covers that explicitly — so the seat routes them and does not pick.

<!-- os-decision-facets -->

  • ① 项目长远合理性(权重 ≥50%,领起推荐) —— 真正的错位不在依赖标记,而在**「官方运行时镜像」这个东西承诺了什么**:它存在的理由就是「拿去跑生产」,它自己的 README 在教 Postgres 启动命令,而它跑不了那条命令。同理,脚手架生成的 compose 文件把 postgres:17 写死在里面,生成的 package.json 却不装连它的驱动 —— 生成器承诺了一个数据库,又不给连它的东西。长远终态很朴素:谁承诺,谁负责装上。①指向「让承诺和安装对齐」,而不是只在文档里补一句提醒。
  • ② 实际业务拉动 —— 满格,而且是最贵的那一种拉动:新用户的第一次运行npm create objectstackdocker compose up,官方默认路线,起不来。这不是边角配置,是入门路径本身。而且是外部使用者拿已发布的 17.2.0 实测的,不是读源码推的。
  • ③ 防 AI 犯错 —— 这一棱今天是好的,如实记:失败响亮,报错里直接写着 npm install pg --save,没有任何静默。但它有一个 AI 特有的坏处:一个 agent 照文档搭环境撞上这个错之后,会开始改文档没教过的东西 —— 往镜像里塞包、改 Dockerfile、绕开脚手架 —— 从而永久偏离官方路径。响亮的错误配上文档里没有的答案,会把人推向自创解法。 拆出去的 docs: self-hosting's env-var row flags Turso's driver as an optional package but not Postgres's or MySQL's, which are optional the same way #14551 正是关掉这一条。
  • ④ 创业阶段不扩散 —— 这一棱反对方向 1,必须写明:往官方镜像预装 pg + mysql2,是让每一个用户(包括只用 SQLite 的)背上两个永久依赖 —— 镜像体积、CVE 面、升级义务全由平台扛。remove 优于 declare-and-maintain,在这里读作:能靠「谁承诺谁安装」解决的,不要靠给所有人预装解决。

推荐:A = 方向 2 + 3。 脚手架按它自己生成的 compose 目标装驱动,并让运行时层真的拿得到(卡面已指出:只加进 package.json 不够,runtime stage 只 copy 了 dist/objectstack.json);文档补齐 —— 后者已拆为 #14551,今天就能派。①要求承诺与安装对齐,而脚手架的承诺是具体的(它写死了 postgres:17),镜像的承诺是泛的;②的满格拉动正集中在脚手架路径;④明确反对给所有人预装。
回退:B = 方向 1(镜像预装 pg + mysql2)。 若维护者认定官方镜像的定位就是「开箱即可跑它 README 里的任何一条命令」,那 ④ 的代价是自愿付的,走这条,并把镜像的依赖清单当成一条公开承诺来维护。
置信缺口(本分析看不见什么): Docker 那一整段是推断,不是实测 —— 卡面作者明说容器里没有 docker daemon,镜像端到端从未启动过。「镜像里没有 pg」是从 Dockerfile 加上复现出的 npm 树推出来的。⚠️ 有 daemon 的人必须先跑一次官方镜像 + Postgres URL 再选方向:如果镜像出于某种原因其实带着 pg,方向 1 整个不存在,问题只剩脚手架与文档,推荐的形状也要跟着变。

Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:clipriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions