The image builds were never exercised: the docker job needs lint, and
lint was failing, so nothing downstream of it ever ran. Four defects
had accumulated behind that gate, each fatal on its own.
- pnpm creates no node_modules for a package without dependencies, and
@source/shared has none. Three COPY lines named that path and failed.
- The production stage copied apps/api/dist, which tsc never wrote:
tsconfig.base.json sets noEmit and no package overrides it. Rather
than turn emit on — every @source/* package points main at its
TypeScript source, and @source/proto derives daemon.proto's location
from a /src/index.ts module URL — the stage now runs the sources
through tsx, exactly as the migrate stage has always done.
- tsx lives in apps/api/node_modules/.bin under pnpm's isolated layout,
so the command only resolves from the package directory.
- Both healthchecks probed localhost, which musl resolves to ::1 while
the servers bind IPv4. Every probe was refused, so the containers sat
unhealthy forever — and `docker compose up --wait`, which is how a
panel installs this stack, waits for healthy.
Also fixed the postgres healthcheck in both compose files. pg_isready
without -h asks over the unix socket, which answers during the image's
init phase before the server listens on TCP; the migrate container then
started and died with ECONNREFUSED against a container Compose had just
called healthy.
Verified by running the full stack from docker-compose.panel.yml with
locally built images: migrations and seed complete, api, web, daemon,
postgres and redis all report healthy, and /api/health answers 200
through the web container's proxy.
docker-compose.panel.yml deploys every service from a published image.
A control panel writes only a compose file and an .env into its project
directory, so the build: stanzas of docker-compose.yml cannot resolve
their context there.
CI pushes api, migrate, web and daemon images to the Gitea container
registry on v* tags. The migrate stage ships as its own image because
the panel compose runs it as a one-shot service before the API starts.
The web port is named HOST_PORT: panels reverse-proxy "the" port of an
installation and need to know which one that is when a stack publishes
more than one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>