Cómo desplegar un monorepo

Para desplegar un monorepo, compila y empaqueta solo la app que vas a publicar, junto con los paquetes del workspace de los que depende. Herramientas como pnpm deploy o turbo prune generan esa carpeta autosuficiente.

Actualizado el 6 min de lectura

Objetivos de aprendizaje

Después de leer este artículo podrás:

  • Entender por qué un monorepo no se despliega como un proyecto de un solo paquete
  • Generar una carpeta autosuficiente para una app con pnpm deploy o turbo prune
  • Automatizar el build y el deploy de cada app en cada push
En esta página

Un monorepo guarda varios proyectos en un solo repositorio: una API, un sitio web, un bot y el código compartido entre ellos. Es cómodo mientras desarrollas, porque un solo cambio puede tocar una biblioteca y todas las apps que la usan. Al desplegar, en cambio, se complica: un servidor quiere una app, no el árbol completo.

El problema de fondo es que tu app no se sostiene sola. Importa packages/shared, que solo existe dentro del repositorio. Si comprimes apps/api por separado, el import se rompe. Si comprimes todo, subes e instalas mucho más de lo que la app necesita. Desplegar un monorepo consiste en recortar exactamente la porción correcta.

my-repo/apps/apipackages/sharedapps/webpnpm-lock.yamlpnpm deployout/apiapishared+ node_modulesde producciónout/webwebshared+ node_modulesde producción
Cada app se convierte en su propia carpeta con su código y los paquetes del workspace que importa, como shared.

¿Qué cambia al desplegar un monorepo?#

En un proyecto de un solo paquete, la carpeta que despliegas es el repositorio. En un monorepo cambian tres cosas:

  • Dependencias locales. Los paquetes del workspace se enlazan con el protocolo workspace:, que pnpm resuelve a tu carpeta local y nunca al registro de npm. La carpeta desplegada tiene que contenerlos.
  • Un solo lockfile en la raíz. Las versiones se fijan para todo el repositorio, no por app.
  • Muchas apps, un repositorio. Un push puede cambiar una app, varias o ninguna, y normalmente quieres volver a desplegar solo las afectadas.

El objetivo es una carpeta autosuficiente por app: el código de la app, sus dependencias del workspace ya compiladas y su node_modules de producción. Si la idea de enviar código a un servidor es nueva para ti, lee primero qué es un deploy.

¿Cómo es el repositorio?#

Esta guía usa una estructura común con workspaces de pnpm:

text
my-repo/
  apps/
    api/            # la app que vamos a desplegar
      package.json
      src/index.ts
  packages/
    shared/         # importado por api
      package.json
      src/index.ts
  package.json
  pnpm-workspace.yaml
  pnpm-lock.yaml

El archivo del workspace le dice a pnpm dónde están los paquetes:

