---
title: "Instalar Quire con Docker Compose"
description: "Instala Quire en tu propia infraestructura con Docker Compose."
image: "https://docs.quirelms.com/og.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.quirelms.com/es-419/llms.txt
> Use this file to discover all available pages before exploring further.

# Instalar Quire con Docker Compose

<span id="installing-quire-with-docker-compose"></span>

Este es el producto completo en un solo host: el LMS, sus tareas en segundo plano, los servicios de colaboración y tiempo real, y todos los servicios opcionales detrás de perfiles. El diseño se describe en la sección 2 de `docs/architecture/23-ops.md`.

Otros destinos: [Vercel](/es-419/ops/vercel/) y [Cloudflare Workers](/es-419/ops/cloudflare/) ejecutan solo la capa web. Consulta [upgrade.md](/es-419/ops/upgrade/) para las actualizaciones y [backup-restore.md](/es-419/ops/backup-restore/) para respaldos y el simulacro de restauración.

## Requisitos <!--quire:what-you-need-->

- Docker Engine 27 o posterior, con el complemento Compose 2.30 o posterior.
- 4 núcleos de CPU y 8 GB de memoria para la configuración predeterminada; 8 núcleos y 16 GB con `--profile full` (solo ClamAV necesita aproximadamente 1.5 GB para las firmas).
- Un nombre DNS para la capa web y otro para el contenido no confiable. Deben ser hosts distintos: los paquetes SCORM y el HTML cargado se ejecutan desde el origen de contenido, así nunca pueden leer las cookies del LMS.
- Para una prueba local, `lvh.me` y `*.localhost` resuelven a 127.0.0.1, que es lo que configura `docker/.env.example`. El servicio `proxy` de la pila sirve ambos por https con una autoridad de certificación local, así que no necesitas instalar nada más (consulta «TLS»).
- Los puertos 80 y 443 deben estar disponibles en el host (`QUIRE_PROXY_HTTP_PORT` y `QUIRE_PROXY_HTTPS_PORT` permiten cambiarlos).

## Primera ejecución <!--quire:first-run-->

```sh
QUIRE_APP_ORIGIN=https://learn.example.org \
QUIRE_CONTENT_ORIGIN=https://content.example-content.org \
QUIRE_SETUP_ADMIN_EMAIL=you@example.org \
  docker/scripts/init-env.sh
docker compose -f docker/compose.yaml up -d --build
docker compose -f docker/compose.yaml logs init
```

`docker/scripts/init-env.sh` crea `docker/.env` a partir de `docker/.env.example` y genera todos los secretos (contraseñas de la base de datos, claves de firma y maestra, y el par de claves para el lanzamiento de contenido); guarda la clave de firma del punto de control de auditoría en `docker/secrets/audit-signing-key.pem`, que Compose monta en los workers como secreto. Solo requiere `sh`, `awk` y `openssl`, y se niega a sobrescribir un archivo `docker/.env` existente. Copia ambos archivos fuera del host: sin `QUIRE_MASTER_KEY`, una base de datos restaurada no puede descifrar las credenciales guardadas. Para completar el archivo manualmente, ejecuta `cp docker/.env.example docker/.env`; el mismo archivo explica cómo generar cada secreto.

Ambos orígenes deben usar `https`: el servicio de contenido rechaza conexiones http sin cifrar en producción y no pueden compartir un dominio registrable. El servicio `proxy` termina TLS para ambos (consulta «TLS»); `init-env.sh` rechaza un origen `http://`.

La pila se inicia en un orden fijo y cada paso espera al anterior:

1. `postgres` queda en estado saludable. En el primer inicio, su script de inicialización (`docker/postgres/init/90-passwords.sh`) establece las contraseñas de los cuatro roles.
2. `migrate` aplica todas las migraciones e inicializa la cola de tareas en la base de datos de control y en cada base de datos de tenant dedicada, confirma que coincidan y termina (consulta docs/ops/upgrade.md). Las migraciones se ejecutan cada vez que se inicia el servicio y son idempotentes; por eso, para actualizar basta con una imagen nueva y un reinicio.
3. `init` (`apps/web/src/first-run.ts`) registra la base de datos de la aplicación en `QUIRE_DATABASE_ID` y, si se configuró `QUIRE_SETUP_ADMIN_EMAIL`, crea la primera organización y su administrador. La dirección de inicio de sesión y una contraseña generada se muestran una sola vez en `docker compose logs init`.
4. Se inician `web`, `content`, `worker`, `scheduler`, `collab` y `centrifugo`.
5. `proxy` se inicia cuando `web` y `content` están saludables.

