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
Elegir y configurar el adaptador de base de datos
En
payload.config.ts, declara el adaptador:mongooseAdapterpara MongoDB opostgresAdapterpara PostgreSQL, leyendo la URL de conexión desdeDATABASE_URI. Esta elección es estructural: PostgreSQL exige migraciones, MongoDB es más flexible con el esquema.Preparar las variables de entorno
Crea un archivo
.env.productioncon 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.tsPAYLOAD_SECRETcifra los tokens JWT y las cookies de sesión — un valor corto o predecible debilita todo el sistema de autenticación. Genéralo conopenssl rand -hex 32.NEXT_PUBLIC_SERVER_URLdebe coincidir con la URL pública final: Payload lo usa para construir los enlaces de los medios y las URLs de correo.Escribir Docker Compose con healthchecks
Un
docker-compose.ymlrobusto 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.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 appPara PostgreSQL,
npx payload migrateaplica los archivos de migración generados ensrc/migrations/. Si no existen todavía, genéralos primero connpx payload migrate:create. Para MongoDB, el esquema se aplica automáticamente en el primer arranque.Configurar Nginx como reverse proxy
Apunta un vhost a
http://127.0.0.1:3000(puerto por defecto de Next.js). Aumentaclient_max_body_sizepara las subidas de medios y añade las cabecerasX-Forwarded-Protopara que Payload genere URLs HTTPS correctas.Asegurar con SSL y fijar la URL del servidor
Ejecuta
certbot --nginx -d example.com, luego verifica queserverURL: 'https://example.com'esté definido enpayload.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 appNunca 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
| Criterio | Payload CMS | Strapi |
|---|---|---|
| Enfoque de configuración | Code-first en TypeScript, versionado con Git | GUI-first mediante Content-Type Builder |
| Integración front | Nativa en Next.js (un solo runtime) | Desacoplado, front y CMS separados |
| Bases soportadas | MongoDB y PostgreSQL | PostgreSQL, MySQL, SQLite |
| Tipado | TypeScript de extremo a extremo nativo | Tipos generados, integración menos ajustada |
| Acceso a datos en servidor | Local API sin llamadas HTTP | API REST/GraphQL por HTTP |
| Modelado de contenido | En el código, por desarrolladores | En la interfaz, accesible a no-devs |
| RAM necesaria para el build | Alta (build Next.js) | Alta (build React admin) |
| Gestión de migraciones | Archivos versionados, `payload migrate` | Migraciones automáticas via Strapi CLI |
| API keys nativas | Sí, por colección con hash | Sí, mediante tokens de API |
| Hooks de colección | Nativos, tipados en TypeScript | Lifecycle hooks via module middleware |
| Ideal para | Equipos dev, proyectos Next.js tipados | Equipos 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.