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.

Setup

1. Run the cache server

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

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.

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.

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.

Related

Remote Caching Setup