---
title: "Installare Quire con Docker Compose"
description: "Installa Quire sulla tua infrastruttura con Docker Compose."
image: "https://docs.quirelms.com/og.png"
---

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

# Installare Quire con Docker Compose

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

Questo è il prodotto completo su un solo host: l'LMS, il suo lavoro in background, i
servizi realtime e di modifica collaborativa, e ogni servizio facoltativo
dietro un profilo. Il progetto è nella sezione 2 di `docs/architecture/23-ops.md`.

Altri target: [Vercel](/it/ops/vercel/) e [Cloudflare Workers](/it/ops/cloudflare/) eseguono
solo il livello web. Gli aggiornamenti sono in [upgrade.md](/it/ops/upgrade/), e backup e
prova di ripristino in [backup-restore.md](/it/ops/backup-restore/).

## Cosa serve <!--quire:what-you-need-->

- Docker Engine 27 o successivo con plugin Compose 2.30 o successivo.
- 4 core CPU e 8 GB di memoria per lo stack predefinito; 8 core e 16 GB
  con `--profile full` (il solo ClamAV occupa circa 1,5 GB di firme).
- Un nome DNS per il livello web e un secondo per i contenuti non fidati. Devono
  essere host diversi: pacchetti SCORM e HTML caricati girano sull'origine
  dei contenuti così non possono mai leggere i cookie dell'LMS.
- Per una prova locale, `lvh.me` e `*.localhost` risolvono a 127.0.0.1, come
  usa `docker/.env.example`. Il servizio `proxy` proprio dello stack serve
  entrambi via https con un'autorità di certificazione locale, quindi non si installa
  nient'altro (vedi "TLS").
- Porte 80 e 443 libere sull'host (`QUIRE_PROXY_HTTP_PORT` e
  `QUIRE_PROXY_HTTPS_PORT` le spostano).

## Prima esecuzione <!--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` scrive `docker/.env` da `docker/.env.example`
con ogni segreto generato (password dei database, chiavi di firma e master,
coppia di chiavi di avvio dei contenuti) e la chiave di firma del checkpoint di audit in
`docker/secrets/audit-signing-key.pem`, che Compose monta nei worker
come segreto. Servono solo `sh`, `awk` e `openssl`, e rifiuta di
sovrascrivere un `docker/.env` esistente. Copia entrambi i file fuori dall'host: senza
`QUIRE_MASTER_KEY` un database ripristinato non può decifrare le sue credenziali archiviate.
Per compilare il file a mano, `cp docker/.env.example docker/.env`; il
file spiega come generare ciascun segreto.

Entrambe le origini devono essere `https`: il servizio dei contenuti rifiuta http in chiaro in
produzione, e non devono condividere un dominio registrabile. Il servizio `proxy`
termina TLS per entrambe (vedi "TLS"); `init-env.sh` rifiuta un'origine
`http://`.

Lo stack parte in ordine fisso, e ciascun passaggio attende quello precedente:

1. `postgres` diventa sano. Al primissimo avvio il suo script di init
   (`docker/postgres/init/90-passwords.sh`) imposta le password dei quattro ruoli.
2. `migrate` applica ogni migrazione e avvia la coda dei job nel
   database di controllo e in ogni database tenant dedicato, verifica che siano
   tutti d'accordo, poi esce (docs/ops/upgrade.md).
   Le migrazioni girano a ogni avvio e sono idempotenti, quindi un aggiornamento è una nuova
   immagine e un riavvio.
3. `init` (`apps/web/src/first-run.ts`) registra il database applicativo sotto
   `QUIRE_DATABASE_ID` e, quando `QUIRE_SETUP_ADMIN_EMAIL` è impostato, crea la
   prima organizzazione e il suo amministratore. L'indirizzo di accesso e una
   password generata sono stampati una volta, in `docker compose logs init`.
4. `web`, `content`, `worker`, `scheduler`, `collab` e `centrifugo` partono.
5. `proxy` parte una volta che `web` e `content` sono sani.

