Guide de déploiement
Déployez sur Cloudflare avec Alchemy, gérez les environnements et validez la configuration.
| Outil | Rôle |
|---|---|
| Alchemy | Infrastructure-as-code pour Cloudflare |
| Wrangler | CLI Cloudflare pour le dev/debug |
| Infisical | Stockage des secrets, injection d’environnement local et livraison des secrets CI/runtime |
| D1 | Base SQLite à l’edge |
| R2 | Stockage objet |
| KV | Stockage clé-valeur |
Configuration de l’environnement
Section intitulée « Configuration de l’environnement »Utilisez Environnements et secrets as the source guide for Infisical, Cloudflare Worker secrets, and deploy-time environment handling. Short version:
- commitez
infisical.json, ne commitez jamais.env,.env.localni.dev.vars; - exécutez les commandes locales avec
infisical run --env=dev -- pnpm dev; - déployez avec
infisical run --env=staging -- pnpm deploy:stagingou depuis la CI après récupération des secrets Infisical; - synchronisez uniquement les secrets runtime vers Cloudflare Workers et gardez la configuration non secrète dans les
varsAlchemy/Wrangler ; - validez à la fois l’environnement de déploiement côté Node et les bindings Worker
envavec Zod.
Validation de l’environnement avec Zod
Section intitulée « Validation de l’environnement avec Zod »Ne faites jamais confiance à process.env. Validez au démarrage.
import { z } from 'zod'
const envSchema = z.object({ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), DATABASE_URL: z.string().min(1), BETTER_AUTH_SECRET: z.string().min(32), BETTER_AUTH_URL: z.string().url(), GOOGLE_CLIENT_ID: z.string().optional(), GOOGLE_CLIENT_SECRET: z.string().optional(), GITHUB_CLIENT_ID: z.string().optional(), GITHUB_CLIENT_SECRET: z.string().optional(), CORS_ORIGIN: z.string().url().optional(),})
export type Env = z.infer<typeof envSchema>
export function validateEnv(): Env { const result = envSchema.safeParse(process.env)
if (!result.success) { console.error('Invalid environment variables:') console.error(result.error.flatten().fieldErrors) throw new Error('Invalid environment configuration') }
return result.data}Type des bindings Cloudflare
Section intitulée « Type des bindings Cloudflare »export interface CloudflareEnv { DB: D1Database KV: KVNamespace R2: R2Bucket BETTER_AUTH_SECRET: string BETTER_AUTH_URL: string GOOGLE_CLIENT_ID?: string GOOGLE_CLIENT_SECRET?: string}
declare global { interface Env extends CloudflareEnv {}}Accès à l’environnement runtime (Cloudflare Workers)
Section intitulée « Accès à l’environnement runtime (Cloudflare Workers) »// In Workers, env comes from request context, not process.envexport default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { // env.DB, env.KV, etc. are available here const db = drizzle(env.DB) },}Default d’Hebergement Cloudflare
Section intitulée « Default d’Hebergement Cloudflare »Pour les nouveaux projets Fenod, preferer Cloudflare Workers comme surface de deploiement, y compris Workers Static Assets pour les sites statiques/docs/contenu. Workers couvre assets statiques, apps framework, APIs, bindings et routing de requetes dans un seul modele.
Utiliser Cloudflare Pages quand un projet docs/statique est deja connecte a GitHub et que l’integration Pages reste le chemin de publication le plus simple. Ce Fenod Stack Handbook peut rester sur Pages pour cette raison, mais les nouveaux starters d’app doivent cibler Workers d’abord.
Default Path: Wrangler
Section intitulée « Default Path: Wrangler »Pour une app ou un site PME typique, utiliser wrangler.jsonc et wrangler deploy. Un projet avec un Worker, D1, KV, R2, static assets et secrets n’a pas besoin d’un framework IaC.
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "my-app", "main": "./dist/worker.js", "compatibility_date": "2026-06-01", "assets": { "directory": "./dist/client", "binding": "ASSETS", "run_worker_first": ["/api/*", "/analytics/*"] }, "observability": { "enabled": true, "head_sampling_rate": 1 }, "d1_databases": [ { "binding": "DB", "database_name": "my-app-prod", "database_id": "<database-id>" } ], "vars": { "APP_ENV": "production" }}Deployer avec un contexte d’environnement explicite:
infisical run --env=staging -- wrangler deploy --env staginginfisical run --env=prod -- wrangler deploy --env productionUtiliser les environnements Wrangler pour l’isolation simple dev/staging/prod. Garder les secrets dans Infisical, le stockage secret CI, ou Worker secrets; garder uniquement la config non secrete dans vars.
Quand Utiliser Alchemy
Section intitulée « Quand Utiliser Alchemy »Un projet utilise Alchemy v2 au lieu de Wrangler seul quand une de ces conditions est vraie:
- 4+ ressources Cloudflare dont le cycle de vie doit etre cree/mis a jour/supprime ensemble;
- 3+ stages isoles au-dela de ce que les environnements Wrangler gerent proprement;
- tests infra-level ou wiring OTel as code;
- plusieurs comptes Cloudflare.
Tout le reste (site/app PME typique: un Worker, un D1, peut-etre un R2): Wrangler seulement.
Setup Alchemy v2
Section intitulée « Setup Alchemy v2 »Alchemy v2 est une reecriture basee sur Effect et est actuellement en beta. Pour un projet client, valider la version courante d’Alchemy et les docs au moment de l’adoption. Ne pas utiliser les anciens exemples de package legacy ou d’initializer v1 phase/run pour les nouveaux projets. Installer le package courant:
pnpm add -D alchemy effectForme minimale des docs Alchemy v2:
import * as Alchemy from 'alchemy'import * as Cloudflare from 'alchemy/Cloudflare'import * as Effect from 'effect/Effect'import Worker from './src/worker'
export default Alchemy.Stack( 'MyApp', { providers: Cloudflare.providers(), state: Cloudflare.state(), }, Effect.gen(function* () { const worker = yield* Worker return { url: worker.url } }),)import * as Cloudflare from 'alchemy/Cloudflare'import * as Effect from 'effect/Effect'import * as HttpServerResponse from 'effect/unstable/http/HttpServerResponse'
export default Cloudflare.Worker( 'Worker', { main: import.meta.filename }, Effect.gen(function* () { return { fetch: Effect.gen(function* () { return HttpServerResponse.text('Hello, world!') }), } }),)Pour les credentials Alchemy locaux, utiliser alchemy login; Alchemy stocke les profils dans ~/.alchemy/profiles.json. Ne pas exporter CLOUDFLARE_API_TOKEN pour le travail Alchemy local.
Deployer seulement apres la quality gate normale:
pnpm checkpnpm exec alchemy deployDetruire les ressources uniquement depuis une session explicitement approuvee:
pnpm exec alchemy destroyWrangler Commands
Section intitulée « Wrangler Commands »Development
Section intitulée « Development »# Start local dev serverwrangler dev
# Dev with remote D1 (useful for testing with real data)wrangler dev --remote
# Dev with local D1wrangler dev --localDatabase (D1)
Section intitulée « Database (D1) »# List databaseswrangler d1 list
# Create databasewrangler d1 create my-db
# Run SQL locallywrangler d1 execute my-db --local --command "SELECT * FROM users LIMIT 5"
# Run SQL on remotewrangler d1 execute my-db --remote --command "SELECT * FROM users LIMIT 5"
# Apply migration locallywrangler d1 execute my-db --local --file ./db/migrations/0001_init.sql
# Apply migration to productionwrangler d1 execute my-db --remote --file ./db/migrations/0001_init.sql
# Export databasewrangler d1 export my-db --output backup.sql
# Import databasewrangler d1 execute my-db --file backup.sql# List namespaceswrangler kv namespace list
# Create namespacewrangler kv namespace create my-kv
# Get valuewrangler kv key get --namespace-id <id> "my-key"
# Put valuewrangler kv key put --namespace-id <id> "my-key" "my-value"
# Delete valuewrangler kv key delete --namespace-id <id> "my-key"
# List keyswrangler kv key list --namespace-id <id># List bucketswrangler r2 bucket list
# Create bucketwrangler r2 bucket create my-bucket
# Upload filewrangler r2 object put my-bucket/path/file.txt --file ./local-file.txt
# Download filewrangler r2 object get my-bucket/path/file.txt --file ./downloaded.txt
# Delete objectwrangler r2 object delete my-bucket/path/file.txtLogs & Debugging
Section intitulée « Logs & Debugging »# Stream live logswrangler tail
# Filter by statuswrangler tail --status error
# Filter by search termwrangler tail --search "userId"
# JSON outputwrangler tail --format json
# Specific workerwrangler tail my-worker-nameDeployment
Section intitulée « Deployment »# Deploy workerwrangler deploy
# Deploy to specific environmentwrangler deploy --env production
# Dry run (see what would deploy)wrangler deploy --dry-run
# Check current deploymentwrangler deployments listDrizzle Migrations
Section intitulée « Drizzle Migrations »Configuration
Section intitulée « Configuration »import { defineConfig } from 'drizzle-kit'
export default defineConfig({ schema: './packages/db/src/schema/*.ts', out: './packages/db/migrations', dialect: 'sqlite', driver: 'd1-http', dbCredentials: { accountId: process.env.CLOUDFLARE_ACCOUNT_ID!, databaseId: process.env.CLOUDFLARE_D1_DATABASE_ID!, token: process.env.CLOUDFLARE_API_TOKEN!, },})Commands
Section intitulée « Commands »# Generate migration from schema changespnpm drizzle-kit generate
# Push schema directly (dev only, no migration file)pnpm drizzle-kit push
# Open Drizzle Studiopnpm drizzle-kit studio
# Drop all and regenerate (dev only!)pnpm drizzle-kit dropMigration Workflow
Section intitulée « Migration Workflow »# 1. Make schema changes in packages/db/src/schema/*.ts
# 2. Generate migrationpnpm drizzle-kit generate
# 3. Review generated SQL in packages/db/migrations/
# 4. Apply to local D1wrangler d1 execute my-db --local --file ./packages/db/migrations/0002_new_feature.sql
# 5. Test locally
# 6. Apply to production D1wrangler d1 execute my-db --remote --file ./packages/db/migrations/0002_new_feature.sqlWrangler Configuration
Section intitulée « Wrangler Configuration »wrangler.jsonc (Recommended)
Section intitulée « wrangler.jsonc (Recommended) »{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-app-api", "main": "src/index.ts", "compatibility_date": "2025-01-01", "compatibility_flags": ["nodejs_compat_v2"],
// Smart Placement: auto-locate compute near data sources "placement": { "mode": "smart" },
// Observability (10% sampling) "observability": { "enabled": true, "head_sampling_rate": 0.1 },
// Environment variables "vars": { "BETTER_AUTH_URL": "http://localhost:8787" },
// D1 Database "d1_databases": [ { "binding": "DB", "database_name": "my-db", "database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } ],
// KV Namespace "kv_namespaces": [ { "binding": "KV", "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } ],
// R2 Bucket "r2_buckets": [ { "binding": "R2", "bucket_name": "my-uploads" } ],
// Queues "queues": { "producers": [{ "binding": "MY_QUEUE", "queue": "my-queue" }] },
// Vectorize "vectorize": [ { "binding": "VECTORIZE", "index_name": "doc-search" } ],
// Workers AI "ai": { "binding": "AI" },
// Workflows "workflows": [ { "name": "user-lifecycle", "binding": "USER_WORKFLOW", "class_name": "UserLifecycleWorkflow" } ],
// Durable Objects (for Agents SDK) "durable_objects": { "bindings": [ { "name": "CHAT_AGENT", "class_name": "ChatAgent" } ] },
// Cron Triggers "triggers": { "crons": ["0 */6 * * *"] },
// Environment overrides "env": { "production": { "vars": { "BETTER_AUTH_URL": "https://api.myapp.com" }, "routes": [{ "pattern": "api.myapp.com", "custom_domain": true }], "d1_databases": [ { "binding": "DB", "database_name": "prod-db", "database_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" } ] } }}Key Configuration Options
Section intitulée « Key Configuration Options »| Option | Rôle |
|---|---|
compatibility_date | Utilisez current date for new projects |
compatibility_flags | nodejs_compat_v2 enables Node.js built-ins |
placement.mode: "smart" | Auto-locate compute near data sources |
observability | Enable tracing with configurable sampling |
For Workers, Dynamic Workers, and Containers, see the Cloudflare Compute decision matrix.
Legacy wrangler.toml
Section intitulée « Legacy wrangler.toml »Click to expand wrangler.toml format
name = "my-app-api"main = "src/index.ts"compatibility_date = "2025-01-01"compatibility_flags = ["nodejs_compat_v2"]
[placement]mode = "smart"
[vars]BETTER_AUTH_URL = "http://localhost:8787"
[[d1_databases]]binding = "DB"database_name = "dev-db"database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
[[kv_namespaces]]binding = "KV"id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
[[r2_buckets]]binding = "R2"bucket_name = "dev-uploads"
[env.production]vars = { BETTER_AUTH_URL = "https://api.myapp.com" }
[[env.production.d1_databases]]binding = "DB"database_name = "prod-db"database_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"CI/CD Pipeline
Section intitulée « CI/CD Pipeline »GitHub Actions
Section intitulée « GitHub Actions »name: Deploy
on: push: branches: [main]
env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v2 with: version: 9 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - run: pnpm install - run: pnpm test:run - run: pnpm build
deploy: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v2 with: version: 9 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - run: pnpm install - run: pnpm build
# Option A: Wrangler - name: Deploy with Wrangler run: wrangler deploy --env production
# Option B: Alchemy - name: Deploy with Alchemy run: ALCHEMY_PHASE=production pnpm alchemy deployPreview Deployments
Section intitulée « Preview Deployments »name: Preview
on: pull_request: branches: [main]
jobs: preview: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v2 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - run: pnpm install - run: pnpm build
- name: Deploy Preview id: deploy run: | OUTPUT=$(wrangler pages deploy ./dist --project-name my-app --branch ${{ github.head_ref }}) echo "url=$(echo $OUTPUT | grep -oP 'https://[^\s]+')" >> $GITHUB_OUTPUT env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Comment PR uses: actions/github-script@v7 with: script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: `Preview deployed to: ${{ steps.deploy.outputs.url }}` })Production Checklist
Section intitulée « Production Checklist »Pre-Deploy
Section intitulée « Pre-Deploy »- All tests passing
- React Doctor passes for the branch diff
- Environment variables validated with Zod
- Infisical environment selected (
dev,staging, orprod) - Runtime secrets synced to Cloudflare Workers or injected during Alchemy deploy
- Observability enabled and error alert configured (see /fr/observability/)
- Database migrations applied
- Build succeeds locally
- Déployer en staging first
- Verify staging works
- Déployer en production
- Check wrangler tail for errors
Post-Deploy
Section intitulée « Post-Deploy »- Verify all routes work
- Check auth flows
- Monitor error rates
- Check performance metrics
- Verify database connections
Rollback Plan
Section intitulée « Rollback Plan »# List recent deploymentswrangler deployments list
# Rollback to previous versionwrangler rollback
# Rollback to specific versionwrangler rollback --version <version-id>Troubleshooting
Section intitulée « Troubleshooting »Common Issues
Section intitulée « Common Issues »“Binding not found”
- Check wrangler.toml bindings match code
- Ensure correct environment is deployed
“D1 connection failed”
- Verify database_id in wrangler.toml
- Check database exists:
wrangler d1 list
“Secret not set”
- Preferred: sync the secret from Infisical to the target Cloudflare Worker/environment
- Deploy-time fallback:
infisical run --env=staging -- pnpm deploy:staging - Manual fallback:
wrangler secret put SECRET_NAME - Verify:
wrangler secret list
“Migration failed”
- Check SQL syntax in migration file
- Test locally first with
--localflag
Debug Commands
Section intitulée « Debug Commands »# Check what's deployedwrangler deployments list
# Check bindingswrangler whoami
# Test locally with remote datawrangler dev --remote
# Stream errors onlywrangler tail --status error