Ir al contenido

Instalación

Hay dos formas de levantar el sistema, y las dos acaban en el mismo sitio: el asistente del navegador. Esa es la idea de fondo — arrancar y configurar son cosas distintas. Arrancar puede hacerse con Docker, con npm o con un botón de la nube; configurar se hace siempre igual, y se hace sin terminal.


Terminal window
docker compose -f docker-compose.full.yml up -d

Y ya. Levanta cuatro piezas: Postgres, el backend, el panel y la tienda. (Redis ya no hace falta: la cola de avisos, los flujos y las tareas viven en Postgres.)

La primera vez tarda unos minutos, porque construye las tres imágenes. Después arranca en unos diez segundos: el contenedor del backend crea el esquema si la base está vacía —o aplica lo que falte si no—, prepara la tienda y arranca. Es lo mismo que hace preparar a mano (más abajo), y se repite sin miedo en cada arranque. Al abrir el panel te recibe el asistente: cuatro comprobaciones, el nombre de la tienda, tu cuenta y, si quieres, un catálogo de ejemplo para ver el circuito completo funcionando.

Para seguir el arranque:

Terminal window
docker compose -f docker-compose.full.yml logs -f backend

Copia .env.docker.example a .env y toca dos cosas:

  1. Los dominios (TIENDA_URL, PANEL_URL, BACKEND_URL). De ahí salen los CORS y los enlaces de los correos.
  2. La contraseña de Postgres. La de ejemplo es comercio; cámbiala.

Los secretos de sesión puedes dejarlos vacíos: el contenedor genera unos la primera vez y los guarda en su volumen. Ponlos a mano solo si vas a levantar varias réplicas del backend — cada una generaría los suyos y las sesiones dejarían de valer al saltar de una a otra.

Cambias el .env, reinicias, y ya:

Terminal window
docker compose -f docker-compose.full.yml up -d

Nada de esto obliga a reconstruir imágenes, y no es lo que sale por defecto con Next: las variables que llegan al navegador se incrustan al compilar, de modo que corregir un NIF exigiría reconstruir el panel entero. Aquí el servidor lee la configuración al arrancar y se la entrega al navegador con cada página (apps/admin/src/lib/config.ts), que es el patrón que documenta Next para “construir una vez y desplegar donde sea”. Lo mismo vale para el nombre de la tienda, el color de marca y los datos fiscales de las facturas.

Lo único que sigue atado al momento de construir es el tema del escaparate.

En cuanto le pongas un dominio con HTTPS vas a tener delante un proxy inverso —Apache, nginx, Cloudflare, Traefik—. Cuando lo hagas, pon esto en el .env:

TRUST_PROXY=1

Si el backend se ve directamente desde fuera, déjalo vacío.

Parece un detalle y no lo es. El formulario de contacto es la única puerta de la tienda por la que se puede escribir sin haber entrado con una cuenta, así que tiene un límite: cinco mensajes cada diez minutos desde la misma IP. La pregunta es de dónde sale esa IP.

Con un proxy delante, todas las visitas le llegan al backend desde la misma dirección: la del proxy. Si no le dices que está ahí, tu tienda entera comparte un solo contador y la sexta persona que escriba en diez minutos se lleva un bloqueo sin haber hecho nada. Un límite mal puesto es peor que no tener ninguno.

Y al revés también duele. La IP real del visitante viaja en una cabecera (X-Forwarded-For) que el propio visitante puede escribir. Si no hay proxy que la reemplace, cualquiera se inventa una distinta en cada petición y el límite deja de existir: comprobado aquí, con el límite en cinco, ocho peticiones rotando esa cabecera pasaban las ocho. Por eso hay que decirlo, y por eso lo seguro es lo que sale por defecto.

Con Docker no tienes que hacer nada más: la variable ya está cableada al contenedor del backend. Sin Docker, ponla en el entorno del proceso.

TEMA=<carpeta de themes/> en el .env, y reconstruir el escaparate:

