Files
pig/deploy/README.md
T
karti a6167629cc
CI / verify (push) Successful in 3m23s
Move from npm to pnpm across the workspace, CI and the image
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>
2026-08-13 04:15:54 -07:00

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`.