Abre `https://demo.` seguido del dominio de tu aplicación (el registro de `init` muestra la dirección exacta de inicio de sesión) e inicia sesión. En una instalación local, primero confía en la autoridad de certificación del proxy (consulta «TLS»). Cambia la contraseña generada en `/account/security`.

Un proceso sin un secreto obligatorio se niega a iniciarse e indica en sus registros cuál configuración falta. Nada se inicia con una configuración incompleta.

## Servicios y perfiles <!--quire:services-and-profiles-->

| Servicio | Perfil | Función |
| --- | --- | --- |
| postgres | siempre | Base de datos (PostgreSQL 18 con pgvector, compilada desde `docker/postgres.Dockerfile`), con archivo WAL desde el primer inicio |
| migrate, init | siempre | Tareas de una sola ejecución: migraciones y primera ejecución |
| web | siempre | LMS, en `QUIRE_HTTP_PORT` (8080) |
| content | siempre | Origen de contenido no confiable, en `QUIRE_CONTENT_PORT` (8081) |
| worker | siempre | Tareas en segundo plano: correo, informes, procesamiento de archivos y webhooks |
| scheduler | siempre | Tareas periódicas: registra las 64 programaciones en tiempo de ejecución y las asigna al worker; solo hay un líder a la vez |
| collab | siempre | WebSocket para edición colaborativa, en `QUIRE_COLLAB_HTTP_PORT` (1234) |
| centrifugo | siempre | Distribución de eventos en tiempo real, en `QUIRE_REALTIME_PORT` (8000) |
| proxy | siempre | Caddy, la entrada TLS en los puertos 80 y 443 (consulta «TLS») |
| valkey | `cache` | Caché y límites de solicitudes |
| clamav | `scan` | Análisis de malware en archivos cargados |
| gotenberg | `preview` | Vistas previas de Office a PDF y generación de certificados |
| imgproxy | `images` | Imágenes redimensionadas y convertidas |
| transcoder | `video` | Imagen del worker con ffmpeg, únicamente LGPL, para generar versiones de video |
| seaweedfs | `storage` | Almacenamiento de objetos compatible con S3 en este host |
| otelcol | `observability` | Colector de OpenTelemetry |
| mailpit | `devmail` | Captura todos los correos salientes para probar Quire |
| backup | `backup` | Respaldo base de una sola ejecución; consulta backup-restore.md |
| backup-scheduler, backup-offsite | `backup` | Respaldo base cada `QUIRE_BACKUP_INTERVAL_HOURS` y copias cifradas fuera del host, con un simulacro de verificación semanal |
| h5p | `h5p` | Imagen de la herramienta H5P LTI 1.3 que proporcionas en `QUIRE_H5P_IMAGE`, en `QUIRE_H5P_PORT` (8090); consulta «Conectar un proveedor H5P» |

`--profile full` inicia todos los servicios opcionales excepto `backup` y `h5p`. Inicia uno con `docker compose -f docker/compose.yaml --profile scan up -d`. Quire sigue funcionando sin un servicio opcional e indica qué falta: sin analizador, los archivos se guardan sin revisar y se avisa al administrador; sin Gotenberg, los archivos se pueden descargar en lugar de previsualizarse; sin transcodificador, el video se reproduce como archivo original.

Todas las imágenes de terceros y sus obligaciones de licencia se enumeran en `docker/third-party-containers.yaml`.

### Conectar un proveedor H5P <!--quire:connecting-an-h5p-provider-->

