a6167629cc
CI / verify (push) Successful in 3m23s
The monorepo was on npm workspaces. pnpm gives it a content-addressed store shared between the eight packages, a lockfile that records the whole graph rather than a flattened view of it, and — the reason this mattered in practice — `workspace:*`, which makes an internal dependency unambiguous instead of a version range that npm may satisfy from the registry. Mechanics: - `packageManager: pnpm@11.21.0` pins the version; corepack installs it in CI and in the image, so all three environments resolve identically. - The npm `workspaces` array is replaced by `pnpm-workspace.yaml`. pnpm ignores the former, and keeping both would leave two sources of truth. - All six internal dependencies moved to `workspace:*`. - Root scripts use `pnpm -r --if-present` and `pnpm -F <pkg>`. Two findings worth recording, both from running it rather than reading it: `tsx` was a devDependency, but the server runs TypeScript directly in production — the container's command is `pnpm exec tsx apps/api/src/server.ts`. Under npm this was concealed by the runtime stage re-installing tsx by hand after pruning dev dependencies. Under `pnpm install --prod` that sleight of hand stops working and the image simply fails to start. tsx is now declared in `dependencies`, which is what it has always actually been. The first image build failed with ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY. That is not a pnpm bug: it had decided the modules directory was stale and wanted confirmation before deleting it, which a non-interactive build cannot give. The trigger was the host's `node_modules` reaching the build context — there was no `.dockerignore` at all. pnpm's tree is symlinks into a content-addressed store, so copying it into an image produces dangling links and a directory pnpm rightly considers corrupt. Fixed by adding `.dockerignore` and setting `CI=true`, which is required in any non-interactive pnpm build. `esbuild` is denied install scripts via `allowBuilds`. Its platform binary arrives through the optional dependency `@esbuild/linux-x64` and the postinstall only verifies it; confirmed by running the binary directly, which reports 0.25.12. Verified under pnpm: typecheck clean, 150 tests / 0 failures, e2e passes, web builds. The image was built and booted against a real Postgres — health ok, `/api/dashboard` 401 with an issuer configured, `/` and `/capacity` serve the SPA, `/og.png` serves as image/png, and the migrator runs from the pruned runtime stage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
135 lines
5.1 KiB
Markdown
135 lines
5.1 KiB
Markdown
# Deploying PIG
|
|
|
|
PIG is an API/web container, a private Piggy worker/chat container and Postgres,
|
|
behind any reverse proxy that terminates TLS. Nothing here is specific to a
|
|
particular host.
|
|
|
|
## 1. DNS
|
|
|
|
Point the apex and `www` at the machine. Both must resolve before the proxy can
|
|
obtain a certificate.
|
|
|
|
```
|
|
A primeintellectgrowth.com -> <your IP>
|
|
A www.primeintellectgrowth.com -> <your IP>
|
|
```
|
|
|
|
## 2. Configuration
|
|
|
|
```bash
|
|
cp .env.example .env # then edit
|
|
```
|
|
|
|
The values that must be set for a production start:
|
|
|
|
| Variable | Why |
|
|
|---|---|
|
|
| `POSTGRES_PASSWORD` | Generate a fresh one; never reuse another service's |
|
|
| `PIG_PUBLIC_URL` | The single origin the app is served from |
|
|
| `SUPABASE_URL` / `SUPABASE_ANON_KEY` | Authentication. The app refuses to start in production without a Supabase URL, because it would otherwise serve the whole CRM unauthenticated |
|
|
| `PIG_ADMIN_EMAILS` | Who may administer. **Every address here must already have an account** — an unregistered address listed as an admin is a standing offer of admin rights to whoever claims it first |
|
|
| `PIGGY_INFERENCE_API_KEY` | Model credential held only by the Piggy process |
|
|
| `PIGGY_INTERNAL_TOKEN` | A generated 32+ character bearer token shared only by API and Piggy |
|
|
|
|
Optional: `PRIME_API_KEY` (scope it to `Availability → Read` only),
|
|
`PIGGY_ENABLED`, and the Slack and Buzz credentials.
|
|
|
|
Piggy listens on `piggy:8931` inside the Compose network. The port is exposed to
|
|
other containers but never published to the host, and Caddy must not route to
|
|
it. The CRM API authenticates the user, forwards only bounded chat context, and
|
|
uses `PIGGY_INTERNAL_TOKEN` in an Authorization header. Never put that token in
|
|
a query string, where proxies and access logs can retain it.
|
|
|
|
## 3. Start
|
|
|
|
```bash
|
|
docker compose -p pig up -d --build
|
|
docker compose -p pig exec app pnpm exec tsx packages/db/src/migrate.ts
|
|
docker compose -p pig exec app pnpm exec tsx packages/db/src/seed/index.ts # optional
|
|
```
|
|
|
|
When Piggy is enabled, start its private profile as well:
|
|
|
|
```bash
|
|
docker compose -p pig --profile piggy up -d --build
|
|
```
|
|
|
|
Use `-p pig`. A compose project that shares a name with a neighbouring stack
|
|
will adopt its volumes, which is a memorable way to lose a database.
|
|
|
|
## 4. Reverse proxy
|
|
|
|
See `Caddyfile.example`. Serve the app and API from the **same** origin.
|
|
|
|
Two things that will otherwise cost you an hour:
|
|
|
|
- **If other sites on the host use `bind <address>`, yours must too.** Caddy
|
|
groups site blocks into servers by listen address. A block without `bind`
|
|
lands in a *separate* server on `:443`, and the more specific listener wins
|
|
for traffic arriving on that address — which is all public traffic after NAT.
|
|
The symptom is a valid certificate, a 200 response, an empty body, and none
|
|
of your headers. It looks like the app is broken; it is that the request
|
|
never reached it.
|
|
|
|
- **The CSP must carry the hash of the inline theme script** in `index.html`.
|
|
That script sets light or dark before first paint so dark-mode users do not
|
|
get a white flash. Editing it changes the hash and CSP will silently block
|
|
it — the browser console prints the hash it expects.
|
|
|
|
## 5. Verify
|
|
|
|
```bash
|
|
curl -s https://primeintellectgrowth.com/api/health
|
|
# {"ok":true,"service":"pig","version":"0.1.0"}
|
|
```
|
|
|
|
## Upgrading
|
|
|
|
```bash
|
|
git pull
|
|
docker compose -p pig up -d --build
|
|
docker compose -p pig exec app pnpm exec tsx packages/db/src/migrate.ts
|
|
```
|
|
|
|
Migrations are additive and safe to re-run; Drizzle tracks what has been
|
|
applied. Take a dump before a major upgrade anyway:
|
|
|
|
```bash
|
|
docker compose -p pig exec db pg_dump -U pig pig | gzip > pig-$(date +%F).sql.gz
|
|
```
|
|
|
|
## On-premises: using your own identity provider
|
|
|
|
PIG authenticates against any standards-compliant OIDC provider, which is how
|
|
an install inside your own network works. Set:
|
|
|
|
```bash
|
|
PIG_OIDC_ISSUER=https://id.yourcompany.internal
|
|
PIG_OIDC_AUDIENCE=pig # the client/app id you registered for PIG
|
|
```
|
|
|
|
That is usually the whole configuration — the JWKS is discovered from the
|
|
issuer. On an air-gapped network, set `PIG_OIDC_JWKS_URI` too and no discovery
|
|
request is made.
|
|
|
|
`PIG_OIDC_ISSUER` takes precedence over `SUPABASE_URL`, so the hosted values
|
|
can stay in the environment file without quietly taking over.
|
|
|
|
**Set the audience.** Without it, any token your provider issued for any
|
|
application in the same tenant verifies here — a token minted for an unrelated
|
|
internal tool would be accepted as a PIG session. PIG warns about this at boot
|
|
but cannot refuse, because some providers legitimately issue single-audience
|
|
tokens.
|
|
|
|
**Provisioning stays in PIG.** Authenticating proves who someone is; it does
|
|
not make them a member. They still need an invite, and their team and role live
|
|
in PIG's database. That is deliberate — your directory should not have to model
|
|
"supply lead versus demand member" for one application.
|
|
|
|
## A note on the auth project
|
|
|
|
PIG verifies JWTs but authorizes from its own `users` table. If the Supabase
|
|
project is shared with another application, its users get **nothing** here until
|
|
they are explicitly invited. That is deliberate, and it is why a valid token can
|
|
still return `403 needs_profile`.
|