Docker para Next.js
Use para a imagem do aplicativo. Os fatos standalone do Next.js estão em nextjs-production (references/self-hosting.md); o que executa a imagem no Kubernetes é helm-nextjs; como o CI constrói e envia a imagem é github-actions.
O modelo apps/<app>/Dockerfile é a fonte da verdade; references/Dockerfile.md o contém por completo com notas. Construa a partir da raiz do repositório: docker build -f apps/<app>/Dockerfile -t <app> .
Regras
-
Cinco estágios:
base→pruner→deps→builder→runner.base: Node + pnpm, nada mais.pruner:turbo prune <app> --dockerno repositório completo, deixando apenas o aplicativo e suas dependências do workspace.deps: copiar apenasout/json/(manifestos) eout/pnpm-lock.yaml, entãopnpm fetch+pnpm install --frozen-lockfile --offline. Esta camada é reutilizada até que um manifesto ou o arquivo de lock mude, portanto uma edição de código fonte não reinstala.builder: copiarout/full/(códigos fonte),pnpm exec turbo run build --filter=<app>.runner: uma imagem Node nova com apenas a saída standalone. Por quê: mudanças no código fonte recompilam apenas os dois últimos estágios, e o runner não tem pnpm, códigos fonte ou dependências de desenvolvimento.
-
Fixar a imagem Node por versão exata e digest, correspondendo ao
.node-version.node:26.12.0-alpine3.24@sha256:6f08…em umARG NODE_IMAGE, usado tanto porbasequanto porrunner. O digest é o índice multi-arquitetura, então tanto amd64 quanto arm64 resolvem. Por que Alpine em vez de distroless: distroless Node publica apenas tags flutuantes (latest,nonroot), então a versão exata do Node não pode ser correspondida ao.node-version, e não temwgetpara o HEALTHCHECK. Alpine é pequeno, fixa exatamente, esharpfornece binários musl pré-construídos, então a otimização de imagem funciona sem pacotes extras. -
Instalar pnpm a partir do binário da release do GitHub, verificado por SHA-256. Nunca
npm i -g pnpm, nunca Corepack, nunca umcurl | shnão verificado. UmARG PNPM_VERSIONigual apackageManager, mais um checksum por arquitetura parapnpm-linux-{x64,arm64}-musl.tar.gz, selecionado porTARGETARCH. Para atualizar: alterar os três ARGs, obtendo os valores sha256 dos digests de assets da release (gh release view v<version> --repo pnpm/pnpm --json assets), e atualizarpackageManagerno mesmo commit. Fallback se o formato da release mudar: basear os estágiospruner/deps/builderemghcr.io/pnpm/pnpm:<version>(sem Node, adicione-o) em vez de inventar um novo método de instalação. -
O runner copia exatamente três coisas.
COPY --from=builder /repo/apps/<app>/.next/standalone ./ COPY --from=builder /repo/apps/<app>/.next/static ./apps/<app>/.next/static COPY --from=builder /repo/apps/<app>/public ./apps/<app>/publicStandalone não inclui
staticoupublic; sem eles o aplicativo não serve JS ou CSS. No monorepo o servidor éapps/<app>/server.js.CMD ["node", "apps/<app>/server.js"], formato exec, então o Node recebe SIGTERM diretamente. -
Executar como uid/gid 1001 com arquivos pertencentes a root. Criar
nextjs:nodejs(1001:1001), deixar os arquivos copiados como propriedade de root (o aplicativo pode ler mas não alterar seu próprio código), echownapenasapps/<app>/.next/cache.USER 1001:1001, numérico, para que o Kubernetes possa verificarrunAsNonRootsem resolver nomes. -
A imagem deve funcionar com um sistema de arquivos raiz somente leitura. As únicas gravações em tempo de execução vão para
.next/cache(otimização de imagem, cache ISR/fetch). O Kubernetes monta umemptyDirlá; localmente use--tmpfs. Teste:docker run --rm -p 3000:3000 --read-only \ --tmpfs /app/apps/<app>/.next/cache:uid=1001,gid=1001 \ --cap-drop ALL --security-opt no-new-privileges <app> -
Definir
NODE_ENV=production,HOSTNAME=0.0.0.0,PORT=3000,NEXT_TELEMETRY_DISABLED=1no runner. SemHOSTNAME=0.0.0.0o servidor standalone vincula ao hostname do container e as sondagens falham. -
HEALTHCHECKacessa/api/healthcomwgetdo busybox. É paradocker run/Compose; o Kubernetes ignora e usa as sondagens do chart no mesmo caminho. -
Argumentos de build são apenas para valores embutidos na construção:
NEXT_PUBLIC_*eNEXT_DEPLOYMENT_ID.NEXT_PUBLIC_*é compilado no JS, portanto deve ser umARG+ENVnobuilder, e é idêntico em todo ambiente em que a imagem executa. Tudo que é por ambiente é uma variável de ambiente em tempo de execução do chart. Para adicionar um:ARG NEXT_PUBLIC_APP_NAME ENV NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME}…mais
build-argsem ambos os workflows eNEXT_PUBLIC_*já está noenvdo turbo. -
Segredos nunca vão em um
ARG,ENVouCOPY. Eles ficariam no histórico da imagem. Use uma montagem de segredo BuildKit, que existe apenas para aqueleRUN:RUN --mount=type=secret,id=npm_token,env=NPM_TOKEN pnpm fetch # registro privadoO passo de build do modelo já declara o segredo opcional
next_server_actions_keydesta forma. -
Armazenar em cache o store do pnpm com uma montagem de cache BuildKit.
--mount=type=cache,id=pnpm-store,target=/pnpm/storeemfetch/install/dlx, compnpm_config_store_dir=/pnpm/store. O store sobrevive entre builds no mesmo builder sem ir para uma camada. No CI, o cache de camada étype=gha(vejagithub-actions). -
.dockerignorena raiz do repositório mantém o contexto pequeno e segredos fora. Excluinode_modules,.next,.turbo,dist,coverage,.env*,.git,.github,charts. Um contexto que incluinode_modulesdo Windows ou macOS quebra pacotes nativos na imagem Linux. -
Multi-arquitetura é
linux/amd64,linux/arm64, construído pelo CI. Localmente, construa para sua própria arquitetura. Cada estágio é neutro em relação à arquitetura, exceto o download do pnpm, que seleciona seu checksum a partir deTARGETARCH. -
Rótulos: o Dockerfile define
org.opencontainers.image.titleelicenses; o CI adicionasource,revision,version,createdatravés dedocker/metadata-action. Não codifique um rótulo de versão. -
Manter estágios livres de hacks específicos do aplicativo. Sem
apk addno runner (o aplicativo não precisa de um conjunto de ferramentas de shell), semnpm, semnode_modulescopiados do host. Se o aplicativo precisar de uma ferramenta nativa em tempo de execução, justifique-a em um comentário.
Depuração
| Sintoma | Causa |
|---|---|
extends: … doesn't resolve durante a construção | Um arquivo de nível raiz que o aplicativo precisa não estava na árvore podada: mova-o para um pacote do workspace |
ERR_PNPM_OUTDATED_LOCKFILE em deps | Lockfile não commitado após uma alteração de manifesto |
ERR_PNPM_IGNORED_BUILDS | Uma nova dependência quer um script de instalação: decida em allowBuilds |
| Aplicativo inicia, sem CSS/JS | .next/static ou public não copiado para apps/<app>/ |
| Sondagem de saúde falha, logs normais | HOSTNAME não é 0.0.0.0 |
EROFS em tempo de execução | Algo escreve fora de .next/cache; encontre, não remova --read-only |
NEXT_PUBLIC_X vazio | Não passado como argumento de build (é assado no build) |
Verificação
docker build -f apps/<app>/Dockerfile -t <app>:local --build-arg NEXT_DEPLOYMENT_ID=local .
docker run -d --name <app> -p 3000:3000 --read-only --tmpfs /app/apps/<app>/.next/cache:uid=1001,gid=1001 --cap-drop ALL <app>:local
curl -fsS localhost:3000/api/health
docker exec <app> id # uid=1001(nextjs)
docker inspect --format '{{.State.Health.Status}}' <app> # healthy após ~30s
docker rm -f <app>
Fontes
- https://nextjs.org/docs/app/api-reference/config/next-config-js/output
- https://nextjs.org/docs/app/guides/self-hosting
- https://turborepo.dev/docs/guides/tools/docker
- https://pnpm.io/docker
- https://pnpm.io/cli/fetch
- https://github.com/pnpm/pnpm/releases
- https://docs.docker.com/build/building/best-practices/
- https://docs.docker.com/build/building/secrets/
- https://docs.docker.com/build/cache/optimize/#use-cache-mounts
- https://docs.docker.com/reference/dockerfile/#healthcheck
- https://hub.docker.com/_/node
- https://github.com/opencontainers/image-spec/blob/main/annotations.md