# Deployment Guide

> Complete guide to deploying Cloudflare Workers with Wrangler, including CI/CD patterns and production best practices.

- Skill: `tools-only/deployment-guide-3` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/deployment-guide-3`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/deployment-guide-3/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-29
- Page: https://skillmd.com/skills/tools-only/deployment-guide-3

---

# Deployment Guide

**Last Updated**: 2025-10-20

Complete guide to deploying Cloudflare Workers with Wrangler, including CI/CD patterns and production best practices.

---

## Table of Contents

1. [Prerequisites](#prerequisites)
2. [Wrangler Commands](#wrangler-commands)
3. [Environment Configuration](#environment-configuration)
4. [CI/CD Pipelines](#cicd-pipelines)
5. [Production Best Practices](#production-best-practices)
6. [Monitoring and Logs](#monitoring-and-logs)

---

## Prerequisites

### 1. Cloudflare Account

Sign up at https://dash.cloudflare.com/sign-up

### 2. Get Account ID

```bash
# Option 1: From dashboard
# Go to: Workers & Pages → Overview → Account ID (right sidebar)

# Option 2: Via Wrangler
wrangler whoami
```

Add to `wrangler.jsonc`:
```jsonc
{
  "account_id": "YOUR_ACCOUNT_ID_HERE"
}
```

### 3. Authenticate Wrangler

```bash
# Login via browser
wrangler login

# Or use API token (for CI/CD)
export CLOUDFLARE_API_TOKEN="your-token"
```

**Create API token**:
1. Go to: https://dash.cloudflare.com/profile/api-tokens
2. Click "Create Token"
3. Use template: "Edit Cloudflare Workers"
4. Copy token (only shown once!)

---

## Wrangler Commands

### Development

```bash
# Start local dev server (http://localhost:8787)
wrangler dev

# Local mode (no network requests to Cloudflare)
wrangler dev --local

# Custom port
wrangler dev --port 3000

# Specific environment
wrangler dev --env staging
```

### Deployment

```bash
# Deploy to production
wrangler deploy

# Deploy to specific environment
wrangler deploy --env staging
wrangler deploy --env production

# Dry run (validate without deploying)
wrangler deploy --dry-run

# Deploy with compatibility date
wrangler deploy --compatibility-date 2025-10-11
```

### Type Generation

```bash
# Generate TypeScript types for bindings
wrangler types

# Output to custom file
wrangler types --output-file=types/worker.d.ts
```

### Logs

```bash
# Tail live logs
wrangler tail

# Filter by status code
wrangler tail --status error

# Filter by HTTP method
wrangler tail --method POST

# Filter by IP
wrangler tail --ip-address 1.2.3.4

# Format as JSON
wrangler tail --format json
```

### Deployments

```bash
# List recent deployments
wrangler deployments list

# View specific deployment
wrangler deployments view DEPLOYMENT_ID

# Rollback to previous deployment
wrangler rollback --deployment-id DEPLOYMENT_ID
```

### Secrets

```bash
# Set secret (interactive)
wrangler secret put MY_SECRET

# Set secret from file
echo "secret-value" | wrangler secret put MY_SECRET

# List secrets
wrangler secret list

# Delete secret
wrangler secret delete MY_SECRET
```

### KV Operations

```bash
# Create KV namespace
wrangler kv namespace create MY_KV

# List namespaces
wrangler kv namespace list

# Put key-value
wrangler kv key put --namespace-id=YOUR_ID "key" "value"

# Get value
wrangler kv key get --namespace-id=YOUR_ID "key"

# List keys
wrangler kv key list --namespace-id=YOUR_ID
```

### D1 Operations

```bash
# Create D1 database
wrangler d1 create my-database

# Execute SQL
wrangler d1 execute my-database --command "SELECT * FROM users"

# Run SQL file
wrangler d1 execute my-database --file schema.sql

# List databases
wrangler d1 list
```

### R2 Operations

```bash
# Create R2 bucket
wrangler r2 bucket create my-bucket

# List buckets
wrangler r2 bucket list

# Upload file
wrangler r2 object put my-bucket/file.txt --file local-file.txt

