每个 container 都由 Durable Object 管理和代理。Durable Object 管理路由和持久状态。容器进程在 Linux VM 中运行你的镜像。
本页记录的 API 在任何具有容器绑定(binding)的 Durable Object 类内以 this.ctx.container 的形式提供。用它直接控制容器进程。
你的 Durable Object 还可以通过 this.ctx.storage 访问 SQLite 存储、警报(alarms) 以及所有其他 Durable Object API。使用存储在容器重启后保留配置和状态。使用警报(alarms)调度工作,而无需保持容器运行。
import { DurableObject } from "cloudflare:workers";
export class MyDurableObject extends DurableObject {
async start() {
const container = this.ctx.container;
if (!container) {
throw new Error("No container is configured for this Durable Object");
}
if (!container.running) {
// With the `default` scheduling policy, call `container.start()` without options.
container.start({
image: container.images.base,
enableInternet: false,
});
await this.ctx.storage.put("lastStartedAt", Date.now());
}
}
async scheduleStart(timestamp) {
await this.ctx.storage.setAlarm(timestamp);
}
async alarm() {
await this.start();
}
}import { DurableObject } from "cloudflare:workers";
interface Env {}
export class MyDurableObject extends DurableObject<Env> {
async start(): Promise<void> {
const container = this.ctx.container;
if (!container) {
throw new Error("No container is configured for this Durable Object");
}
if (!container.running) {
// With the `default` scheduling policy, call `container.start()` without options.
container.start({
image: container.images.base,
enableInternet: false,
});
await this.ctx.storage.put("lastStartedAt", Date.now());
}
}
async scheduleStart(timestamp: number): Promise<void> {
await this.ctx.storage.setAlarm(timestamp);
}
async alarm(): Promise<void> {
await this.start();
}
}images 是从应用 Wrangler 配置中的 images 字段 生成的只读映射。Wrangler 会构建或解析每个具名镜像,并在相同键下公开其按 digest 固定的引用。此属性仅适用于使用 durable_object 调度策略 的应用。
const image = this.ctx.container.images.base;将此映射中的值作为 image 选项传给 start()。
容器正在运行时,running 为 true。它不确认容器已准备好接受请求。
this.ctx.container.running;start() 启动容器。它会在容器准备好接受请求之前返回。发送流量前请确认就绪。
start() 在验证其选项后返回。要捕获后续错误(包括容器启动失败),请使用 monitor()。
所需选项取决于应用的调度策略。
// `default` scheduling policy:
// The image and instance size come from Wrangler configuration.
this.ctx.container.start();
// `durable_object` scheduling policy:
// Pass an image and choose whether to allow outbound Internet access.
// The instance size is optional and defaults to "lite".
this.ctx.container.start({
image: this.ctx.container.images.base,
enableInternet: true,
});options(object,有条件必填):启动选项。使用durable_object调度策略的应用必填,使用default调度策略的应用可选。env(Record<string, string>,可选):要传给容器的环境变量。用exec()启动的进程不会接收这些变量,PATH除外。entrypoint(string[],可选):要在容器中运行的命令和参数。enableInternet(boolean,必填):是否允许出站 Internet 访问。当你传递options时必填。image(string,有条件必填) 仅 durable_object 策略:要启动的镜像引用。除非你传递containerSnapshot,否则必填。传入cloudflare/debian-trixie托管镜像 或来自ctx.container.images的值。instance(string | object,可选) 仅 durable_object 策略:实例规格。传入"lite"、"standard-1"、"standard-2"、"standard-3"、"standard-4",或带有vcpu、memoryMib和diskMb属性的自定义对象。memoryMib以 mebibytes 为单位,diskMb以 megabytes 为单位。两者都必须是正整数。默认为"lite"。containerSnapshot(ContainerSnapshotRestoreParams,可选) 仅 durable_object 策略:启动前要恢复的快照句柄。传入snapshotContainer()返回的ContainerSnapshot,或包含其id的对象。不能同时传递containerSnapshot和image。labels(Record<string, string>,可选):最多 10 个由inspect()返回的标签。标签名必须包含 1 到 16 字节。标签值最多可包含 64 字节。名称和值不能包含控制字符。
当使用 durable_object 调度策略的容器在没有 image 或 containerSnapshot 的情况下启动时,monitor() 会拒绝。
void:无返回值。
- 当容器已在运行时,
start()抛出Error。 - 当同时设置
image和containerSnapshot、任一为空,或instance命名未知类型时,start()抛出TypeError。 - 当自定义
instance值缺失或无效时,start()抛出RangeError。 - 当
labels超出标签限制、环境变量名包含=,或环境变量名或值包含空字符时,start()抛出Error。
inspect() 返回正在运行的容器的镜像和标签。没有容器运行时返回 null。
const containerInfo = await this.ctx.container.inspect();- 无。
Promise<ContainerInfo | null>:没有容器运行时(包括启动失败后或destroy()之后)解析为null。否则,解析为包含以下内容的ContainerInfo对象:
exec() 在已经运行的容器内启动另一个进程。它不会启动已停止的容器。如果容器仍在启动,它会等待。
exec(
cmd: string[],
options?: ContainerExecOptions,
): Promise<ExecProcess>exec() 使用提供的参数直接启动可执行文件。它不会启动 shell,也不会解释管道、重定向、展开或其他 shell 语法。当镜像中存在 Bash 时,使用 ["bash", "-lc", "<COMMAND>"] 显式调用 Bash。对于仅提供 Portable Operating System Interface (POSIX) shell 的镜像,使用 ["sh", "-c", "<COMMAND>"]。
以下 RPC 方法在执行命令前检查容器是否正在运行:
import { DurableObject } from "cloudflare:workers";
export class MyDurableObject extends DurableObject {
async runCommand() {
const container = this.ctx.container;
if (!container) {
throw new Error("No container is configured for this Durable Object");
}
if (!container.running) {
throw new Error("Container is not running");
}
const process = await container.exec(["node", "--version"]);
const output = await process.output();
return {
pid: process.pid,
exitCode: output.exitCode,
stdout: new TextDecoder().decode(output.stdout),
};
}
}import { DurableObject } from "cloudflare:workers";
interface Env {}
export class MyDurableObject extends DurableObject<Env> {
async runCommand() {
const container = this.ctx.container;
if (!container) {
throw new Error("No container is configured for this Durable Object");
}
if (!container.running) {
throw new Error("Container is not running");
}
const process = await container.exec(["node", "--version"]);
const output = await process.output();
return {
pid: process.pid,
exitCode: output.exitCode,
stdout: new TextDecoder().decode(output.stdout),
};
}
}cmd(string[]):可执行文件及其参数。options(ContainerExecOptions,可选):进程配置:stdin(ReadableStream | "pipe",可选):标准输入源。使用"pipe"通过返回的stdin流写入。省略时,标准输入关闭并发送文件结束 (EOF)。stdout("pipe" | "ignore",可选,默认"pipe"):捕获或丢弃标准输出。stderr("pipe" | "ignore" | "combined",可选,默认"pipe"):捕获、丢弃,或将标准错误合并到标准输出。"combined"值需要stdout: "pipe"。合并输出不保证其源之间的顺序。cwd(string,可选):进程的工作目录。env(Record<string, string>,可选):进程的环境变量。除PATH外,不会继承来自start()或镜像的其他变量。除非在此处覆盖,否则PATH使用容器的启动值,包括传给start()的任何覆盖。user(string,可选):进程的数字 Linux 用户 ID 和组 ID,格式为uid:gid。省略时,进程以镜像用户身份运行。signal(AbortSignal,可选):中止该信号会向进程发送SIGKILL。已经中止的信号会使exec()抛出AbortError。pty(boolean | { cols?: number; rows?: number },可选):将进程附加到伪终端运行。true使用 80 列和 24 行。
以下调用以用户 1000 和组 1000 运行命令。它创建的文件属于 1000:1000:
const process = await this.ctx.container.exec(["touch", "/tmp/notes.txt"], {
user: "1000:1000",
});使用 pty 时,stdin 默认为 "pipe",stderr 默认为 "combined" 且不能取其他值。标准输出和标准错误一起到达 stdout。终端将换行转换为 \r\n。
Promise<ExecProcess>:解析为已启动进程的ExecProcess。
ExecProcess 具有这些字段和方法:
stdin(WritableStream | undefined):当stdin为"pipe"时可写的标准输入。stdout(ReadableStream | undefined):管道化时可读的标准输出。stderr(ReadableStream | undefined):单独管道化时可读的标准错误。pid(number):进程标识符。isPty(boolean):进程是否附加到伪终端运行。exitCode(Promise<number>):进程退出时解析。非零代码会正常解析,而不是拒绝。output()(Promise<ExecOutput>):读取一次缓冲输出。ExecOutput包含stdout(ArrayBuffer)、stderr(ArrayBuffer) 和exitCode(number)。被忽略的流产生空缓冲区。使用TextDecoder解码文本。kill(signal?: number)(void):为进程排队一个信号。默认是SIGTERM,信号15。信号必须介于1到64。resize(cols: number, rows: number)(void):更改伪终端大小。两个值都必须介于1到65535。
进程未提供的流为 undefined。例如,使用 stderr: "combined" 或 pty 时,ExecProcess 上的 stderr 为 undefined,ExecOutput 上为空 ArrayBuffer。从 stdout 读取两个输出通道。
Workers TypeScript 类型将 stdin、stdout 和 stderr 声明为 | null。请测试流是否存在,例如 if (process.stderr),而不是与 null 比较。
当多次调用 output(),或任一可读流已开始被消耗后,output() 会抛出 TypeError。
output() 将所有输出保存在 Durable Object 的内存中,并计入内存限制。当输出可能很大时,请改为并发读取两个流。更多信息请参阅流式传输大型输出。
exec() 没有内置超时。使用 kill() 请求终止,然后通过 exitCode 观察完成。进程可以处理或忽略信号,因此这不会强制硬截止时间。不要从信号推断特定退出码。
kill() 和 signal 只到达 exec() 启动的进程。该进程启动的进程(例如 bash -c 脚本中的命令)会继续运行。output() 会等到它们退出。要同时停止它们,请参阅停止命令启动的进程。
取消启动进程的请求不会停止该进程。如果客户端重试,会运行该命令的第二个副本。
要在客户端断开连接时停止进程,请在定义 Durable Object 的 Worker 中开启 enable_request_signal 兼容性标志,并通过其 stub 上的 fetch() 调用 Durable Object。然后在 request.signal 中止时中止 exec() 信号。RPC 方法不会接收请求信号。
export class MyContainer extends DurableObject {
async fetch(request) {
const container = this.ctx.container;
if (!container?.running) {
return new Response("The container is not running", { status: 503 });
}
const controller = new AbortController();
const abort = () => controller.abort();
if (request.signal.aborted) {
abort();
} else {
request.signal.addEventListener("abort", abort, { once: true });
}
const process = await container.exec(["sleep", "60"], {
signal: controller.signal,
});
// Stop forwarding the abort once the process exits.
void process.exitCode.then(() =>
request.signal.removeEventListener("abort", abort),
);
const output = await process.output();
return new Response(`Exit code: ${output.exitCode}`);
}
}export class MyContainer extends DurableObject {
async fetch(request: Request) {
const container = this.ctx.container;
if (!container?.running) {
return new Response("The container is not running", { status: 503 });
}
const controller = new AbortController();
const abort = () => controller.abort();
if (request.signal.aborted) {
abort();
} else {
request.signal.addEventListener("abort", abort, { once: true });
}
const process = await container.exec(["sleep", "60"], {
signal: controller.signal,
});
// Stop forwarding the abort once the process exits.
void process.exitCode.then(() =>
request.signal.removeEventListener("abort", abort),
);
const output = await process.output();
return new Response(`Exit code: ${output.exitCode}`);
}
}- 当容器未运行时,
exec()会抛出。 - 当
cmd为空、选项模式无效、将stderr: "combined"与stdout: "ignore"一起使用,或将pty与非"combined"的stderr值一起使用时,exec()抛出TypeError。 - 当
signal已经中止时,exec()抛出AbortError。 - 如果运行时无法创建或启动进程,
exec()会拒绝。 - 环境变量名不能包含
=或空字符。环境值、cwd和user不能包含空字符。 - 当信号超出支持范围时,
kill()抛出RangeError。 - 当进程没有伪终端时,
resize()抛出TypeError;当维度超出范围时,抛出RangeError。
面向任务的示例请参阅执行命令。
snapshotContainer() 为正在运行的容器的可写根文件系统创建某个时间点的快照。它不会捕获内存、正在运行的进程或单独挂载的文件系统。从快照启动的容器会再次运行其入口点。此方法仅支持使用 durable_object 调度策略 的应用。
const snapshot = await this.ctx.container.snapshotContainer({
name: "before-upgrade",
});options(ContainerSnapshotOptions):快照配置。不设置名称时传入{}:name(string,可选):快照的可读名称。
Promise<ContainerSnapshot>:解析为已存储文件系统快照的不透明句柄。快照数据不会返回给 Worker。该句柄包含:id(string):唯一快照标识符。将返回的ContainerSnapshot作为containerSnapshot传给start(),或存储它以便稍后恢复。size(number):快照大小(字节)。name(string,可选):在options中提供的可读名称。
容器快照是不可变的。快照句柄具有隐式的 30 天生存时间,恢复时会刷新。完整的保存和恢复流程请参阅使用快照。
- 当容器未运行时,
snapshotContainer()抛出Error。 - 当省略
options时,snapshotContainer()抛出TypeError。
destroy() 停止容器,并可包含该操作的可选原因。
await this.ctx.container.destroy("Manually Destroyed");error(any,可选):与销毁操作关联的可选原因。字符串常用于日志记录或调试。
Promise<void>:容器被销毁时解析。
signal() 向容器发送进程间通信 (IPC) 信号,例如 SIGKILL 或 SIGTERM。用它优雅或强制地停止容器。
const SIGTERM = 15;
this.ctx.container.signal(SIGTERM);signal(number):要发送到容器的 POSIX 信号编号,例如SIGTERM(15) 或SIGKILL(9)。
void:无返回值。
setInactivityTimeout() 设置容器在其 Durable Object 变为空闲后继续运行多长时间。如果 Durable Object 在超时结束前处理另一个请求,同一个容器仍在运行。否则,Cloudflare 会停止容器。没有超时的情况下,Cloudflare 会在 Durable Object 变为空闲后不久停止容器。
每个 Durable Object 实例设置自己的超时。重启的 Durable Object(例如部署后)开始时没有超时。在 start() 之后设置超时,并在容器已在运行时在构造函数中再次设置。
setInactivityTimeout(durationMs: number | bigint): Promise<void>import { DurableObject } from "cloudflare:workers";
const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
export class MyDurableObject extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
const container = ctx.container;
if (container?.running) {
void ctx.blockConcurrencyWhile(() =>
container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS),
);
}
}
async ensureRunning() {
const container = this.ctx.container;
if (!container) {
throw new Error("No container is configured for this Durable Object");
}
if (!container.running) {
container.start({
// `cloudflare/debian-trixie` requires the `durable_object` scheduling policy.
image: "cloudflare/debian-trixie",
entrypoint: ["sleep", "infinity"],
enableInternet: false,
});
await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS);
}
}
}import { DurableObject } from "cloudflare:workers";
interface Env {}
const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
export class MyDurableObject extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
const container = ctx.container;
if (container?.running) {
void ctx.blockConcurrencyWhile(() =>
container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS),
);
}
}
async ensureRunning() {
const container = this.ctx.container;
if (!container) {
throw new Error("No container is configured for this Durable Object");
}
if (!container.running) {
container.start({
// `cloudflare/debian-trixie` requires the `durable_object` scheduling policy.
image: "cloudflare/debian-trixie",
entrypoint: ["sleep", "infinity"],
enableInternet: false,
});
await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS);
}
}
}durationMs(number | bigint):空闲超时(毫秒)。该值必须大于0,且最多 6 小时(21600000)。
Promise<void>:超时设置后解析。
- 当
durationMs为0或更小时,setInactivityTimeout()抛出TypeError。 - 当
durationMs大于 6 小时时,返回的 promise 会拒绝。
在 try 块中等待该调用以同时处理这两种情况。
getTcpPort() 返回容器的 TCP 端口。用它通过 TCP 或 HTTP 与容器通信。
const port = this.ctx.container.getTcpPort(8080);
const res = await port.fetch("http://container/set-state", {
body: initialState,
method: "POST",
});const conn = this.ctx.container.getTcpPort(8080).connect("10.0.0.1:8080");
await conn.opened;
try {
if (request.body) {
await request.body.pipeTo(conn.writable);
}
return new Response(conn.readable);
} catch (error) {
console.error("Request body piping failed:", error);
return new Response("Failed to proxy request body", { status: 502 });
}port(number):用于与容器通信的 TCP 端口号。
Fetcher:用于向容器端口发送 HTTP 请求或 TCP 连接的对象。
monitor() 返回一个在容器退出时解析、在容器出错时拒绝的 promise。用它在 Workers 代码中处理容器状态变化。
挂起的 monitor() 调用会阻止驱逐,直到容器退出或最多 15 分钟,以先到者为准。稍后启动另一个操作可以延长 Durable Object 的内存中时间。
import { DurableObject } from "cloudflare:workers";
class MyDurableObject extends DurableObject {
startAndMonitor() {
const container = this.ctx.container;
// With the `default` scheduling policy, call `container.start()` without options.
container.start({
image: container.images.base,
enableInternet: false,
});
this.ctx.waitUntil(
container
.monitor()
.then(() => console.log("Container exited"))
.catch((error) => console.error("Container errored", error)),
);
}
}import { DurableObject } from "cloudflare:workers";
interface Env {}
class MyDurableObject extends DurableObject<Env> {
startAndMonitor() {
const container = this.ctx.container;
// With the `default` scheduling policy, call `container.start()` without options.
container.start({
image: container.images.base,
enableInternet: false,
});
this.ctx.waitUntil(
container
.monitor()
.then(() => console.log("Container exited"))
.catch((error) => console.error("Container errored", error)),
);
}
}Durable Object 重启时(例如部署后),monitor() promise 不会延续。如果 Durable Object 启动时 running 为 true,请再次调用 monitor() 以观察容器。
- 无。
Promise<void>:容器停止时结算,包括在调用前已停止的容器。结果取决于容器如何停止:- 当主进程以代码
0退出,或destroy()在没有错误值的情况下停止容器时,它会解析。 - 当主进程以其他代码退出时,它会拒绝带有
exitCode属性的Error。 - 当向
destroy()传入值时,它会拒绝该值。 - 当容器因其他原因停止时,它会拒绝描述该失败的错误。
- 当主进程以代码
- 当此 Durable Object 实例没有容器时,
monitor()抛出Error。实例在调用start()之后拥有容器,或者在实例启动时容器已经在运行。
interceptOutboundHttp() 将匹配主机名、主机名 glob、IP 地址、IP:port 或 CIDR 范围的出站 HTTP 请求通过 Fetcher 路由。在启动容器之前或之后调用它。拦截持续到容器停止,因此请为每个新容器再次注册拦截。再次注册目标会替换其处理程序,打开的连接会使用新处理程序而不会被断开。
const worker = this.ctx.exports.MyWorker({ props: { message: "hello" } });
// Match a specific hostname
await this.ctx.container.interceptOutboundHttp("api.example.com", worker);
// Match a hostname glob pattern
await this.ctx.container.interceptOutboundHttp("*.example.com", worker);
// Match an IP:port
await this.ctx.container.interceptOutboundHttp("15.0.0.1:80", worker);
// Match an IPv4 CIDR range. Register an IPv6 range separately.
await this.ctx.container.interceptOutboundHttp("203.0.113.0/24", worker);在以 enableInternet: false 启动的容器中,没有公共解析器回答 DNS 查找。查找按如下方式解析:
- 被拦截主机名的
A和AAAA查找返回一个占位地址,将该请求路由到拦截。 interceptAllOutboundHttp()和*主机名目标会使每个主机名解析,包括不存在的主机名。- IP 地址和 CIDR 目标不添加 DNS 答案,
0.0.0.0/0和::/0除外,它们会使每个主机名解析。 - 其他每次查找都会超时,包括在你注册拦截之前进行的查找。
一个容器有 128 个拦截条目,由 interceptOutboundHttp()、interceptOutboundHttps() 和 interceptAllOutboundHttp() 共享。每个新目标按如下方式使用条目:
- 主机名或主机名 glob(包括
*)使用两个条目,一个用于 IPv4,一个用于 IPv6。 - IP 地址、IP:port 或 CIDR 范围使用一个条目。
interceptAllOutboundHttp()规则使用两个条目。
同时为 HTTP 和 HTTPS 拦截的主机名是两个目标,使用四个条目。一个容器最多可以容纳 64 个主机名目标,或最多 128 个 IP 地址和 CIDR 目标。再次注册现有目标不会使用更多条目。
容器运行时无法移除拦截。要路由不断变化的主机名集合,请注册一次 interceptAllOutboundHttp() 或 * 目标。然后在 WorkerEntrypoint 内为每个主机名选择行为,例如从其 props。要更改 props,请使用新 props 再次注册该规则。
当新目标放不下时,等待该调用会抛出消息为 You can't configure more than 128 egress interceptors 的 Error。先前注册的拦截继续有效。
addr(string):要匹配的主机名、主机名 glob(例如*.example.com)、IP 地址、IP:port 或 CIDR 范围。binding(Fetcher):处理匹配请求的 Worker 入口点或服务绑定(binding)。
Promise<void>:拦截规则安装后解析。
- 当
addr是新的且容器没有足够的空闲条目时,等待interceptOutboundHttp()会抛出Error。更多信息请参阅拦截限制。
interceptAllOutboundHttp() 将容器在端口 80 上的出站 HTTP 请求通过 Fetcher 路由,无论目的地是什么。
此规则不拦截其他端口,包括端口 443 上的 HTTPS。要同时拦截 HTTPS,请再注册 interceptOutboundHttps("*")。在以 enableInternet: true 启动的容器中,没有任何拦截覆盖的连接会直接到达 Internet。要阻止它们,请以 enableInternet: false 启动容器。
在 interceptAllOutboundHttp() 之前注册主机名拦截。先由 interceptOutboundHttp() 注册的主机名拦截会继续接收其请求,即使你再次注册此规则。在此规则之后注册的主机名拦截不会接收请求。
await this.ctx.container.interceptAllOutboundHttp(worker);binding(Fetcher):处理所有出站 HTTP 请求的 Worker 入口点或服务绑定(binding)。
Promise<void>:拦截规则安装后解析。
- 当规则尚未注册且容器没有两个空闲条目时,等待
interceptAllOutboundHttp()会抛出Error。更多信息请参阅拦截限制。
interceptOutboundHttps() 将匹配主机名或主机名 glob(带可选端口)的出站 HTTPS 请求通过 Fetcher 路由。它的工作方式类似于 interceptOutboundHttp(),但处理 HTTPS 流量。容器必须信任 /etc/cloudflare/certs/cloudflare-containers-ca.crt 处的 CA 证书才能进行 HTTPS 拦截。
主机名 glob 支持 * 以匹配任何字符序列。
const worker = this.ctx.exports.MyWorker({ props: {} });
// Match a specific hostname
await this.ctx.container.interceptOutboundHttps("api.example.com", worker);
// Match a hostname glob pattern
await this.ctx.container.interceptOutboundHttps("*.example.com", worker);
// Intercept all HTTPS traffic
await this.ctx.container.interceptOutboundHttps("*", worker);interceptOutboundHttps() 默认拦截端口 443 上的 HTTPS。要使用其他端口,请将其包含在目标中,例如 "api.example.com:8443"。它匹配客户端发送的主机名。在以 enableInternet: false 启动的容器中,当注册了 * 拦截时,对裸 IP 地址的 HTTPS 请求会失败。
addr(string):要匹配的主机名或主机名 glob 模式,可带可选端口,例如api.example.com:8443。使用*拦截端口443上的所有 HTTPS 流量。binding(Fetcher):处理匹配请求的 Worker 入口点或服务绑定(binding)。
Promise<void>:拦截规则安装后解析。
- 当
addr是新的且容器没有足够的空闲条目时,等待interceptOutboundHttps()会抛出Error。更多信息请参阅拦截限制。
- Containers API:比较直接运行时控制与
Container类。 - Container 类参考:现有
Container类应用的参考。 - Containers 概述:了解 Cloudflare Containers 如何工作。
- 开始使用 Containers:部署你的第一个容器。
- SQLite 存储 API:在容器重启后持久化状态。
- Durable Objects:为 Containers 提供支持的底层平台。
- 快照:保存和恢复容器文件系统。