Back to monorepo orchestration Target affected workspaces Configure turbo pipelines Compare the Nx approach

Self-Hosting a Turborepo Remote Cache

The hosted Turborepo cache is the default, but plenty of teams need the artifacts to stay inside their own network: air-gapped CI, data-residency rules, or simply avoiding a per-seat bill. Turborepo's remote cache is a small, documented HTTP API, so any server that implements its handful of artifact endpoints works. This page walks through running an open-source cache server, wiring TURBO_API/TURBO_TOKEN/TURBO_TEAM, choosing a filesystem or S3 storage backend, and connecting CI.

Architecture

A self-hosted cache has three parts: the CI runners (and developer machines) that read and write artifacts, a cache server that implements the remote-cache HTTP API, and a storage backend that persists the artifact tarballs. The server is stateless; all durable state lives in the backend.

Self-hosted remote cache architecture CI runners talk to a cache server over the remote-cache API, and the server persists artifacts in a filesystem or object-storage backend. CI runners + dev machines cache server remote-cache API bearer-token auth storage filesystem / S3 HTTPS PUT/GET The server is stateless; durability lives entirely in the storage backend.
CI runners read and write artifacts through the cache server, which persists them to a chosen backend.

How the protocol works

Turborepo's remote cache exposes a small set of artifact endpoints under /v8/artifacts. A client uploads a gzipped task output tarball with PUT /v8/artifacts/{hash} and retrieves it with GET /v8/artifacts/{hash}, where {hash} is the task hash described in Remote Caching Setup. Requests carry a bearer token in the Authorization header and a ?teamId= (or ?slug=) query parameter that namespaces artifacts per team. Optionally, the client signs artifacts with a secret so the server can reject tampered uploads. Because the contract is this small, several open-source servers implement it; the configuration below is intentionally server-agnostic.

How the protocol works Turborepo's remote cache exposes a small set of artifact endpoints under /v8/artifacts. How the protocol works Turborepo's remote cache exposes a small set of artifact endpoints under /v8/artifacts.
How the protocol works — the core idea of this section at a glance.

Setup

1. Run the cache server

Setup Run any remote-cache-compatible server as a container. Setup Run any remote-cache-compatible server as a container.
Setup — the core idea of this section at a glance.

Run any remote-cache-compatible server as a container. It needs a listen port, a turbo token (the bearer secret clients must present), and a storage configuration:

docker run -d --name turbo-cache \
  -p 3000:3000 \
  -e TURBO_TOKEN=$(openssl rand -hex 32) \
  -e STORAGE_PROVIDER=local \
  -e STORAGE_PATH=/data/cache \
  -v turbo-cache-data:/data/cache \
  ghcr.io/example/turbo-cache-server:latest

For production, terminate TLS at a reverse proxy and forward to the container, so client traffic is always HTTPS.

2. Choose a storage backend

The filesystem backend is the simplest and fine for a single server with a persistent volume. For multiple server replicas or durable retention, use S3-compatible object storage so any replica can serve any artifact:

docker run -d --name turbo-cache \
  -p 3000:3000 \
  -e TURBO_TOKEN=$YOUR_CACHE_TOKEN \
  -e STORAGE_PROVIDER=s3 \
  -e STORAGE_BUCKET=turbo-cache \
  -e STORAGE_REGION=us-east-1 \
  -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \
  -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \
  ghcr.io/example/turbo-cache-server:latest
Backend Best for Trade-off
Filesystem Single server, simple setup Tied to one volume; no horizontal scaling
S3-compatible Multiple replicas, long retention Network round-trip per artifact; needs lifecycle rules

3. Point Turborepo at the server

Turborepo reads the API endpoint from TURBO_API, the bearer token from TURBO_TOKEN, and the namespace from TURBO_TEAM. The team slug must be prefixed with team_ because Turborepo treats any non-prefixed value as a username:

export TURBO_API=https://turbo-cache.internal.example.com
export TURBO_TOKEN=$YOUR_CACHE_TOKEN
export TURBO_TEAM=team_yourorg

You can also commit non-secret values to .turbo/config.json so every contributor shares the same endpoint and team:

{
  "apiurl": "https://turbo-cache.internal.example.com",
  "teamslug": "team_yourorg"
}

Keep the token out of .turbo/config.json; supply it via the environment only.

4. Verify a round trip

turbo run build --remote-only --summarize   # writes artifacts
rm -rf .turbo node_modules/.cache
turbo run build --remote-only --summarize   # should replay from the server

CI wiring

