Tutorial

Desarrollo

Desplegar Payload CMS en un VPS: guía completa

Payload CMS 3 se instala directamente dentro de una aplicación Next.js, lo que lo convierte en uno de los CMS headless más próximos al código. Esta guía cubre el despliegue completo en un VPS: configuración de Docker Compose con healthchecks, gestión de usuarios y API keys, hooks de colección, flujo de trabajo de migraciones y resolución de los errores más frecuentes en producción.

10 min de lectura6 pasos
Contenido· Por qué auto-alojar Payload CMS en un VPS1/10
  1. 01Por qué auto-alojar Payload CMS en un VPS
  2. 02Beneficios concretos
  3. 03Requisitos de hardware y software
  4. 04Despliegue paso a paso
  5. 05Autenticación y gestión de accesos
  6. 06Hooks de colección: reaccionar a eventos de contenido
  7. 07Actualizaciones y migraciones de esquema
  8. 08Cuando `sharp` se niega a cargar
  9. 09Resolución de errores frecuentes en producción
  10. 10Payload CMS vs Strapi: ¿qué CMS headless auto-alojar?

Por qué auto-alojar Payload CMS en un VPS

Payload CMS adopta un enfoque radicalmente code-first: el esquema de contenido se define en TypeScript en archivos de configuración versionados con Git, y desde la versión 3 Payload se instala directamente en una aplicación Next.js mediante el App Router. Esto significa que un VPS permite alojar el CMS y el front en un único proceso Node, compartiendo el mismo build y runtime. Se obtiene tipado de extremo a extremo, migraciones de esquema gestionadas en el código y ninguna dependencia de una GUI para modelar los contenidos. El auto-alojamiento es la opción natural: Payload está diseñado para desplegarse como cualquier app Next.js, y un VPS da el control total sobre la base de datos (MongoDB o PostgreSQL), las subidas y las variables de entorno, sin plataforma intermediaria.

Beneficios concretos

  • Esquema de contenido definido en TypeScript y versionado con Git: revisión de código e historial completos
  • CMS y front Next.js en un único runtime: un solo build, un solo proceso a desplegar
  • Tipado de extremo a extremo entre la config de Payload, la API y el front, sin generación manual
  • Elección de base de datos: MongoDB, PostgreSQL o SQLite mediante adaptadores oficiales
  • Migraciones de esquema pilotadas por código (payload migrate), reproducibles entre entornos
  • Local API: acceso directo a los datos sin llamadas HTTP desde el código de servidor Next.js
  • Sin coste de licencia: Payload CMS 3 es open-source (MIT) — solo pagas el VPS

Requisitos de hardware y software

Como Payload funciona sobre Next.js, el build es exigente: planifica un VPS con 2 vCPU y 4 GB de RAM para construir y ejecutar la aplicación con comodidad. Instala Node.js 22 o 24 (Payload 3.93 exige ^22.23.4 || >=24.22.1; Node 20 ya no se admite), Docker y Docker Compose. Para la base de datos, aprovisiona MongoDB 7 o PostgreSQL 16 según el adaptador elegido (@payloadcms/db-mongodb o @payloadcms/db-postgres). Apunta tu dominio a la IP del VPS. Reserva 15 GB de disco para node_modules, el build .next, las subidas y las copias de seguridad de la base de datos.

Despliegue paso a paso

  1. Elegir y configurar el adaptador de base de datos

    En payload.config.ts, declara el adaptador: mongooseAdapter para MongoDB o postgresAdapter para PostgreSQL, leyendo la URL de conexión desde DATABASE_URI. Esta elección es estructural: PostgreSQL exige migraciones, MongoDB es más flexible con el esquema.

  2. Preparar las variables de entorno

    Crea un archivo .env.production con como mínimo:

    PAYLOAD_SECRET=tu-secreto-de-32-caracteres-minimo
    DATABASE_URI=postgresql://user:pass@postgres:5432/payload
    NEXT_PUBLIC_SERVER_URL=https://example.com
    NODE_ENV=production
    PAYLOAD_CONFIG_PATH=src/payload.config.ts

    PAYLOAD_SECRET firma los tokens JWT de sesión y cifra las API keys — un valor corto o predecible debilita todo el sistema de autenticación. Genéralo con openssl rand -hex 32. NEXT_PUBLIC_SERVER_URL debe coincidir con la URL pública final: Payload lo usa para construir los enlaces de los medios y las URLs de correo.

  3. Escribir Docker Compose con healthchecks

    Un docker-compose.yml robusto incluye healthchecks para que la app solo arranque tras la disponibilidad de la base de datos:

    services:
      app:
        build: .
        ports:
          - "127.0.0.1:3000:3000"
        env_file: .env.production
        depends_on:
          postgres:
            condition: service_healthy
        volumes:
          - uploads:/app/public/media
    
      postgres:
        image: postgres:16-alpine
        environment:
          POSTGRES_USER: payload
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
          POSTGRES_DB: payload
        volumes:
          - pgdata:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U payload"]
          interval: 5s
          timeout: 5s
          retries: 10
    
    volumes:
      pgdata:
      uploads:

    Sin el healthcheck y la condición service_healthy, la app arranca antes de que PostgreSQL acepte conexiones, el pool de conexiones falla y el contenedor reinicia en bucle.

  4. Construir la imagen y ejecutar las migraciones

    Construye y ejecuta las migraciones en este orden:

    docker compose build
    docker compose up -d postgres
    docker compose run --rm app npx payload migrate
    docker compose up -d app

    Para PostgreSQL, npx payload migrate aplica los archivos de migración generados en src/migrations/. Si no existen todavía, genéralos primero con npx payload migrate:create. Para MongoDB, el esquema no requiere migraciones: migrate solo ejecuta las migraciones de datos que hayas escrito tú mismo con migrate:create.

  5. Configurar Nginx como reverse proxy

    Apunta un vhost a http://127.0.0.1:3000 (puerto por defecto de Next.js). Aumenta client_max_body_size para las subidas de medios y añade las cabeceras X-Forwarded-Proto para que Payload genere URLs HTTPS correctas.

  6. Asegurar con SSL y fijar la URL del servidor

    Ejecuta certbot --nginx -d example.com, luego verifica que serverURL: 'https://example.com' esté definido en payload.config.ts. Esta URL rige los enlaces de los medios, los correos de restablecimiento de contraseña y el correcto funcionamiento del panel de admin detrás del proxy.