Quire no integra ni incluye un entorno de ejecución de H5P ni un servicio auxiliar (ADR 0019). Si usas H5P, proporciona tu propia suscripción alojada u opera una instancia autoalojada de H5P por separado de Quire. Registra ese proveedor como herramienta externa LTI 1.3 y agrega su contenido a cursos como actividad de herramienta. Quire intercambia calificaciones y avances de actividades y evaluaciones mediante LTI Assignment and Grade Services (AGS). Si el proveedor también envía declaraciones xAPI, configura por separado el envío al almacén de declaraciones xAPI de Quire; el intercambio de calificaciones y avances de AGS no envía declaraciones xAPI. Al importar cursos de Moodle, se indica que las actividades H5P necesitan una conexión con una herramienta LTI. El proveedor sigue siendo responsable de su entorno H5P, la creación y reproducción del contenido, el banco de contenido y el historial de intentos.

Para ejecutar tu propia instancia autoalojada en este host, configura `QUIRE_H5P_IMAGE` con su imagen e inicia el perfil `h5p`. Compose la publica en `QUIRE_H5P_PORT` (8090) y guarda sus datos en el volumen `h5p-data`; la imagen y sus obligaciones siguen siendo tuyas.

## Configuración <!--quire:settings-->

Todos los procesos leen `docker/.env`. La plantilla `docker/.env.example` enumera cada configuración con su valor predeterminado. Estos son los grupos:

### Direcciones <!--quire:addresses-->

| Configuración | Significado |
| --- | --- |
| `QUIRE_APP_ORIGIN` | Dirección pública del LMS, por ejemplo, `https://learn.example.com` |
| `QUIRE_CONTENT_ORIGIN` | Origen de contenido, en otro host |
| `QUIRE_PLATFORM_DOMAINS` | Dominios que alojan organizaciones, separados por comas |
| `QUIRE_MARKETING_ORIGIN` | Optional. The marketing site, default `https://quirelms.com`. The only origin the waitlist form (`POST /api/waitlist`, `POST /waitlist`) accepts and redirects to. Comma separated; a `www.` variant is allowed only if listed |
| `QUIRE_DEPLOY_TARGET` | Aquí, `compose`. Consulta las otras guías para `vercel` y `cloudflare` |
| `QUIRE_TRUSTED_PROXY_CIDRS` | Proxies cuyos valores de `X-Forwarded-For` se consideran confiables |

### Secretos <!--quire:secrets-->

| Configuración | Significado |
| --- | --- |
| `QUIRE_SECRET_KEY` | Firma sesiones y tokens. 64 caracteres hexadecimales |
| `QUIRE_MASTER_KEY` | Envuelve credenciales guardadas, como secretos de SSO y webhooks. 32 bytes en base64. La capa web y el worker deben usar el mismo valor. Rotación: [key-rotation.md](/es-419/ops/key-rotation/) |
| `QUIRE_MASTER_KEY_VERSION` | Etiqueta de versión de la clave maestra, `v1` si no se configura. Auméntala al rotarla |
| `QUIRE_MASTER_KEY_RETIRED` | Claves maestras anteriores que todavía se necesitan para leer lo que cifraron, como `v1=<base64>`. Elimínalas cuando termine la rotación sin valores pendientes de resolver |
| `QUIRE_COLLAB_SIGNING_KEY` | La comparten web y collab para firmar tokens de edición |
| `QUIRE_BACKUP_SIGNING_KEY` | Firma respaldos de cursos (opcional) |

Guarda una copia de `QUIRE_MASTER_KEY` fuera de este host. Sin ella, una base de datos restaurada no puede descifrar las credenciales que contiene.

### Base de datos <!--quire:database-->

| Configuración | Significado |
| --- | --- |
| `POSTGRES_PASSWORD` | Contraseña de superusuario, usada por el contenedor y los respaldos |
| `QUIRE_DB_APP_PASSWORD`, `QUIRE_DB_MIGRATOR_PASSWORD`, `QUIRE_DB_REPORT_PASSWORD`, `QUIRE_DB_AUDIT_PASSWORD` | Contraseñas de roles, establecidas en el primer inicio |
| `DATABASE_URL` | Rol de la aplicación. Todas sus consultas están sujetas a seguridad a nivel de fila |
| `DATABASE_MIGRATOR_URL`, `QUIRE_MIGRATION_URL` | Rol de migración, para `migrate` e `init` |
| `QUIRE_SUPERUSER_URL` | Se usa solo durante la primera ejecución |
| `QUIRE_REPORT_DATABASE_URL` | Rol de informes de solo lectura, para informes y su generador |
| `QUIRE_AUDIT_DATABASE_URL` | Rol de auditoría, para la consola de auditoría y la exportación SIEM |
| `QUIRE_DATABASE_ID` | Cualquier UUID, fijo durante toda la vida de la instalación |

