跳转到内容

Docker 打包

多个生成器(例如 ts#strands-agentpy#strands-agent)会生成一个 Docker 镜像,该镜像被推送到 Amazon ECR 并由 AWS 基础设施使用。本指南描述了它们遵循的模式,以便您可以将其应用于其他用例——例如,在 Amazon ECS 上运行 py#fast-api 项目,或部署容器化的 Express 服务器。

推荐的模式包含三个部分:

  1. 项目上的 bundle 目标,它生成一个包含运行时构件的自包含目录。对于 TypeScript,这是由 Rolldown 生成的经过 tree-shaking 的单文件 JavaScript 包;对于 Python,这是由 uv 生成的 requirements.txt 和已安装的依赖项。
  2. 最小化的 Dockerfile,它只是将包输出 COPY 到基础镜像中。由于打包已经处理了 tree-shaking 和依赖项安装,Dockerfile 不需要运行 npm installuv sync
  3. docker 目标,它将 Dockerfile 复制到包输出旁边(以便 Docker 构建上下文仅包含运行时所需的文件),然后运行 docker build

Docker 构建上下文被写入项目的 dist 文件夹。然后您的基础设施即代码(CDK 或 Terraform)指向该目录以将镜像推送到 ECR。

配置一个调用 Rolldown 的 bundle 目标。如果您从 ts#project 开始,请将以下内容添加到您的 project.json:

{
"targets": {
"bundle": {
"cache": true,
"executor": "nx:run-commands",
"outputs": ["{workspaceRoot}/dist/{projectRoot}/bundle"],
"options": {
"command": "rolldown -c rolldown.config.ts",
"cwd": "{projectRoot}"
},
"dependsOn": ["compile"]
}
}
}

并在项目根目录创建一个 rolldown.config.ts:

rolldown.config.ts
import { defineConfig } from 'rolldown';
export default defineConfig([
{
tsconfig: 'tsconfig.lib.json',
input: 'src/index.ts',
output: {
file: '../../dist/packages/my-project/bundle/index.js',
format: 'cjs',
inlineDynamicImports: true,
},
platform: 'node',
},
]);

运行 bundle 目标以生成 dist/packages/my-project/bundle/index.js:

Terminal window
pnpm nx bundle my-project

在项目源目录中创建一个 Dockerfile。该文件只是将包 COPY 到 Node 基础镜像中,加上 npm install 任何无法打包的 external 包。将 RUN npm install 步骤放在 COPY 之前,这样 Docker 可以缓存已安装的 node_modules 层,并且只在依赖项列表实际更改时重新运行它:

FROM public.ecr.aws/docker/library/node:lts
WORKDIR /app
# Install packages that cannot be bundled (declared as "external" in rolldown.config.ts).
# Kept above the COPY so this layer is cached and only invalidated when the install list changes.
RUN npm install @aws/aws-distro-opentelemetry-node-autoinstrumentation@0.10.0
# Copy bundled application
COPY index.js /app
EXPOSE 8080
CMD ["node", "index.js"]

添加一个 docker 目标,它:

  1. Dockerfile 复制到包输出目录(以便构建上下文仅包含包 + Dockerfile),以及
  2. 运行 docker build(对于 CDK 是可选的——见下文)。
{
"targets": {
"docker": {
"cache": true,
"executor": "nx:run-commands",
"options": {
"commands": [
"ncp packages/my-project/src/Dockerfile dist/packages/my-project/bundle/Dockerfile",
"docker build --platform linux/arm64 -t my-scope-my-project:latest dist/packages/my-project/bundle"
],
"parallel": false
},
"dependsOn": ["bundle"]
}
}
}

运行此目标会生成一个标记为 my-scope-my-project:latest 的本地镜像,从 dist/packages/my-project/bundle/ 的最小上下文构建:

Terminal window
pnpm nx docker my-project

配置一个使用 uv 为目标平台导出和安装依赖项的 bundle 目标。py#project 生成器和 py#lambda-function 生成器都会为您配置这个。目标配置如下所示:

{
"targets": {
"bundle-arm": {
"cache": true,
"executor": "nx:run-commands",
"outputs": ["{workspaceRoot}/dist/{projectRoot}/bundle-arm"],
"options": {
"commands": [
"uv export --frozen --no-dev --no-editable --project {projectRoot} --package my_project -o dist/{projectRoot}/bundle-arm/requirements.txt",
"uv pip install -n --no-deps --no-installer-metadata --no-compile-bytecode --python-platform aarch64-manylinux_2_28 --target dist/{projectRoot}/bundle-arm -r dist/{projectRoot}/bundle-arm/requirements.txt"
],
"parallel": false
},
"dependsOn": ["compile"]
}
}
}

运行 nx bundle my-project 会生成 dist/packages/my-project/bundle-arm/,其中包含项目的源代码、依赖项和 requirements.txt——镜像在运行时需要的一切。

Dockerfile 只是将包复制到 Python 基础镜像中。因为 uv 已经将所有依赖项安装到包目录中,所以您不需要在镜像内运行 pip install:

FROM public.ecr.aws/docker/library/python:3.12-slim
WORKDIR /app
# Copy bundled package (source + installed dependencies)
COPY . /app
EXPOSE 8080
ENV PYTHONPATH=/app
ENV PATH="/app/bin:${PATH}"
CMD ["python", "-m", "my_project.main"]

添加一个 docker 目标,它将 Dockerfile 复制到包输出目录,然后运行 docker build:

{
"targets": {
"docker": {
"cache": true,
"executor": "nx:run-commands",
"options": {
"commands": [
"rimraf dist/packages/my-project/docker",
"make-dir dist/packages/my-project/docker",
"ncp dist/packages/my-project/bundle-arm dist/packages/my-project/docker",
"ncp packages/my-project/src/Dockerfile dist/packages/my-project/docker/Dockerfile",
"docker build --platform linux/arm64 -t my-scope-my-project:latest dist/packages/my-project/docker"
],
"parallel": false
},
"dependsOn": ["bundle-arm"]
}
}
}

这会清除输出目录,然后将包内容和 Dockerfile 都复制到 dist/.../docker 中,该目录成为 Docker 构建上下文。

Terminal window
pnpm nx docker my-project

将生成的构建上下文目录连接到基础设施即代码对于 TypeScript 和 Python 都是相同的——只有构建上下文目录的路径不同(TypeScript 为 dist/packages/my-project/bundle,Python 为 dist/packages/my-project/docker)。

使用 CDK 的 DockerImageAsset 指向构建上下文目录。CDK 将在部署时构建镜像并将其发布到 CDK 资产 ECR 仓库:

import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets';
import { findWorkspaceRoot } from ':my-scope/common-constructs';
import * as path from 'path';
import * as url from 'url';
const image = new DockerImageAsset(this, 'MyImage', {
directory: path.join(
// Resolve from the compiled construct location to the workspace root
findWorkspaceRoot(url.fileURLToPath(new URL(import.meta.url))),
'dist/packages/my-project/bundle',
),
platform: Platform.LINUX_ARM64,
});

findWorkspaceRoot 辅助函数由 ts#infra 生成器生成并从 :my-scope/common-constructs 导出。如果您不使用共享构造,可以硬编码相对于 cdk 调用位置(通常是工作区根目录)的 dist 目录路径,并完全省略 findWorkspaceRoot 调用。

DockerImageAsset 与任何接受容器镜像的 AWS 构造一起使用,例如 aws_ecs.ContainerImage.fromDockerImageAsset(image)