Autenticación y gestión de accesos

Payload 3 gestiona los usuarios mediante la colección users definida en payload.config.ts. Activa la autenticación en la colección con auth: true — esto añade automáticamente los endpoints /api/users/login, /api/users/logout, /api/users/me y /api/users/refresh-token.

Para los roles, Payload no impone un modelo: define un campo role (tipo select) en la colección users, luego controla el acceso mediante funciones access a nivel de colección y operación:

access: {
  read: ({ req: { user } }) => user?.role === 'admin',
  create: isAdmin,
  update: isAdminOrSelf,
  delete: isAdmin,
}

Para el acceso programático (CI, integraciones de terceros), usa las API keys: activa useAPIKey: true en la configuración de autenticación de la colección. Los usuarios con acceso de admin pueden generar, regenerar o revocar una clave desde el panel de admin. Pásala en la cabecera Authorization: users API-Key tu-clave. La clave solo se muestra una vez: Payload la cifra en la base de datos con PAYLOAD_SECRET y no vuelve a mostrarla — una clave perdida se regenera, y cambiar PAYLOAD_SECRET invalida todas las claves existentes.

Hooks de colección: reaccionar a eventos de contenido

Los collection hooks de Payload 3 permiten ejecutar código antes o después de cada operación CRUD. Reemplazan elegantemente a los webhooks cuando la lógica vive en el mismo runtime:

hooks: {
  afterChange: [
    async ({ doc, operation }) => {
      if (operation === 'create') {
        await notifySubscribers(doc)
      }
    },
  ],
  beforeDelete: [
    async ({ id }) => {
      await cleanupMedia(id)
    },
  ],
}

Los hooks de colección son beforeOperation, beforeValidate, beforeChange, afterChange, beforeRead, afterRead, beforeDelete, afterDelete, afterOperation y afterError; las colecciones de autenticación añaden, entre otros, beforeLogin, afterLogin y afterLogout. Un hook afterChange es ideal para invalidar una caché CDN, enviar una notificación o sincronizar con un servicio externo tras la publicación.

Para webhooks HTTP salientes (notificar a un sitio estático, Slack o un pipeline de build), Payload no dispone de módulo dedicado pero los hooks afterChange bastan: un simple fetch al endpoint objetivo dentro del hook cubre el caso de uso.

Actualizaciones y migraciones de esquema

Payload 3 sigue el versionado semántico. Las actualizaciones de patch (3.x.y → 3.x.z) son seguras sin migraciones. Las actualizaciones minor (3.x → 3.y) pueden añadir columnas o índices y requieren ejecutar payload migrate.

Flujo de trabajo recomendado para cada actualización:

# 1. Actualizar la versión en package.json
npm install [email protected] @payloadcms/[email protected]

# 2. Generar archivo de migración si el esquema cambió
npx payload migrate:create

# 3. Probar localmente sobre una copia de la base de datos
npx payload migrate

# 4. Confirmar el archivo de migración con el bump de versión
git add src/migrations/ package.json package-lock.json
git commit -m "chore: payload 3.93.0"

# 5. En producción: detener app, aplicar migraciones, reiniciar
docker compose run --rm app npx payload migrate
docker compose up -d app

Nunca edites manualmente una migración de src/migrations/ ya aplicada: Payload registra cada migración ejecutada por su nombre en la colección payload-migrations y no la vuelve a ejecutar, así que el cambio nunca llega a producción. Escribe una nueva migración en su lugar. Para deshacer una migración: npx payload migrate:down.