yaml
packages:
  - apps/*
  - packages/*

Y apps/api/package.json depende del paquete compartido mediante el protocolo del workspace:

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

¿Cómo compilas solo una app?#

Usa la opción --filter de pnpm. Añadir ... al nombre selecciona el paquete y todo aquello de lo que depende, directa o indirectamente:

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

pnpm compila shared primero y api después, en orden de dependencias. Los paquetes que la API nunca importa se omiten. Con Turborepo, el equivalente es turbo run build --filter=api..., que además guarda los resultados en caché entre ejecuciones.

Para que funcione, shared necesita su propio script build y un campo main o exports que apunte a su salida compilada. Un paquete del workspace que exporta TypeScript sin compilar suele funcionar en desarrollo, pero puede fallar en un runtime de Node.js normal.

¿Cómo generas una carpeta desplegable con pnpm deploy?#

pnpm deploy copia un paquete y sus dependencias de producción a un directorio de destino, resolviendo por el camino los paquetes del workspace:

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

--prod omite las devDependencies, que es lo que quieres después del build. Por defecto el comando copia todos los archivos del paquete, pero respeta primero el campo files, luego .npmignore y luego .gitignore. Si dist está en .gitignore y no hay campo files, la salida del build se queda fuera, y por eso tanto api como shared deben declarar "files": ["dist"]. La carpeta de destino tiene que estar vacía; si no, pnpm falla con ERR_PNPM_DEPLOY_DIR_NOT_EMPTY, salvo que pases --force. Desde pnpm v12.2.0 el comando ya no exige el ajuste injectWorkspacePackages. Si el deploy falla con ERR_PNPM_DEPLOY_AMBIGUOUS_PEER, activar ese ajuste o fijar la peer dependency con overrides lo soluciona.

Comprueba el resultado antes de enviarlo:

bash
cd out/api
node dist/index.js

Si la app arranca solo desde esa carpeta, la carpeta es autosuficiente.

¿Cómo podas con Turborepo?#

turbo prune está pensado para Docker y CI. Copia la app, los paquetes del workspace que necesita y un lockfile correspondiente:

bash
npx turbo prune api --docker

Con --docker, la salida en out/ se divide en dos para que la instalación quede en caché aparte de los cambios de código:

  • out/json/ contiene solo los archivos package.json.
  • out/full/ contiene los archivos de código.
  • El lockfile podado queda en la raíz de out/.

Un build típico copia primero out/json/, instala las dependencias, luego copia out/full/ y compila. Como la capa de instalación solo cambia cuando cambia un package.json, los builds repetidos siguen siendo rápidos. El mismo patrón sirve si quieres dockerizar la app.

¿Cómo despliegas solo las apps que cambiaron?#

Recompilar todas las apps en cada push es perder tiempo. Dos soluciones habituales:

  1. Filtros de ruta en CI. Un workflow de GitHub Actions puede dispararse solo cuando cambian archivos en apps/api/** o packages/shared/**.
  2. Filtrado por el grafo de tareas. Turborepo puede compilar solo lo que afecta un cambio, por ejemplo turbo run build --filter=...[HEAD^1] (paquetes cambiados desde el commit anterior, más los que dependen de ellos). El checkout necesita suficiente historial de Git para esa comparación.

Este workflow despliega la API cuando cambia su código o su paquete compartido. Compila, empaqueta la app con pnpm deploy y deja la subida a tu 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 # lee la versión de "packageManager" en 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
      # Añade aquí tu comando de despliegue, ejecutado desde out/api

Recuerda incluir el lockfile y los paquetes compartidos en paths. Si no, un cambio en una biblioteca no volverá a desplegar las apps que la usan. Para el patrón completo con secretos y deploys condicionados, consulta deploys automáticos con GitHub Actions.

¿Cuáles son los errores más comunes?#

  • Falta un paquete del workspace. La app falla con Cannot find module 'shared' porque el paquete no se compiló o no se copió. Compila con el filtro ... y despliega con pnpm deploy.
  • Ejecutar desde la raíz del repositorio. El comando de inicio tiene que ejecutarse desde la carpeta empaquetada, no desde la raíz, o las rutas relativas se rompen.
  • Dependencias de desarrollo en producción. Olvidar --prod infla la subida y la memoria que usa la app.
  • Lockfile desactualizado. Usa --frozen-lockfile en CI para que una discrepancia haga fallar el build en lugar de instalar otras versiones.
  • Secretos compartidos. Mantén las variables de entorno por app. Un secreto puesto en la raíz del repositorio se filtra con facilidad a apps que no lo necesitan. Consulta qué son las variables de entorno.

¿Cómo sabes que el deploy funcionó?#

Comprueba tres niveles: el comando de inicio funciona dentro de la carpeta empaquetada, el log del build muestra shared y api compilados y la app en ejecución registra su línea de arranque sin errores de módulo. Tras el primer deploy correcto, cambia una línea en packages/shared, haz push y confirma que la app recoge el cambio. Eso demuestra que el grafo de dependencias está bien conectado, no solo la app.

Cómo hacerlo en Square Cloud#

En Square Cloud, despliegas subiendo un ZIP en el panel, con la CLI, desde GitHub (con la GitHub Action oficial o un webhook) o con la extensión de VS Code. No hay opción de directorio raíz: elijas la forma que elijas, trata cada app de tu monorepo como una aplicación propia y envía la carpeta autosuficiente que generaste arriba, después de comprobar que la app arranca desde ella. La plataforma no tiene etapa de build y solo instala las dependencias al iniciar, así que ejecuta el build en CI o en el comando de inicio.

El archivo de configuración squarecloud.app (nombre, archivo principal, memoria, versión) es opcional: el panel lo genera al desplegar, o puedes escribirlo a mano en la raíz del proyecto. Las variables de entorno se definen por aplicación en el panel, la API o la CLI, lo que encaja con el consejo de secretos por app. La RAM de tu plan es un pool compartido entre aplicaciones, con un mínimo de 256 MB para una aplicación en segundo plano, como un bot, y 512 MB para una aplicación web.

Para automatizarlo, la GitHub Action oficial ejecuta un comando de la CLI en cada push, release o ejecución manual, y solo necesita una API key guardada como secreto. Empieza por la CLI de Square Cloud.

Deploy

Sube desde GitHub, la CLI o un ZIP y publica en segundos

Más información

Preguntas frecuentes

¿Tengo que desplegar todo el monorepo?

No. Despliegas una app a la vez, y cada app solo necesita su propio código más los paquetes del workspace que importa. Podar el repositorio a ese subconjunto mantiene la subida pequeña y la instalación rápida.

¿Qué hace el protocolo workspace:?

Le indica a pnpm que resuelva la dependencia desde el workspace local y nunca desde el registro de npm. Como el paquete solo existe dentro de tu repositorio, la carpeta desplegada tiene que incluirlo.

¿Qué diferencia hay entre pnpm deploy y turbo prune?

pnpm deploy copia un paquete y sus dependencias de producción a una carpeta de destino. turbo prune copia el paquete, los paquetes del workspace que necesita y un lockfile correspondiente, para que instales y compiles en otro lugar.

¿Puedo desplegar dos apps del mismo repositorio?

Sí. Ejecuta los pasos de build y empaquetado una vez por app y envía cada resultado a su propia aplicación en el servidor. Los filtros de ruta en tu CI evitan que un cambio en una app vuelva a desplegar la otra.

Siguiente artículoPM2 y reinicio automático: cómo mantener vivo el procesoUsa PM2 para reiniciar tu proceso Node.js cuando se cae, se queda sin memoria o el servidor se reinicia. Incluye ecosystem file, límites y uso en Docker.

Más en Deploy

Ver los 33 artículos