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.
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 logsfor tasks another machine already cached. - The storage backend grows by one tarball per unique task hash; confirm with
ls /data/cacheoraws s3 ls s3://turbo-cache/. - Hitting the server with a wrong token returns
403, proving auth is enforced.
CI guardrails
- Rotate
TURBO_TOKENon 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 — the cache model and task hash that artifacts are keyed on.
- Fixing Turborepo Remote Cache Misses — debugging misses once your server is wired up.
- Turborepo Pipeline Configuration — declaring the tasks whose outputs get cached.
- Lockfile Management Strategies — keeping the dependency hash stable across runners hitting your cache.