Las contraseñas de roles solo se aplican cuando se crea por primera vez el volumen de la base de datos. Para cambiar una después, usa `ALTER ROLE` y luego actualiza la URL correspondiente.

`QUIRE_REPORT_DATABASE_URL` se usa para la base de datos física configurada en `DATABASE_URL`. Para cualquier otra base de datos física registrada, configura su propia URL de conexión `quire_report` en los entornos de web y del worker, y luego incluye el nombre de la variable en el campo **Variable de entorno de informes** de esa base como `env:NAME`. La referencia debe apuntar a la misma base de datos que la conexión de la aplicación, idealmente a su réplica de lectura. Todas las funciones de informes siguen la conexión de informes propia de la base de datos del tenant: el generador y los informes guardados, las entregas programadas, las exportaciones, las analíticas, el registro de auditoría, los recursos de auditoría REST y la búsqueda de auditoría del asistente. Ninguna usa la URL de informes de otra base de datos. Si una base de datos no tiene conexión para informes, los informes comunes se ejecutan con su propia conexión de aplicación, pero las analíticas y todas las consultas de auditoría se rechazan y lo indican, porque el rol de aplicación no puede leer el registro de auditoría.

### Controladores <!--quire:drivers-->

| Configuración | Valores de esta versión | Notas |
| --- | --- | --- |
| `QUIRE_STORAGE_DRIVER` | `local` (predeterminado), `s3` o `azure` | `local` guarda archivos en el volumen `files`. `s3` admite AWS S3, interoperabilidad con R2 y GCS, y otros almacenamientos compatibles con S3, con cargas multiparte reanudables |
| `QUIRE_REALTIME_DRIVER` | `inprocess` (predeterminado), `sse`, `centrifugo` o `durable_objects` | `inprocess` funciona con un solo contenedor web; usa `centrifugo` o `sse` si tienes varios |
| `QUIRE_CACHE_DRIVER` | `memory` (predeterminado), `postgres` o `valkey` | `memory` se limita a cada proceso; usa `valkey` o `postgres` para aplicar límites de solicitudes entre contenedores |
| `QUIRE_VIDEO_DRIVER` | `ffmpeg` (predeterminado) o `progressive_mp4` | También puedes usar un proveedor alojado: Cloudflare Stream, Mux o Bunny, mediante sus claves |
| `QUIRE_IMAGE_DRIVER` | `noop` (predeterminado), `imgproxy` o `cloudflare` | `noop` sirve cada imagen en su tamaño original. `imgproxy` requiere el perfil `images` y las configuraciones de abajo; `cloudflare` usa Cloudflare Images |
| `QUIRE_MEETING_PROVIDER` | `bbb`, `zoom`, `teams`, `meet`, `jitsi` o `in_process` | Proveedor predeterminado de la plataforma para sesiones en vivo. Si no se configura, las sesiones se muestran como no configuradas hasta que la organización conecta su propia cuenta en Integraciones, Proveedor de sesiones en vivo. La cuenta propia de una organización siempre tiene prioridad. Las configuraciones del proveedor (`BBB_URL` y `BBB_SECRET`, las variables `ZOOM_*`, `TEAMS_*`, `GOOGLE_MEET_*` y `JITSI_*`) solo se leen para el proveedor indicado aquí |
| `QUIRE_MEETING_REGIONS` | Lista de `eu`, `uk`, `us`, separada por comas | Regiones donde el proveedor predeterminado procesa reuniones. Si no se configura, no se compara con una organización fijada a una región, igual que antes. La organización especifica en su página las regiones de su propia cuenta |

Si esta versión no incluye un valor de controlador, la capa web se niega a iniciar e indica qué configuración falta, en vez de cambiarlo silenciosamente por el valor predeterminado.

### Imágenes <!--quire:images-->

