Strapi Cloud o autoalojamiento: lo que pierde, lo que gana
Strapi Cloud es práctico para empezar, pero impone restricciones que se vuelven rápidamente bloqueantes en producción real. El plan Free limita los tipos de contenido y los usuarios administradores; los planes de pago facturan según el número de peticiones a la API y de archivos subidos. Sobre todo, sus datos residen en la infraestructura de Strapi Inc. — algo incompatible con los requisitos de localización de datos de muchos clientes (RGPD estricto, contratos sectoriales, datos sensibles). Al autoalojar en un VPS, recupera la propiedad completa de la base PostgreSQL, la elección del almacenamiento (disco local o bucket compatible con S3), la libertad de instalar cualquier plugin de la comunidad y volúmenes de contenido que ya no dependen de una tabla de precios. La contrapartida es operativa — usted gestiona las actualizaciones, las copias de seguridad y la monitorización. Esta guía le muestra exactamente cómo hacerlo.
Ventajas de autoalojar Strapi en un VPS
- Propiedad de los datos — su base PostgreSQL y sus archivos subidos permanecen en su VPS, bajo su control exclusivo
- Ningún cupo de plan — el número de entradas, de tipos de contenido y de cuentas de administrador ya no lo fija una suscripción, sino los recursos de su VPS
- Plugins de la comunidad — cualquier plugin npm se instala libremente, sin validación ni lista blanca de la plataforma
- API en su dominio — REST y GraphQL expuestos en
cms.mondomaine.com, sin intermediario ni límite de tasa impuesto - Pila unificada — Strapi y un frontend Nuxt o Next.js conviven en el mismo VPS, detrás de un único reverse proxy
- Localización de los datos — esencial para el cumplimiento del RGPD y los requisitos contractuales de sus clientes
- Coste previsible — el VPS tiene una tarifa fija mensual, independiente del volumen de contenido o del tráfico de la API
- Control de las migraciones — usted elige cuándo y cómo aplicar las actualizaciones de Strapi
Requisitos previos: hardware, software y dominio
Strapi consume más recursos que la mayoría de los frameworks porque el build del admin React consume por sí solo más de 2 GB de RAM. Un VPS de 1 GB de RAM fallará sistemáticamente por OOM durante la fase de build — no lo infradimensione. Objetivos mínimos: 2 vCPU, 4 GB de RAM. En producción estable, una vez terminado el build, la aplicación funciona con holgura con 512 MB a 1 GB. En el plano del software: Docker 24+ y Docker Compose v2 (docker compose, no docker-compose), Git para recuperar su código en el servidor, y Certbot para el TLS. En cuanto a la base de datos: se recomienda PostgreSQL 16 en producción — SQLite es funcional en desarrollo, pero demasiado limitado bajo carga concurrente e incompatible con algunos plugins. En cuanto a la red: apunte cms.mondomaine.com a la IP de su VPS antes de empezar, y prevea 20 GB de disco como mínimo (node_modules, archivos subidos, volcados de copia de seguridad).
Preparar el proyecto antes de tocar el servidor
Generar el proyecto Strapi
En su equipo, genere el proyecto con el comando oficial
npx create-strapi-app@latest mon-cms --dbclient=postgres. Elija TypeScript si su equipo lo domina. Inicialice el repositorio Git, súbalo a su forja y luego clónelo en el VPS en/srv/mon-cms. Todo lo que sigue se prepara en ese repositorio y no en el servidor: el VPS solo debe recibir código ya versionado.Conectar la configuración de base de datos a PostgreSQL
En
config/database.js— oconfig/database.tsen TypeScript —, la función exportada recibeenvy devuelve un objetoconnection. Declareclient: 'postgres'y, a continuación, seis claves leídas desde el entorno en el subobjetoconnection:host: env('DATABASE_HOST', '127.0.0.1'),port: env.int('DATABASE_PORT', 5432),database: env('DATABASE_NAME', 'strapi'),user: env('DATABASE_USERNAME', 'strapi'),password: env('DATABASE_PASSWORD', '')yssl: env.bool('DATABASE_SSL', false). El segundo argumento deenv()no es más que un valor de reserva para el desarrollo: ningún valor real debe figurar en este archivo, que acaba en Git.Generar los cinco secretos de producción
Strapi se niega a arrancar en producción sin
APP_KEYS,API_TOKEN_SALT,ADMIN_JWT_SECRET,JWT_SECRETyTRANSFER_TOKEN_SALT. Genere cada uno conopenssl rand -base64 32;APP_KEYSespera varias, separadas por comas, así que produzca al menos dos. Escríbalos en/srv/mon-cms/.enven el VPS — nunca en Git — junto conDATABASE_HOST,DATABASE_NAME,DATABASE_USERNAME,DATABASE_PASSWORD,NODE_ENV=productionyURL=https://cms.mondomaine.com. Una clave que falte se traduce en un rechazo de arranque, no en una advertencia.Escribir el Dockerfile multietapa
Dos etapas bastan para mantener ligera la imagen de producción. La etapa
builderparte denode:20-alpine, fijaWORKDIR /app, copiapackage*.json, lanzanpm ci, copia el resto del código y luego compila el admin conNODE_ENV=production npm run build. La etaparunnerparte de la misma imagen base y solo copia debuildertres cosas: la carpeta de build,node_modulesypackage.json. Termine conEXPOSE 1337y unCMDque llame anpm run start— es el script de arranque que Strapi instala, no intente lanzar un archivo de servidor a mano. Si el build muere por OOM, pongaNODE_OPTIONS=--max-old-space-size=4096antes denpm run build.
Desplegar la pila en el VPS
Describir los servicios en docker-compose.yml
El archivo declara dos servicios y un volumen con nombre. El servicio
postgresutiliza la imagenpostgres:16-alpine, recibePOSTGRES_DB,POSTGRES_USERyPOSTGRES_PASSWORDen su bloqueenvironment, monta el volumenpgdataen/var/lib/postgresql/datay pasa arestart: unless-stopped. El serviciostrapise construye desde el Dockerfile local conbuild: ., lee sus variables medianteenv_file: .env, declaradepends_on: postgres, monta./public/uploadsen/app/public/uploadsy publica su puerto conports: 127.0.0.1:1337:1337. Esta vinculación al bucle local es el punto importante: Nginx se convierte en el único punto de entrada público, el puerto 1337 nunca se expone al exterior. Un servicioredissigue siendo opcional, para la caché de sesiones o las colas de procesamiento.Construir la imagen y arrancar la pila
Lance
docker compose buildy luegodocker compose up -d. Siga el arranque condocker compose logs -f strapi. Strapi aplica sus migraciones de esquema en el primer arranque en cuantoNODE_ENV=productionestá puesto. Cuente de dos a cinco minutos: la construcción del admin React es con diferencia la etapa más lenta. Espere la líneaStrapi started successfullyantes de continuar.Poner Nginx como reverse proxy
Cree el vhost
/etc/nginx/sites-available/cms.mondomaine.com. Escucha enlisten 80sobreserver_name cms.mondomaine.com, lleva unclient_max_body_size 50M— imprescindible para las subidas de medios — y un único bloquelocation /que haceproxy_pass http://127.0.0.1:1337. Añádale las cuatro cabeceras que Strapi espera detrás de un proxy:Host,X-Real-IP,X-Forwarded-ForyX-Forwarded-Proto, cada una puesta por una directivaproxy_set_header. Active el vhost con un enlace simbólico haciasites-enabled, y luego valide y recargue connginx -t && systemctl reload nginx.Activar HTTPS y fijar la variable URL
Obtenga el certificado con
certbot --nginx -d cms.mondomaine.com: Certbot reescribe el vhost para redirigir HTTP hacia HTTPS. Compruebe después queURL=https://cms.mondomaine.comfigura correctamente en el.env, sin barra final. Es esta variable la que Strapi utiliza para construir los enlaces de los medios y las redirecciones del admin; sin ella, los archivos subidos salen con direcciones falsas y el panel de administración se comporta mal detrás del proxy. Reinicie el contenedor tras cualquier modificación del.env: las variables se leen al arrancar.Comprobar que la instancia responde de verdad
Abra
https://cms.mondomaine.com/adminy cree la primera cuenta de administrador — Strapi la pide en el primer acceso y no volverá a pedirla. Controle después tres puntos:docker compose psmuestra los dos contenedores en estadorunning, la API pública responde en/api, y el envío de un archivo de prueba desde la mediateca funciona. Un502 Bad Gatewayen esta fase indica casi siempre un contenedorstrapidetenido o todavía en construcción: releadocker compose logs strapiantes de tocar Nginx.
Sacar los medios del disco: el provider S3
Por defecto, Strapi guarda los archivos subidos en public/uploads, en el disco del VPS. El montaje ./public/uploads del docker-compose hace que sobrevivan a una recreación de contenedor, pero crecen con la mediateca, entran en cada copia de seguridad y desaparecen con el servidor. El provider oficial resuelve los tres problemas: npm install @strapi/provider-upload-aws-s3. Declárelo después en config/plugins.js, bajo la clave upload y luego config: provider: 'aws-s3' y un objeto providerOptions que lee cuatro valores del entorno — accessKeyId: env('AWS_ACCESS_KEY_ID'), secretAccessKey: env('AWS_ACCESS_SECRET'), region: env('AWS_REGION') y params: { Bucket: env('AWS_BUCKET') }. Cualquier almacenamiento compatible con S3 sirve: Scaleway Object Storage, Wasabi, Cloudflare R2. Dos trampas merecen conocerse antes del cambio. Los medios ya subidos no se migran solos — cambie el provider antes de la puesta en línea, o copie a mano el contenido de public/uploads al bucket. Y las URLs de los medios cambian de dominio: si su frontend las almacena en caché o las reescribe, verifíquelo tras el cambio.
Automatizar la copia de seguridad de la base
Crear la carpeta de destino
En el VPS,
mkdir -p /srv/backups/strapi. Mantenga esta carpeta fuera del repositorio Git y fuera de cualquier ruta montada en un contenedor: un volcado nunca debe acabar en una imagen ni en una release.Localizar el nombre real del contenedor PostgreSQL
docker compose psda el nombre exacto, de la formamon-cms-postgres-1. Deriva del nombre de la carpeta del proyecto: no lo copie de una guía, léalo en su máquina. Un script de copia de seguridad que apunta a un contenedor inexistente falla en silencio una vez colocado en el cron.Escribir el comando de volcado
Basta con una sola línea:
docker exec mon-cms-postgres-1 pg_dump -U strapi strapi | gzip > /srv/backups/strapi/strapi-$(date +%Y%m%d).sql.gz. Elpg_dumpse ejecuta en el contenedor; la compresión y la escritura se hacen en el host. Láncelo una primera vez a mano y compruebe el tamaño del archivo producido: un volcado de unos pocos bytes indica un error de autenticación tragado por la tubería.Purgar los volcados demasiado antiguos
Añada a continuación
find /srv/backups/strapi -name '*.sql.gz' -mtime +7 -delete. Sin esta línea, el disco se llena en unas semanas: es la avería más banal de una copia de seguridad diaria — la base cae porque la copia ha saturado el volumen.Planificar y luego sacar los volcados del servidor
Ponga los dos comandos en un script
/etc/cron.daily/strapi-backup, con#!/bin/bashen la primera línea y unchmod +xpara hacerlo ejecutable. Añádale unrsynchacia un almacenamiento externo, o un envío al mismo bucket S3 que los medios. Una copia de seguridad que se queda en la máquina que respalda no protege de nada; pruebe una restauración completa al menos una vez antes de depender de ella.
Actualizar Strapi sin romper las migraciones
Una actualización se pilota desde el repositorio, no en el servidor: cambie la versión en package.json, suba los cambios y luego, en el VPS, encadene git pull y docker compose build && docker compose up -d. Strapi detecta y aplica las migraciones de esquema al arrancar mientras NODE_ENV=production esté puesto; confírmelo con docker compose logs strapi | grep -i migrat. Tres precauciones merecen el rodeo. Tome un pg_dump justo antes de la subida de versión, y no solo el de la noche anterior — una migración fallida se repara con una restauración, nunca con un segundo intento. Lea las notas de versión de los plugins que tenga instalados: una subida mayor de Strapi rompe más a menudo un plugin de la comunidad que el propio núcleo. Por último, si una migración falla, no relance la pila en bucle: cada reinicio repite la misma migración sobre una base ya modificada a medias. Detenga los contenedores, lea el registro completo, restaure el volcado si es necesario y luego corrija.
Solución de problemas: los errores más frecuentes
OOM durante el build (Killed o JavaScript heap out of memory): el build del admin React supera la memoria disponible. Dos remedios — añadir NODE_OPTIONS=--max-old-space-size=4096 en la etapa builder del Dockerfile, o aumentar temporalmente el swap del VPS con fallocate -l 2G /swapfile, chmod 600 /swapfile, mkswap /swapfile y luego swapon /swapfile. Si el VPS sigue bloqueado, construya la imagen en una máquina más potente y súbala a un registro.
Cannot find module @strapi/plugin-*: nunca comparta la carpeta node_modules entre un entorno de desarrollo local y la imagen de producción mediante un volumen Docker. El node_modules local está compilado para su sistema operativo, no para el Alpine Linux del contenedor. Elimine todo volumen node_modules del docker-compose y deje que el npm ci del Dockerfile se encargue.
URL mismatch en el admin, o medios con ruta relativa: la variable URL del .env debe corresponder exactamente a la dirección pública HTTPS de su Strapi, sin barra final. Cualquier divergencia rompe los enlaces de los medios subidos y provoca errores CORS en el panel de administración.
413 Request Entity Too Large: la directiva client_max_body_size de Nginx es demasiado baja. Súbala a 50M como mínimo, o a 100M si sube vídeos, y luego recargue Nginx.
password authentication failed for user al arrancar: la contraseña del .env cambió después de la creación del volumen pgdata. PostgreSQL solo relee su contraseña al inicializar el volumen; alinee el .env con la contraseña existente, o parta de un volumen nuevo tras haber respaldado la base.
Tres reglas no negociables para una instancia Strapi en producción: S3 para los archivos subidos (@strapi/provider-upload-aws-s3) — sus medios sobreviven a cualquier recreación de contenedor y no engordan el disco del VPS; pg_dump diario automatizado copiado fuera del servidor — una copia de seguridad solo local desaparece con el VPS; NODE_ENV=production imperativamente — el modo desarrollo recompila el admin en caliente, expone el Content-Type Builder y desactiva las optimizaciones de caché, algo que hay que proscribir en producción.
Strapi y un frontend Nuxt o Next.js en el mismo VPS
Es perfectamente posible hacer convivir Strapi y un frontend en el mismo VPS, siempre que haya suficiente RAM — cuente 8 GB para los dos, ya que ambos builds pueden dispararse al mismo tiempo. La arquitectura más simple utiliza Nginx como despachador: las peticiones hacia cms.mondomaine.com se proxian hacia el puerto 1337 (Strapi), y las dirigidas a mondomaine.com hacia el puerto 3000 (Nuxt o Next.js). Strapi expone su API REST en /api — el frontend la consume directamente en la red Docker interna, sin pasar por Nginx, lo que reduce la latencia. Si su frontend genera páginas estáticas (nuxt generate, o next build en exportación estática), Nginx puede servir los archivos desde el disco y proxiar solo las rutas dinámicas. Esta arquitectura monolítica es ideal para un proyecto de tamaño medio — un solo servidor, un solo certificado TLS, un solo punto de supervisión. Si prefiere separar el frontend, nuestras guías de despliegue de Nuxt y Node.js retoman el mismo método en un segundo VPS Cloud dimensionado para el build.