mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 18:11:27 -07:00
update skills
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# Cloudflare Containers Skill Reference
|
||||
|
||||
**APPLIES TO: Cloudflare Containers ONLY - NOT general Cloudflare Workers**
|
||||
|
||||
Use when working with Cloudflare Containers: deploying containerized apps on Workers platform, configuring container-enabled Durable Objects, managing container lifecycle, or implementing stateful/stateless container patterns.
|
||||
|
||||
## Beta Status
|
||||
|
||||
⚠️ Containers is currently in **beta**. API may change without notice. No SLA guarantees. Custom instance types added Jan 2026.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
**Container as Durable Object:** Each container is a Durable Object with persistent identity. Accessed via `getByName(id)` or `getRandom()`.
|
||||
|
||||
**Image deployment:** Images pre-fetched globally. Deployments use rolling strategy (not instant like Workers).
|
||||
|
||||
**Lifecycle:** cold start (2-3s) → running → `sleepAfter` timeout → stopped. No autoscaling - manual load balancing via `getRandom()`.
|
||||
|
||||
**Persistent identity, ephemeral disk:** Container ID persists, but disk resets on stop. Use Durable Object storage for persistence.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { Container } from "@cloudflare/containers";
|
||||
|
||||
export class MyContainer extends Container {
|
||||
defaultPort = 8080;
|
||||
sleepAfter = "30m";
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
const container = env.MY_CONTAINER.getByName("instance-1");
|
||||
await container.startAndWaitForPorts();
|
||||
return container.fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Reading Order
|
||||
|
||||
| Task | Files |
|
||||
|------|-------|
|
||||
| Setup new container project | README → configuration.md |
|
||||
| Implement container logic | README → api.md → patterns.md |
|
||||
| Choose routing pattern | patterns.md (routing section) |
|
||||
| Debug issues | gotchas.md |
|
||||
| Production hardening | gotchas.md → patterns.md (lifecycle) |
|
||||
|
||||
## Routing Decision Tree
|
||||
|
||||
**How should requests reach containers?**
|
||||
|
||||
- **Same user/session → same container:** Use `getByName(sessionId)` for session affinity
|
||||
- **Stateless, spread load:** Use `getRandom()` for load balancing
|
||||
- **Job per container:** Use `getByName(jobId)` + explicit lifecycle management
|
||||
- **Single global instance:** Use `getByName("singleton")`
|
||||
|
||||
## When to Use Containers vs Workers
|
||||
|
||||
**Use Containers when:**
|
||||
- Need stateful, long-lived processes (sessions, WebSockets, games)
|
||||
- Running existing containerized apps (Node.js, Python, custom binaries)
|
||||
- Need filesystem access or specific system dependencies
|
||||
- Per-user/session isolation with dedicated compute
|
||||
|
||||
**Use Workers when:**
|
||||
- Stateless HTTP handlers
|
||||
- Sub-millisecond cold starts required
|
||||
- Auto-scaling to zero critical
|
||||
- Simple request/response patterns
|
||||
|
||||
## In This Reference
|
||||
|
||||
- **[configuration.md](configuration.md)** - Wrangler config, instance types, Container class properties, environment variables, account limits
|
||||
- **[api.md](api.md)** - Container class API, startup methods, communication (HTTP/TCP/WebSocket), routing helpers, lifecycle hooks, scheduling, state inspection
|
||||
- **[patterns.md](patterns.md)** - Routing patterns (session affinity, load balancing, singleton), WebSocket forwarding, graceful shutdown, Workflow/Queue integration
|
||||
- **[gotchas.md](gotchas.md)** - Critical gotchas (WebSocket, startup methods), common errors with solutions, specific limits, beta caveats
|
||||
|
||||
## See Also
|
||||
|
||||
- [Durable Objects](../durable-objects/) - Containers extend Durable Objects
|
||||
- [Workflows](../workflows/) - Orchestrate container operations
|
||||
- [Queues](../queues/) - Trigger containers from queue messages
|
||||
- [Cloudflare Docs](https://developers.cloudflare.com/containers/)
|
||||
187
.agents/skills/cloudflare-deploy/references/containers/api.md
Normal file
187
.agents/skills/cloudflare-deploy/references/containers/api.md
Normal file
@@ -0,0 +1,187 @@
|
||||
## Container Class API
|
||||
|
||||
```typescript
|
||||
import { Container } from "@cloudflare/containers";
|
||||
|
||||
export class MyContainer extends Container {
|
||||
defaultPort = 8080;
|
||||
requiredPorts = [8080];
|
||||
sleepAfter = "30m";
|
||||
enableInternet = true;
|
||||
pingEndpoint = "/health";
|
||||
envVars = {};
|
||||
entrypoint = [];
|
||||
|
||||
onStart() { /* container started */ }
|
||||
onStop() { /* container stopping */ }
|
||||
onError(error: Error) { /* container error */ }
|
||||
onActivityExpired(): boolean { /* timeout, return true to stay alive */ }
|
||||
async alarm() { /* scheduled task */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Routing
|
||||
|
||||
**getByName(id)** - Named instance for session affinity, per-user state
|
||||
**getRandom()** - Random instance for load balancing stateless services
|
||||
|
||||
```typescript
|
||||
const container = env.MY_CONTAINER.getByName("user-123");
|
||||
const container = env.MY_CONTAINER.getRandom();
|
||||
```
|
||||
|
||||
## Startup Methods
|
||||
|
||||
### start() - Basic start (8s timeout)
|
||||
|
||||
```typescript
|
||||
await container.start();
|
||||
await container.start({ envVars: { KEY: "value" } });
|
||||
```
|
||||
|
||||
Returns when **process starts**, NOT when ports ready. Use for fire-and-forget.
|
||||
|
||||
### startAndWaitForPorts() - Recommended (20s timeout)
|
||||
|
||||
```typescript
|
||||
await container.startAndWaitForPorts(); // Uses requiredPorts
|
||||
await container.startAndWaitForPorts({ ports: [8080, 9090] });
|
||||
await container.startAndWaitForPorts({
|
||||
ports: [8080],
|
||||
startOptions: { envVars: { KEY: "value" } }
|
||||
});
|
||||
```
|
||||
|
||||
Returns when **ports listening**. Use before HTTP/TCP requests.
|
||||
|
||||
**Port resolution:** explicit ports → requiredPorts → defaultPort → port 33
|
||||
|
||||
### waitForPort() - Wait for specific port
|
||||
|
||||
```typescript
|
||||
await container.waitForPort(8080);
|
||||
await container.waitForPort(8080, { timeout: 30000 });
|
||||
```
|
||||
|
||||
## Communication
|
||||
|
||||
### fetch() - HTTP with WebSocket support
|
||||
|
||||
```typescript
|
||||
// ✅ Supports WebSocket upgrades
|
||||
const response = await container.fetch(request);
|
||||
const response = await container.fetch("http://container/api", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ data: "value" })
|
||||
});
|
||||
```
|
||||
|
||||
**Use for:** All HTTP, especially WebSocket.
|
||||
|
||||
### containerFetch() - HTTP only (no WebSocket)
|
||||
|
||||
```typescript
|
||||
// ❌ No WebSocket support
|
||||
const response = await container.containerFetch(request);
|
||||
```
|
||||
|
||||
**⚠️ Critical:** Use `fetch()` for WebSocket, not `containerFetch()`.
|
||||
|
||||
### TCP Connections
|
||||
|
||||
```typescript
|
||||
const port = this.ctx.container.getTcpPort(8080);
|
||||
const conn = port.connect();
|
||||
await conn.opened;
|
||||
|
||||
if (request.body) await request.body.pipeTo(conn.writable);
|
||||
return new Response(conn.readable);
|
||||
```
|
||||
|
||||
### switchPort() - Change default port
|
||||
|
||||
```typescript
|
||||
this.switchPort(8081); // Subsequent fetch() uses this port
|
||||
```
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
### onStart()
|
||||
|
||||
Called when container process starts (ports may not be ready). Runs in `blockConcurrencyWhile` - no concurrent requests.
|
||||
|
||||
```typescript
|
||||
onStart() {
|
||||
console.log("Container starting");
|
||||
}
|
||||
```
|
||||
|
||||
### onStop()
|
||||
|
||||
Called when SIGTERM received. 15 minutes until SIGKILL. Use for graceful shutdown.
|
||||
|
||||
```typescript
|
||||
onStop() {
|
||||
// Save state, close connections, flush logs
|
||||
}
|
||||
```
|
||||
|
||||
### onError()
|
||||
|
||||
Called when container crashes or fails to start.
|
||||
|
||||
```typescript
|
||||
onError(error: Error) {
|
||||
console.error("Container error:", error);
|
||||
}
|
||||
```
|
||||
|
||||
### onActivityExpired()
|
||||
|
||||
Called when `sleepAfter` timeout reached. Return `true` to stay alive, `false` to stop.
|
||||
|
||||
```typescript
|
||||
onActivityExpired(): boolean {
|
||||
if (this.hasActiveConnections()) return true; // Keep alive
|
||||
return false; // OK to stop
|
||||
}
|
||||
```
|
||||
|
||||
## Scheduling
|
||||
|
||||
```typescript
|
||||
export class ScheduledContainer extends Container {
|
||||
async fetch(request: Request) {
|
||||
await this.schedule(Date.now() + 60000); // 1 minute
|
||||
await this.schedule("2026-01-28T00:00:00Z"); // ISO string
|
||||
return new Response("Scheduled");
|
||||
}
|
||||
|
||||
async alarm() {
|
||||
// Called when schedule fires (SQLite-backed, survives restarts)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ Don't override `alarm()` directly when using `schedule()` helper.**
|
||||
|
||||
## State Inspection
|
||||
|
||||
### External state check
|
||||
|
||||
```typescript
|
||||
const state = await container.getState();
|
||||
// state.status: "starting" | "running" | "stopping" | "stopped"
|
||||
```
|
||||
|
||||
### Internal state check
|
||||
|
||||
```typescript
|
||||
export class MyContainer extends Container {
|
||||
async fetch(request: Request) {
|
||||
if (this.ctx.container.running) { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ Use `getState()` for external checks, `ctx.container.running` for internal.**
|
||||
@@ -0,0 +1,188 @@
|
||||
## Wrangler Configuration
|
||||
|
||||
### Basic Container Config
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-worker",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2026-01-10",
|
||||
"containers": [
|
||||
{
|
||||
"class_name": "MyContainer",
|
||||
"image": "./Dockerfile", // Path to Dockerfile or directory with Dockerfile
|
||||
"instance_type": "standard-1", // Predefined or custom (see below)
|
||||
"max_instances": 10
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "MY_CONTAINER",
|
||||
"class_name": "MyContainer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["MyContainer"] // Must use new_sqlite_classes
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Key config requirements:
|
||||
- `image` - Path to Dockerfile or directory containing Dockerfile
|
||||
- `class_name` - Must match Container class export name
|
||||
- `max_instances` - Max concurrent container instances
|
||||
- Must configure Durable Objects binding AND migrations
|
||||
|
||||
### Instance Types
|
||||
|
||||
#### Predefined Types
|
||||
|
||||
| Type | vCPU | Memory | Disk |
|
||||
|------|------|--------|------|
|
||||
| lite | 1/16 | 256 MiB | 2 GB |
|
||||
| basic | 1/4 | 1 GiB | 4 GB |
|
||||
| standard-1 | 1/2 | 4 GiB | 8 GB |
|
||||
| standard-2 | 1 | 6 GiB | 12 GB |
|
||||
| standard-3 | 2 | 8 GiB | 16 GB |
|
||||
| standard-4 | 4 | 12 GiB | 20 GB |
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"containers": [
|
||||
{
|
||||
"class_name": "MyContainer",
|
||||
"image": "./Dockerfile",
|
||||
"instance_type": "standard-2" // Use predefined type
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Custom Types (Jan 2026 Feature)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"containers": [
|
||||
{
|
||||
"class_name": "MyContainer",
|
||||
"image": "./Dockerfile",
|
||||
"instance_type_custom": {
|
||||
"vcpu": 2, // 1-4 vCPU
|
||||
"memory_mib": 8192, // 512-12288 MiB (up to 12 GiB)
|
||||
"disk_mib": 16384 // 2048-20480 MiB (up to 20 GB)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Custom type constraints:**
|
||||
- Minimum 3 GiB memory per vCPU
|
||||
- Maximum 2 GB disk per 1 GiB memory
|
||||
- Max 4 vCPU, 12 GiB memory, 20 GB disk per container
|
||||
|
||||
### Account Limits
|
||||
|
||||
| Resource | Limit | Notes |
|
||||
|----------|-------|-------|
|
||||
| Total memory (all containers) | 400 GiB | Across all running containers |
|
||||
| Total vCPU (all containers) | 100 | Across all running containers |
|
||||
| Total disk (all containers) | 2 TB | Across all running containers |
|
||||
| Image storage per account | 50 GB | Stored container images |
|
||||
|
||||
### Container Class Properties
|
||||
|
||||
```typescript
|
||||
import { Container } from "@cloudflare/containers";
|
||||
|
||||
export class MyContainer extends Container {
|
||||
// Port Configuration
|
||||
defaultPort = 8080; // Default port for fetch() calls
|
||||
requiredPorts = [8080, 9090]; // Ports to wait for in startAndWaitForPorts()
|
||||
|
||||
// Lifecycle
|
||||
sleepAfter = "30m"; // Inactivity timeout (5m, 30m, 2h, etc.)
|
||||
|
||||
// Network
|
||||
enableInternet = true; // Allow outbound internet access
|
||||
|
||||
// Health Check
|
||||
pingEndpoint = "/health"; // Health check endpoint path
|
||||
|
||||
// Environment
|
||||
envVars = { // Environment variables passed to container
|
||||
NODE_ENV: "production",
|
||||
LOG_LEVEL: "info"
|
||||
};
|
||||
|
||||
// Startup
|
||||
entrypoint = ["/bin/start.sh"]; // Override image entrypoint (optional)
|
||||
}
|
||||
```
|
||||
|
||||
**Property details:**
|
||||
|
||||
- **`defaultPort`**: Port used when calling `container.fetch()` without explicit port. Falls back to port 33 if not set.
|
||||
|
||||
- **`requiredPorts`**: Array of ports that must be listening before `startAndWaitForPorts()` returns. First port becomes default if `defaultPort` not set.
|
||||
|
||||
- **`sleepAfter`**: Duration string (e.g., "5m", "30m", "2h"). Container stops after this period of inactivity. Timer resets on each request.
|
||||
|
||||
- **`enableInternet`**: Boolean. If `true`, container can make outbound HTTP/TCP requests.
|
||||
|
||||
- **`pingEndpoint`**: Path used for health checks. Should respond with 2xx status.
|
||||
|
||||
- **`envVars`**: Object of environment variables. Merged with runtime-provided vars (see below).
|
||||
|
||||
- **`entrypoint`**: Array of strings. Overrides container image's CMD/ENTRYPOINT.
|
||||
|
||||
### Runtime Environment Variables
|
||||
|
||||
Cloudflare automatically provides these environment variables to containers:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `CLOUDFLARE_APPLICATION_ID` | Worker application ID |
|
||||
| `CLOUDFLARE_COUNTRY_A2` | Two-letter country code of request origin |
|
||||
| `CLOUDFLARE_LOCATION` | Cloudflare data center location |
|
||||
| `CLOUDFLARE_REGION` | Region identifier |
|
||||
| `CLOUDFLARE_DURABLE_OBJECT_ID` | Container's Durable Object ID |
|
||||
|
||||
Custom `envVars` from Container class are merged with these. Custom vars override runtime vars if names conflict.
|
||||
|
||||
### Image Management
|
||||
|
||||
**Distribution model:** Images pre-fetched to all global locations before deployment. Ensures fast cold starts (2-3s typical).
|
||||
|
||||
**Rolling deploys:** Unlike Workers (instant), container deployments roll out gradually. Old versions continue running during rollout.
|
||||
|
||||
**Ephemeral disk:** Container disk is ephemeral and resets on each stop. Use Durable Object storage (`this.ctx.storage`) for persistence.
|
||||
|
||||
## wrangler.toml Format
|
||||
|
||||
```toml
|
||||
name = "my-worker"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2026-01-10"
|
||||
|
||||
[[containers]]
|
||||
class_name = "MyContainer"
|
||||
image = "./Dockerfile"
|
||||
instance_type = "standard-2"
|
||||
max_instances = 10
|
||||
|
||||
[[durable_objects.bindings]]
|
||||
name = "MY_CONTAINER"
|
||||
class_name = "MyContainer"
|
||||
|
||||
[[migrations]]
|
||||
tag = "v1"
|
||||
new_sqlite_classes = ["MyContainer"]
|
||||
```
|
||||
|
||||
Both `wrangler.jsonc` and `wrangler.toml` are supported. Use `wrangler.jsonc` for comments and better IDE support.
|
||||
@@ -0,0 +1,178 @@
|
||||
## Critical Gotchas
|
||||
|
||||
### ⚠️ WebSocket: fetch() vs containerFetch()
|
||||
|
||||
**Problem:** WebSocket connections fail silently
|
||||
|
||||
**Cause:** `containerFetch()` doesn't support WebSocket upgrades
|
||||
|
||||
**Fix:** Always use `fetch()` for WebSocket
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG
|
||||
return container.containerFetch(request);
|
||||
|
||||
// ✅ CORRECT
|
||||
return container.fetch(request);
|
||||
```
|
||||
|
||||
### ⚠️ startAndWaitForPorts() vs start()
|
||||
|
||||
**Problem:** "connection refused" after `start()`
|
||||
|
||||
**Cause:** `start()` returns when process starts, NOT when ports ready
|
||||
|
||||
**Fix:** Use `startAndWaitForPorts()` before requests
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG
|
||||
await container.start();
|
||||
return container.fetch(request);
|
||||
|
||||
// ✅ CORRECT
|
||||
await container.startAndWaitForPorts();
|
||||
return container.fetch(request);
|
||||
```
|
||||
|
||||
### ⚠️ Activity Timeout on Long Operations
|
||||
|
||||
**Problem:** Container stops during long work
|
||||
|
||||
**Cause:** `sleepAfter` based on request activity, not internal work
|
||||
|
||||
**Fix:** Renew timeout by touching storage
|
||||
|
||||
```typescript
|
||||
const interval = setInterval(() => {
|
||||
this.ctx.storage.put("keepalive", Date.now());
|
||||
}, 60000);
|
||||
|
||||
try {
|
||||
await this.doLongWork(data);
|
||||
} finally {
|
||||
clearInterval(interval);
|
||||
}
|
||||
```
|
||||
|
||||
### ⚠️ blockConcurrencyWhile for Startup
|
||||
|
||||
**Problem:** Race conditions during initialization
|
||||
|
||||
**Fix:** Use `blockConcurrencyWhile` for atomic initialization
|
||||
|
||||
```typescript
|
||||
await this.ctx.blockConcurrencyWhile(async () => {
|
||||
if (!this.initialized) {
|
||||
await this.startAndWaitForPorts();
|
||||
this.initialized = true;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### ⚠️ Lifecycle Hooks Block Requests
|
||||
|
||||
**Problem:** Container unresponsive during `onStart()`
|
||||
|
||||
**Cause:** Hooks run in `blockConcurrencyWhile` - no concurrent requests
|
||||
|
||||
**Fix:** Keep hooks fast, avoid long operations
|
||||
|
||||
### ⚠️ Don't Override alarm() When Using schedule()
|
||||
|
||||
**Problem:** Scheduled tasks don't execute
|
||||
|
||||
**Cause:** `schedule()` uses `alarm()` internally
|
||||
|
||||
**Fix:** Implement `alarm()` to handle scheduled tasks
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Container start timeout"
|
||||
|
||||
**Cause:** Container took >8s (`start()`) or >20s (`startAndWaitForPorts()`)
|
||||
|
||||
**Solutions:**
|
||||
- Optimize image (smaller base, fewer layers)
|
||||
- Check `entrypoint` correct
|
||||
- Verify app listens on correct ports
|
||||
- Increase timeout if needed
|
||||
|
||||
### "Port not available"
|
||||
|
||||
**Cause:** Calling `fetch()` before port ready
|
||||
|
||||
**Solution:** Use `startAndWaitForPorts()`
|
||||
|
||||
### "Container memory exceeded"
|
||||
|
||||
**Cause:** Using more memory than instance type allows
|
||||
|
||||
**Solutions:**
|
||||
- Use larger instance type (standard-2, standard-3, standard-4)
|
||||
- Optimize app memory usage
|
||||
- Use custom instance type
|
||||
|
||||
```jsonc
|
||||
"instance_type_custom": {
|
||||
"vcpu": 2,
|
||||
"memory_mib": 8192
|
||||
}
|
||||
```
|
||||
|
||||
### "Max instances reached"
|
||||
|
||||
**Cause:** All `max_instances` slots in use
|
||||
|
||||
**Solutions:**
|
||||
- Increase `max_instances`
|
||||
- Implement proper `sleepAfter`
|
||||
- Use `getRandom()` for distribution
|
||||
- Check for instance leaks
|
||||
|
||||
### "No container instance available"
|
||||
|
||||
**Cause:** Account capacity limits reached
|
||||
|
||||
**Solutions:**
|
||||
- Check account limits
|
||||
- Review instance types across containers
|
||||
- Contact Cloudflare support
|
||||
|
||||
## Limits
|
||||
|
||||
| Resource | Limit | Notes |
|
||||
|----------|-------|-------|
|
||||
| Cold start | 2-3s | Image pre-fetched globally |
|
||||
| Graceful shutdown | 15 min | SIGTERM → SIGKILL |
|
||||
| `start()` timeout | 8s | Process start |
|
||||
| `startAndWaitForPorts()` timeout | 20s | Port ready |
|
||||
| Max vCPU per container | 4 | standard-4 or custom |
|
||||
| Max memory per container | 12 GiB | standard-4 or custom |
|
||||
| Max disk per container | 20 GB | Ephemeral, resets |
|
||||
| Account total memory | 400 GiB | All containers |
|
||||
| Account total vCPU | 100 | All containers |
|
||||
| Account total disk | 2 TB | All containers |
|
||||
| Image storage | 50 GB | Per account |
|
||||
| Disk persistence | None | Use DO storage |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use `startAndWaitForPorts()` by default** - Prevents port errors
|
||||
2. **Set appropriate `sleepAfter`** - Balance resources vs cold starts
|
||||
3. **Use `fetch()` for WebSocket** - Not `containerFetch()`
|
||||
4. **Design for restarts** - Ephemeral disk, implement graceful shutdown
|
||||
5. **Monitor resources** - Stay within account limits
|
||||
6. **Keep hooks fast** - Run in `blockConcurrencyWhile`
|
||||
7. **Renew activity for long ops** - Touch storage to prevent timeout
|
||||
|
||||
## Beta Caveats
|
||||
|
||||
⚠️ Containers in **beta**:
|
||||
|
||||
- **API may change** without notice
|
||||
- **No SLA** guarantees
|
||||
- **Limited regions** initially
|
||||
- **No autoscaling** - manual via `getRandom()`
|
||||
- **Rolling deploys** only (not instant like Workers)
|
||||
|
||||
Plan for API changes, test thoroughly before production.
|
||||
@@ -0,0 +1,202 @@
|
||||
## Routing Patterns
|
||||
|
||||
### Session Affinity (Stateful)
|
||||
|
||||
```typescript
|
||||
export class SessionBackend extends Container {
|
||||
defaultPort = 3000;
|
||||
sleepAfter = "30m";
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
const sessionId = request.headers.get("X-Session-ID") || crypto.randomUUID();
|
||||
const container = env.SESSION_BACKEND.getByName(sessionId);
|
||||
await container.startAndWaitForPorts();
|
||||
return container.fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Use:** User sessions, WebSocket, stateful games, per-user caching.
|
||||
|
||||
### Load Balancing (Stateless)
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
const container = env.STATELESS_API.getRandom();
|
||||
await container.startAndWaitForPorts();
|
||||
return container.fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Use:** Stateless HTTP APIs, CPU-intensive work, read-only queries.
|
||||
|
||||
### Singleton Pattern
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
const container = env.GLOBAL_SERVICE.getByName("singleton");
|
||||
await container.startAndWaitForPorts();
|
||||
return container.fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Use:** Global cache, centralized coordinator, single source of truth.
|
||||
|
||||
## WebSocket Forwarding
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
if (request.headers.get("Upgrade") === "websocket") {
|
||||
const sessionId = request.headers.get("X-Session-ID") || crypto.randomUUID();
|
||||
const container = env.WS_BACKEND.getByName(sessionId);
|
||||
await container.startAndWaitForPorts();
|
||||
|
||||
// ⚠️ MUST use fetch(), not containerFetch()
|
||||
return container.fetch(request);
|
||||
}
|
||||
return new Response("Not a WebSocket request", { status: 400 });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**⚠️ Critical:** Always use `fetch()` for WebSocket.
|
||||
|
||||
## Graceful Shutdown
|
||||
|
||||
```typescript
|
||||
export class GracefulContainer extends Container {
|
||||
private connections = new Set<WebSocket>();
|
||||
|
||||
onStop() {
|
||||
// SIGTERM received, 15 minutes until SIGKILL
|
||||
for (const ws of this.connections) {
|
||||
ws.close(1001, "Server shutting down");
|
||||
}
|
||||
this.ctx.storage.put("shutdown-time", Date.now());
|
||||
}
|
||||
|
||||
onActivityExpired(): boolean {
|
||||
return this.connections.size > 0; // Keep alive if connections
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Concurrent Request Handling
|
||||
|
||||
```typescript
|
||||
export class SafeContainer extends Container {
|
||||
private initialized = false;
|
||||
|
||||
async fetch(request: Request) {
|
||||
await this.ctx.blockConcurrencyWhile(async () => {
|
||||
if (!this.initialized) {
|
||||
await this.startAndWaitForPorts();
|
||||
this.initialized = true;
|
||||
}
|
||||
});
|
||||
return super.fetch(request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use:** One-time initialization, preventing concurrent startup.
|
||||
|
||||
## Activity Timeout Renewal
|
||||
|
||||
```typescript
|
||||
export class LongRunningContainer extends Container {
|
||||
sleepAfter = "5m";
|
||||
|
||||
async processLongJob(data: unknown) {
|
||||
const interval = setInterval(() => {
|
||||
this.ctx.storage.put("keepalive", Date.now());
|
||||
}, 60000);
|
||||
|
||||
try {
|
||||
await this.doLongWork(data);
|
||||
} finally {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use:** Long operations exceeding `sleepAfter`.
|
||||
|
||||
## Multiple Port Routing
|
||||
|
||||
```typescript
|
||||
export class MultiPortContainer extends Container {
|
||||
requiredPorts = [8080, 8081, 9090];
|
||||
|
||||
async fetch(request: Request) {
|
||||
const path = new URL(request.url).pathname;
|
||||
if (path.startsWith("/grpc")) this.switchPort(8081);
|
||||
else if (path.startsWith("/metrics")) this.switchPort(9090);
|
||||
return super.fetch(request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use:** Multi-protocol services (HTTP + gRPC), separate metrics endpoints.
|
||||
|
||||
## Workflow Integration
|
||||
|
||||
```typescript
|
||||
import { WorkflowEntrypoint } from "cloudflare:workers";
|
||||
|
||||
export class ProcessingWorkflow extends WorkflowEntrypoint {
|
||||
async run(event, step) {
|
||||
const container = this.env.PROCESSOR.getByName(event.payload.jobId);
|
||||
|
||||
await step.do("start", async () => {
|
||||
await container.startAndWaitForPorts();
|
||||
});
|
||||
|
||||
const result = await step.do("process", async () => {
|
||||
return container.fetch("/process", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(event.payload.data)
|
||||
}).then(r => r.json());
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use:** Orchestrating multi-step container operations, durable execution.
|
||||
|
||||
## Queue Consumer Integration
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async queue(batch, env) {
|
||||
for (const msg of batch.messages) {
|
||||
try {
|
||||
const container = env.PROCESSOR.getByName(msg.body.jobId);
|
||||
await container.startAndWaitForPorts();
|
||||
|
||||
const response = await container.fetch("/process", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(msg.body)
|
||||
});
|
||||
|
||||
response.ok ? msg.ack() : msg.retry();
|
||||
} catch (err) {
|
||||
console.error("Queue processing error:", err);
|
||||
msg.retry();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Use:** Asynchronous job processing, batch operations, event-driven execution.
|
||||
Reference in New Issue
Block a user