Las páginas solicitan imágenes en cuatro tamaños fijos mediante `/api/files/{id}/image/{size}`; la ruta verifica que la persona tenga el mismo acceso que al archivo y luego redirige al servicio de imágenes. Cada organización puede solicitar `QUIRE_IMAGE_SPECS_PER_HOUR` pares nuevos de imagen y tamaño por hora (2000 de forma predeterminada); no cuentan los tamaños generados durante esa hora. Si tienes varios contenedores web, usa `valkey` o `postgres` para `QUIRE_CACHE_DRIVER`, así el límite se aplica entre todos.

| Configuración | Controlador | Notas |
| --- | --- | --- |
| `IMGPROXY_URL` | `imgproxy` | Dirección que usan los navegadores para llegar a imgproxy, por ejemplo, `https://images.example.org`. El perfil `images` lo publica en `QUIRE_IMAGES_PORT` (8082) |
| `IMGPROXY_KEY`, `IMGPROXY_SALT` | `imgproxy` | Cadenas hexadecimales, con los mismos valores con los que inicia imgproxy. Genera cada una con `openssl rand -hex 32`. Quire firma todas las direcciones de imágenes con ellas, así imgproxy no procesa nada que Quire no haya solicitado |
| `QUIRE_IMAGE_SOURCE_ORIGIN` | `imgproxy` con almacenamiento local | Origen desde donde imgproxy obtiene los originales. Compose configura `http://web:3000`. Con almacenamiento `s3` o `azure`, imgproxy obtiene los archivos del bucket y no usa este valor |
| `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_IMAGES_TOKEN`, `CLOUDFLARE_IMAGES_ACCOUNT_HASH` | `cloudflare` | Token de API con permiso de edición de Images y hash de cuenta que aparece en Images, Developer resources. Activa las variantes flexibles para la cuenta |
| `CLOUDFLARE_IMAGES_SIGNING_KEY` | `cloudflare` | Opcional. Si se configura, las imágenes son privadas y cada dirección está firmada y vence. Si no, las imágenes son públicas en direcciones derivadas de `QUIRE_SECRET_KEY` que nadie puede adivinar |

Cloudflare Images guarda su propia copia de cada original que sirve. Cuando se elimina un archivo, el worker borra esa copia antes que el original.

### Cola <!--quire:queue-->

Las tareas en segundo plano usan pg-boss en la misma base de Postgres; no hace falta ejecutar ni configurar un servicio de cola. Las tareas se agregan a la cola en la misma transacción que el cambio que las originó, así que un fallo no puede perder una ni ejecutarla dos veces. `QUIRE_QUEUE_DRIVER` es `pgboss` aquí, su valor predeterminado; `vercel` y `cloudflare` trasladan solo las entregas ligeras de notificaciones y webhooks a la cola propia de cada plataforma. Las guías de Vercel y Cloudflare las describen y explican cómo sus capas web agregan tareas.

### Correo electrónico <!--quire:email-->

Configura una de estas opciones:

- `QUIRE_EMAIL_PROVIDER_CONFIG`: un objeto JSON que identifica un proveedor HTTP y sus credenciales, por ejemplo, `{"provider":"postmark","token":"..."}`. Se admiten Postmark, Amazon SES, Mailgun, SendGrid y Resend.
- `QUIRE_SMTP_URL`: `smtp://user:password@host:587`. Solo para este destino; los entornos sin servidor bloquean SMTP.

`QUIRE_MAIL_FROM` es la dirección del remitente. Para probar Quire, inicia el perfil `devmail`, configura `QUIRE_SMTP_URL=smtp://mailpit:1025` y revisa el correo en `http://localhost:8025`.

### Servicios opcionales <!--quire:optional-services-->

| Configuración | Con el perfil |
| --- | --- |
| `CLAMAV_URL=tcp://clamav:3310` | `scan` |
| `GOTENBERG_URL=http://gotenberg:3000` | `preview` |
| `IMGPROXY_KEY`, `IMGPROXY_SALT` | `images` |
| `VALKEY_URL=redis://valkey:6379` | `cache` |
| `QUIRE_OPENSEARCH_URL` o `QUIRE_MEILISEARCH_URL` | Búsqueda externa; de lo contrario, búsqueda de texto completo de Postgres |
| `QUIRE_BREACH_CHECK_PROVIDER=off`, `QUIRE_BREACH_CHECK_URL` | Verificación de contraseñas filtradas. Está activa de forma predeterminada y consulta `api.pwnedpasswords.com` (solo envía un prefijo de cinco caracteres del hash); `off` la desactiva y la URL apunta a una API de rangos que alojes tú |

