Skip to main content

Deployment

Alchemy makes it simple to deploy infrastructure to production. This guide covers deployment strategies, best practices, and CI/CD integration.

Basic Deployment

Deploy your application by running your alchemy.run.ts script:
bun ./alchemy.run.ts
This will:
  1. Create or update resources
  2. Save state to .alchemy/
  3. Display resource URLs and outputs

Deployment Phases

Alchemy supports three phases:

Up Phase (Default)

Create or update resources:
bun ./alchemy.run.ts
# Or explicitly:
bun ./alchemy.run.ts --stage production

Read Phase

Read resource state without making changes:
bun ./alchemy.run.ts --read
Use read mode to inspect current infrastructure state.

Destroy Phase

Delete all resources:
bun ./alchemy.run.ts --destroy
This will permanently delete all resources in the current stage. Use with caution.

Stages

Stages allow you to deploy multiple independent environments (dev, staging, production):

Using Stages

# Development stage (default: your username)
bun ./alchemy.run.ts

# Staging stage
bun ./alchemy.run.ts --stage staging

# Production stage
bun ./alchemy.run.ts --stage production
Each stage has isolated state:
.alchemy/
  my-app/
    john/           # Development (your username)
      state.json
    staging/
      state.json
    production/
      state.json

Environment-Specific Configuration

Use different configurations per stage:
alchemy.run.ts
import alchemy from "alchemy";
import { Worker } from "alchemy/cloudflare";

const app = await alchemy("my-app");
const isProd = app.stage === "production";

const worker = await Worker("api", {
  entrypoint: "./src/index.ts",
  bindings: {
    LOG_LEVEL: isProd ? "error" : "debug",
    API_URL: isProd 
      ? "https://api.example.com"
      : "https://staging-api.example.com"
  }
});

console.log(`Deployed to: ${worker.url}`);

await app.finalize();

State Management

Local State (Default)

By default, Alchemy stores state locally in .alchemy/:
.alchemy/
  my-app/
    production/
      state.json      # Resource state
Local state is fine for development, but use remote state stores for CI/CD and teams.

Remote State

For production and teams, use a remote state store:
import { CloudflareStateStore } from "alchemy/state";

const app = await alchemy("my-app", {
  stateStore: (scope) => new CloudflareStateStore(scope)
});
When running in CI, Alchemy requires a remote state store. Set ALCHEMY_CI_STATE_STORE_CHECK=false to disable this check (not recommended).

Secrets Management

Setting Passwords

Secrets require a password for encryption:
export ALCHEMY_PASSWORD="your-secure-password"
bun ./alchemy.run.ts
Or set it programmatically:
const app = await alchemy("my-app", {
  password: process.env.ALCHEMY_PASSWORD
});

Per-Stage Passwords

Use different passwords for different stages:
# Development
export ALCHEMY_PASSWORD="dev-password"
bun ./alchemy.run.ts --stage dev

# Production
export ALCHEMY_PASSWORD="prod-password"
bun ./alchemy.run.ts --stage production

CI/CD Integration

