Read the Docs Build Isolation: Keeping Secrets Out of Public Output
Read the Docs builds docs in a container with access to secrets, then publishes static files. This note covers the build boundary, checks, and failure modes.
15 Dec 2025, 03:14 UTC

Read the Docs (RTD) builds documentation inside a container that can read project environment variables and secrets, then publishes only the generated HTML, CSS, and JavaScript to a public web server. The practical problem is that a build command, Sphinx template, or MkDocs plugin can accidentally write a secret into those static files. Because RTD gates deployment on the build exit code, a failed build stops the update, but a successful build that leaks a secret still deploys. The useful design rule is to treat the build container as the only place where secrets may exist, and the published site as a secret-free boundary.
Requirements
Before changing a Read the Docs configuration, confirm these constraints:
- The build runs in an ephemeral container provisioned by RTD, not on your local machine.
- Build commands can execute arbitrary code and can read secrets stored in RTD project settings.
- Only files in the configured output directory are copied to the public hosting layer.
- A non-zero exit code from the build command aborts deployment; the previous successful build remains live.
Smallest suitable design
Use a .readthedocs.yaml file at the repository root to pin the build environment and make the output directory explicit. This example is representative; confirm exact key names against your RTD provider's schema, because managed Read the Docs and self-hosted installations can differ.
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
commands:
- python -m pip install -r docs/requirements.txt
- sphinx-build -b html docs/source _build/html
sphinx:
configuration: docs/source/conf.py
fail_on_warning: true
Run this configuration from the repository root on Linux or macOS. No special permissions are required to read the file, but you need write access to the repository to push it and RTD project administration to enable builds. The expected result is a successful build that places HTML under _build/html and nothing else.
Trust and data boundaries
The trust boundary sits between the build container and the static hosting layer. The container is the untrusted execution stage: it runs your build commands and can access environment variables such as API keys or version tokens. The public hosting layer receives only rendered assets and should never see those variables.
If your repository accepts pull requests from untrusted contributors, a malicious .readthedocs.yaml or documentation file can exfiltrate secrets during the build. Require review of configuration changes and disable builds from forks where possible.
Never interpolate a secret directly into a template. For example, do not write {{ build_secret }} into a Sphinx HTML template or a MkDocs page. Use secrets only for external actions inside the build, such as downloading a private dependency, and confirm the value does not appear in the output.
Operational checks
Perform these checks before relying on automated deployment.
1. Local build validation
Where to run: your local terminal in the repository root. Permissions: read access to the repository and a Python environment with Sphinx installed.
sphinx-build -b html docs/source _build/local
Expected check: exit code 0 and HTML files present under _build/local. Risk: none to RTD; this only validates your local copy.
2. Secret leak scan
Where to run: your local terminal after the local build. Placeholder: replace TEST_SECRET with a dummy value you added to RTD project settings.
grep -r "TEST_SECRET" _build/local || echo "no secret matches"
Expected check: no matches. If the string appears, the build is rendering secrets into public output and the configuration must be changed before pushing. Risk: grep returns a non-zero exit code when no matches are found; use || true in scripts to avoid false failures.
3. RTD build gate
Where to run: the Read the Docs web interface after pushing to a protected branch. Permissions: RTD project access to view build logs.
Expected check: the build log shows a zero exit code and the site version updates. A non-zero exit code aborts deployment and leaves the previous version live. Risk: this check does not detect secrets that were successfully written into HTML, so combine it with the leak scan.
Failure modes
| Failure mode | Effect on published site | Mitigation |
|---|---|---|
| Sphinx or MkDocs syntax error | Non-zero exit code; deployment aborted; last successful build stays live | Run sphinx-build locally before pushing |
Dependency conflict in requirements.txt | Build fails; site remains stale | Pin dependency versions |
| Build timeout | No new build; previous version remains public | Simplify build steps or reduce generated assets |
| Secret rendered into template | Build succeeds; secret is published in static HTML | Scan output for secret values; remove template interpolation |
The last row is the most important limitation: RTD's exit-code gate cannot catch a secret that the build successfully writes into a file. You must verify the output separately.
Conditions that would change the design
- If you need dynamic behavior such as authenticated search or user comments, static hosting is insufficient; pair RTD with a separate application.
- If untrusted contributors can trigger builds from forks, treat the build configuration as sensitive code and require pull-request review.
- If you maintain multiple language versions or a monorepo, use separate RTD projects or subproject configuration rather than one monolithic build.
Practical verification
Add a dummy secret to RTD project settings, reference it nowhere in your documentation, trigger a build, and download the published HTML. Search the downloaded files for the dummy value. If it is absent and the build log shows a zero exit code, the boundary is behaving as intended. Repeat this check after any change to .readthedocs.yaml or build templates.
This note describes established Read the Docs build behavior. It does not cover self-hosted RTD internals, which may differ from the managed service.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.