Apri `https://demo.` seguito dal tuo dominio applicativo (il registro `init`
stampa l'indirizzo di accesso esatto), e accedi. Su un'installazione locale, considera prima
attendibile l'autorità di certificazione del proxy (vedi "TLS"). Cambia la password generata in `/account/security`.

Un processo avviato senza un segreto richiesto rifiuta di avviarsi e nomina
l'impostazione mancante nel suo registro. Nulla parte mezzo configurato.

## Servizi e profili <!--quire:services-and-profiles-->

| Servizio | Profilo | Cosa fa |
| --- | --- | --- |
| postgres | sempre | Il database (PostgreSQL 18 con pgvector, compilato da `docker/postgres.Dockerfile`), con WAL archiviato dal primo avvio |
| migrate, init | sempre | One-shot: migrazioni, poi prima esecuzione |
| web | sempre | L'LMS, su `QUIRE_HTTP_PORT` (8080) |
| content | sempre | L'origine dei contenuti non fidati, su `QUIRE_CONTENT_PORT` (8081) |
| worker | sempre | Job in background: email, report, elaborazione file, webhook |
| scheduler | sempre | Job ricorrenti: registra le 64 programmazioni runtime e le affida al worker; un leader alla volta |
| collab | sempre | Websocket di modifica collaborativa, su `QUIRE_COLLAB_HTTP_PORT` (1234) |
| centrifugo | sempre | Fan-out realtime, su `QUIRE_REALTIME_PORT` (8000) |
| proxy | sempre | Caddy, la porta TLS sulle porte 80 e 443 (vedi "TLS") |
| valkey | `cache` | Cache e limiti di frequenza |
| clamav | `scan` | Scansione malware dei caricamenti |
| gotenberg | `preview` | Anteprime da Office a PDF, rendering certificati |
| imgproxy | `images` | Immagini ridimensionate e convertite |
| transcoder | `video` | L'immagine worker con ffmpeg solo LGPL, per le versioni video |
| seaweedfs | `storage` | Object storage compatibile S3 su questo host |
| otelcol | `observability` | Un collector OpenTelemetry |
| mailpit | `devmail` | Cattura tutta la posta in uscita, per provare Quire |
| backup | `backup` | Backup di base one-shot; vedi backup-restore.md |
| backup-scheduler, backup-offsite | `backup` | Un backup di base ogni `QUIRE_BACKUP_INTERVAL_HOURS`, e copie cifrate fuori host con prova di verifica settimanale |
| h5p | `h5p` | L'immagine strumento H5P LTI 1.3 fornita da te in `QUIRE_H5P_IMAGE`, su `QUIRE_H5P_PORT` (8090); vedi "Collegare un provider H5P" |

`--profile full` avvia ogni servizio facoltativo tranne `backup` e `h5p`.
Avviane uno con `docker compose -f docker/compose.yaml --profile scan up -d`.
Senza un servizio facoltativo Quire funziona comunque e dice cosa manca: senza
scanner i caricamenti sono archiviati non scansionati e l'amministratore è avvisato; senza
Gotenberg i file offrono il download invece dell'anteprima; senza transcoder i
video vengono riprodotti come file originale.

Ogni immagine di terze parti e i suoi obblighi di licenza sono elencati in
`docker/third-party-containers.yaml`.

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

Quire non integra né fornisce un runtime o sidecar H5P (ADR 0019). Se usi
H5P, procurati un abbonamento ospitato oppure gestisci separatamente da Quire una tua istanza H5P
autonoma. Registra quel provider come strumento esterno LTI 1.3
e aggiungi i suoi contenuti ai corsi come attività strumento. Quire scambia voti
e avanzamento di attività/valutazione tramite LTI Assignment and Grade Services (AGS).
Se il provider invia anche istruzioni xAPI, configurale separatamente per
l'archivio istruzioni xAPI di Quire; lo scambio voti/avanzamento AGS non invia istruzioni
xAPI. Le importazioni Moodle segnalano le attività H5P come bisognose di una connessione
a uno strumento LTI. Il provider resta responsabile del suo runtime H5P, della creazione,
della banca dei contenuti e della cronologia dei tentativi.

Per eseguire una tua istanza autonoma su questo host, imposta `QUIRE_H5P_IMAGE` sulla sua
immagine e avvia il profilo `h5p`. Compose la pubblica su `QUIRE_H5P_PORT`
(8090) e ne conserva i dati nel volume `h5p-data`; l'immagine, e gli
obblighi che comporta, restano tuoi.

## Impostazioni <!--quire:settings-->

Ogni processo legge `docker/.env`. Il modello, `docker/.env.example`, elenca
ciascuna impostazione col suo default. I gruppi:

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

| Impostazione | Significato |
| --- | --- |
| `QUIRE_APP_ORIGIN` | L'indirizzo pubblico dell'LMS, ad esempio `https://learn.example.com` |
| `QUIRE_CONTENT_ORIGIN` | L'origine dei contenuti, un host diverso |
| `QUIRE_PLATFORM_DOMAINS` | Domini sotto cui vivono le organizzazioni, separati da virgole |
| `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` | `compose` qui. Vedi le altre guide per `vercel` e `cloudflare` |
| `QUIRE_TRUSTED_PROXY_CIDRS` | Proxy il cui `X-Forwarded-For` è considerato attendibile |

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

| Impostazione | Significato |
| --- | --- |
| `QUIRE_SECRET_KEY` | Firma sessioni e token. 64 caratteri esadecimali |
| `QUIRE_MASTER_KEY` | Avvolge le credenziali archiviate come segreti SSO e webhook. 32 byte, base64. Il livello web e il worker necessitano dello stesso valore. Rotazione: [key-rotation.md](/it/ops/key-rotation/) |
| `QUIRE_MASTER_KEY_VERSION` | L'etichetta di versione della chiave master, `v1` se non impostata. Alzala quando ruoti |
| `QUIRE_MASTER_KEY_RETIRED` | Chiavi master precedenti ancora necessarie per leggere ciò che hanno sigillato, come `v1=<base64>`. Rimuovile dopo che una rotazione termina senza irrisolti |
| `QUIRE_COLLAB_SIGNING_KEY` | Condivisa da web e collab per firmare i token di modifica |
| `QUIRE_BACKUP_SIGNING_KEY` | Firma i backup dei corsi (facoltativo) |

Conserva una copia di `QUIRE_MASTER_KEY` altrove rispetto a questo host. Un database
ripristinato senza di essa non può decifrare le credenziali che contiene.

### Database <!--quire:database-->

| Impostazione | Significato |
| --- | --- |
| `POSTGRES_PASSWORD` | Il superuser, usato dal contenitore e dai backup |
| `QUIRE_DB_APP_PASSWORD`, `QUIRE_DB_MIGRATOR_PASSWORD`, `QUIRE_DB_REPORT_PASSWORD`, `QUIRE_DB_AUDIT_PASSWORD` | Password dei ruoli, impostate al primo avvio |
| `DATABASE_URL` | Il ruolo applicativo. La sicurezza a livello di riga si applica a ogni query che esegue |
| `DATABASE_MIGRATOR_URL`, `QUIRE_MIGRATION_URL` | Il ruolo migratore, per `migrate` e `init` |
| `QUIRE_SUPERUSER_URL` | Usato solo dalla prima esecuzione |
| `QUIRE_REPORT_DATABASE_URL` | Il ruolo report di sola lettura, per report e generatore di report |
| `QUIRE_AUDIT_DATABASE_URL` | Il ruolo audit, per la console di audit ed esportazione SIEM |
| `QUIRE_DATABASE_ID` | Un UUID qualsiasi, fisso per la vita dell'installazione |

Le password dei ruoli sono applicate solo quando il volume del database viene creato per la prima volta.
Per cambiarne una in seguito, usa `ALTER ROLE` e poi aggiorna l'URL corrispondente.

`QUIRE_REPORT_DATABASE_URL` è usato per il database fisico configurato da
`DATABASE_URL`. Per ogni altro database fisico registrato, imposta il suo
URL di connessione `quire_report` negli ambienti web e worker, poi inserisci
il nome della variabile nel campo **Variabile d'ambiente di reporting** di quel database
come `env:NAME`. Il riferimento deve puntare allo stesso database della sua connessione
app, idealmente la sua replica di lettura. Ogni superficie di report segue il
tenant alla connessione report del proprio database: il generatore di report e i
report salvati, le consegne programmate, le esportazioni di report, le analisi, il registro
di audit, le risorse audit REST e la ricerca audit dell'assistente. Nessuna di
esse prende mai in prestito l'URL report di un altro database. Quando un database non ha
una connessione report, i report ordinari girano sulla connessione applicativa del database stesso,
mentre le analisi e ogni lettura audit rifiutano e lo dicono, perché
il ruolo applicativo non può leggere la traccia di audit.

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

| Impostazione | Questa release | Note |
| --- | --- | --- |
| `QUIRE_STORAGE_DRIVER` | `local` (predefinito), `s3` oppure `azure` | `local` conserva i file nel volume `files`. `s3` copre AWS S3, R2, interoperabilità GCS e altri store compatibili S3, con caricamenti multipart ripristinabili |
| `QUIRE_REALTIME_DRIVER` | `inprocess` (predefinito), `sse`, `centrifugo` oppure `durable_objects` | `inprocess` va bene per un solo contenitore web; usa `centrifugo` oppure `sse` quando ce ne sono diversi |
| `QUIRE_CACHE_DRIVER` | `memory` (predefinito), `postgres` oppure `valkey` | `memory` è per processo; usa `valkey` oppure `postgres` così i limiti di frequenza valgono tra contenitori |
| `QUIRE_VIDEO_DRIVER` | `ffmpeg` (predefinito) oppure `progressive_mp4` | Oppure un provider ospitato: Cloudflare Stream, Mux o Bunny, con le loro chiavi |
| `QUIRE_IMAGE_DRIVER` | `noop` (predefinito), `imgproxy` oppure `cloudflare` | `noop` serve ogni immagine alla sua dimensione originale. `imgproxy` richiede il profilo `images` e le impostazioni sotto; `cloudflare` usa Cloudflare Images |
| `QUIRE_MEETING_PROVIDER` | `bbb`, `zoom`, `teams`, `meet`, `jitsi` oppure `in_process` | Il default di piattaforma per le sessioni live. Non impostato, le sessioni live dicono di non essere configurate, finché un'organizzazione non collega il proprio account in Integrazioni, Provider sessioni live. L'account proprio di un'organizzazione vince sempre su questo valore. Le impostazioni proprie di ciascun provider (`BBB_URL` e `BBB_SECRET`, le variabili `ZOOM_*`, `TEAMS_*`, `GOOGLE_MEET_*` e `JITSI_*`) sono lette solo per il provider qui nominato |
| `QUIRE_MEETING_REGIONS` | Un elenco di `eu`, `uk`, `us` separato da virgole | Dove il provider default di piattaforma elabora le riunioni. Non impostato, non viene confrontato con un'organizzazione vincolata a un'area geografica, come prima. L'account proprio di un'organizzazione dichiara le sue aree nella sua pagina |

Un valore driver non incluso in questa release viene rifiutato all'avvio del livello web,
col nome dell'impostazione, anziché sostituito in silenzio dal
default.

### Immagini <!--quire:images-->

Le pagine chiedono immagini in quattro dimensioni fisse tramite
`/api/files/{id}/image/{size}`, che verifica lo stesso accesso del file
stesso e poi reindirizza al servizio immagini. Ciascuna organizzazione può chiedere
`QUIRE_IMAGE_SPECS_PER_HOUR` (predefinito 2000) nuove coppie immagine e dimensione all'ora;
le dimensioni già prodotte in quell'ora non contano. Usa `valkey` oppure
`postgres` per `QUIRE_CACHE_DRIVER` con più di un contenitore web, così il
limite vale tra essi.

| Impostazione | Driver | Note |
| --- | --- | --- |
| `IMGPROXY_URL` | `imgproxy` | L'indirizzo a cui i browser raggiungono imgproxy, ad esempio `https://images.example.org`. Il profilo `images` lo pubblica su `QUIRE_IMAGES_PORT` (8082) |
| `IMGPROXY_KEY`, `IMGPROXY_SALT` | `imgproxy` | Stringhe esadecimali, gli stessi valori con cui imgproxy è avviato. Genera ciascuna con `openssl rand -hex 32`. Quire firma ogni indirizzo immagine con esse, così imgproxy non renderizza nulla che Quire non abbia chiesto |
| `QUIRE_IMAGE_SOURCE_ORIGIN` | `imgproxy` con storage locale | Da dove imgproxy preleva gli originali. Compose imposta `http://web:3000`. Con storage `s3` oppure `azure` imgproxy preleva dal bucket e questo non è usato |
| `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_IMAGES_TOKEN`, `CLOUDFLARE_IMAGES_ACCOUNT_HASH` | `cloudflare` | Un token API con permesso Images edit, e l'hash account da Images, Risorse sviluppatore. Attiva le varianti flessibili per l'account |
| `CLOUDFLARE_IMAGES_SIGNING_KEY` | `cloudflare` | Facoltativo. Se impostato, le immagini sono private e ogni indirizzo è firmato e scade. Senza di esso, le immagini sono pubbliche ad indirizzi derivati da `QUIRE_SECRET_KEY` che nessuno può indovinare |

Cloudflare Images conserva una propria copia di ciascun originale servito. Quando un
file viene eliminato, il worker elimina quella copia prima dell'originale.

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

I job in background usano pg-boss nello stesso database Postgres, quindi non c'è
un servizio di coda da eseguire e nulla da configurare. I job sono accodati nella stessa
transazione della modifica che li ha causati, così un crash non può perderne uno né
inviarne uno due volte. `QUIRE_QUEUE_DRIVER` qui è `pgboss`, il suo default; `vercel`
e `cloudflare` spostano solo le consegne leggere di notifiche e webhook alla
coda propria della piattaforma, e le guide Vercel e Cloudflare le descrivono
e spiegano come i loro livelli web accodano.

### Email <!--quire:email-->

Impostane una tra:

- `QUIRE_EMAIL_PROVIDER_CONFIG`: un oggetto JSON che nomina un provider HTTP e
  le sue credenziali, ad esempio `{"provider":"postmark","token":"..."}`. Postmark,
  Amazon SES, Mailgun, SendGrid e Resend sono supportati.
- `QUIRE_SMTP_URL`: `smtp://user:password@host:587`. Solo questo target; i
  target serverless bloccano SMTP.

`QUIRE_MAIL_FROM` è il mittente. Per provare Quire, avvia il profilo `devmail`,
imposta `QUIRE_SMTP_URL=smtp://mailpit:1025`, e leggi la posta in
`http://localhost:8025`.

### Servizi facoltativi <!--quire:optional-services-->

| Impostazione | Con profilo |
| --- | --- |
| `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` oppure `QUIRE_MEILISEARCH_URL` | Ricerca esterna; altrimenti full text Postgres |
| `QUIRE_BREACH_CHECK_PROVIDER=off`, `QUIRE_BREACH_CHECK_URL` | Controllo violazioni password. Attivo per impostazione predefinita contro `api.pwnedpasswords.com` (viene inviato solo un prefisso hash di cinque caratteri); `off` lo disabilita, e l'URL punta a una range API ospitata da te |

### Osservabilità <!--quire:observability-->

`OTEL_EXPORTER_OTLP_ENDPOINT` nomina il collector a cui ogni processo invia
tracce e metriche; con il profilo `observability` è
`http://otelcol:4318`, e `docker/otel-collector.yaml` è dove aggiungi
l'esporter per il tuo backend. I processi web, worker, scheduler, content e collab
esportano span su OTLP/HTTP (richieste web, transazioni database tenant,
job worker e chiamate in uscita) quando è impostato, e metriche allo
stesso endpoint ogni minuto (`OTEL_METRICS_EXPORTER=none` le disattiva).
`OTEL_TRACES_SAMPLER_ARG` imposta la quota di tracce conservate. I log vanno sullo standard
output a `LOG_LEVEL`, e Compose li ruota. Le tracce non contengono mai dati personali.

### Egress regionale (residenza dati UE) <!--quire:regional-egress-eu-data-residency-->

`QUIRE_REGION=eu` indica che lo stack serve organizzazioni dell'Unione europea. Il
worker trattiene allora ogni richiesta in uscita effettuata per un'organizzazione vincolata alla
UE a un allowlist (sezione 8.1 di 21-compliance.md). L'allowlist è gli
host dichiarati per l'area geografica dai servizi configurati (l'endpoint
storage, il provider email, un provider video ospitato, i target storage propri
dell'organizzazione, provider IA e account email), gli host di ogni servizio
sotto deroga attiva, e gli host elencati da te in
`QUIRE_EGRESS_ALLOW_HOSTS`. Una richiesta a qualsiasi altro host pubblico viene rifiutata
prima dell'invio, il rifiuto è scritto nella traccia di audit dell'organizzazione come
`privacy/egress_refused`, ed è elencato in Conformità, Residenza dati.

| Impostazione | Valori | Effetto |
| --- | --- | --- |
| `QUIRE_EGRESS_ALLOW_HOSTS` | Un elenco di hostname separato da virgole, oppure `*.example.org` per ogni sottodominio | Host extra che un'organizzazione UE può raggiungere. Endpoint webhook, xAPI e SIEM, feed blog e host Amazon SES appartengono qui, perché sono scelta propria di un'organizzazione e nessun servizio li dichiara. Loopback, indirizzi privati e nomi a singola etichetta come `web` o `clamav` sono la tua rete e non vengono mai verificati |

Le organizzazioni UK e US non sono vincolate a un elenco host; conservano i controlli
di area geografica dei servizi. Imposta l'elenco sul worker; la pagina admin lo legge sul livello web
per mostrare l'allowlist, quindi mettilo in `docker/.env`, che ogni servizio
legge.

Il controllo applicativo dà un errore chiaro e una voce di audit, e non è
la garanzia: il codice può sbagliare. La garanzia è la rete. Compose non
la applica per te. Per uno stack regionale, metti i servizi `worker` e `web`
su una rete `internal: true` la cui unica via d'uscita sia un proxy di egress
(ad esempio un contenitore Squid o tinyproxy) che consente gli stessi host
di `QUIRE_EGRESS_ALLOW_HOSTS` più gli host dei tuoi servizi configurati, e
imposta `HTTPS_PROXY` per quei servizi. La pagina di residenza elenca gli host esatti
consentiti dall'applicazione, così le due liste si possono confrontare.

## Stato <!--quire:health-->

| Endpoint | Significato |
| --- | --- |
| `/healthz` | Liveness: il processo risponde. I controlli di salute Compose usano questo |
| `/readyz` | Readiness: dipendenze raggiungibili, e ciascun servizio facoltativo segnalato come configurato o no. Punta qui il tuo bilanciatore |

`docker compose -f docker/compose.yaml ps` mostra lo stato di ciascun servizio.

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

Il servizio `proxy` (Caddy, Apache-2.0, `docker/caddy/Caddyfile`) fa parte dello
stack predefinito. Risponde sulle porte 80 e 443 e instrada:

| Host o percorso | Va a |
| --- | --- |
| `QUIRE_PROXY_CONTENT_HOST` | `content` |
| `QUIRE_PROXY_APP_HOST`, ogni sottodominio tenant e dominio personalizzato | `web` |
| `/_collab/` su quegli host | `collab` (websocket, `QUIRE_COLLAB_URL`) |
| `/_realtime/connection/` su quegli host | il websocket client di `centrifugo`; la sua API server non è mai esposta |
| `/_images/` su quegli host | `imgproxy`, con il profilo `images` (`IMGPROXY_URL`) |

`init-env.sh` deriva `QUIRE_PROXY_APP_HOST`, `QUIRE_PROXY_CONTENT_HOST`,
`QUIRE_PROXY_HTTPS_PORT`, `QUIRE_COLLAB_URL` e `IMGPROXY_URL` dalle due
origini, così non possono divergere. Modificali insieme se cambi un'origine
a mano.

I certificati seguono `QUIRE_PROXY_TLS`:

- `internal` (il default): l'autorità di certificazione propria di Caddy, per
  `localhost`, `*.localhost` e `lvh.me`. Considera attendibile la sua radice una volta, poi naviga:

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

  Aggiungi `quire-local-ca.crt` al trust store di sistema o del browser. `curl`
  lo accetta con `--cacert`.
- Un indirizzo e-mail: certificati ACME automatici (Let's Encrypt, poi
  ZeroSSL) per hostname reali. Il DNS di entrambe le origini e di ogni host tenant
  deve puntare qui, e le porte 80 e 443 devono essere raggiungibili da internet.

Gli host tenant sono emessi su richiesta, alla prima visita, e solo quando web
conferma che il nome appartiene a questa installazione (`/tls-allowed`, chiesto sulla
rete Compose). Non servono certificati wildcard né plugin DNS, e un estraneo che punta un nome all'host non può fargli richiedere
certificati. Certificati e autorità locale vivono nel
volume `caddy-data`; includilo nel backup col resto se usi `internal`.

Web considera `X-Forwarded-For` solo dal proxy: il proxy ha un indirizzo fisso
(`QUIRE_PROXY_ADDRESS`, default `172.29.64.10`) su una sottorete fissa
(`QUIRE_COMPOSE_SUBNET`), e `QUIRE_TRUSTED_PROXY_CIDRS` nomina quell'indirizzo.
Se la sottorete collide con una rete sull'host, cambia entrambi ed esegui
`docker compose down` prima di `up`.

## Dietro il proprio reverse proxy <!--quire:behind-your-own-reverse-proxy-->

Per usare un bilanciatore o proxy già in uso, lascia fuori `proxy`
(`docker compose up -d --scale proxy=0`) e termina TLS davanti a `web`
(8080), `content` (8081), `collab` (1234, websocket) e `centrifugo` (8000,
websocket). Imposta gli indirizzi pubblici in `QUIRE_APP_ORIGIN`,
`QUIRE_CONTENT_ORIGIN` e `QUIRE_COLLAB_URL` (`wss://`), e l'intervallo di indirizzi del tuo proxy
in `QUIRE_TRUSTED_PROXY_CIDRS`.

## Risoluzione dei problemi <!--quire:troubleshooting-->

- `init` esce con "QUIRE_DATABASE_ID is not a UUID": impostalo con `uuidgen`.
- `web` si riavvia con "did not start on compose": il registro elenca ciascuna impostazione
  che non può onorare e cosa usare invece.
- Cambiare una password di ruolo in `.env` dopo il primo avvio non fa nulla: lo
  script di init gira una volta sola. Usa `ALTER ROLE`.
- I caricamenti falliscono con errore di scansione mentre `CLAMAV_URL` è impostato: ClamAV scarica
  le sue firme al primo avvio, il che richiede qualche minuto.

Source: https://docs.quirelms.com/it/ops/install/index.mdx