GitHub Actions

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: oven-sh/setup-bun@v2
      
      - name: Install dependencies
        run: bun install
      
      - name: Deploy to production
        env:
          ALCHEMY_PASSWORD: ${{ secrets.ALCHEMY_PASSWORD }}
          CLOUDFLARE_API_KEY: ${{ secrets.CLOUDFLARE_API_KEY }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
        run: bun ./alchemy.run.ts --stage production

GitLab CI

.gitlab-ci.yml
stages:
  - deploy

deploy:production:
  stage: deploy
  image: oven/bun:latest
  only:
    - main
  script:
    - bun install
    - bun ./alchemy.run.ts --stage production
  variables:
    ALCHEMY_PASSWORD: $ALCHEMY_PASSWORD
    CLOUDFLARE_API_KEY: $CLOUDFLARE_API_KEY
    CLOUDFLARE_ACCOUNT_ID: $CLOUDFLARE_ACCOUNT_ID

CircleCI

.circleci/config.yml
version: 2.1

jobs:
  deploy:
    docker:
      - image: oven/bun:latest
    steps:
      - checkout
      - run:
          name: Install dependencies
          command: bun install
      - run:
          name: Deploy
          command: bun ./alchemy.run.ts --stage production

workflows:
  deploy:
    jobs:
      - deploy:
          filters:
            branches:
              only: main

Deployment Strategies

Blue-Green Deployment

Deploy to a new stage, test, then switch:
1
Deploy to Green Stage
2
bun ./alchemy.run.ts --stage green
3
Test Green Stage
4
curl https://green-my-app.workers.dev/health
5
Switch Traffic
6
Update DNS or routing to point to green stage.
7
Destroy Blue Stage
8
bun ./alchemy.run.ts --destroy --stage blue

Canary Deployment

Gradually roll out changes:
const app = await alchemy("my-app");

// Deploy canary version
if (process.argv.includes("--canary")) {
  const canary = await Worker("api-canary", {
    entrypoint: "./src/index.ts",
    routes: [{ pattern: "api.example.com/canary/*" }]
  });
}

// Main version
const main = await Worker("api", {
  entrypoint: "./src/index.ts",
  routes: [{ pattern: "api.example.com/*" }]
});

await app.finalize();

Rollback

Quick Rollback

Destroy the current deployment and redeploy a previous version:
# Destroy current deployment
bun ./alchemy.run.ts --destroy --stage production

# Check out previous version
git checkout <previous-commit>

# Redeploy
bun ./alchemy.run.ts --stage production

State-Based Rollback

Alchemy maintains state history. You can manually restore previous state:
# Backup current state
cp .alchemy/my-app/production/state.json .alchemy/my-app/production/state.json.backup

# Restore previous state
cp .alchemy/my-app/production/state.json.previous .alchemy/my-app/production/state.json

# Apply
bun ./alchemy.run.ts --stage production

Monitoring Deployments

Deployment Logs

Alchemy logs all resource operations:
Create Worker "my-app-production-api"
Update D1Database "my-app-production-db"
Delete R2Bucket "old-bucket"

Quiet Mode

Suppress logs in CI:
bun ./alchemy.run.ts --quiet --stage production
Or programmatically:
const app = await alchemy("my-app", {
  quiet: true
});

Telemetry

Alchemy sends anonymous telemetry by default. Opt out:
export ALCHEMY_TELEMETRY_DISABLED=1
bun ./alchemy.run.ts
Or:
const app = await alchemy("my-app", {
  noTrack: true
});

Best Practices

1
Use Remote State in CI
2
Never use local state in CI/CD:
3
import { CloudflareStateStore } from "alchemy/state";

const app = await alchemy("my-app", {
  stateStore: (scope) => new CloudflareStateStore(scope)
});
4
Separate Stages
5
Use dedicated stages for each environment:
6
# Development
bun ./alchemy.run.ts --stage dev

# Staging
bun ./alchemy.run.ts --stage staging

# Production
bun ./alchemy.run.ts --stage production
7
Store Secrets Securely
8
Use GitHub Secrets, AWS Secrets Manager, or similar:
9
env:
  ALCHEMY_PASSWORD: ${{ secrets.ALCHEMY_PASSWORD }}
  CLOUDFLARE_API_KEY: ${{ secrets.CLOUDFLARE_API_KEY }}
10
Test Before Production
11
Always test in staging first:
12
jobs:
  deploy-staging:
    steps:
      - run: bun ./alchemy.run.ts --stage staging
      - run: bun test
  
  deploy-production:
    needs: deploy-staging
    steps:
      - run: bun ./alchemy.run.ts --stage production
13
Use Consistent Naming
14
Follow a naming convention:
15
const app = await alchemy("my-company-my-app");
// Resources: my-company-my-app-production-api
16
Enable Adoption for Existing Resources
17
When migrating to Alchemy:
18
const app = await alchemy("my-app", {
  adopt: true  // Adopt existing resources
});

Multi-Region Deployment

Deploy to multiple regions:
import alchemy from "alchemy";
import { Worker } from "alchemy/cloudflare";

const regions = ["us-east", "us-west", "eu-central"];

for (const region of regions) {
  const app = await alchemy(`my-app-${region}`, {
    stage: "production"
  });
  
  const worker = await Worker(`api-${region}`, {
    entrypoint: "./src/index.ts",
    bindings: {
      REGION: region
    }
  });
  
  console.log(`${region}: ${worker.url}`);
  
  await app.finalize();
}

Troubleshooting

State Conflicts

If multiple deployments run simultaneously:
Error: State conflict detected
Solution: Use a state store with locking (CloudflareStateStore, S3StateStore).

Missing Credentials

Error: CLOUDFLARE_API_KEY is required
Ensure all required environment variables are set:
export CLOUDFLARE_API_KEY=your-key
export CLOUDFLARE_ACCOUNT_ID=your-id

Orphaned Resources

If resources aren’t cleaned up:
bun ./alchemy.run.ts --force --stage production
This forces a full reconciliation.

Example: Production Deployment

import "dotenv/config";
import alchemy from "alchemy";
import { Worker, D1Database, R2Bucket } from "alchemy/cloudflare";
import { CloudflareStateStore } from "alchemy/state";
import path from "node:path";

// Production-ready configuration
const app = await alchemy("my-app", {
  // Use remote state in production
  stateStore: (scope) => new CloudflareStateStore(scope),
  // Require password for secrets
  password: process.env.ALCHEMY_PASSWORD,
  // Disable telemetry if needed
  noTrack: process.env.ALCHEMY_TELEMETRY_DISABLED === "1"
});

// Infrastructure
const db = await D1Database("database", {
  name: `${app.appName}-${app.stage}-db`
});

const bucket = await R2Bucket("storage", {
  name: `${app.appName}-${app.stage}-storage`
});

// Application
const worker = await Worker("api", {
  entrypoint: path.join(import.meta.dirname, "src", "index.ts"),
  bindings: {
    DB: db,
    BUCKET: bucket,
    API_KEY: alchemy.secret.env.API_KEY,
    LOG_LEVEL: app.stage === "production" ? "error" : "debug"
  }
});

console.log(`
Deployment complete!

Stage: ${app.stage}
Worker: ${worker.url}
Database: ${db.name}
Bucket: ${bucket.name}
`);

await app.finalize();

Next Steps

Build docs developers (and LLMs) love