通过 MCP 协议将 docker-compose.yml 直接部署到托管容器平台,AI Agent 自动完成配置转换绕过传统迁移的三种路径。
把一个 docker-compose.yml 直接甩给 Claude,它就能把应用瞬间部署上线
原来你可以把 docker-compose.yml 扔给 Claude,让它对接某个容器平台的 MCP server,直接得到一个可用的托管部署。
这事儿挺酷的,因为把本地的 compose 栈迁移到托管平台上,通常意味着三选一:用目标平台的配置语言重写部署形态、跑一个只能处理 compose 和平台共同理解的部分的转换工具、或者干脆留在 VM 上直接跑 docker compose up。
每一种都需要人工介入,因为 compose 描述的是容器拓扑结构,而现代托管平台要跑好你的应用,还需要更多东西——包括从 git 源码托管构建、自动分配公网 URL、跨服务密钥引用、以及每个容器的扩缩容和资源规格。
MCP + agent 这条路把这三种选择全部跳过了:把平台的配置 API 通过 MCP server 暴露出来,让 AI 编码 agent 按需从你的 compose 文件做翻译,你的 compose 文件永远不需要变成别的东西。
MCP(Model Context Protocol)是一个开放标准,用来给 LLM 提供结构化的、有权限的外部工具访问能力。如果一个平台提供 MCP 工具比如 create_environment、add_container、add_volume、set_secret,agent 就能读取 compose 文件、翻译拓扑结构、生成它假设是外部的密钥、把自动分配的 URL 回填到环境变量里、标记缺失的持久化配置,并在实际部署之前停下来——让人类最后按下发布。
要看实际效果,我让 Claude(通过 Claude Code)对接了 Suga 的 MCP server,让它在一个全新的环境里把一个真实 Laravel 应用的 compose.yaml 跑起来。
这个例子来自我一个副业项目,是一个叫 Suga 的托管平台(https://suga.app),既然我在做 Suga,就想看看这样能不能直接跑通。
先来看 compose.yaml,这样我们在逐步讲解翻译过程时有原始材料对照:
x-app-env: &app-env
APP_NAME: myapp
APP_ENV: production
APP_KEY: ${APP_KEY:?APP_KEY must be set (e.g. in .env)}
APP_DEBUG: "false"
APP_URL: http://localhost:8080
LOG_CHANNEL: stderr
LOG_LEVEL: info
DB_CONNECTION: pgsql
DB_HOST: postgres
DB_PORT: "5432"
DB_DATABASE: myapp
DB_USERNAME: myapp
DB_PASSWORD: myapp
REDIS_HOST: redis
REDIS_PORT: "6379"
SESSION_DRIVER: redis
CACHE_STORE: redis
QUEUE_CONNECTION: redis
x-depends: &app-depends
postgres:
condition: service_healthy
redis:
condition: service_healthy
services:
app:
image: myapp:latest
environment: *app-env
depends_on: *app-depends
ports:
- "8080:8080"
worker:
image: myapp:latest
command: ["php", "artisan", "queue:work", "--tries=3"]
environment: *app-env
depends_on: *app-depends
scheduler:
image: myapp:latest
command: ["php", "artisan", "schedule:work"]
environment: *app-env
depends_on: *app-depends
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD: myapp
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
postgres-data:
三个 PHP 服务共用一个从仓库根目录 Dockerfile 构建的镜像,通过 x-app-env 这个 YAML anchor 引入一大块共享环境变量;postgres 则用了一个命名卷挂载到数据目录,这样容器重启后数据不会丢失。这是一个相当标准的 Laravel + Redis + Postgres 拓扑,任何在本地跑过 compose 的人都能立即认出这个结构。
Suga 的模型,简要说明
要跟上后面的内容,你需要先快速了解一下 Suga 是如何建模一个环境的,因为它和 compose 服务不是一一对应关系:
一个 environment 包含一个 draft,也就是一个容器和卷的画布。
每个容器要么有一个镜像引用、要么有一个构建源(一个关联的 GitHub 仓库),再加上环境变量、私有网络、可选的公网 HTTPS。
容器之间可以相互引用对方的值,格式类似 {{<id>.variables.KEY}},其中 <id> 是 Suga 在容器创建时分配的一个短 nanoid。
MCP server 暴露了用于塑形 draft 的工具,包括 create_environment、add_container、add_volume、set_env_variable、set_secret、connect_build_repository 和 update_build_repository。
MCP 没有 deploy 工具——这是设计层面的安全护栏:agent 负责制作 draft,由人类审批并部署。
把 Claude Code 对接到 Suga
我在 Claude Code 里注册了 Suga 的 MCP server:
claude mcp add --transport http suga https://dashboard.suga.app/api/mcp
然后在 Claude Code 里运行了 /mcp 来对我的 Suga 账号做认证,之后这篇文章里提到的所有 Suga 工具都可以直接从 Claude Code 对话中调用。如果你用的是 Cursor、Cline 或其他支持 MCP 的编码 agent 而不是 Claude Code,Suga 的 MCP 连接指南里有等效的安装说明。
我给 Claude 的提示词本质上就是一句话:
把仓库根目录的 compose.yaml 部署到 Suga,作为我现有项目里的一个新环境,不要附带任何说明或提示。
这个方向足够清晰,它就能回来一个计划,然后输出一连串 MCP 调用。
它确实有访问这个 Laravel 项目的权限。
逐步翻译,每个决策详解
services: 变成五个 add_container 调用
每个 compose 服务对应一个 add_container 调用,其中服务名同时作为容器的 displayName 和 networking.private.hostname。让 hostname 和 compose 服务名保持一致很重要,因为应用的环境变量是通过服务名来引用服务的(DB_HOST=postgres、REDIS_HOST=redis),Suga 私有网络上跨容器 DNS 解析依赖容器的主机名,所以应用可以用和本地一样的名字访问 postgres 和 redis。
image: myapp:latest 变成 GitHub 构建源
compose 里的 myapp:latest 标签实际上是"跑 docker build 得到的镜像"的简写,所以在 Suga 上的自然翻译就是给每个 PHP 服务一个真正能构建出那个镜像的构建源。agent 通过 connect_build_repository 把 app、worker 和 scheduler 都关联到我 GitHub 仓库的 main 分支,三者都指向仓库根目录的同一个 Dockerfile。
这里有一个值得指出的设计选择:每个 Suga 容器有自己独立的构建源,这意味着你可以让三个容器指向同一个仓库和 Dockerfile(单仓库模式),或者指向完全不同的仓库和 Dockerfile(多仓库模式),取决于你的服务是怎么组织的。Compose 里通过共享 image 字段实现的隐式"一个镜像在多个服务间复用"模式在这种对比下反而是少数情况——如果你想在 Suga 上实现同样的共享镜像行为,正确做法是构建一次、推送到 registry,然后每个容器都引用那个 tag。
agent 这里选择了同一仓库这条路,因为它是最直接地翻译 compose 文件已经表达的内容(三服务共用一个 Dockerfile、一次构建、一个镜像),而且全程在 Suga 内完成(三次 connect_build_repository 调用),不需要把一个独立的 registry push 作为附带任务。
ports: "8080:8080" 变成公网 HTTPS
app 服务的端口发布在 draft 里变成了 networking.public.https[{port: 8080}],此时 Suga 自动在集群上分配了一个主机名并立即返回一个 publicUrl,整个过程甚至在部署发生之前就完成了。响应载荷大致如下:
https://<container-id>-<env>-<cluster>.<region>.suga.run
因为这个 URL 在创建容器的同一次 MCP 调用中就已经可用了,agent 可以在后续调用中把它回填到 app 的 APP_URL 环境变量里,在同一次传递中就完成了接线,不需要第二步来日后填充这个值。
${APP_KEY:?...} 变成一个生成的密钥
Compose 的 ${APP_KEY:?APP_KEY must be set (e.g. in .env)} 语法意思是"这个变量必须来自某个地方,如果没有设置就大声失败"。agent 识别出这是 Laravel 的 APP_KEY,用 openssl rand -base64 32 生成了一个,然后作为敏感变量存到 draft 里,之后这个值就是只写的了。
DB_PASSWORD: myapp 变成跨容器的敏感引用
compose 文件在 postgres 服务和 app 服务两边都硬编码了数据库密码,这对本地开发很方便,但在真实部署里值得改进。agent 在两个选择之间做了取舍:也在 Suga 两边硬编码同样的字面密码,或者从 postgres 的 secret 做个引用到 app 里。它选择了引用形式:
DB_PASSWORD = {{<postgres-id>.variables.POSTGRES_PASSWORD}}
这个表达式里的 <postgres-id> 占位符对应 postgres 容器的资源 ID,也就是 Suga 在容器创建时分配的一个 nanoid(类似 k3n8vpqm0xrs 这样的形状)。这种引用形式在 compose 里完全没有类比,这就是它有意思的地方:现在要轮换 postgres 密码的话,在下一次部署时所有消费容器都会自动更新,不需要 agent 记住保持两个字面量值同步。
depends_on: {condition: service_healthy} 被丢弃了
现代服务通常默认会在启动时重试依赖项,而不是在第一次连接失败时就崩溃,所以 agent 通过信任这种行为、让应用自然启动来处理 compose 里的 depends_on 门控。这样 draft 里就没有 compose 特定的启动编排逻辑了,大多数现代框架内置的重试逻辑(Laravel 也不例外)在运行时自己搞定剩下的。
postgres-data:/var/lib/postgresql/data 变成了一个 1GB 的 Suga 卷,挂载到 postgres 容器上同一个路径,这和 compose 声明的形态基本一致,只是加了个容量规格。
Agent 注意到的、compose 文件里没有的东西
compose 文件没有提到 Redis 持久化,这是一个值得指出的细微缺口:生产环境的 Redis 通常带 --appendonly yes 运行,并挂载一个卷,这样容器重启不会擦掉 Laravel 放在那里的缓存、会话存储和队列状态。agent 没有悄悄在 draft 里加上其中任何一项——它忠实地遵循了收到的 compose 文件——但确实在总结里标记了这个缺口,并指出这种形态的真实部署在发布之前大概需要配置持久化。这种观察来自于一个同时手握 compose 文件、又具备 Redis 这类服务在生产环境通常怎么运行的整体知识的 agent,因为基于规则的转换器只能看到 compose 里写明的东西。
几分钟后、几条 MCP 调用之后,draft 就填充好、可以审查了:
create_environment 调用创建了新环境
add_container 调用添加了 app、worker、scheduler、postgres 和 redis
add_volume 调用添加了 postgres-data 并挂载到 postgres 上
set_env_variable 调用把自动分配的公网 URL 回填到 app 容器上的 APP_URL
connect_build_repository 调用把 app、worker 和 scheduler 都关联到 GitHub 仓库的 main 分支
我打开了 Suga 链接,登录,审查了画布,然后点了 Deploy。
大约九十秒后,部署失败了,错误信息干净且具体,直接 surfaced 在部署记录里:
Build failed for service "worker": failed to solve: failed to read
dockerfile: open Dockerfile: no such file or directory
三个源构建服务各报了一次同样的错误——这是分支名猜测追上我了。我的 main 分支在这个仓库里恰好几乎是空的,因为所有真实代码(包括 Dockerfile)都在 develop 分支上,但 agent 没有办法知道这一点,理所当然地选了 main 作为默认值。
我用一句话把正确的分支名告诉 agent,它就做了三次 update_build_repository 调用(每个源构建容器各一个)来就地修补每个构建配置,其余的环境部分纹丝不动。
我在同一个审查链接上又点了一次 Deploy,第二次尝试从头到尾走完了。

应用启动后开始响应请求的地址,就是 Suga 在 add_container 期间分配的那个公网 URL,这意味着我之前已经填入 app 容器 APP_URL 的那个 URL 仍然是正确的——预分配流程的承诺完全兑现了。
从日志里还值得注意一件事:Suga 会并行启动全部五个容器,这是让部署最快跑起来的做法。在前三十秒里,scheduler 的 Laravel 引导程序在 postgres 还在完成 initdb 时尝试连接了两次 postgres:5432,两次都记录为预期的连接错误,随后 postgres 就绪后 scheduler 的健康检查就正常了(不管是 Laravel 内部重试还是容器重启后干净地重新启动)。两种路径都是现代应用处理下游服务预热时需要一点时间的正常方式。对于那种自己无法恢复的应用(比如一次性初始化容器或严格的迁移任务),在第二阶段加一个容器级重试循环是值得做的小优化——此时第一次部署已经在线,你可以看到所有服务之间的通信情况。
这件事的真正意义
对于应用开发者来说,这个模式真正有意义的地方在于迭代速度。docker compose up 能在几秒内让你的栈在本地跑起来,把所有依赖描述在一个文件里,让你在需要时快速重建对系统的心理模型——这就是 compose 在 2026 年仍然是大多数多服务应用默认本地开发设置的原因。摩擦点出现在你想要同一个栈跑在托管平台上而不是原始 VM 上的时候,因为你的 compose.yaml 只描述了平台需要知道的一半信息,你要么手工把缺失的另一半翻译成平台期望的配置语言,要么花真实的构建周期推送到 CI 才能迭代部署形态。
MCP 驱动的流程几乎去掉了所有这些摩擦。你的 compose 文件始终是本地应用如何运行的事实来源,每次改动它,同样的那一句话提示词就能给你一份全新的生产 draft——密钥已生成、构建源已接好、公网 URL 已分配、跨服务引用已就位。"本地改了 compose 文件"到"审查完一份生产 draft"之间的循环压缩到大约你输入提示词的时间,这开始更接近本地开发的速度,而不是普通部署流水线的速度。
如果你想让 Claude 对接你自己的 compose.yaml,安装步骤很短,而且可以免费从头到尾试一遍。你首先需要一个 Suga 账号,可以在 suga.app 上免费注册,注册完成后按照 MCP 连接指南把 Suga MCP server 接到 Claude Code(或者你偏好的任何支持 MCP 的 agent)。接好 server 之后,把 compose 文件甩给 agent 就是一句话的事,你会走过这篇文章里演示的同一个翻译流程,直到你作为人类来点 Deploy。