# Download file
wrangler r2 object get my-bucket/file.txt --file local-file.txt
```

---

## Environment Configuration

### Single Environment (Default)

```jsonc
{
  "name": "my-worker",
  "account_id": "YOUR_ACCOUNT_ID",
  "main": "src/index.ts",
  "compatibility_date": "2025-10-11",
  "vars": {
    "ENV": "production"
  },
  "kv_namespaces": [
    { "binding": "MY_KV", "id": "production-kv-id" }
  ]
}
```

### Multiple Environments

```jsonc
{
  "name": "my-worker",
  "account_id": "YOUR_ACCOUNT_ID",
  "main": "src/index.ts",
  "compatibility_date": "2025-10-11",

  // Shared configuration
  "observability": {
    "enabled": true
  },

  // Environment-specific configuration
  "env": {
    "staging": {
      "name": "my-worker-staging",
      "vars": {
        "ENV": "staging",
        "API_URL": "https://api-staging.example.com"
      },
      "kv_namespaces": [
        { "binding": "MY_KV", "id": "staging-kv-id" }
      ],
      "d1_databases": [
        { "binding": "DB", "database_name": "my-db-staging", "database_id": "staging-db-id" }
      ]
    },

    "production": {
      "name": "my-worker-production",
      "vars": {
        "ENV": "production",
        "API_URL": "https://api.example.com"
      },
      "kv_namespaces": [
        { "binding": "MY_KV", "id": "production-kv-id" }
      ],
      "d1_databases": [
        { "binding": "DB", "database_name": "my-db", "database_id": "production-db-id" }
      ],
      "routes": [
        { "pattern": "example.com/*", "zone_name": "example.com" }
      ]
    }
  }
}
```

Deploy:
```bash
wrangler deploy --env staging
wrangler deploy --env production
```

### Environment Detection in Code

```typescript
app.get('/api/info', (c) => {
  const env = c.env.ENV || 'development'
  const apiUrl = c.env.API_URL || 'http://localhost:3000'

  return c.json({ env, apiUrl })
})
```

---

## CI/CD Pipelines

### GitHub Actions

Create `.github/workflows/deploy.yml`:

```yaml
name: Deploy to Cloudflare Workers

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    name: Deploy

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

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

**With environment-specific deployment**:

```yaml
name: Deploy

on:
  push:
    branches:
      - main
      - staging

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

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Deploy to staging
        if: github.ref == 'refs/heads/staging'
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: deploy --env staging

      - name: Deploy to production
        if: github.ref == 'refs/heads/main'
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: deploy --env production
```

**Add secrets to GitHub**:
1. Go to: Repository → Settings → Secrets → Actions
2. Add `CLOUDFLARE_API_TOKEN`
3. Add `CLOUDFLARE_ACCOUNT_ID`

### GitLab CI/CD

Create `.gitlab-ci.yml`:

```yaml
image: node:20

stages:
  - test
  - deploy

test:
  stage: test
  script:
    - npm ci
    - npm test

deploy_staging:
  stage: deploy
  script:
    - npm ci
    - npx wrangler deploy --env staging
  only:
    - staging
  environment:
    name: staging

deploy_production:
  stage: deploy
  script:
    - npm ci
    - npx wrangler deploy --env production
  only:
    - main
  environment:
    name: production
```

**Add variables to GitLab**:
1. Go to: Settings → CI/CD → Variables
2. Add `CLOUDFLARE_API_TOKEN` (masked)
3. Add `CLOUDFLARE_ACCOUNT_ID`

### Manual Deployment Script

Create `scripts/deploy.sh`:

```bash
#!/bin/bash
set -e

ENV=${1:-production}

echo "🚀 Deploying to $ENV..."

# Run tests
echo "Running tests..."
npm test

# Type check
echo "Type checking..."
npm run type-check

# Build
echo "Building..."
npm run build

# Deploy
echo "Deploying to Cloudflare..."
if [ "$ENV" = "production" ]; then
  wrangler deploy --env production
else
  wrangler deploy --env staging
fi

echo "✅ Deployment complete!"
echo "🔗 Check logs: wrangler tail --env $ENV"
```

Usage:
```bash
chmod +x scripts/deploy.sh
./scripts/deploy.sh staging
./scripts/deploy.sh production
```

---

## Production Best Practices

### 1. Use Compatibility Dates

Always set a recent `compatibility_date`:

```jsonc
{
  "compatibility_date": "2025-10-11"
}
```

**Why**: Ensures consistent behavior and access to new features.

**Update regularly**: Check https://developers.cloudflare.com/workers/configuration/compatibility-dates/

### 2. Enable Observability

```jsonc
{
  "observability": {
    "enabled": true
  }
}
```

**Provides**:
- Real-time metrics
- Error tracking
- Performance monitoring

### 3. Set Resource Limits

```jsonc
{
  "limits": {
    "cpu_ms": 50  // Maximum CPU time per request (paid plan)
  }
}
```

### 4. Configure Custom Domains

```jsonc
{
  "routes": [
    {
      "pattern": "api.example.com/*",
      "zone_name": "example.com"
    }
  ]
}
```

**Or via dashboard**:
1. Workers & Pages → Your Worker → Triggers
2. Add Custom Domain

### 5. Use Secrets for Sensitive Data

```bash
# Never commit secrets to git
wrangler secret put API_KEY
wrangler secret put DATABASE_URL
```

```typescript
// Access in code
const apiKey = c.env.API_KEY
```

### 6. Implement Rate Limiting

```typescript
import { Hono } from 'hono'

const app = new Hono()

app.use('/api/*', async (c, next) => {
  const ip = c.req.header('cf-connecting-ip')
  const key = `rate-limit:${ip}`

  const count = await c.env.MY_KV.get(key)
  if (count && parseInt(count) > 100) {
    return c.json({ error: 'Rate limit exceeded' }, 429)
  }

  await c.env.MY_KV.put(key, (parseInt(count || '0') + 1).toString(), {
    expirationTtl: 60  // 1 minute
  })

  await next()
})
```

### 7. Add Health Check Endpoint

