Pular para o conteúdo

Docker Bundling

Vários geradores (como ts#agent e py#agent) produzem uma imagem Docker que é enviada para o Amazon ECR e consumida pela infraestrutura AWS. Este guia descreve o padrão que eles seguem para que você possa aplicá-lo a outros casos de uso — por exemplo, executar um projeto FastAPI no Amazon ECS, ou implantar um servidor Express containerizado.

Diagram

O padrão recomendado tem três partes:

  1. Um target bundle no seu projeto que produz um diretório autocontido de artefatos de runtime. Para TypeScript, isso é um bundle JavaScript de arquivo único com tree-shaking produzido pelo Rolldown; para Python, isso é um requirements.txt e dependências instaladas produzidas pelo uv.
  2. Um Dockerfile mínimo que simplesmente faz COPY da saída do bundle para uma imagem base. Como o bundling já tratou do tree-shaking e da instalação de dependências, o Dockerfile não precisa executar npm install ou uv sync.
  3. Um target docker que copia o Dockerfile junto com a saída do bundle (para que o contexto de build do Docker contenha apenas os arquivos necessários em runtime), e então executa docker build.

O contexto de build do Docker é escrito na pasta dist do seu projeto. Seu código de infraestrutura (CDK ou Terraform) então aponta para esse diretório para enviar a imagem para o ECR.

Configure um target bundle que invoca o Rolldown. Se você está começando de um ts#project, adicione o seguinte ao seu project.json:

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

E um rolldown.config.ts na raiz do seu projeto:

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',
codeSplitting: false,
},
platform: 'node',
},
]);

Execute o target bundle para produzir dist/packages/my-project/bundle/index.js:

Terminal window
pnpm nx bundle my-project

Crie um Dockerfile no diretório de origem do seu projeto. O arquivo não faz nada além de aplicar as atualizações de segurança da imagem base, fazer COPY do bundle para uma imagem base Node, mais npm install de quaisquer pacotes external que não puderam ser empacotados. Coloque a etapa RUN npm install antes do COPY, para que o Docker possa fazer cache da camada node_modules instalada e só executá-la novamente quando a lista de dependências realmente mudar:

FROM public.ecr.aws/docker/library/node:lts
# Apply the distribution's security updates: base image tags lag behind the
# security suite, so their OS packages carry vulnerabilities that already have a
# published fix.
RUN apt-get update && \
apt-get upgrade -y --no-install-recommends && \
rm -rf /var/lib/apt/lists/*
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"]

Adicione um target docker que:

  1. Copia o Dockerfile para o diretório de saída do bundle (para que o contexto de build contenha apenas o bundle + Dockerfile), e
  2. Executa docker build (opcional para CDK — veja abaixo).
{
"targets": {
"docker": {
"cache": false,
"executor": "nx:run-commands",
"options": {
"commands": [
"shx cp 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"]
}
}
}

Executar este target produz uma imagem local com tag my-scope-my-project:latest, construída a partir do contexto mínimo em dist/packages/my-project/bundle/:

Terminal window
pnpm nx docker my-project

Configure um target bundle que usa uv para exportar e instalar dependências para sua plataforma alvo. O gerador py#project e o gerador py#lambda-function configuram isso para você. A configuração do target se parece com:

{
"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"]
}
}
}

Executar nx bundle my-project produz dist/packages/my-project/bundle-arm/ contendo o código-fonte do seu projeto, suas dependências e um requirements.txt — tudo que a imagem precisa em runtime.

O Dockerfile aplica as atualizações de segurança da imagem base, e então copia o bundle para uma imagem base Python. Como o uv já instalou todas as dependências no diretório do bundle, você não precisa executar pip install dentro da imagem:

FROM public.ecr.aws/docker/library/python:3.14-slim
# Apply the distribution's security updates: base image tags lag behind the
# security suite, so their OS packages carry vulnerabilities that already have a
# published fix.
RUN apt-get update && \
apt-get upgrade -y --no-install-recommends && \
rm -rf /var/lib/apt/lists/*
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"]

Adicione um target docker que copia o Dockerfile para o diretório de saída do bundle, e então executa docker build:

{
"targets": {
"docker": {
"cache": false,
"executor": "nx:run-commands",
"options": {
"commands": [
"shx rm -rf dist/packages/my-project/docker",
"shx mkdir -p dist/packages/my-project/docker",
"shx cp -R dist/packages/my-project/bundle-arm/. dist/packages/my-project/docker",
"shx cp 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"]
}
}
}

Isso limpa o diretório de saída, e então copia tanto o conteúdo do bundle quanto o Dockerfile para dist/.../docker, que se torna o contexto de build do Docker.

Terminal window
pnpm nx docker my-project

É uma boa prática escanear suas imagens em busca de vulnerabilidades conhecidas. Os geradores que seguem este padrão adicionam um target trivy que escaneia a imagem construída com Trivy, executando a partir da imagem Trivy hospedada no ECR, e sai com código diferente de zero em descobertas HIGH ou CRITICAL.

O target copia um .trivyignore da raiz do seu projeto para o diretório de escaneamento e o passa para o Trivy com --ignorefile, então crie esse arquivo primeiro — um vazio está bom, e é onde você suprime descobertas mais tarde:

Janela do terminal
touch packages/my-project/.trivyignore

Então adicione um target trivy que depende (dependsOn) do seu target docker. Ele salva a imagem construída em um tarball e a escaneia através de um bind mount relativo ao workspace, para que o mesmo comando funcione tanto com docker quanto com finch:

{
"targets": {
"trivy": {
"cache": false,
"outputs": ["{workspaceRoot}/dist/{projectRoot}/trivy"],
"executor": "nx:run-commands",
"options": {
"commands": [
"shx rm -rf dist/packages/my-project/trivy",
"shx mkdir -p dist/packages/my-project/trivy",
"shx cp packages/my-project/.trivyignore dist/packages/my-project/trivy/.trivyignore",
"docker save -o dist/packages/my-project/trivy/image.tar my-scope-my-project:latest",
"docker run --rm -v \"./dist/packages/my-project/trivy\":/scan public.ecr.aws/aquasecurity/trivy:0.72.0 image --input /scan/image.tar --ignorefile /scan/.trivyignore --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 --no-progress -q"
],
"parallel": false
},
"dependsOn": ["docker"]
}
}
}

Os targets de escaneamento por imagem de cada projeto são agregados sob um target trivy em todo o workspace, que o script raiz trivy fornecido executa (nx run-many --target trivy).

Terminal window
pnpm trivy

Conectar o diretório de contexto de build resultante ao código de infraestrutura é o mesmo para TypeScript e Python — apenas o caminho para o diretório de contexto de build difere (dist/packages/my-project/bundle para TypeScript, dist/packages/my-project/docker para Python).

Use o DockerImageAsset do CDK apontando para o diretório de contexto de build. O CDK construirá a imagem e a publicará no repositório ECR de assets do CDK no momento do deploy:

Diagram
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,
});

O helper findWorkspaceRoot é gerado pelo gerador ts#infra e exportado de @my-scope/common-constructs. Se você não está usando constructs compartilhados, você pode codificar o caminho para o diretório dist relativo a onde cdk é invocado — tipicamente a raiz do workspace — e omitir completamente a chamada findWorkspaceRoot.

Use o DockerImageAsset com qualquer construct AWS que aceite uma imagem de contêiner, por exemplo aws_ecs.ContainerImage.fromDockerImageAsset(image).