Turborepo Remote Cache Self-Hosted — Vercel 없이 AWS S3 + GitHub Actions로 CI 빌드 시간을 6분에서 45초로 단축하기
Pull request 하나 올릴 때마다 CI가 6분씩 돌아가는 상황, 겪어보신 적 있으신가요? 바꾼 건 packages/ui의 버튼 컴포넌트 하나뿐인데 전체 앱이 다시 빌드되는 걸 보면서 "이게 맞나..." 싶었던 순간들이 있었습니다. Turborepo의 Remote Cache는 정확히 이 문제를 해결하기 위해 만들어진 기능입니다. 팀원 A가 이미 빌드한 결과를 팀원 B가 그대로 가져다 쓸 수 있으니까요.
그런데 공식 원격 캐시를 쓰려면 Vercel 계정이 필요합니다. Vercel은 현재 개인 계정 기준 무료 원격 캐시를 제공하고 있지만, 팀 단위로 사용하거나 캐시 용량·속도 제한 없이 쓰려면 유료 플랜이 필요해집니다(최신 Vercel 플랜 정보). 사내 인프라에 캐시 데이터를 두고 싶거나, Vercel 생태계에 종속되길 원치 않는 팀이라면 자연스럽게 "직접 구축"을 떠올리게 됩니다. 저도 처음엔 이게 꽤 복잡하지 않을까 걱정했는데, 막상 해보니 생각보다 훨씬 단순했습니다.
이 글은 ducktors/turborepo-remote-cache를 사용해 AWS S3를 백엔드로 하는 self-hosted 캐시 서버를 구축하고 GitHub Actions와 연동하는 과정을 다룹니다. 실제 적용 사례에서 CI 빌드 시간이 6분에서 45초로 줄어든 경우도 보고되고 있습니다. Docker로 5분 만에 서버를 띄우는 것부터 시작해서, Lambda 서버리스 배포, MinIO 온프레미스 구성, 그리고 실무에서 자주 실수하는 보안 설정까지 자연스럽게 이어집니다.
이 글은 Docker와 AWS S3의 기본 사용 경험이 있는 독자를 전제합니다. GitHub Actions 설정에 익숙하다면 더 수월하게 따라오실 수 있습니다.
핵심 개념
Turborepo Remote Cache는 어떻게 동작하나요
Turborepo는 태스크를 실행하기 전에 해당 태스크의 입력값(소스 코드 + 환경 변수)을 해싱합니다. 그 해시값을 캐시 서버에 보내서 이미 결과가 저장되어 있으면 작업을 건너뛰고, 없으면 태스크를 실행한 뒤 결과를 업로드하는 방식입니다.
[Turborepo 클라이언트]
│
├── GET /v8/artifacts/{hash} → 캐시 히트: 아티팩트 다운로드 후 재사용
│ → 캐시 미스: 태스크 실행
│
└── PUT /v8/artifacts/{hash} → 태스크 완료 후 결과 업로드핵심은 Turborepo가 공개 API 스펙을 사용한다는 점입니다. Vercel의 서버가 아니어도, 이 API 스펙을 구현한 서버라면 어디든 붙일 수 있습니다. ducktors/turborepo-remote-cache는 Node.js(Fastify) 기반으로 이 스펙을 완전히 구현한 오픈소스 프로젝트입니다.
Turborepo Remote Cache API: Vercel이 공개한 캐시 프로토콜로,
GET /v8/artifacts/{hash}로 캐시를 조회하고PUT /v8/artifacts/{hash}로 캐시를 저장하는 HTTP API 스펙입니다. 이 스펙을 구현한 서버라면 공식 Turborepo 클라이언트와 연동 가능합니다.
캐시가 깨지는 경우 — 예상치 못한 캐시 미스 디버깅
자체 캐시 서버를 운영하다 보면 "분명히 코드 안 건드렸는데 왜 캐시가 안 맞지?" 싶은 순간이 옵니다. Turborepo가 캐시 해시를 계산할 때 포함하는 요소는 다음과 같습니다.
- 해당 패키지의 소스 파일 내용
package.json의존성 버전turbo.json의env필드에 명시된 환경 변수 값- 의존 관계에 있는 상위 패키지의 변경 여부
즉, 코드를 건드리지 않았더라도 NODE_ENV나 배포 환경 관련 변수가 달라지면 캐시 미스가 발생합니다. 반대로 env 필드에 필요한 환경 변수를 빠뜨리면 다른 환경에서 같은 캐시를 잘못 재사용하는 문제가 생길 수 있습니다. 캐시가 예상대로 히트되지 않는다면 아래 명령으로 해시 계산에 포함된 입력 목록을 확인해볼 수 있습니다.
pnpm turbo run build --dry=json클라이언트 연결에 필요한 세 가지 환경 변수
Turborepo 클라이언트가 외부 캐시 서버를 바라보도록 하려면 세 가지 환경 변수만 설정하면 됩니다.
| 환경 변수 | 역할 | 예시 값 |
|---|---|---|
TURBO_API |
캐시 서버 URL | https://cache.example.com |
TURBO_TOKEN |
Bearer 인증 토큰 | my-secret-token-abc123 |
TURBO_TEAM |
팀 식별자(슬러그) | my-team |
turbo login이나 turbo link 같은 Vercel 전용 명령어는 사용하지 않습니다. 환경 변수를 직접 설정하거나 .turbo/config.json을 수정하는 방식으로 연결합니다.
// .turbo/config.json (로컬 개발 시 활용, git 커밋 가능)
{
"teamId": "my-team",
"apiUrl": "http://localhost:3000"
}토큰(
TURBO_TOKEN)은.turbo/config.json에 포함하지 않습니다. 환경 변수로 별도 설정하거나 CI 시크릿에 등록해야 합니다.config.json파일 자체는 민감 정보가 없으니 git에 커밋해도 안전합니다.
실전 적용
Docker Compose로 캐시 서버 빠르게 띄우기
처음 시도해볼 때 가장 진입 장벽이 낮은 방법입니다. ducktors/turborepo-remote-cache는 Docker 이미지를 공식 제공하기 때문에, docker-compose.yml 하나로 서버를 올릴 수 있습니다. 저도 처음엔 S3 설정까지 한 번에 하려다가 뭔가 꼬여서, 먼저 STORAGE_PROVIDER: local로 동작을 확인하고 나서 S3로 전환했을 때가 훨씬 수월했습니다.
# docker-compose.yml
version: '3.8'
services:
turborepo-cache:
image: ducktors/turborepo-remote-cache:latest
ports:
- "3000:3000"
environment:
NODE_ENV: production
PORT: 3000
TURBO_TOKEN: ${TURBO_TOKEN} # 최소 20자 이상의 랜덤 문자열 권장
STORAGE_PROVIDER: s3
STORAGE_PATH: my-turborepo-cache-bucket
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
AWS_REGION: ap-northeast-2
restart: unless-stopped| 환경 변수 | 설명 |
|---|---|
STORAGE_PROVIDER |
스토리지 백엔드 선택. s3, gcs, azure-blob, local 중 하나 |
STORAGE_PATH |
S3 버킷 이름 또는 로컬 경로 |
TURBO_TOKEN |
클라이언트 인증에 사용할 임의의 시크릿 토큰 |
AWS_REGION |
S3 버킷이 위치한 리전 |
서버가 뜨면 헬스체크 엔드포인트로 동작을 확인할 수 있습니다. 로컬에서 처음 테스트할 때는 localhost:3000으로 요청하고, 이후 프로덕션 URL로 바꾸는 방식으로 진행하시면 됩니다.
# 로컬에서 처음 확인할 때
curl http://localhost:3000/v8/artifacts/status \
-H "Authorization: Bearer ${TURBO_TOKEN}"
# {"status":"enabled"}
# 프로덕션 서버에 배포 후
curl https://cache.example.com/v8/artifacts/status \
-H "Authorization: Bearer ${TURBO_TOKEN}"
# {"status":"enabled"}S3 버킷은 반드시 퍼블릭 접근 차단 상태로 생성하고, IAM 사용자에게는 해당 버킷에 대한 최소 권한만 부여하는 것을 권장합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-turborepo-cache-bucket/*"
}
]
}GitHub Actions와 연동하기
서버가 준비됐다면 GitHub Actions 워크플로에 클라이언트 설정을 추가합니다. 생각보다 간단한데, 환경 변수 세 개만 추가하면 됩니다.
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
env:
TURBO_API: ${{ secrets.TURBO_API }} # 캐시 서버 URL
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} # 위에서 설정한 토큰
TURBO_TEAM: my-team # 팀 슬러그
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm turbo build test lint
# 첫 실행: 캐시 미스 → 실행 후 업로드
# 이후 실행: 캐시 히트 → 건너뜀 (수 초 내 완료)GitHub 리포지토리의 Settings → Secrets and variables → Actions에서 TURBO_API와 TURBO_TOKEN을 등록해두면, 팀 전체가 같은 캐시를 공유하게 됩니다.
Lambda 서버리스로 운영 오버헤드 줄이기
이 방식이 맞는 상황: 별도 서버를 24시간 유지하기 부담스럽고, 요청 빈도가 낮은 소규모 팀이나 사이드 프로젝트에 적합합니다. 요청이 없을 때는 비용이 발생하지 않아 경제적입니다.
ducktors/turborepo-remote-cache는 Fastify 앱을 AWS Lambda 핸들러로 래핑하는 방식을 지원합니다. 핵심 구성은 다음과 같습니다.
// lambda.js
const awsLambdaFastify = require('@fastify/aws-lambda')
const { createApp } = require('turborepo-remote-cache')
const app = createApp({
storageProvider: 's3',
storagePath: process.env.STORAGE_PATH,
awsRegion: process.env.AWS_REGION,
})
const proxy = awsLambdaFastify(app)
exports.handler = async (event, context) => {
await app.ready()
return proxy(event, context)
}Lambda 함수는 IAM 역할 기반 인증을 사용할 수 있어 AWS_ACCESS_KEY_ID 없이도 S3에 접근 가능합니다. 함수에 연결할 IAM 역할에 s3:GetObject, s3:PutObject, s3:DeleteObject 권한만 부여하면 됩니다.
S3 버킷 생성과 캐시 만료 규칙은 어느 배포 방식을 선택하든 동일하게 적용합니다.
# S3 버킷 생성 (퍼블릭 접근 차단 필수)
aws s3api create-bucket \
--bucket my-turborepo-cache \
--region ap-northeast-2 \
--create-bucket-configuration LocationConstraint=ap-northeast-2
# 30일 후 자동 삭제되는 Lifecycle 규칙 적용 (비용 절감)
aws s3api put-bucket-lifecycle-configuration \
--bucket my-turborepo-cache \
--lifecycle-configuration '{
"Rules": [{
"ID": "expire-cache",
"Status": "Enabled",
"Filter": {"Prefix": ""},
"Expiration": {"Days": 30}
}]
}'Lambda 배포 도구(Serverless Framework, AWS SAM, CDK)는 팀의 기존 인프라 구성에 맞는 것을 선택하시면 됩니다. 공식 Lambda 배포 가이드에서 상세한 설정 예시를 확인할 수 있습니다.
MinIO로 완전한 온프레미스 구성
이 방식이 맞는 상황: AWS 계정 없이 자체 서버만으로 운영해야 하거나, 데이터가 외부 클라우드로 전혀 나가서는 안 되는 보안 요구사항이 있는 환경에 적합합니다. 개발 환경에서 AWS 없이 로컬로 전체 스택을 띄워볼 때도 유용합니다.
# docker-compose.yml (MinIO 통합 구성)
version: '3.8'
services:
minio:
image: minio/minio
command: server /data --console-address ":9001"
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: minio-admin
MINIO_ROOT_PASSWORD: change-this-before-use # 예시값 — 실제 환경에서는 반드시 변경할 것
volumes:
- minio-data:/data
turborepo-cache:
image: ducktors/turborepo-remote-cache:latest
ports:
- "3000:3000"
environment:
STORAGE_PROVIDER: s3
STORAGE_PATH: turbo-cache
S3_ENDPOINT: http://minio:9000 # MinIO 엔드포인트 지정
AWS_ACCESS_KEY_ID: minio-admin
AWS_SECRET_ACCESS_KEY: change-this-before-use # 예시값 — 실제 환경에서는 반드시 변경할 것
AWS_REGION: us-east-1 # MinIO는 리전 값이 임의여도 됨
TURBO_TOKEN: ${TURBO_TOKEN}
depends_on:
- minio
volumes:
minio-data:MinIO: AWS S3의 API를 그대로 구현한 오픈소스 오브젝트 스토리지입니다.
S3_ENDPOINT환경 변수로 엔드포인트를 지정하면STORAGE_PROVIDER: s3를 그대로 사용할 수 있습니다. DigitalOcean Spaces도 같은 방식으로 연동 가능합니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 벤더 독립성 | Vercel 계정·플랜 없이 운영 가능. 플랫폼 종속성 없음 |
| 데이터 주권 | 캐시 아티팩트가 자체 S3 버킷에 저장. 외부로 나가지 않음 |
| 다양한 스토리지 지원 | S3, GCS, Azure Blob, MinIO, DO Spaces, 로컬 파일시스템 |
| 빠른 구축 | Docker 이미지 하나로 즉시 운영 가능 |
| CI 시간 단축 | 팀·파이프라인 간 캐시 공유로 중복 빌드 제거. 6분 → 45초 사례 보고 |
| 라이브러리 임베드 | npm 패키지로 제공되어 기존 Node.js 서버에 통합 가능 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 운영 오버헤드 | 서버 호스팅·모니터링·업데이트를 직접 관리해야 함 | Lambda 서버리스 배포로 관리 부담 최소화 |
| 캐시 비용 누적 | 오래된 캐시 객체가 쌓이면 스토리지 비용 증가 | S3 Lifecycle rule로 30~60일 후 자동 삭제 설정 |
| 보안 설정 | 토큰 관리, 버킷 퍼블릭 접근 차단, 민감 정보 캐시 포함 위험 | S3 버킷 퍼블릭 접근 차단 + turbo.json의 env 필드 명시적 관리 |
| 변경 규모에 따른 효과 차이 | 변경된 패키지가 많을수록 캐시 미스 증가, 효과 감소 | 패키지 경계를 명확히 나누는 설계와 함께 사용 |
| 에페머럴 환경 제한 | CI 러너 로컬 캐시 재사용 불가, 반드시 원격 캐시 서버 필요 | 원격 캐시 서버 URL을 모든 파이프라인에 통일 적용 |
에페머럴(Ephemeral) CI 환경: 각 CI 실행마다 새 컨테이너가 생성되고 종료 후 삭제되는 방식입니다. 로컬 파일 캐시가 다음 실행까지 유지되지 않아, 팀 간 캐시 공유는 반드시 원격 서버를 통해야 합니다.
실무에서 가장 흔한 실수
솔직히 아래 실수들은 저도 처음엔 그냥 넘어갔다가 나중에 다시 돌아와서 고친 것들입니다.
-
S3 버킷 퍼블릭 접근을 열어두는 경우: 처음에 "어차피 토큰이 있으니까 버킷 접근은 열어도 되지 않나?" 싶어서 퍼블릭으로 뒀다가 팀에서 지적받은 경험이 있습니다. 캐시 서버 자체가 퍼블릭에 노출되더라도, 백엔드 S3 버킷은 반드시 퍼블릭 접근을 차단하고 IAM 역할이나 액세스 키로만 접근하도록 설정해야 합니다. 빌드 아티팩트에는 소스 코드 스냅샷이 포함될 수 있기 때문입니다.
-
turbo.json의env필드를 빠뜨리는 경우: Turborepo는 빌드 로그도 아티팩트로 캐싱합니다.env필드에 캐시 해시에 포함할 환경 변수를 명시하지 않으면, 데이터베이스 비밀번호 같은 민감한 정보가 로그를 통해 캐시에 섞여 저장될 위험이 있습니다. 다음처럼 캐시 해시에 영향을 줘야 하는 환경 변수를 명시적으로 관리하는 것을 권장합니다.
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"],
"env": ["NODE_ENV", "NEXT_PUBLIC_API_URL"]
}
}
}- S3 Lifecycle rule 설정을 나중으로 미루는 경우: 처음 설정할 때는 "나중에 하지 뭐" 싶지만, 몇 달 후 S3 비용 청구서를 보고 다시 찾아오게 됩니다. 버킷을 생성하는 시점에 위의 Lambda 섹션 CLI 예시처럼 30~60일 만료 규칙을 함께 걸어두시는 것을 강력히 권장합니다.
마치며
ducktors/turborepo-remote-cache와 S3 조합은 Vercel 없이도 팀 전체가 빌드 캐시를 공유할 수 있는 현실적인 선택입니다. Mercari의 사례처럼 대규모 팀에서 CI 잡 시간을 30~50% 단축한 보고도 있지만, 패키지 두세 개짜리 소규모 모노레포에서도 반복 빌드 비용을 눈에 띄게 줄일 수 있습니다. 운영 오버헤드가 걱정된다면 Lambda 방식으로 시작하고, 데이터를 외부로 내보낼 수 없는 환경이라면 MinIO 조합이 있습니다. 상황에 맞는 선택지가 이미 준비되어 있습니다.
지금 바로 시작해볼 수 있는 3단계:
-
로컬에서 먼저 동작을 확인해볼 수 있습니다.
npx turborepo-remote-cache명령으로 로컬 파일시스템 기반의 캐시 서버를 즉시 실행한 뒤, 터미널에 출력된 토큰을TURBO_TOKEN에 붙여넣고TURBO_API=http://localhost:3000으로 설정해 캐시 히트가 발생하는지 확인해보시면 좋습니다. -
S3 버킷과 IAM 권한을 준비하는 것을 권장합니다. 버킷을 새로 생성할 때 퍼블릭 접근 차단을 기본 설정하고, S3 Lifecycle rule로 30일 만료를 함께 걸어두면 나중에 손볼 일이 줄어듭니다. IAM 사용자에게는 해당 버킷에 대한
s3:GetObject,s3:PutObject,s3:DeleteObject권한만 부여하면 충분합니다. -
GitHub Actions 시크릿에
TURBO_API,TURBO_TOKEN을 등록하고 워크플로를 한 번 실행해볼 수 있습니다. Turbo 출력 로그에서FULL TURBO(캐시 히트) 메시지가 보이기 시작하면 캐시가 정상적으로 동작하고 있다는 신호입니다.
참고 자료
- ducktors/turborepo-remote-cache | GitHub
- Turborepo Remote Cache 공식 문서 | ducktors
- Supported Storage Providers | ducktors
- Custom Remote Caching 설정 가이드 | ducktors
- Running in AWS Lambda | ducktors
- ducktors/turborepo-remote-cache | Docker Hub
- Remote Caching 공식 문서 | Turborepo
- Setting Up Turborepo Remote Cache with S3 and GitHub Actions | januschung
- Turborepo Remote Cache로 CI 가속화 | Mercari Engineering
- Alternative remote caching hosts | Turborepo 커뮤니티
- trappar/turborepo-remote-cache-gh-action | GitHub
- Optimizing CI/CD with Turborepo Remote Caching | Leapcell