Como fazer deploy de um monorepo

Para fazer deploy de um monorepo, compile e empacote só o app que você vai publicar, junto com os pacotes do workspace de que ele depende. Ferramentas como pnpm deploy e turbo prune geram essa pasta autossuficiente.

Atualizado em 6 min de leitura

Objetivos de aprendizado

Depois de ler este artigo, você vai conseguir:

  • Entender por que um monorepo não sobe como um projeto de pacote único
  • Gerar uma pasta autossuficiente para um app com pnpm deploy ou turbo prune
  • Automatizar o build e o deploy de cada app a cada push
Nesta página

Um monorepo guarda vários projetos num único repositório: uma API, um site, um bot e o código compartilhado entre eles. Isso é prático enquanto você desenvolve, porque uma única mudança pode mexer numa biblioteca e em todos os apps que a usam. Na hora do deploy, porém, a coisa complica: o servidor quer um app, não a árvore inteira.

O problema central é que o seu app não vive sozinho. Ele importa packages/shared, que só existe dentro do repositório. Se você zipar apps/api sozinho, o import quebra. Se zipar tudo, você envia e instala muito mais do que o app precisa. Fazer deploy de um monorepo é recortar exatamente a fatia certa.

my-repo/apps/apipackages/sharedapps/webpnpm-lock.yamlpnpm deployout/apiapishared+ node_modulesde produçãoout/webwebshared+ node_modulesde produção
Cada app vira uma pasta própria com o código dele e os pacotes do workspace que importa, como o shared.

O que muda no deploy de um monorepo?#

Num projeto de pacote único, a pasta que você publica é o próprio repositório. Num monorepo, três coisas mudam:

  • Dependências locais. Os pacotes do workspace são ligados com o protocolo workspace:, que o pnpm resolve para a sua pasta local e nunca para o registro do npm. A pasta publicada precisa contê-los.
  • Um lockfile só, na raiz. As versões são fixadas para o repositório inteiro, não por app.
  • Vários apps, um repositório. Um push pode mudar um app, vários ou nenhum, e normalmente você quer refazer o deploy só dos afetados.

O objetivo é ter uma pasta autossuficiente por app: o código do app, as dependências do workspace já compiladas e o node_modules de produção. Se a ideia de mandar código para um servidor ainda é nova para você, leia antes o que é um deploy.

Como é o repositório?#

Este guia usa um layout comum com workspaces do pnpm:

text
my-repo/
  apps/
    api/            # o app que vamos publicar
      package.json
      src/index.ts
  packages/
    shared/         # importado pela api
      package.json
      src/index.ts
  package.json
  pnpm-workspace.yaml
  pnpm-lock.yaml

O arquivo do workspace diz ao pnpm onde ficam os pacotes:

