Deployment

A Fougere app is a Nitro app. The build scans the Frond sources once and writes the statement — frond() calls importing every class — which the server bundle carries: nothing reads a source at runtime, and neither TypeScript nor @fougere/compiler ships. This page is what deploying this very site taught — the checklist, then the Docker realization.

The runtime checklist

  1. .output/ — the Nitro build (pnpm build). It holds the Frond code: the statement the build wrote imports it, so the bundle carries it.
  2. The native driver — better-sqlite3 opens beside the app and is no part of a bundle. pnpm deploy --prod writes the closure that holds it; hand-picking packages is re-implementing a tracer, don't.
  3. The rows a file source keeps — the site's posts/, which sources: names by path.
  4. A writable .data/ — the SQLite file and Nuxt Content's index live there. It is the only thing to persist: one volume.
  5. A migrated database — the boot reads the schema and refuses one behind the entities, so fougere migrate --apply runs first, as a release step, from an image that holds the sources. Once per deployment, never per replica. It applies the frozen versions and refuses an entity nobody froze, so the change that moves one carries fougere freeze.
  6. Secrets by env — SITE_AUTH_SECRET (32+ chars), the public SITE_URL.

Measured 2026-10-18: .output, .data, posts/ and better-sqlite3 alone, in an empty directory, boot the site's three Fronds and answer a call.

The verification signal is the boot log and one envelope call — not a 200 on pages (SSR renders happily with zero fronds loaded):

{"jsonrpc":"2.0","id":1,"result":[]}

Docker

The site ships a working pair — site/Dockerfile + site/docker-compose.yml:

SITE_AUTH_SECRET='…32+ chars…' SITE_URL='https://your-domain' \
  docker compose -f site/docker-compose.yml up -d --build

The Dockerfile realizes the checklist in two stages:

# build stage: install workspace → build packages → build site
RUN pnpm --filter "./packages/**" -r run build \
 && pnpm --filter "@fougere/site" run build
# the native driver's closure, by pnpm itself:
RUN pnpm --filter "@fougere/site" deploy --prod --legacy /deploy

# run stage: node:22-bookworm-slim, non-root
COPY --from=build /repo/site/.output ./.output
COPY --from=build /repo/site/posts   ./posts
COPY --from=build /deploy/node_modules ./node_modules
VOLUME /app/.data
CMD ["node", ".output/server/index.mjs"]

State lives in the site-data volume mounted at /app/.data — backups are one file copy.

The compose file runs the release step as a service of its own: migrate is built from the build stage — the one holding the sources and the CLI — mounts the same volume, and runs pnpm -C site migrate. The site waits for it (depends_on: condition: service_completed_successfully), so a server never starts against a database behind it.

Splitting in production

The same image model applies to a split: the host process serving a Frond needs items 1–3 for its fronds, and the consumer declares it in fronds:. A dead host degrades to a typed SERVICE_UNAVAILABLE in pages (The gradient); restarting it is recovery — the consumer is never rebuilt.

Three things get called "together" and only one of them is required. The contract must travel — the consumer has to know what the host serves. The code need not: fougere sync reads the host's identity card and rebuilds a live schema locally, which is how a Frond written in another language is consumed at all. The ports must reach each other, and that is the only one a deployment decides.

The receiver binds loopback unless told otherwise, so the simplest split puts both processes on one machine: the same pod, or one image. hosts: ['0.0.0.0'] widens it in one line, and a receiver widened that way must be able to establish its caller or it will not start. That is a seventh item for the list above: fougere keys once for the system, fougere grant <frond> for each Frond that calls, and three environment variables injected at launch — see Identity across Fronds.

No command performs these steps yet — fougere build writes the statement, not the image. Until one does, this page is the contract.

What a deployed process does on the way out — draining the calls in flight before closing its connection — is one page over: Lifecycle.

Built with Fougere and Nuxt, hosted on GitHub Pages.