Terminal window
docker compose -f docker-compose.full.yml build tienda
docker compose -f docker-compose.full.yml up -d

El panel lee el catálogo de temas del disco (./themes va montado), así que ahí no hace falta reconstruir nada.

La otra variable que se resuelve al construir, por el mismo motivo que el tema: Next incrusta la lista en el paquete.

IMAGE_HOSTS=cdn.mitienda.com,*.midominio.com

Son los servidores de los que el escaparate acepta imágenes. Si las fotos las sirve el propio backend desde su /static, pon ahí su dominio.

Si lo dejas vacío no se rompe nada, pero se apaga el optimizador: las fotos se ven —las carga el navegador, directamente— y pierdes el recorte automático y el WebP. Es a propósito. La alternativa cómoda es aceptar imágenes de cualquier sitio, y eso convierte tu tienda en un mandado: /_next/image?url=… hace que tu servidor se descargue la dirección que le pidan y la procese. Con eso se puede sondear tu red interna desde fuera —el servidor tarda distinto según el puerto conteste o no—, tumbar el proceso con una imagen que al descomprimirse llena la memoria, o simplemente gastarte el ancho de banda.

Estaba así hasta hace poco, y se comprobó: pidiendo una imagen de un servidor cualquiera de internet, la tienda la descargaba y devolvía 200.

Al cambiarla, reconstruye el escaparate igual que con el tema.

Cuatro volúmenes, y conviene saber cuál es cuál antes de borrar alguno:

Volumen Qué hay
db La base de datos. Todo.
static Las imágenes subidas desde el panel.
datos Secretos generados y la clave publicable.

Lo que hace Docker por dentro, a mano. Hace falta Node 22 o más y un Postgres.

Terminal window
npm ci # SIEMPRE ci, no install
npm run build -w @pcreative/commerce-backend # compila a apps/backend/dist
cd apps/backend
cp .env.example .env # DATABASE_URL y poco más
npm run preparar # instala o migra, y prepara la tienda
npm start # corre el compilado

preparar mira la base y decide solo: si está vacía crea el esquema entero (173 tablas, sin nada más instalado); si ya tiene cosas, aplica lo que falte. Y en los dos casos deja la tienda en condiciones de vender: la fila de la tienda, un canal de venta y la clave publicable que el escaparate necesita para hablar con ella. Es idempotente — se puede correr en cada arranque, que es lo que hace el contenedor.

Después, el asistente en el navegador, igual que con Docker.

Detrás de un gestor de procesos o un balanceador

Sección titulada «Detrás de un gestor de procesos o un balanceador»

El servidor tiene dos puertas de salud, y no son la misma cosa:

  • /salud → 200 mientras el proceso responda. Para «¿está vivo?».
  • /listo → 200 solo si puede atender clientes. Pasa a 503 en cuanto empieza a cerrarse, y también si algún plugin no cargó al arrancar — un servidor que vende a otro precio no está listo.

El gestor de procesos mira /salud para decidir si reinicia; el balanceador mira /listo para decidir si manda tráfico. Al revés se pierden peticiones: un balanceador que mira /salud sigue mandando clientes a un servidor que ya se está yendo, y un gestor que mira /listo mata el proceso mientras termina un cobro. Con PCC_MARGEN_CIERRE_MS=5000 el servidor espera ese margen entre avisar por /listo y cerrar el puerto, para que el balanceador se aparte.

Igual, pero sin compilar y recargando al guardar:

Terminal window
docker compose up -d # solo Postgres
npm ci
cd apps/backend && npm run dev # tsx watch
cd apps/admin && npm run dev

  • Un comando npx create-pcreative-commerce para quien no quiera saber nada de Docker. Ahora sería un envoltorio fino de lo de arriba.
  • Botones de despliegue para Coolify, Railway o DigitalOcean.
  • Hosting compartido. Y esto no va a llegar: Node no está instalado en los hostings de 3 € donde vive medio WooCommerce. Es la desventaja estructural frente a PHP y no se arregla con un instalador mejor.