yaml
packages:
  - apps/*
  - packages/*

E o apps/api/package.json depende do pacote compartilhado pelo protocolo do workspace:

json
{
  "name": "api",
  "private": true,
  "files": ["dist"],
  "scripts": {
    "build": "tsc -p .",
    "start": "node dist/index.js"
  },
  "dependencies": {
    "shared": "workspace:*"
  }
}

Como compilar só um app?#

Use a flag --filter do pnpm. Acrescentar ... ao nome seleciona o pacote e tudo de que ele depende, direta ou indiretamente:

bash
pnpm install --frozen-lockfile
pnpm --filter "api..." build

O pnpm compila shared primeiro e api depois, na ordem das dependências. Pacotes que a API nunca importa ficam de fora. Com o Turborepo, o equivalente é turbo run build --filter=api..., que ainda guarda os resultados em cache entre execuções.

Para isso funcionar, shared precisa de um script build próprio e de um campo main ou exports que aponte para a saída compilada. Um pacote do workspace que exporta TypeScript cru costuma funcionar em desenvolvimento, mas pode falhar num runtime Node.js comum.

Como gerar uma pasta pronta para deploy com pnpm deploy?#

O pnpm deploy copia um pacote e as dependências de produção dele para uma pasta de destino, resolvendo os pacotes do workspace no caminho:

bash
pnpm --filter api --prod deploy out/api

O --prod deixa de fora as devDependencies, que é o que você quer depois do build. Por padrão o comando copia todos os arquivos do pacote, mas respeita primeiro o campo files, depois o .npmignore e depois o .gitignore. Se dist está no .gitignore e não há campo files, a saída do build fica para trás, e é por isso que api e shared devem declarar "files": ["dist"]. A pasta de destino precisa estar vazia, senão o pnpm falha com ERR_PNPM_DEPLOY_DIR_NOT_EMPTY, a menos que você passe --force. Desde o pnpm v12.2.0 o comando não exige mais a configuração injectWorkspacePackages. Se o deploy falhar com ERR_PNPM_DEPLOY_AMBIGUOUS_PEER, ativar essa configuração ou fixar a peer dependency com overrides resolve.

Confira o resultado antes de enviar:

bash
cd out/api
node dist/index.js

Se o app sobe a partir dessa pasta sozinha, a pasta é autossuficiente.

Como podar com o Turborepo?#

O turbo prune foi feito para Docker e CI. Ele copia o app, os pacotes do workspace de que ele precisa e um lockfile correspondente:

bash
npx turbo prune api --docker

Com --docker, a saída em out/ vem dividida em duas partes, para a instalação ficar em cache separada das mudanças de código:

  • out/json/ guarda só os arquivos package.json.
  • out/full/ guarda os arquivos de código.
  • O lockfile podado fica na raiz de out/.

Um build típico copia out/json/ primeiro, instala as dependências, depois copia out/full/ e compila. Como a camada de instalação só muda quando um package.json muda, builds repetidos continuam rápidos. O mesmo padrão vale se você quiser colocar o app no Docker.

Como publicar só os apps que mudaram?#

Recompilar todos os apps a cada push desperdiça tempo. Duas soluções comuns:

  1. Filtros de caminho no CI. Um workflow do GitHub Actions pode disparar só quando arquivos em apps/api/** ou packages/shared/** mudam.
  2. Filtro pelo grafo de tarefas. O Turborepo consegue compilar só o que uma mudança afeta, por exemplo turbo run build --filter=...[HEAD^1] (pacotes alterados desde o commit anterior, mais os que dependem deles). O checkout precisa de histórico do Git suficiente para essa comparação.

Este workflow publica a API quando o código dela ou o pacote compartilhado muda. Ele compila, empacota o app com pnpm deploy e deixa o envio para o seu comando de deploy:

yaml
name: Deploy API

on:
  push:
    branches: [main]
    paths:
      - "apps/api/**"
      - "packages/shared/**"
      - "pnpm-lock.yaml"

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: pnpm/action-setup@v6 # lê a versão do "packageManager" no package.json
      - uses: actions/setup-node@v7
        with:
          node-version: lts/*
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm --filter "api..." build
      - run: pnpm --filter api --prod deploy out/api
      # Coloque aqui o seu comando de deploy, rodando a partir de out/api

Lembre-se de listar o lockfile e os pacotes compartilhados em paths. Senão, uma mudança numa biblioteca não refaz o deploy dos apps que a usam. Para o padrão completo com secrets e deploys condicionados, veja deploy automático com GitHub Actions.

Quais são os erros mais comuns?#

  • Pacote do workspace faltando. O app quebra com Cannot find module 'shared' porque o pacote não foi compilado ou não foi copiado. Compile com o filtro ... e publique com pnpm deploy.
  • Rodar a partir da raiz do repositório. O comando de início precisa rodar dentro da pasta empacotada, não na raiz, senão os caminhos relativos quebram.
  • Dependências de desenvolvimento em produção. Esquecer o --prod incha o upload e a memória que o app usa.
  • Lockfile desatualizado. Use --frozen-lockfile no CI para que uma divergência faça o build falhar em vez de instalar outras versões.
  • Segredos compartilhados. Mantenha as variáveis de ambiente por app. Um segredo colocado na raiz do repositório vaza fácil para apps que não precisam dele. Veja o que são variáveis de ambiente.

Como saber se o deploy funcionou?#

Confira três níveis: o comando de início funciona dentro da pasta empacotada, o log do build mostra shared e api compilados e o app em execução registra a linha de inicialização sem erro de módulo. Depois do primeiro deploy bem-sucedido, mude uma linha em packages/shared, faça push e confirme que o app pegou a mudança. Isso prova que o grafo de dependências está ligado certo, não só o app.

Como fazer isso na Square Cloud#

Na Square Cloud, você faz o deploy por upload de ZIP no painel, pela CLI, pelo GitHub (com a GitHub Action oficial ou por webhook) ou pela extensão do VS Code. Não existe opção de diretório raiz: seja qual for a forma, trate cada app do monorepo como uma aplicação própria e envie a pasta autossuficiente gerada acima, depois de conferir que o app sobe a partir dela. A plataforma não tem etapa de build e só instala as dependências no start, então rode o build no CI ou no comando de início.

O arquivo de configuração squarecloud.app (nome, arquivo principal, memória, versão) é opcional: o painel o gera no deploy, ou você o escreve à mão na raiz do projeto. As variáveis de ambiente ficam por aplicação no painel, na API ou na CLI, o que combina com a dica de segredos por app. A RAM do plano é um pool compartilhado entre as aplicações, com mínimo de 256 MB para uma aplicação de fundo, como um bot, e 512 MB para uma aplicação web.

Para automatizar, a GitHub Action oficial roda um comando da CLI a cada push, release ou execução manual, e só precisa de uma API key guardada como secret. Comece pela CLI da Square Cloud.

Deploy

Envie pelo GitHub, pela CLI ou em ZIP e publique em segundos

Saiba mais

Perguntas frequentes

Preciso fazer deploy do monorepo inteiro?

Não. Você publica um app por vez, e cada app só precisa do próprio código mais os pacotes do workspace que ele importa. Podar o repositório para esse subconjunto deixa o upload pequeno e a instalação rápida.

O que faz o protocolo workspace:?

Ele manda o pnpm resolver a dependência a partir do workspace local, nunca do registro do npm. Como o pacote só existe dentro do seu repositório, a pasta publicada precisa incluí-lo.

Qual a diferença entre pnpm deploy e turbo prune?

O pnpm deploy copia um pacote e as dependências de produção dele para uma pasta de destino. O turbo prune copia o pacote, os pacotes do workspace de que ele precisa e um lockfile correspondente, para você instalar e compilar em outro lugar.

Dá para publicar dois apps do mesmo repositório?

Sim. Rode as etapas de build e empacotamento uma vez por app e envie cada resultado para a própria aplicação no servidor. Filtros de caminho no CI evitam que uma mudança num app refaça o deploy do outro.

Próximo artigoPM2 e reinício automático: mantendo o processo vivoUse o PM2 para reiniciar seu processo Node.js quando ele cair, estourar a memória ou o servidor reiniciar. Veja ecosystem file, limites e uso em Docker.