```typescript
app.get('/health', (c) => {
  return c.json({
    status: 'ok',
    version: '1.0.0',
    timestamp: new Date().toISOString()
  })
})
```

### 8. Implement Error Tracking

```typescript
app.onError((err, c) => {
  console.error('Error:', err)

  // Send to error tracking service
  // await sendToSentry(err)

  return c.json({
    error: 'Internal Server Error',
    requestId: c.req.header('cf-ray')
  }, 500)
})
```

### 9. Use Structured Logging

```typescript
import { logger } from 'hono/logger'

app.use('*', logger())

app.get('/api/users', (c) => {
  console.log(JSON.stringify({
    level: 'info',
    message: 'Fetching users',
    userId: c.req.header('x-user-id'),
    timestamp: new Date().toISOString()
  }))

  return c.json({ users: [] })
})
```

### 10. Test Before Deploying

```bash
# Run tests
npm test

# Type check
npm run type-check

# Lint
npm run lint

# Test locally
wrangler dev --local

# Test remotely (without deploying)
wrangler dev
```

---

## Monitoring and Logs

### Real-Time Logs

```bash
# Tail all requests
wrangler tail

# Filter by status
wrangler tail --status error
wrangler tail --status ok

# Filter by method
wrangler tail --method POST

# Filter by search term
wrangler tail --search "error"

# Output as JSON
wrangler tail --format json
```

### Analytics Dashboard

View in Cloudflare Dashboard:
1. Workers & Pages → Your Worker → Metrics
2. See:
   - Requests per second
   - Errors
   - CPU time
   - Response time

### Custom Metrics

```typescript
app.use('*', async (c, next) => {
  const start = Date.now()

  await next()

  const duration = Date.now() - start

  console.log(JSON.stringify({
    type: 'metric',
    name: 'request_duration',
    value: duration,
    path: c.req.path,
    method: c.req.method,
    status: c.res.status
  }))
})
```

### External Monitoring

**Use Workers Analytics Engine**:

```typescript
app.use('*', async (c, next) => {
  await next()

  // Write to Analytics Engine
  c.env.ANALYTICS.writeDataPoint({
    indexes: [c.req.path],
    blobs: [c.req.method, c.req.header('user-agent')],
    doubles: [Date.now(), c.res.status]
  })
})
```

**Or send to external services**:

```typescript
// Send to Datadog, New Relic, etc.
await fetch('https://api.datadoghq.com/api/v1/logs', {
  method: 'POST',
  headers: {
    'DD-API-KEY': c.env.DATADOG_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    message: 'Request processed',
    status: c.res.status,
    path: c.req.path
  })
})
```

---

## Rollback Strategy

### Immediate Rollback

```bash
# List recent deployments
wrangler deployments list

# Rollback to specific deployment
wrangler rollback --deployment-id DEPLOYMENT_ID
```

### Gradual Rollout

```jsonc
{
  "name": "my-worker-canary",
  "routes": [
    {
      "pattern": "example.com/*",
      "zone_name": "example.com",
      "script": "my-worker"
    }
  ]
}
```

1. Deploy new version to `-canary` worker
2. Route 10% of traffic to canary
3. Monitor metrics
4. Gradually increase to 100%
5. Promote canary to main

---

## Performance Optimization

### 1. Minimize Bundle Size

```bash
# Check bundle size
wrangler deploy --dry-run --outdir=dist

# Analyze
ls -lh dist/
```

**Tips**:
- Avoid large dependencies
- Use dynamic imports for heavy modules
- Tree-shake unused code

### 2. Use Edge Caching

```typescript
app.get('/api/data', async (c) => {
  const cache = caches.default
  const cacheKey = new Request(c.req.url, c.req.raw)

  let response = await cache.match(cacheKey)

  if (!response) {
    // Fetch data
    const data = await fetchData()
    response = c.json(data)

    // Cache for 5 minutes
    response.headers.set('Cache-Control', 'max-age=300')
    c.executionCtx.waitUntil(cache.put(cacheKey, response.clone()))
  }

  return response
})
```

### 3. Optimize Database Queries

```typescript
// ❌ Bad: N+1 queries
for (const user of users) {
  const posts = await c.env.DB.prepare('SELECT * FROM posts WHERE user_id = ?').bind(user.id).all()
}

// ✅ Good: Single query
const posts = await c.env.DB.prepare('SELECT * FROM posts WHERE user_id IN (?)').bind(userIds).all()
```

---

## Troubleshooting Deployments

### Deployment Fails

```bash
# Check configuration
wrangler deploy --dry-run

# Verbose output
WRANGLER_LOG=debug wrangler deploy

# Check account access
wrangler whoami
```

### Build Errors

```bash
# Clear cache
rm -rf node_modules/.wrangler
rm -rf .wrangler

# Reinstall dependencies
npm ci

# Try again
npm run deploy
```

### Routes Not Working

```bash
# List routes
wrangler routes list

# Check zone assignment
# Dashboard → Workers & Pages → Your Worker → Triggers
```

---

**Production-tested deployment patterns** ✅
**CI/CD examples validated** ✅
**Monitoring strategies proven** ✅

