Skip to content

Deploying to production

The default configuration rebuilds your document on every request — perfect for local development, wrong for production. This guide covers the production story: generate in CI, commit the artifact, serve it without re-analyzing, gate who can see it, and let CI catch breaking changes.

  1. Generate in CI and commit the artifact. Docuccino’s output is deterministic, so a committed docs/openapi.json is a reviewable, diffable record of your API.

    Terminal window
    php artisan docuccino:export --fail-on=warning
  2. Serve the committed file, not a fresh build. Point the viewer at the artifact so no request ever triggers analysis:

    // config/docuccino.php → documents.default.viewer
    'source' => 'artifact', // re-emits the committed export.path; never re-analyzes
  3. Gate the viewer route so only the right people reach it (below).

  4. Diff on every change to enforce your versioning policy in CI (below).

Generation is safe to run in any environment, including a CI container with no services attached: the pipeline reads types, so it makes no database queries, sends no mail, and dispatches no jobs.

Because the analysis lives in docuccino/inference-phpstan and you install it as a dev dependency, composer install --no-dev deploys the adapter alone: no PHPStan, no Larastan, nothing that reads your source at runtime. The production dependency chain is docuccino/laraveldocuccino/core → a JSON-Schema validator, a YAML parser and two small parsing libraries. Serving from artifact or cache then costs one file read.

That is also why the export belongs in CI: the machine that has your dev dependencies is the one that can analyse your code. A production box that only serves the committed document never needs to.

This guide assumes your app serves the viewer. If it doesn’t — because you read the docs only in development, or you publish the spec to an external host — install the adapter as a dev dependency too and production ships nothing of Docuccino at all. Everything below about generating and diffing in CI still applies; only the viewer sections stop mattering. See the installation scenarios.

viewer.source decides where the served spec comes from. In production, serve pre-built bytes:

source Where the spec comes from Use it when
generate Rebuilt on every request Local development, or a gated internal viewer on a small app
artifact The committed export.path, re-emitted You commit the spec — the usual production answer
cache The docuccino:cache-warmed payload High-traffic or public viewers; you’d rather warm on deploy than commit

The viewer guide has the rest of the viewer’s options.

For cache, warm it on deploy and clear it when you regenerate:

Terminal window
php artisan docuccino:cache # build and store each document's payload
php artisan docuccino:clear # forget the cached payload

Set the backing store with cache.store. A cold cache falls back to generating the spec and logs a warning, so a forgotten warm-up degrades loudly instead of serving nothing. See the commands reference.

By default the viewer is reachable only in your local environment. To expose it anywhere else, name a viewer.gate ability and define it in a service provider — the gate guards the HTML and .json routes, and you should keep throttle in viewer.middleware when the spec endpoint is public. The viewer guide walks through it step by step.

To turn the runtime viewer off for one document, set its viewer.route to null — export still works. To turn Docuccino off entirely in an environment, use the master switch:

DOCUCCINO_ENABLED=false

With enabled false, no viewer routes are registered at all and every command except docuccino:clear aborts with a non-zero exit — so a production box can ship the package without serving or generating anything.

Regenerate whenever the code that shapes your API changes — new routes, changed validation, new resources or error handling. The natural home for that is a pull-request job: export, then diff the fresh document against the committed one and hold the changeset to your versioning policy.

It’s the same job when your docs live on an external host: export with --out= to wherever your upload step reads from, and add that upload after the checks below pass.

Terminal window
# 1. Regenerate; fail if anything got worse than a warning.
php artisan docuccino:export --fail-on=warning
# 2. Structural check against the UIR schema.
php artisan docuccino:validate
# 3. Compare with the committed artifact and enforce the versioning policy.
php artisan docuccino:diff docs/openapi.json --against=HEAD --enforce

--against=HEAD reads the old side out of git (git show HEAD:docs/openapi.json), so the job works on a clean checkout without a second build. The final git diff --exit-code is the step that keeps contributors honest: because identical code produces identical bytes, a stale committed spec fails the build.

An OpenAPI artifact is the friendliest thing to review, and it’s what most tooling wants. Commit a UIR document instead (--format=uir) when you also want stable identities and provenance in the repo — a richer diff, at the cost of a noisier file. Either way, pair it with --provenance=none if you’d rather source line numbers didn’t churn in your PRs.

If your API is served per tenant on a subdomain, describe it once with a server variable rather than a server per tenant:

// config/docuccino.php → documents.default.servers
'servers' => [
['url' => 'https://{tenant}.example.com', 'variables' => [
'tenant' => ['default' => 'acme', 'description' => 'Tenant slug'],
]],
],

The viewer renders the variable as an editable field, and the value flows into try-it-out requests. Server variables are emitted verbatim — see servers in the configuration reference.

Commit the output

Deterministic bytes mean a clean PR diff every time. The committed spec is your source of truth.

Gate then cache

Serve artifact or a warmed cache behind a gate — never generate on a hot public route.