inngest-local
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSelf-Hosted Inngest on macOS
在macOS上自托管Inngest
This skill sets up Inngest as a self-hosted durable workflow engine on a Mac. Inngest gives you event-driven functions where each step retries independently — if step 3 of 5 fails, only step 3 retries.
本指南介绍如何在Mac上部署自托管的持久化工作流引擎Inngest。Inngest提供事件驱动函数能力,每个步骤可独立重试——如果5个步骤中的第3步失败,仅重试第3步即可。
Before You Start
开始前准备
Required:
- macOS with Docker (Docker Desktop, OrbStack, or Colima)
- Bun or Node.js for the worker process
Optional:
- k8s cluster (k3d, Talos, etc.) for persistent deployment
- Redis (for state sharing between functions and gateway integration)
必需条件:
- 安装了Docker的macOS设备(Docker Desktop、OrbStack或Colima均可)
- 运行工作进程所需的Bun或Node.js环境
可选条件:
- 用于持久化部署的k8s集群(k3d、Talos等)
- Redis(用于函数间状态共享和网关集成)
Intent Alignment
需求对齐
Ask the user these questions to determine scope.
通过询问用户以下问题明确部署范围:
Question 1: What are you building?
问题1:你的使用场景是什么?
- Quick experiment — I want to try Inngest, run a function, see the dashboard
- Persistent setup — I want this running all the time, surviving reboots, with real workflows
- Full infrastructure — I want k8s-deployed Inngest with persistent storage, integrated with an agent gateway
- 快速实验 —— 我想试用Inngest,运行简单函数、查看仪表盘
- 持久化部署 —— 我需要服务长期运行、重启不丢失数据,用于实际工作流
- 完整基础设施 —— 我需要k8s部署的带持久化存储的Inngest,和Agent网关集成
Question 2: What runtime for the worker?
问题2:工作进程使用什么运行时?
- Bun — fast, good TypeScript support, what joelclaw uses
- Node.js — standard, widest compatibility
- Existing framework — I have a Next.js/Express/Hono app already
- Bun —— 速度快、TypeScript支持好,是joelclaw的常用选择
- Node.js —— 标准运行时,兼容性最广
- 现有框架 —— 我已经有Next.js/Express/Hono应用
Question 3: What kind of work?
问题3:主要用于什么类型的任务?
- AI agent tasks — coding loops, content processing, transcription pipelines
- General background jobs — scheduled tasks, webhooks, data processing
- Both — mixed workloads
- AI Agent任务 —— 编码循环、内容处理、转录流水线
- 通用后台任务 —— 定时任务、webhook、数据处理
- 两者都有 —— 混合工作负载
Setup Tiers
部署层级
Signing Keys (required)
签名密钥(必需)
As of Feb 2026, requires signing keys. Without them the container crash-loops with .
inngest/inngest:latestError: signing-key is requiredbash
undefined从2026年2月起,镜像要求配置签名密钥,否则容器会 crash-loop 并报错 。
inngest/inngest:latestError: signing-key is requiredbash
undefinedGenerate once, reuse across tiers
生成一次,可在所有部署层级复用
INNGEST_SIGNING_KEY="signkey-dev-$(openssl rand -hex 16)"
INNGEST_EVENT_KEY="evtkey-dev-$(openssl rand -hex 16)"
echo "INNGEST_SIGNING_KEY=$INNGEST_SIGNING_KEY" >> .env.inngest
echo "INNGEST_EVENT_KEY=$INNGEST_EVENT_KEY" >> .env.inngest
undefinedINNGEST_SIGNING_KEY="signkey-dev-$(openssl rand -hex 16)"
INNGEST_EVENT_KEY="evtkey-dev-$(openssl rand -hex 16)"
echo "INNGEST_SIGNING_KEY=$INNGEST_SIGNING_KEY" >> .env.inngest
echo "INNGEST_EVENT_KEY=$INNGEST_EVENT_KEY" >> .env.inngest
undefinedTier 1: Docker One-Liner (experiment)
层级1:Docker单行命令部署(实验用途)
Get Inngest running in 30 seconds:
bash
docker run -d --name inngest \
-p 8288:8288 \
-e INNGEST_SIGNING_KEY="$INNGEST_SIGNING_KEY" \
-e INNGEST_EVENT_KEY="$INNGEST_EVENT_KEY" \
inngest/inngest:latest \
inngest start --host 0.0.0.0Open http://localhost:8288 — you should see the Inngest dashboard.
Limitation: No persistent state. Container restart = lost history. Fine for experimenting.
30秒内启动Inngest:
bash
docker run -d --name inngest \
-p 8288:8288 \
-e INNGEST_SIGNING_KEY="$INNGEST_SIGNING_KEY" \
-e INNGEST_EVENT_KEY="$INNGEST_EVENT_KEY" \
inngest/inngest:latest \
inngest start --host 0.0.0.0打开 http://localhost:8288 即可看到Inngest仪表盘。
限制: 无持久化状态,容器重启会丢失历史数据,适合实验使用。
Tier 2: Persistent Docker (daily driver)
层级2:持久化Docker部署(日常使用)
Add a volume for SQLite state:
bash
docker run -d --name inngest \
-p 8288:8288 \
-v inngest-data:/var/lib/inngest \
-e INNGEST_SIGNING_KEY="$INNGEST_SIGNING_KEY" \
-e INNGEST_EVENT_KEY="$INNGEST_EVENT_KEY" \
--restart unless-stopped \
inngest/inngest:latest \
inngest start --host 0.0.0.0Now Inngest state survives container restarts. brings it back after Docker restarts.
--restart unless-stopped新增SQLite状态存储卷:
bash
docker run -d --name inngest \
-p 8288:8288 \
-v inngest-data:/var/lib/inngest \
-e INNGEST_SIGNING_KEY="$INNGEST_SIGNING_KEY" \
-e INNGEST_EVENT_KEY="$INNGEST_EVENT_KEY" \
--restart unless-stopped \
inngest/inngest:latest \
inngest start --host 0.0.0.0现在Inngest状态在容器重启后仍然保留,配置会在Docker重启后自动恢复服务。
--restart unless-stoppedTier 3: Kubernetes (production-grade)
层级3:Kubernetes部署(生产级)
For full persistence with proper health checks. Requires a k8s cluster (k3d, Talos, etc.).
yaml
undefined适用于带健康检查的全持久化场景,需要提前准备k8s集群(k3d、Talos等)。
yaml
undefinedinngest.yaml
inngest.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: inngest
namespace: default
spec:
serviceName: inngest-svc # NOT "inngest" — avoids env var collision
replicas: 1
selector:
matchLabels:
app: inngest
template:
metadata:
labels:
app: inngest
spec:
containers:
- name: inngest
image: inngest/inngest:latest
command: ["inngest", "start", "--host", "0.0.0.0"]
ports:
- containerPort: 8288
volumeMounts:
- name: data
mountPath: /var/lib/inngest
volumeClaimTemplates:
- metadata: name: data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 5Gi
apiVersion: v1
kind: Service
metadata:
name: inngest-svc # CRITICAL: not "inngest" — k8s creates INNGEST_PORT env var that conflicts
namespace: default
spec:
type: NodePort
selector:
app: inngest
ports:
- port: 8288 targetPort: 8288 nodePort: 8288
Apply:
```bash
kubectl apply -f inngest.yaml⚠️ GOTCHA: Never name a k8s Service the same as the binary it runs. A Service named creates . The Inngest binary expects to be an integer. Name it .
inngestINNGEST_PORT=tcp://10.43.x.x:8288INNGEST_PORTinngest-svcapiVersion: apps/v1
kind: StatefulSet
metadata:
name: inngest
namespace: default
spec:
serviceName: inngest-svc # 不能命名为"inngest",避免环境变量冲突
replicas: 1
selector:
matchLabels:
app: inngest
template:
metadata:
labels:
app: inngest
spec:
containers:
- name: inngest
image: inngest/inngest:latest
command: ["inngest", "start", "--host", "0.0.0.0"]
ports:
- containerPort: 8288
volumeMounts:
- name: data
mountPath: /var/lib/inngest
volumeClaimTemplates:
- metadata: name: data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 5Gi
apiVersion: v1
kind: Service
metadata:
name: inngest-svc # 重要:不能命名为"inngest",k8s会生成冲突的INNGEST_PORT环境变量
namespace: default
spec:
type: NodePort
selector:
app: inngest
ports:
- port: 8288 targetPort: 8288 nodePort: 8288
应用配置:
```bash
kubectl apply -f inngest.yaml⚠️ 注意事项: 永远不要将k8s Service命名为和运行的二进制文件同名。名为的Service会生成的环境变量,但Inngest二进制期望是整数,因此要命名为。
inngestINNGEST_PORT=tcp://10.43.x.x:8288INNGEST_PORTinngest-svcBuild a Worker
构建工作进程
Step 1: Initialize
步骤1:初始化
bash
mkdir my-worker && cd my-worker
bun init -y
bun add inngest @inngest/ai honobash
mkdir my-worker && cd my-worker
bun init -y
bun add inngest @inngest/ai honoStep 2: Create the Inngest client
步骤2:创建Inngest客户端
typescript
// src/inngest.ts
import { Inngest } from "inngest";
// Type your events for full type safety
type Events = {
"task/process": { data: { url: string; outputPath: string } };
"task/completed": { data: { url: string; result: string } };
};
export const inngest = new Inngest({
id: "my-worker",
schemas: new EventSchemas().fromRecord<Events>(),
});typescript
// src/inngest.ts
import { Inngest } from "inngest";
// 为事件定义类型实现完整类型安全
type Events = {
"task/process": { data: { url: string; outputPath: string } };
"task/completed": { data: { url: string; result: string } };
};
export const inngest = new Inngest({
id: "my-worker",
schemas: new EventSchemas().fromRecord<Events>(),
});Step 3: Write your first function
步骤3:编写第一个函数
typescript
// src/functions/process-task.ts
import { inngest } from "../inngest";
export const processTask = inngest.createFunction(
{
id: "process-task",
concurrency: { limit: 1 }, // one at a time
retries: 3,
},
{ event: "task/process" },
async ({ event, step }) => {
// Step 1: Download — retries independently on failure
const localPath = await step.run("download", async () => {
const response = await fetch(event.data.url);
const buffer = await response.arrayBuffer();
const path = `/tmp/downloads/${crypto.randomUUID()}.bin`;
await Bun.write(path, buffer);
return path; // Only the path is stored in step state (claim-check pattern)
});
// Step 2: Process — if this fails, download doesn't re-run
const result = await step.run("process", async () => {
const data = await Bun.file(localPath).text();
// ... your processing logic
return { processed: true, size: data.length };
});
// Step 3: Emit completion event — chains to other functions
await step.sendEvent("notify-complete", {
name: "task/completed",
data: { url: event.data.url, result: JSON.stringify(result) },
});
return { status: "done", result };
}
);typescript
// src/functions/process-task.ts
import { inngest } from "../inngest";
export const processTask = inngest.createFunction(
{
id: "process-task",
concurrency: { limit: 1 }, // 一次仅运行一个实例
retries: 3,
},
{ event: "task/process" },
async ({ event, step }) => {
// 步骤1:下载 —— 失败时独立重试
const localPath = await step.run("download", async () => {
const response = await fetch(event.data.url);
const buffer = await response.arrayBuffer();
const path = `/tmp/downloads/${crypto.randomUUID()}.bin`;
await Bun.write(path, buffer);
return path; // 仅路径存储在步骤状态中(凭证模式)
});
// 步骤2:处理 —— 如果该步骤失败,下载步骤不会重新运行
const result = await step.run("process", async () => {
const data = await Bun.file(localPath).text();
// ... 你的处理逻辑
return { processed: true, size: data.length };
});
// 步骤3:发送完成事件 —— 触发其他关联函数
await step.sendEvent("notify-complete", {
name: "task/completed",
data: { url: event.data.url, result: JSON.stringify(result) },
});
return { status: "done", result };
}
);Step 4: Serve it
步骤4:启动服务
typescript
// src/serve.ts
import { Hono } from "hono";
import { serve as inngestServe } from "inngest/hono";
import { inngest } from "./inngest";
import { processTask } from "./functions/process-task";
const app = new Hono();
// Health check
app.get("/", (c) => c.json({ status: "running", functions: 1 }));
// Inngest endpoint — registers functions with the server
app.on(
["GET", "POST", "PUT"],
"/api/inngest",
inngestServe({ client: inngest, functions: [processTask] })
);
export default {
port: 3111,
fetch: app.fetch,
};typescript
// src/serve.ts
import { Hono } from "hono";
import { serve as inngestServe } from "inngest/hono";
import { inngest } from "./inngest";
import { processTask } from "./functions/process-task";
const app = new Hono();
// 健康检查
app.get("/", (c) => c.json({ status: "running", functions: 1 }));
// Inngest端点 —— 向服务端注册函数
app.on(
["GET", "POST", "PUT"],
"/api/inngest",
inngestServe({ client: inngest, functions: [processTask] })
);
export default {
port: 3111,
fetch: app.fetch,
};Step 5: Run it
步骤5:运行服务
bash
INNGEST_DEV=1 bun run src/serve.tsThe worker starts, registers with Inngest at localhost:8288, and your function appears in the dashboard.
bash
INNGEST_DEV=1 bun run src/serve.ts工作进程启动后会自动向localhost:8288的Inngest注册,你的函数会出现在仪表盘上。
Step 6: Test it
步骤6:测试功能
Send an event via the dashboard (Events → Send Event) or curl:
bash
curl -X POST http://localhost:8288/e/key \
-H "Content-Type: application/json" \
-d '{"name": "task/process", "data": {"url": "https://example.com/file.txt", "outputPath": "/tmp/out"}}'Watch it execute step-by-step in the dashboard.
通过仪表盘(事件 → 发送事件)或curl发送事件:
bash
curl -X POST http://localhost:8288/e/key \
-H "Content-Type: application/json" \
-d '{"name": "task/process", "data": {"url": "https://example.com/file.txt", "outputPath": "/tmp/out"}}'在仪表盘中可以查看步骤级的执行过程。
Patterns
常用模式
Event Chaining
事件链式触发
Function A emits an event that triggers Function B:
typescript
// In function A:
await step.sendEvent("chain", { name: "pipeline/step-two", data: { result } });
// Function B triggers on that event:
export const stepTwo = inngest.createFunction(
{ id: "step-two" },
{ event: "pipeline/step-two" },
async ({ event, step }) => { /* ... */ }
);函数A发送事件触发函数B:
typescript
// 在函数A中:
await step.sendEvent("chain", { name: "pipeline/step-two", data: { result } });
// 函数B基于该事件触发:
export const stepTwo = inngest.createFunction(
{ id: "step-two" },
{ event: "pipeline/step-two" },
async ({ event, step }) => { /* ... */ }
);Concurrency Keys
并发控制键
Run one instance per project, but allow parallel across projects:
typescript
concurrency: {
key: "event.data.project",
limit: 1,
}每个项目仅运行一个实例,但不同项目可并行执行:
typescript
concurrency: {
key: "event.data.project",
limit: 1,
}Cron Functions
定时函数
typescript
export const heartbeat = inngest.createFunction(
{ id: "heartbeat" },
[{ cron: "*/15 * * * *" }],
async ({ step }) => {
await step.run("check-health", async () => {
// ... system health checks
});
}
);typescript
export const heartbeat = inngest.createFunction(
{ id: "heartbeat" },
[{ cron: "*/15 * * * *" }],
async ({ step }) => {
await step.run("check-health", async () => {
// ... 系统健康检查逻辑
});
}
);Claim-Check Pattern
凭证模式(处理大数据)
Large data between steps: write to file, pass path.
typescript
// ❌ DON'T: return large data from a step
const transcript = await step.run("transcribe", async () => {
return { text: hugeString }; // Step output has size limits!
});
// ✅ DO: write to file, return path
const transcriptPath = await step.run("transcribe", async () => {
const result = await transcribe(audioPath);
await Bun.write("/tmp/transcript.json", JSON.stringify(result));
return "/tmp/transcript.json";
});步骤间传递大数据时写入文件,仅传递路径:
typescript
// ❌ 不要这样做:从步骤返回大量数据
const transcript = await step.run("transcribe", async () => {
return { text: hugeString }; // 步骤输出有大小限制!
});
// ✅ 推荐做法:写入文件,返回路径
const transcriptPath = await step.run("transcribe", async () => {
const result = await transcribe(audioPath);
await Bun.write("/tmp/transcript.json", JSON.stringify(result));
return "/tmp/transcript.json";
});Make It Survive Reboots
实现重启后自动恢复
Worker via launchd
通过launchd托管工作进程
xml
<!-- ~/Library/LaunchAgents/com.you.inngest-worker.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.you.inngest-worker</string>
<key>ProgramArguments</key>
<array>
<string>/Users/you/.bun/bin/bun</string>
<string>run</string>
<string>/path/to/your/worker/src/serve.ts</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>INNGEST_DEV</key><string>1</string>
<key>HOME</key><string>/Users/you</string>
<key>PATH</key><string>/usr/local/bin:/usr/bin:/bin:/Users/you/.bun/bin</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/tmp/inngest-worker.log</string>
<key>StandardErrorPath</key><string>/tmp/inngest-worker.log</string>
<key>WorkingDirectory</key><string>/path/to/your/worker</string>
</dict>
</plist>Load:
bash
launchctl load ~/Library/LaunchAgents/com.you.inngest-worker.plistxml
<!-- ~/Library/LaunchAgents/com.you.inngest-worker.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.you.inngest-worker</string>
<key>ProgramArguments</key>
<array>
<string>/Users/you/.bun/bin/bun</string>
<string>run</string>
<string>/path/to/your/worker/src/serve.ts</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>INNGEST_DEV</key><string>1</string>
<key>HOME</key><string>/Users/you</string>
<key>PATH</key><string>/usr/local/bin:/usr/bin:/bin:/Users/you/.bun/bin</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/tmp/inngest-worker.log</string>
<key>StandardErrorPath</key><string>/tmp/inngest-worker.log</string>
<key>WorkingDirectory</key><string>/path/to/your/worker</string>
</dict>
</plist>加载配置:
bash
launchctl load ~/Library/LaunchAgents/com.you.inngest-worker.plistWhat happens on reboot
重启后的执行流程
- Docker starts → Inngest server comes up with persisted state (SQLite)
- launchd starts → worker process registers functions
- Any incomplete function runs resume from their last completed step
- Docker启动 → Inngest服务启动,加载持久化的SQLite状态
- launchd启动 → 工作进程启动并注册函数
- 所有未完成的函数会从最后完成的步骤恢复执行
Gotchas
注意事项
-
is a required peer dep.
@inngest/aialone isn't enough — the SDK importsbun add inngestat startup. Worker crashes with@inngest/ai. Always install both.Cannot find module '@inngest/ai' -
Docker-to-host networking. If Inngest runs in Docker and the worker on the host, the server can't reach. Pass
localhost:3111on the docker run command. This is Docker Desktop/OrbStack-specific; Linux Docker needs--sdk-url http://host.docker.internal:3111/api/inngest.--add-host=host.docker.internal:host-gateway -
Service naming in k8s: Never name a Service the same as the binary.env var collision crashes the container.
INNGEST_PORT -
Step output size: Keep step return values small. Use claim-check pattern for large data.
-
Worker re-registration: After Inngest server restart, the worker needs to re-register. Restart the worker or hit the registration endpoint.
-
Trigger drift: Functions register their triggers at startup. If you change a trigger in code but the server has stale state, the old trigger stays active. Build an auditor or restart both server and worker.
-
: Required for local development. Without it, the worker tries to register with Inngest Cloud.
INNGEST_DEV=1 -
Concurrency = 1 for GPU work: Transcription, inference — anything that saturates a GPU needs.
concurrency: { limit: 1 }
-
是必需的peer依赖。 仅执行
@inngest/ai是不够的,SDK启动时会导入bun add inngest,缺少会报错@inngest/ai,务必同时安装两个包。Cannot find module '@inngest/ai' -
Docker到宿主机的网络问题。 如果Inngest运行在Docker中,工作进程运行在宿主机,服务端无法访问,需要在docker run命令中添加
localhost:3111。该配置仅适用于Docker Desktop/OrbStack,Linux Docker需要添加--sdk-url http://host.docker.internal:3111/api/inngest。--add-host=host.docker.internal:host-gateway -
k8s服务命名问题: 永远不要将Service命名为和二进制文件同名,环境变量冲突会导致容器崩溃。
INNGEST_PORT -
步骤输出大小限制: 步骤返回值尽量精简,大数据使用凭证模式传递。
-
工作进程重新注册: Inngest服务重启后,工作进程需要重新注册,可以重启工作进程或访问注册端点。
-
触发器漂移: 函数在启动时注册触发器,如果代码中修改了触发器但服务端仍有旧状态,旧触发器会继续生效,建议实现审计逻辑或同时重启服务端和工作进程。
-
: 本地开发必需配置,否则工作进程会尝试向Inngest Cloud注册。
INNGEST_DEV=1 -
GPU任务并发设置为1: 转录、推理等会占满GPU的任务需要配置。
concurrency: { limit: 1 }
Verification
验证项
- Inngest dashboard accessible at http://localhost:8288
- Worker shows as registered in dashboard (Functions tab)
- Send a test event — function executes in dashboard
- Kill the worker mid-function — restart worker, function resumes from last step
- (Tier 2+) Restart Docker — Inngest state is preserved
- (launchd) Reboot Mac — worker and Inngest both come back automatically
- Inngest仪表盘可通过http://localhost:8288访问
- 工作进程在仪表盘的函数 tab 中显示为已注册
- 发送测试事件,可在仪表盘中看到函数执行过程
- 函数执行中途杀死工作进程,重启后函数从最后完成的步骤恢复
- (层级2+)重启Docker,Inngest状态保留
- (配置launchd后)重启Mac,工作进程和Inngest都自动恢复运行
Setup Script (curl-first)
部署脚本(curl一键安装)
For automated setup, the user can run:
bash
curl -sL joelclaw.com/scripts/inngest-setup.sh | bashOr with a specific tier:
bash
curl -sL joelclaw.com/scripts/inngest-setup.sh | bash -s -- 2The script is idempotent, detects existing state, and scaffolds a worker with typed events.
如需自动化部署,用户可执行:
bash
curl -sL joelclaw.com/scripts/inngest-setup.sh | bash指定部署层级:
bash
curl -sL joelclaw.com/scripts/inngest-setup.sh | bash -s -- 2脚本是幂等的,会自动检测现有状态,并生成带类型事件的工作进程脚手架。
Decision Chain (compressed ADRs)
决策链(精简架构决策记录)
This skill's architecture is backed by a chain of Architecture Decision Records. Unfurl as needed for tradeoff context.
ADR-0010 → ADR-0029 → current state
| Decision | Choice | Key Tradeoff | Link |
|---|---|---|---|
| Workflow engine | Inngest (self-hosted) | Step-level durability vs complexity. Cron+scripts has no per-step retry. | ADR-0010 |
| Container runtime | Colima (VZ framework) | Replaces Docker Desktop. Free, headless, less RAM. | ADR-0029 |
| k8s for 3 containers | Yes (k3d → Talos) | 380MB overhead for reconciliation loop + multi-node future. Docker Compose = no self-healing. | joel-deploys-k8s |
| Service naming | | k8s injects | Hard-won debugging |
| Worker runtime | Bun + Hono | Faster cold start than Node. Hono = minimal HTTP. launchd KeepAlive for persistence. | Practical choice |
| Step data pattern | Claim-check (file path) | Step outputs have size limits. Write large data to disk, pass path between steps. | Inngest docs |
| Trigger auditing | Heartbeat cron auditor | Silent trigger drift broke promote function for days. Now audited every 15 min. | ADR-0037 |
本指南的架构基于一系列架构决策记录,可按需查看权衡上下文。
ADR-0010 → ADR-0029 → 当前状态
| 决策 | 选择 | 核心权衡 | 链接 |
|---|---|---|---|
| 工作流引擎 | Inngest(自托管) | 步骤级持久化 vs 复杂度。Cron+脚本没有步骤级重试能力。 | ADR-0010 |
| 容器运行时 | Colima(VZ框架) | 替代Docker Desktop,免费、无UI、内存占用更低。 | ADR-0029 |
| 3个容器是否用k8s | 是(k3d → Talos) | 380MB开销用于协调循环+未来多节点扩展。Docker Compose没有自愈能力。 | joel-deploys-k8s |
| 服务命名 | | k8s注入 | 踩坑经验总结 |
| 工作进程运行时 | Bun + Hono | 冷启动比Node更快,Hono是轻量HTTP框架,launchd的KeepAlive实现持久化。 | 实践最优选择 |
| 步骤数据模式 | 凭证模式(文件路径) | 步骤输出有大小限制,大数据写入磁盘,步骤间传递路径。 | Inngest官方文档 |
| 触发器审计 | 心跳定时审计 | 静默触发器漂移曾导致升级功能中断数天,现在每15分钟审计一次。 | ADR-0037 |
Credits
致谢
- Inngest — the workflow engine
- joelclaw.com/inngest-is-the-nervous-system — architecture narrative
- joelclaw.com/self-hosting-inngest-background-tasks — human summary
- Inngest —— 本指南使用的工作流引擎
- joelclaw.com/inngest-is-the-nervous-system —— 架构说明
- joelclaw.com/self-hosting-inngest-background-tasks —— 人工说明文档