### Observabilidad <!--quire:observability-->

`OTEL_EXPORTER_OTLP_ENDPOINT` indica el colector al que todos los procesos envían trazas y métricas; con el perfil `observability` es `http://otelcol:4318`, y en `docker/otel-collector.yaml` agregas el exportador de tu backend. Cuando se configura, los procesos de web, worker, scheduler, content y collab exportan spans por OTLP/HTTP (solicitudes web, transacciones de bases de datos de tenants, tareas del worker y llamadas salientes) y envían métricas al mismo endpoint cada minuto (`OTEL_METRICS_EXPORTER=none` las desactiva). `OTEL_TRACES_SAMPLER_ARG` establece la proporción de trazas que se conservan. Los registros se envían a la salida estándar con el nivel `LOG_LEVEL`, y Compose los rota. Las trazas nunca incluyen datos personales.

### Salida de red regional (residencia de datos en la UE) <!--quire:regional-egress-eu-data-residency-->

`QUIRE_REGION=eu` indica que la pila da servicio a organizaciones de la Unión Europea. En ese caso, el worker limita las solicitudes salientes que se hacen para una organización fijada a la UE a una lista de hosts permitidos (sección 8.1 de 21-compliance.md). La lista incluye los hosts que los servicios configurados declaran para esa región (el endpoint de almacenamiento, el proveedor de correo, uno de video alojado, los destinos de almacenamiento propios de la organización, los proveedores de IA y la cuenta de correo), los hosts de servicios cubiertos por una excepción vigente y los que especifiques en `QUIRE_EGRESS_ALLOW_HOSTS`. Las solicitudes a cualquier otro host público se rechazan antes de enviarse; el rechazo se registra en el historial de auditoría de la organización como `privacy/egress_refused` y aparece en Cumplimiento, Residencia de datos.

| Configuración | Valores | Efecto |
| --- | --- | --- |
| `QUIRE_EGRESS_ALLOW_HOSTS` | Lista de nombres de host separados por comas o `*.example.org` para todos los subdominios | Hosts adicionales a los que puede acceder una organización de la UE. Aquí se agregan endpoints de webhook, xAPI y SIEM, fuentes de blogs y hosts de Amazon SES, porque son opciones de la organización y ningún servicio los declara. Las direcciones de loopback, privadas y los nombres de una sola etiqueta, como `web` o `clamav`, pertenecen a tu propia red y nunca se verifican |

Las organizaciones del Reino Unido y Estados Unidos no están sujetas a una lista de hosts; mantienen las comprobaciones de región del servicio. Configura la lista en el worker; la página de administración la lee en la capa web para mostrar los hosts permitidos, así que inclúyela en `docker/.env`, que leen todos los servicios.

La comprobación de la aplicación muestra un error claro y agrega un registro de auditoría, pero no es la garantía: el código puede tener errores. La garantía la da la red. Compose no la impone por su cuenta. Para una pila regional, coloca los servicios `worker` y `web` en una red `internal: true` cuya única salida sea un proxy de egreso (por ejemplo, un contenedor Squid o tinyproxy) que permita los mismos hosts que `QUIRE_EGRESS_ALLOW_HOSTS`, además de los hosts de los servicios configurados, y establece `HTTPS_PROXY` para esos servicios. La página de residencia muestra los hosts exactos que permite la aplicación, así que puedes comparar ambas listas.

## Estado de salud <!--quire:health-->

| Endpoint | Significado |
| --- | --- |
| `/healthz` | Disponibilidad: el proceso responde. Compose usa este endpoint para sus comprobaciones de salud |
| `/readyz` | Preparación: las dependencias son accesibles y cada servicio opcional indica si está configurado. Apunta aquí tu balanceador de carga |

`docker compose -f docker/compose.yaml ps` muestra el estado de cada servicio.

## TLS <!--quire:tls-->

El servicio `proxy` (Caddy, Apache-2.0, `docker/caddy/Caddyfile`) forma parte de la configuración predeterminada. Responde en los puertos 80 y 443 y enruta:

| Host o ruta | Destino |
| --- | --- |
| `QUIRE_PROXY_CONTENT_HOST` | `content` |
| `QUIRE_PROXY_APP_HOST`, cada subdominio de tenant y dominio personalizado | `web` |
| `/_collab/` en esos hosts | `collab` (WebSocket, `QUIRE_COLLAB_URL`) |
| `/_realtime/connection/` en esos hosts | WebSocket de cliente de `centrifugo`; su API de servidor nunca se expone |
| `/_images/` en esos hosts | `imgproxy`, con el perfil `images` (`IMGPROXY_URL`) |

`init-env.sh` obtiene `QUIRE_PROXY_APP_HOST`, `QUIRE_PROXY_CONTENT_HOST`, `QUIRE_PROXY_HTTPS_PORT`, `QUIRE_COLLAB_URL` e `IMGPROXY_URL` a partir de los dos orígenes, para que no haya diferencias entre ellos. Si cambias un origen manualmente, edítalos también.

Los certificados dependen de `QUIRE_PROXY_TLS`:

- `internal` (opción predeterminada): autoridad de certificación propia de Caddy, para `localhost`, `*.localhost` y `lvh.me`. Confía una vez en su certificado raíz y luego abre:

  ```sh
  docker compose -f docker/compose.yaml cp \
    proxy:/data/caddy/pki/authorities/local/root.crt ./quire-local-ca.crt
  ```

  Agrega `quire-local-ca.crt` al almacén de confianza del sistema o del navegador. `curl` lo recibe mediante `--cacert`.
- Una dirección de correo electrónico: certificados ACME automáticos (Let's Encrypt y luego ZeroSSL) para nombres de host reales. El DNS de ambos orígenes y de cada host de tenant debe apuntar aquí; además, los puertos 80 y 443 deben ser accesibles desde Internet.

Los hosts de tenants reciben certificados al visitarse por primera vez y solo si web confirma que el nombre pertenece a esta instalación (`/tls-allowed`, mediante la red de Compose). No necesitas un certificado comodín ni un complemento del proveedor DNS, y una persona ajena que apunte un nombre a este host no puede hacer que solicite certificados. Los certificados y la autoridad local están en el volumen `caddy-data`; si usas `internal`, inclúyelo en los demás respaldos.

Web solo confía en `X-Forwarded-For` cuando lo envía el proxy: este tiene una dirección fija (`QUIRE_PROXY_ADDRESS`, `172.29.64.10` de forma predeterminada) en una subred fija (`QUIRE_COMPOSE_SUBNET`), y `QUIRE_TRUSTED_PROXY_CIDRS` identifica esa dirección. Si la subred coincide con una red del host, cambia ambas y ejecuta `docker compose down` antes de `up`.

## Detrás de tu propio proxy inverso <!--quire:behind-your-own-reverse-proxy-->

Para usar el balanceador de carga o proxy que ya tienes en lugar de `proxy`, déjalo fuera (`docker compose up -d --scale proxy=0`) y termina TLS frente a `web` (8080), `content` (8081), `collab` (1234, WebSocket) y `centrifugo` (8000, WebSocket). Configura las direcciones públicas en `QUIRE_APP_ORIGIN`, `QUIRE_CONTENT_ORIGIN` y `QUIRE_COLLAB_URL` (`wss://`), y el rango de direcciones de tu proxy en `QUIRE_TRUSTED_PROXY_CIDRS`.

## Solución de problemas <!--quire:troubleshooting-->

- `init` termina con «QUIRE_DATABASE_ID is not a UUID»: configúralo con `uuidgen`.
- `web` se reinicia con «did not start on compose»: el registro enumera cada parámetro que no puede usar y qué valor debes configurar.
- Cambiar una contraseña de rol en `.env` después del primer inicio no tiene efecto: el script de inicialización se ejecuta una sola vez. Usa `ALTER ROLE`.
- Si las cargas fallan con un error de análisis y está configurado `CLAMAV_URL`: ClamAV descarga sus firmas durante el primer inicio, lo que puede tardar unos minutos.

Source: https://docs.quirelms.com/es-419/ops/install/index.mdx