CI wiring Use --frozen-lockfile so the dependency set feeding the task hash is deterministic, consistent with Lockfile Management CI wiring Use --frozen-lockfile so the dependency set feeding the task hash is deterministic, consistent with Lockfile Management Strategies.
CI wiring — the core idea of this section at a glance.
name: ci
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      TURBO_API: https://turbo-cache.internal.example.com
      TURBO_TEAM: team_yourorg
      TURBO_TOKEN: ${{ secrets.TURBO_CACHE_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: turbo run build test lint
      # Fail loudly if the self-hosted cache was unreachable
      - run: |
          if grep -q "failed to contact remote cache" turbo.log; then
            echo "Remote cache unreachable"; exit 1
          fi

Use --frozen-lockfile so the dependency set feeding the task hash is deterministic, consistent with Lockfile Management Strategies. If the runners live in a private network, the cache server must be reachable from them — either on the same VPC or through an internal load balancer.

Validation

  • A clean checkout that has never built the code reports cache hit, replaying logs for tasks another machine already cached.
  • The storage backend grows by one tarball per unique task hash; confirm with ls /data/cache or aws s3 ls s3://turbo-cache/.
  • Hitting the server with a wrong token returns 403, proving auth is enforced.
Validation Validation in production JavaScript package workflows. Validation Validation in production JavaScript package workflows.
Validation — the core idea of this section at a glance.

CI guardrails

  • Rotate TURBO_TOKEN on a schedule and store it only as a CI secret, never in .turbo/config.json.
  • Always serve the cache over TLS; a plaintext bearer token over HTTP is a credential leak.
  • Use a read-only token for fork PRs so untrusted code cannot write (poison) artifacts.
  • Set object-storage lifecycle rules to expire artifacts after a few weeks; the cache is regenerable, so unbounded growth is wasted spend.
  • Add the "remote cache unreachable" CI check above so a downed server fails fast instead of silently rebuilding everything.
CI guardrails CI guardrails in production JavaScript package workflows. CI guardrails CI guardrails in production JavaScript package workflows.
CI guardrails — the core idea of this section at a glance.

Securing a self-hosted cache

A self-hosted remote cache holds build artifacts that runners replay verbatim, which makes it a trust boundary demanding the same rigor as any service that serves executable content. The core protections are to scope write access to trusted branch builds so a pull request cannot poison the cache, keep pull-request builds read-only against it, and put the whole thing behind TLS so tokens and artifacts are never transmitted in the clear. Including the lockfile in the cache key ensures a dependency change cannot replay an artifact built against the old graph, closing a subtle staleness-and-tampering gap.

Cache as trust boundary Scope writes, TLS, lockfile key, clean population. trusted-branch writes no PR poisoning TLS tokens + artifacts protected lockfile in key no stale replay frozen + ignore-scripts clean population
A self-hosted replayed cache is a trust boundary — secure it as production infrastructure.

The builds that populate the cache matter as much as the access controls. Running installs with ignored scripts on the runners that write the cache prevents a compromised dependency from executing during the build that produces a cached artifact, so what other runs replay was produced by a controlled process. Combined with a frozen lockfile, this means the cache is populated only from reviewed, reproducible builds. Treating the self-hosted cache as production infrastructure — with backups, monitoring, and access review — is what lets you rely on a cache hit being genuinely equivalent to a rerun rather than a potential injection point, which is the whole value of caching in the first place.

When self-hosting is worth the operational cost

Self-hosting a remote cache trades convenience for control, so it is worth being clear about when that trade pays off. The hosted service handles storage, availability, TLS, and access with almost no setup, which is the right choice for most teams. Self-hosting earns its operational cost when you have a specific requirement the hosted service cannot meet: an air-gapped network with no external egress, strict data-residency rules that forbid artifacts leaving your infrastructure, or a compliance regime that requires you to own and audit the storage and access path directly.

Self-host or hosted Whether self-hosting's cost is justified. Do you have an air-gap or data-residency need? yes self-host compliance ownership self-host no use hosted
Self-host for a genuine constraint; otherwise the hosted model is the better trade.

The operational cost is real and ongoing — running the server, securing it, backing up the storage, monitoring availability, and rotating credentials — so it should be a deliberate decision driven by a genuine constraint rather than a default. A team without an air-gap or data-residency requirement usually gets more value from the hosted cache's zero-maintenance model, spending its effort on the pipeline rather than on cache infrastructure. When the constraint is real, though, self-hosting is what makes remote caching possible at all in an environment that cannot use the hosted service, and the same cache-key discipline and trusted-write policy that secure a hosted cache apply — only now you own the server enforcing them.

Choosing storage and a server implementation

A self-hosted Turborepo cache is a server implementing the cache API backed by a storage layer, and both choices shape its reliability. Several open-source implementations exist that speak the Turborepo cache protocol, typically backed by object storage — an S3-compatible bucket, a cloud blob store, or a filesystem for small setups. Object storage is the usual choice because it is durable, scalable, and cheap for the write-once-read-many pattern a cache exhibits, and it separates the stateless cache server from the durable artifacts so the server can be restarted or scaled without losing the cache.

Storage choice A server speaking the API, backed by object storage. cache server Turborepo API object storage durable artifacts lifecycle policy auto-expire old
Object storage with a lifecycle policy makes a self-hosted cache durable and self-cleaning.

The storage decision also determines the cache's durability and cleanup story. Artifacts accumulate as builds run, so the storage needs a retention or pruning policy — expiring artifacts older than some window, or capping total size — so it does not grow unbounded. Object storage lifecycle rules handle this cleanly, expiring old cache entries automatically. Choosing a mature server implementation backed by durable object storage with a lifecycle policy gives you a cache that is reliable, self-cleaning, and separable from the server process, which is what makes self-hosting sustainable rather than a maintenance burden that degrades as artifacts pile up.

Operating the cache as production infrastructure

Once a self-hosted cache is serving builds, it is production infrastructure that developers and CI depend on, so it deserves the operational rigor of any such service. Availability matters because a cache that is down does not break builds — Turborepo falls back to computing locally — but it does remove the speed-up, so an outage manifests as a suddenly-slow pipeline. Monitoring the cache's availability and hit rate surfaces both an outage and a gradual degradation, so you notice a problem before the team files complaints about slow CI.

Operating the cache as production infrastructure Once a self-hosted cache is serving builds, it is production infrastructure that developers and CI depend on, so it dese Operating the cache as production infrastructure Once a self-hosted cache is serving builds, it is production infrastructure that developers and CI depend on, so it deserves the operational rigor of any such s
Operating the cache as production infrastructure — the core idea of this section at a glance.

Credential rotation and access review are the other ongoing tasks. The tokens that grant write access to trusted builds and read access to everyone should be rotated on a schedule and scoped narrowly, and the access list should be reviewed so a departed team member or a decommissioned CI system no longer holds a token. Backups of the storage protect against accidental deletion or corruption, though a cache is inherently reconstructible by re-running builds, so the backup priority is lower than for irreplaceable data. Treating the self-hosted cache with production discipline — monitoring, rotation, access review, and a storage lifecycle policy — is what keeps it a reliable accelerator rather than a fragile dependency that occasionally slows the whole team down.

Frequently Asked Questions

Do I need Vercel to use a Turborepo remote cache? No. The remote cache is a documented HTTP API, and several open-source servers implement it. Set TURBO_API to your own server's URL, supply a TURBO_TOKEN it recognizes, and Turborepo treats it identically to the hosted cache.

Why does my self-hosted cache return 403 even with a token? Two common causes: the token in your environment does not match the one the server was started with, or TURBO_TEAM lacks the required team_ prefix and the server rejects the namespace. Confirm both, and that TURBO_API points at the server (not the public API).

Should I use the filesystem or S3 backend? Use the filesystem backend for a single server with a persistent volume — it is the simplest setup. Move to S3-compatible storage when you run multiple server replicas or want durable, long-retention artifacts that survive a server rebuild.

Where do I put the endpoint so every developer shares it? Commit the non-secret apiurl and teamslug to .turbo/config.json in the repo. Keep the token out of that file and supply it through the TURBO_TOKEN environment variable on each machine and in CI secrets.

How do I point Turborepo at a self-hosted cache?

Set TURBO_API to your cache endpoint, plus TURBO_TOKEN and TURBO_TEAM for authentication. Run an open-source server that implements the Turborepo cache API, backed by object storage and behind TLS, and Turborepo stores and retrieves artifacts against it like the hosted service.

How do I secure a self-hosted remote cache?

Scope write access to trusted branch builds, keep PR builds read-only, put it behind TLS, include the lockfile in the cache key, and populate it only from frozen, script-free installs. A replayed cache is a trust boundary, so treat it as production infrastructure with backups and monitoring.

When should I self-host instead of using the hosted cache?

When a specific constraint requires it — an air-gapped network, data-residency rules, or a compliance regime that requires you to own the storage and access path. Otherwise the hosted service's zero-maintenance model is usually the better trade.

What storage should back a self-hosted Turborepo cache?

Object storage (an S3-compatible bucket or cloud blob store) is the usual choice — durable, scalable, and cheap for the write-once-read-many pattern, and it separates the stateless server from the artifacts. Add a lifecycle policy to expire old entries so it does not grow unbounded.

Related

Remote Caching Setup