Cuando `sharp` se niega a cargar

Es el obstáculo más común al desplegar en un VPS, y se presenta en tres formas distintas. La más desconcertante es Unsupported CPU: Prebuilt binaries for Linux x64 require v2 microarchitecture («linux-x64» antes de sharp 0.35): no viene de tu código ni de tus dependencias, sino del procesador de la máquina. Los binarios precompilados apuntan a un conjunto de instrucciones que las CPU más antiguas no exponen — es un criterio de selección de VPS, no un bug. La segunda, Could not load the "sharp" module using the linux-x64 runtime, casi siempre indica un node_modules construido en otra plataforma — reinstala en la máquina objetivo. La tercera es propia de Docker multi-stage: sharp instalado en la etapa de construcción pero ausente de la etapa de ejecución — instálalo explícitamente en la etapa final.

Resolución de errores frecuentes en producción

Build OOM — Killed o JavaScript heap out of memory.
Ocurre durante npm run build cuando el VPS va justo de RAM y el heap de Node.js alcanza su límite. Establece NODE_OPTIONS=--max-old-space-size=3072 en el entorno antes del build, o construye la imagen en una máquina más potente y súbela a un registry.

PostgreSQL — Error: connect ECONNREFUSED 127.0.0.1:5432.
La app intenta conectarse antes de que PostgreSQL esté listo, o la URL de conexión apunta a localhost en lugar del nombre del servicio Docker (postgres). Verifica que DATABASE_URI use el nombre del servicio y que el healthcheck esté configurado (ver paso 3).

Admin inaccesible en prod — /admin devuelve 404 o redirección en bucle.
Dos causas principales: serverURL no definido o no coincidente con la URL real, o la cabecera X-Forwarded-Proto: https ausente del proxy Nginx. Añade proxy_set_header X-Forwarded-Proto $scheme; al vhost y verifica que serverURL coincida exactamente con el origen público, sin barra diagonal final.

CORS — Access-Control-Allow-Origin ausente en la API.
Payload lee cors desde payload.config.ts. En producción, lista explícitamente los orígenes autorizados: cors: { origins: ['https://example.com'] }. cors: { origins: [serverURL] } es la configuración mínima segura.

Una migración retocada no se aplica.
Un archivo en src/migrations/ se editó después de haberse aplicado. Payload registra cada migración ejecutada por su nombre y nunca la vuelve a ejecutar: el cambio se ignora sin ningún error. Restaura el original desde Git, genera un nuevo archivo de migración si el esquema cambió, y aplícalo.

Payload CMS vs Strapi: ¿qué CMS headless auto-alojar?

Desplace la tabla

CriterioPayload CMSStrapi
Enfoque de configuraciónCode-first en TypeScript, versionado con GitGUI-first mediante Content-Type Builder
Integración frontNativa en Next.js (un solo runtime)Desacoplado, front y CMS separados
Bases soportadasMongoDB, PostgreSQL y SQLitePostgreSQL, MySQL, MariaDB, SQLite
TipadoTypeScript de extremo a extremo nativoTipos generados, integración menos ajustada
Acceso a datos en servidorLocal API sin llamadas HTTPAPI REST/GraphQL por HTTP
Modelado de contenidoEn el código, por desarrolladoresEn la interfaz, accesible a no-devs
RAM necesaria para el buildAlta (build Next.js)Alta (build React admin)
Gestión de migracionesArchivos versionados, `payload migrate`Esquema sincronizado al arrancar, migraciones en `database/migrations` (sin CLI)
API keys nativasSí, por colección, cifradas en la baseSí, mediante tokens de API
Hooks de colecciónNativos, tipados en TypeScriptLifecycle hooks (`lifecycles.js`) y middlewares del Document Service
Ideal paraEquipos dev, proyectos Next.js tipadosEquipos mixtos, modelado visual

Aprovecha la Local API de Payload en tus componentes de servidor Next.js: en lugar de llamar a tu propia API mediante fetch, importa getPayload y consulta la base de datos directamente (payload.find({ collection: 'posts' })). Eliminas un viaje HTTP y ganas tanto en latencia como en seguridad. Para los medios, conecta el plugin @payloadcms/storage-s3 para almacenar las subidas fuera del VPS, y automatiza un volcado diario de la base de datos mediante cron para garantizar copias de seguridad externas. Por último, activa el flujo de trabajo de borrador/previsualización nativo de Payload para ofrecer a los editores un paso de previsualización antes de publicar, sin ningún plugin de terceros.

Despliegue Payload CMS en un stack unificado

El VPS Cloud de ServOrbit ofrece la potencia necesaria para el build Next.js de Payload y un entorno Docker listo para MongoDB o PostgreSQL, para alojar su CMS code-first y su front en la misma máquina.