Configuring Redoc for Enterprise API Documentation
Enterprise Redoc deployments fail in predictable ways: non-reproducible builds, missing security headers, no authentication layer, and multi-second first renders on large specs. This guide is part of Redocly & OpenAPI UI Configuration and the broader Developer Portal Frameworks & UI Setup section. It gives exact @redocly/cli commands, an annotated redocly.yaml, an Express security wrapper, and the performance knobs that matter once a spec passes a few hundred operations.
This scenario arises when a public-facing reference graduates to an internal, access-controlled portal that compliance reviews — at which point ad-hoc redoc-cli invocations and an unhardened static host stop being acceptable.
Problem & Context
A typical broken state: docs are built locally with the deprecated redoc-cli, pushed to an S3 bucket with no headers, and reachable by anyone with the URL. The build is not reproducible because nobody pinned the CLI, so two engineers produce different HTML from the same spec. Compliance flags the missing Content-Security-Policy and Strict-Transport-Security, and the page takes four seconds to become interactive because the renderer resolves every $ref at runtime. Each of these is solvable with configuration rather than code changes.
The goal is a deterministic pipeline: one pinned CLI, one config file, a hardened serving layer, and a bundle that the browser can paint quickly.
Step-by-Step Solution
1. Install the supported CLI
Use @redocly/cli, not the deprecated redoc-cli, and add it as a dev dependency so CI installs the exact tree from your lockfile:
npm install --save-dev @redocly/cli
2. Lint, bundle, and build in one chain
npx @redocly/cli lint openapi.yaml
npx @redocly/cli bundle openapi.yaml --output dist/bundle.yaml --dereferenced
npx @redocly/cli build-docs dist/bundle.yaml --output dist/index.html --config redocly.yaml
Expected output on success:
Linting openapi.yaml...
No errors or warnings.
Bundling openapi.yaml...
dist/bundle.yaml: bundle created.
Building docs from dist/bundle.yaml...
dist/index.html: static docs created.
The --dereferenced flag flattens every $ref at build time, which both speeds the first render and sidesteps the circular-reference crash described below.
3. Drive all theming from redocly.yaml
The theme.openapi block maps directly to Redoc’s React theme object. Annotated config:
# redocly.yaml
extends:
- recommended
rules:
no-unused-components: error # fail the build on dead components
operation-summary: warn # nudge authors to summarize every operation
theme:
openapi:
disableTryItButton: false # set true to hide the console in read-only prod
showExtensions: false # hide x- vendor extensions from readers
lazyRendering: true # defer off-screen operations for large specs
colors:
primary:
main: "#0052CC"
typography:
fontFamily: "Inter, system-ui, sans-serif"
fontSize: "15px"
sidebar:
backgroundColor: "#F8FAFC"
Inject custom CSS through --theme.openapi.customCSS or a linked stylesheet rather than overriding internal Redoc class names — those change between minor versions.
4. Harden the serving layer with security headers
Redoc ships no authentication or header layer, so wrap the static output:
// server.js
const express = require('express');
const path = require('path');
const app = express();
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
);
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
});
app.use(express.static(path.join(__dirname, 'dist')));
app.listen(3000);
'unsafe-inline' is required by Redoc’s generated CSS. If your policy forbids it, switch to a nonce-based CSP and inject the nonce into the HTML template during the build step.
5. Add SSO in front of the static site
Redoc has no login layer, so place the static docs behind an authentication proxy — OAuth2 Proxy, Cloudflare Access, or AWS Cognito with CloudFront. The proxy authenticates the request before the static asset is served; Redoc never sees credentials.
6. Tune for large schemas
# Flatten refs and pre-build so the browser does no runtime resolution
npx @redocly/cli bundle openapi.yaml --output dist/bundle.yaml --dereferenced
Beyond lazyRendering: true, strip example payloads from the production spec to shrink the bundle, and pre-compress the static output with Brotli where your web server supports it.
Complete Working Example
A single GitHub Actions workflow that pins the CLI through npm ci, runs the full chain, and publishes to S3:
# .github/workflows/docs.yml
name: Build API Docs
on:
push:
branches: [main]
paths: ['openapi.yaml', 'redocly.yaml']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npx @redocly/cli lint openapi.yaml
- run: npx @redocly/cli bundle openapi.yaml --output dist/bundle.yaml --dereferenced
- run: npx @redocly/cli build-docs dist/bundle.yaml --output dist/index.html --config redocly.yaml
- run: aws s3 sync dist/ s3://api-docs-bucket/latest/ --delete
Keep redocly.yaml from the steps above in the repo root so both local builds and CI produce byte-identical HTML.
Gotchas & Edge Cases
Maximum call stackduring build. Circular$refchains overflow the resolver. Bundle with--dereferencedto flatten references beforebuild-docsever runs.- CORS preflight failures in Try It. The browser blocks cross-origin requests from the docs domain to the API. Either enable CORS on the API for the docs origin, or set
disableTryItButton: truein production and route developers to a sandbox. - Theme not applied after deploy. The theme is compiled into the static HTML at build time, so a
redocly.yamlchange does nothing until you rebuild. Any CSS variables referenced by the theme object must be defined in the page stylesheet before the bundle loads.
A reference build has exactly two inputs, and both must be pinned. The resolved specification and the renderer version between them determine every byte of the output, so an unpinned CLI means the published page can change with no commit in your repository to explain it. Record both, and treat a renderer upgrade as a reviewed change whose effect you look at on a preview before it ships.
Bundle before you build, and assert the result. The failure this prevents produces no error: an unresolved reference leaves a schema table empty while the surrounding page renders perfectly, so the build succeeds and the defect is found by a reader. Counting external references in the bundled document and failing when the count is not zero costs one line and converts the most common enterprise-portal complaint into a red check on a branch.
Customize through the theme object and nowhere else. Anything reachable there is a maintained interface; anything reached by targeting generated class names breaks silently on the next minor upgrade, because CSS that no longer matches produces no error. If a requirement genuinely cannot be met through the theme, isolate those rules in one commented file and re-verify them on every upgrade rather than scattering them through a stylesheet.
FAQ
How do I handle OpenAPI 3.1 vs 3.0 compatibility in Redoc?
@redocly/cli v2.x validates OpenAPI 3.1 fully, while the Redoc renderer has only partial 3.1 support — webhooks and some JSON Schema 2020-12 keywords may render with fallback behavior. Check the Redoc changelog for the current 3.1 feature status before relying on 3.1-specific constructs in a customer-facing reference.
Can I conditionally render endpoints based on user roles?
Redoc has no runtime role-based filtering. Use @redocly/cli split or a custom script to produce role-specific spec files, then build a separate static bundle per audience and route each behind the appropriate access policy in your SSO proxy.
Why does the Redoc bundle exceed 5 MB?
The usual causes are large inline example values, base64-encoded images in x-logo, and unminified payloads in examples blocks. Strip those fields from the production spec — for instance yq del '.paths[][][].requestBody.content.*.example' openapi.yaml — before passing it to build-docs. Managing big payloads is covered in Managing Large Example Payloads in API Specs.
Related
- Redocly & OpenAPI UI Configuration
- Redoc vs Scalar Comparison
- Customizing Swagger UI with CSS and Plugins
- Managing Large Example Payloads in API Specs
- Developer Portal Frameworks & UI Setup
Finally, decide where the reference sits relative to the rest of the portal before people start linking into it. A self-contained build served on its own path is the simplest arrangement and leaves a visible seam between guides and reference; embedding it inside a larger site gives one navigation and one search at the cost of fitting a self-contained artifact into a framework that expects to own the page. Either is defensible, but changing your mind later breaks every deep link into an operation, so make the choice deliberately and share a header between the two so the transition does not read as leaving the site.
The recurring theme is that an enterprise reference should be dull to operate: two pinned inputs, an explicit bundle step, an artifact nobody edits, and customization confined to the supported surface. Each deviation from that buys something small and costs an upgrade path, which is a bad trade for a page that has to keep working for years.
Get those four right and the reference becomes a build artifact like any other, which is the state you want it in.
Anything less predictable than that becomes somebody else’s problem to debug at an inconvenient moment.