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:
This will:
Create or update resources
Save state to .alchemy/
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:
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:
Cloudflare Durable Objects
AWS S3
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
.github/workflows/deploy.yml
.github/workflows/preview.yml
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
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
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:
bun ./alchemy.run.ts --stage green
curl https://green-my-app.workers.dev/health
Update DNS or routing to point to green stage.
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-commi t >
# 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
Never use local state in CI/CD:
import { CloudflareStateStore } from "alchemy/state" ;
const app = await alchemy ( "my-app" , {
stateStore : ( scope ) => new CloudflareStateStore ( scope )
});
Use dedicated stages for each environment:
# Development
bun ./alchemy.run.ts --stage dev
# Staging
bun ./alchemy.run.ts --stage staging
# Production
bun ./alchemy.run.ts --stage production
Use GitHub Secrets, AWS Secrets Manager, or similar:
env :
ALCHEMY_PASSWORD : ${{ secrets.ALCHEMY_PASSWORD }}
CLOUDFLARE_API_KEY : ${{ secrets.CLOUDFLARE_API_KEY }}
Always test in staging first:
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
Follow a naming convention:
const app = await alchemy ( "my-company-my-app" );
// Resources: my-company-my-app-production-api
Enable Adoption for Existing Resources
When migrating to Alchemy:
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
alchemy.run.ts
.github/workflows/deploy.yml
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