Tutorial

Desplegar Payload CMS en un VPS: guía completa

Desarrollo10 min de lectura6 pasos

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.

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 o PostgreSQL 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 20 LTS, 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 cifra los tokens JWT y las cookies de sesión — 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 se aplica automáticamente en el primer arranque.

  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. Cada usuario puede generar una clave desde el panel de admin. Pásala en la cabecera Authorization: users API-Key tu-clave. Las API keys se hashean en la base de datos — una clave perdida no se recupera, se regenera.

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 disponibles son beforeOperation, beforeValidate, beforeChange, afterChange, beforeRead, afterRead, beforeDelete y afterDelete. 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.88.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 los archivos generados en src/migrations/: Payload los verifica por hash. Un archivo modificado hace fallar migrate con Error: Migration file has been modified since it was created. 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: 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 la RAM disponible es inferior a 3 GB. Node.js está limitado a ~1.8 GB por defecto. 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.

Error: Migration file has been modified since it was created.
Un archivo en src/migrations/ fue editado manualmente. Restaura el original desde Git, genera un nuevo archivo de migración si el esquema cambió, y vuelve a aplicar en orden.

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 y PostgreSQLPostgreSQL, MySQL, 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`Migraciones automáticas via Strapi CLI
API keys nativasSí, por colección con hashSí, mediante tokens de API
Hooks de colecciónNativos, tipados en TypeScriptLifecycle hooks via module middleware
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.

¿Necesita ayuda?

Consulte nuestro centro de ayuda y nuestra FAQ, o contacte con nuestro equipo: llamada, WhatsApp o correo electrónico. Soporte en francés, inglés y árabe.

Escribir por WhatsAppse abre en una pestaña nueva