Skip to content

CDN Service Deployment ​

Deploy the CDN Service to Cloudflare Workers or as a Docker container.

Prerequisites ​

  • Node.js 20+
  • Cloudflare account (for Workers deployment)
  • Docker (for Docker deployment)

Cloudflare Workers Deployment ​

1. Clone the Repository ​

bash
git clone https://github.com/epinnock/scry-cdn-service.git
cd scry-cdn-service
npm install

2. Create R2 Bucket ​

bash
wrangler r2 bucket create scry-static-sites

3. Create KV Namespace ​

bash
# Production namespace
wrangler kv:namespace create CDN_CACHE

# Preview namespace (for local development)
wrangler kv:namespace create CDN_CACHE --preview

Note the IDs returned for each namespace.

4. Configure wrangler.toml ​

toml
name = "scry-cdn-service"
main = "cloudflare/index.ts"
compatibility_date = "2024-01-01"

# R2 Bucket Binding
[[r2_buckets]]
binding = "STATIC_SITES"
bucket_name = "scry-static-sites"
preview_bucket_name = "scry-static-sites-staging"

# KV Namespace Binding
[[kv_namespaces]]
binding = "CDN_CACHE"
id = "your-production-kv-id"
preview_id = "your-preview-kv-id"

# Worker Routes
routes = [
  { pattern = "view.scrymore.com/*", zone_name = "scrymore.com" }
]

# Environment Variables
[vars]
PLATFORM = "cloudflare"
CACHE_CONTROL = "public, max-age=31536000, immutable"
ALLOWED_ORIGINS = "*"

5. Set Secrets (if needed) ​

bash
wrangler secret put FIREBASE_PROJECT_ID
wrangler secret put FIREBASE_CLIENT_EMAIL
wrangler secret put FIREBASE_PRIVATE_KEY
wrangler secret put PREVIEW_TOKEN_SECRET

Bindings and secrets ​

NameKindPurpose
STATIC_SITESR2 bindingBucket holding the build archives
CDN_CACHEKV bindingCached ZIP central directories
FIREBASE_PROJECT_ID / FIREBASE_CLIENT_EMAIL / FIREBASE_PRIVATE_KEYSecretsVerify sessions for private projects
PREVIEW_TOKEN_SECRETSecretVerifies signed preview tokens minted by the dashboard
PREVIEW_TOKEN_SECRET_PREVIOUSSecretOptional. Lets the secret rotate without invalidating live tokens
CORS_ALLOWED_ORIGINSVarOrigins allowed to embed a story
CORS_FORCE_WILDCARDVarDevelopment escape hatch; leave off in production

PREVIEW_TOKEN_SECRET must match the dashboard's, since the dashboard mints the token and the CDN verifies it. Rotate by setting the new value and moving the old one into PREVIEW_TOKEN_SECRET_PREVIOUS until outstanding tokens expire.

Deploy stamps (SCRY_ENV, SCRY_COMMIT, SCRY_BRANCH, SCRY_BUILD_TIME, SCRY_DEPLOY_ID, SCRY_ACTOR) are injected at build time and reported by the health endpoints.

6. Deploy ​

bash
npm run build:cloudflare
wrangler deploy

Docker Deployment ​

1. Build Image ​

bash
npm run docker:build
# or
docker build -t scry-cdn-service .

2. Run Container ​

bash
docker run -d \
  -p 3000:3000 \
  -e PLATFORM=docker \
  -e STORAGE_TYPE=filesystem \
  -e STORAGE_PATH=/data \
  -v /var/data/static-sites:/data \
  scry-cdn-service:latest

Using R2 with Docker ​

bash
docker run -d \
  -p 3000:3000 \
  -e PLATFORM=docker \
  -e STORAGE_TYPE=r2 \
  -e R2_BUCKET=scry-static-sites \
  -e R2_ACCOUNT_ID=your-account-id \
  -e R2_ACCESS_KEY_ID=your-access-key \
  -e R2_SECRET_ACCESS_KEY=your-secret-key \
  scry-cdn-service:latest

Docker Compose ​

yaml
version: '3.8'

services:
  cdn-service:
    build: .
    ports:
      - "3000:3000"
    environment:
      - PLATFORM=docker
      - STORAGE_TYPE=r2
      - R2_BUCKET=${R2_BUCKET}
      - R2_ACCOUNT_ID=${R2_ACCOUNT_ID}
      - R2_ACCESS_KEY_ID=${R2_ACCESS_KEY_ID}
      - R2_SECRET_ACCESS_KEY=${R2_SECRET_ACCESS_KEY}
    restart: unless-stopped

DNS Configuration ​

Cloudflare DNS ​

The CDN routes by path (/{projectId}/{versionId}/{file}), so it needs one hostname, not a wildcard:

TypeNameContentProxy
AAAAview100::Enabled

Other DNS Providers ​

Point your domain to Cloudflare nameservers, then configure DNS in Cloudflare.

Environment Variables ​

VariableDefaultDescription
PLATFORM-cloudflare or docker
STORAGE_TYPEfilesystemfilesystem or r2 (Docker only)
STORAGE_PATH/dataPath for filesystem storage
R2_BUCKET-R2 bucket name
R2_ACCOUNT_ID-Cloudflare account ID
R2_ACCESS_KEY_ID-R2 access key
R2_SECRET_ACCESS_KEY-R2 secret key
CACHE_CONTROLpublic, max-age=31536000Cache-Control header
ALLOWED_ORIGINS*CORS allowed origins

Verification ​

Health Check ​

bash
curl https://view-test.scrymore.com/health

Expected response:

json
{
  "status": "healthy",
  "service": "scry-cdn-service",
  "platform": "cloudflare",
  "timestamp": "2024-01-15T10:30:00.000Z"
}

Test File Serving ​

bash
# Upload a test ZIP
wrangler r2 object put STATIC_SITES/test-project.zip --file=test.zip

# Access by path
curl http://localhost:8787/test-project/v1/index.html

CI/CD Deployment ​

GitHub Actions ​

yaml
name: Deploy CDN Service

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci
      - run: npm run build:cloudflare

      - name: Deploy to Cloudflare
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          workingDirectory: cloudflare

Monitoring ​

Cloudflare Dashboard ​

  • Workers → Analytics
  • View request counts, errors, latency
  • Monitor KV and R2 usage

Real-time Logs ​

bash
wrangler tail

Docker Logs ​

bash
docker logs -f scry-cdn-service

Scaling ​

Cloudflare Workers ​

  • Automatic global distribution
  • No configuration needed
  • Scales to millions of requests

Docker ​

  • Use Kubernetes or Docker Swarm
  • Add load balancer
  • Configure horizontal pod autoscaler

Troubleshooting ​

"Central directory not found" ​

  • Ensure ZIP file exists in R2/storage
  • Check file path format: {project}.zip

"KV namespace not found" ​

  • Verify KV namespace ID in wrangler.toml
  • Ensure KV binding name matches code

"Route not matching" ​

  • Check DNS configuration
  • Verify route pattern in wrangler.toml
  • Ensure zone_name matches your domain

CORS Errors ​

  • Check ALLOWED_ORIGINS configuration
  • Verify CORS headers in response

Next Steps ​