---
title: "Installare Zitadel 4 in produzione su Debian senza Docker"
description: "Zitadel 4 in produzione su Debian senza container: due componenti, tre file di configurazione, unità systemd blindata, fasi init e setup, e lo split del reverse proxy."
canonical: "https://www.mauriziofonte.it/blog/post/zitadel-produzione-debian-senza-docker.html"
author: "Maurizio Fonte"
date: "2026-07-30T15:12:49+02:00"
modified: "2026-07-30T15:12:49+02:00"
section: "IAM & SSO"
tags: ["IAM & SSO", "Linux & Server"]
license: "Riuso e citazione consentiti con attribuzione e link alla fonte"
---

[IAM & SSO](https://www.mauriziofonte.it/blog/cat/iam-sso.html) [Linux & Server](https://www.mauriziofonte.it/blog/cat/linux.html)

# [Installare Zitadel 4 in produzione su Debian senza Docker](https://www.mauriziofonte.it/blog/post/zitadel-produzione-debian-senza-docker.html)

Giovedì 30 Luglio 2026, alle 15:12 ![Installare Zitadel 4 in produzione su Debian senza Docker](https://www.mauriziofonte.it/image/005480200-zitadel-produzione-debian-senza-docker.jpg)

La pagina della documentazione ufficiale di Zitadel che spiega come installare il prodotto su Linux si apre con un avviso che vale la pena leggere due volte: **non vale per la versione 4**, e rimanda al deploy con container. Già prima di quell'avviso la stessa pagina dichiarava di essere pensata per scopi di sviluppo e dimostrazione. Per l'alta disponibilità la raccomandazione è Kubernetes con il chart ufficiale.

Detto in modo diretto: **la via che sto per descrivere non è quella che il progetto documenta**. Non la nascondo e non la presento come un trucco: è una scelta di deploy consapevole, con le sue ragioni e i suoi costi, che ho portato in produzione e che continua a funzionare perché i binari nativi vengono ancora prodotti a ogni rilascio. Fra gli artefatti della **4.16.1** ci sono sia `zitadel-linux-amd64.tar.gz` sia `zitadel-login.tar.gz`, con le varianti per le altre architetture e il file dei checksum. Il codice per girare senza container c'è; quello che manca è la documentazione.

Nei primi tre articoli di questa serie ho spiegato [il modello con cui Zitadel organizza identità e autorizzazioni](https://www.mauriziofonte.it/blog/post/zitadel-oidc-organizzazioni-progetti-grant-audience.html), [perché l'ho scelto al posto di Keycloak](https://www.mauriziofonte.it/blog/post/zitadel-vs-keycloak-identity-provider-self-hosted-pmi.html) e [cosa cambia self-hostarlo invece di appoggiarsi ad Auth0 o Okta](https://www.mauriziofonte.it/blog/post/zitadel-self-hosted-vs-auth0-okta-sovranita-costi.html). In questo articolo passo a metterlo in produzione su Debian, senza container, e la prima cosa da chiarire è che **la versione 4 ha cambiato la forma di ciò che installi**.

## Perché scegliere il deploy nativo quando la documentazione dice il contrario?

Perché in molti contesti reali il container non risolve un problema, ne aggiunge uno. Le ragioni per cui l'ho fatto, in ordine di peso.

**Il resto dell'infrastruttura non è containerizzata.** Introdurre un runtime di container su macchine gestite con pacchetti e unità di sistema significa introdurre un secondo modello mentale di deploy, un secondo posto dove guardare i log, un secondo meccanismo di aggiornamento e un secondo insieme di cose che possono rompersi. Su un parco macchine piccolo questo costo è reale e ricorrente.

**Il binario Go è statico e non ha dipendenze di runtime.** È esattamente il tipo di software che non ha bisogno di essere isolato in un'immagine: non trascina un interprete, non pretende librerie di sistema particolari, non scrive dove non deve. Il container risolverebbe un problema di dipendenze che non esiste.

**L'hardening con systemd è più semplice di quello equivalente in un container**, e questo sorprende chi si aspetta il contrario. Le direttive che vedrai più avanti danno un filesystem in sola lettura, nessuna capability, nessuna possibilità di elevare i privilegi e nessun accesso alle home, in undici righe leggibili da chiunque faccia un audit senza dover conoscere il modello di sicurezza di un runtime di container.

Il costo di questa scelta è uno e lo dichiaro subito: **stai facendo una cosa che la documentazione non copre**, quindi quando qualcosa non torna sei tu la documentazione. È accettabile se il sistema è tuo e ne conosci ogni pezzo; non lo è se domani deve prenderlo in mano qualcuno che si aspetta la procedura standard.

## Cosa installi davvero: due componenti, non uno

Questa è la differenza principale rispetto a qualunque guida scritta per la versione 2 o 3, incluse le mie note interne di allora, e ignorarla porta a un impianto che si avvia senza errori e in cui **nessuno riesce a fare login**.

Nella versione 4 l'interfaccia di autenticazione non è più servita dal binario del core. È un'applicazione separata che **ascolta sulla porta 3000 e serve il percorso `/ui/v2/login`**, e che dialoga con il core attraverso le API autenticandosi come un utente macchina. Sulle istanze nuove il core reindirizza lì ogni autenticazione: se a quel percorso non risponde nessuno, il login interattivo semplicemente non esiste, e non lo capisci dai log del core perché dal suo punto di vista sta funzionando tutto.

L'utente macchina in questione va creato nella console e gli va assegnato il ruolo **`IAM_LOGIN_CLIENT`**, come descritto nella [documentazione ufficiale del client di login](https://zitadel.com/docs/self-hosting/manage/login-client). Poi si emette per lui un token di accesso personale, e lo si passa all'applicazione di login tramite la variabile d'ambiente **`ZITADEL_SERVICE_USER_TOKEN`**, oppure, ed è la forma che preferisco su una macchina di produzione, tramite **`ZITADEL_SERVICE_USER_TOKEN_FILE`**, che punta a un file leggibile solo dall'utente di servizio. La differenza non è cosmetica: una variabile d'ambiente è leggibile da chiunque possa ispezionare il processo, un file ha permessi.

Esiste una via d'uscita documentata, e va conosciuta perché in alcune migrazioni è la scelta giusta: impostando `ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED=false` si riattiva la login classica dentro il core, e si torna a un solo componente. La uso quando devo aggiornare un impianto esistente in due tempi, portando prima il core e poi la login, invece di cambiare due cose insieme.

> Il modo più veloce per capire se un'installazione della versione 4 è fatta bene: apri il percorso `/ui/v2/login` dal reverse proxy. Se risponde il core con un errore invece dell'applicazione di login, lo split di routing è sbagliato, e tutto il resto funzionerà benissimo tranne la cosa per cui hai installato un identity provider.

## Preparazione della macchina

Do per scontato un Debian aggiornato e un PostgreSQL raggiungibile e amministrato a parte. Sulla versione del database vale la pena essere espliciti: **dalla versione 3 CockroachDB non è più supportato** e PostgreSQL è l'unica opzione, quindi qualunque guida che offra la scelta fra i due è vecchia di almeno due major.

Si parte da un utente di servizio senza shell e senza home, e da una struttura di cartelle di proprietà sua:

```bash
sudo useradd --system --no-create-home --shell /usr/sbin/nologin zitadel
sudo mkdir -p /opt/zitadel/etc /opt/zitadel/bin
sudo chown -R zitadel:zitadel /opt/zitadel/etc /opt/zitadel/bin
sudo chmod 750 /opt/zitadel/etc /opt/zitadel/bin
```

Poi la chiave maestra, che è il segreto con cui Zitadel cifra i dati sensibili nel database. Deve essere **esattamente di 32 byte**, identica su tutti i nodi, e va custodita fuori dalla macchina come custodiresti la chiave di un backup cifrato: se la perdi, il contenuto del database non è più leggibile, e non esiste un percorso di recupero.

```bash
tr -dc A-Za-z0-9 </dev/urandom | head -c 32 | sudo tee /opt/zitadel/etc/master.key
sudo chown zitadel:zitadel /opt/zitadel/etc/master.key
sudo chmod 400 /opt/zitadel/etc/master.key
sudo chattr +i /opt/zitadel/etc/master.key
```

Quell'ultima riga rende il file immutabile anche per l'utente amministrativo, ed è una precauzione che consiglio: protegge da uno script di deploy scritto male molto più che da un attaccante.

L'installazione del binario è uno scaricamento e uno spostamento, e il dettaglio che fa la differenza fra uno script casalingo e uno che puoi rieseguire è usare `install(1)` invece di `mv`, così proprietario, gruppo e permessi sono impostati nell'operazione stessa:

```bash
sudo install -o zitadel -g zitadel -m 0755 \
    "${TMPDIR}/zitadel-linux-amd64/zitadel" /opt/zitadel/bin/zitadel
sudo -u zitadel /opt/zitadel/bin/zitadel --version
```

## I tre file di configurazione, e perché sono tre

Zitadel accetta più file passati con `--config`, e li **fonde nell'ordine in cui li elenchi**. È una proprietà utile che vale la pena sfruttare per separare le cose secondo chi le può leggere, invece di avere un unico file che finisce per non stare da nessuna parte.

Il primo file contiene la configurazione pubblica e deterministica, quella che puoi tenere sotto controllo di versione senza pensarci:

```yaml
Log:
  Level: warn
  Formatter:
    Format: json

Telemetry:
  Enabled: false

Port: 8080
ExternalPort: 443
ExternalDomain: auth.example.com
ExternalSecure: true

# TLS terminata dal reverse proxy, il core parla HTTP in rete interna
TLS:
  Enabled: false

WebAuthNName: "Example Identity"

Database:
  postgres:
    Host: db.example.com
    Port: 5432
    Database: zitadel
    MaxOpenConns: 200
    MaxIdleConns: 20
    MaxConnLifetime: 30m
    MaxConnIdleTime: 5m
    User:
      Username: zitadel_user
      SSL:
        Mode: disable
    Admin:
      Username: postgres
      SSL:
        Mode: disable

Caches:
  Postgres:
    Enabled: true
    AutoPrune:
      Interval: 15m
      TimeOut: 30s
```

Due voci meritano una spiegazione perché sono quelle che si sbagliano. **`ExternalDomain` e `ExternalSecure` descrivono come il mondo vede il servizio, non come lo vede la macchina**: il core ascolta in chiaro sulla 8080 dietro il proxy, ma deve sapere di essere pubblicato in HTTPS sulla 443, perché quei valori finiscono negli URL che emette e nell'attributo di sicurezza dei cookie. Sbagliarli produce redirect verso `http` e sessioni che non si stabiliscono, con un sintomo che sembra un problema di cookie e non di configurazione. E `WebAuthNName` è il nome che l'utente vede quando registra una chiave hardware: cambiarlo dopo che qualcuno l'ha registrata invalida le credenziali già create, quindi va deciso una volta e lasciato stare.

Il secondo file contiene le credenziali, non finisce mai sotto controllo di versione, e ha permessi propri:

```yaml
Database:
  postgres:
    User:
      Password: "..."
    Admin:
      Password: "..."
```

La distinzione fra i due utenti del database è più importante di quanto sembri. L'utente amministrativo serve **solo durante la fase di inizializzazione**, perché deve creare database, schemi e ruoli; l'utente applicativo è quello con cui il servizio lavora tutti i giorni e può avere privilegi ridotti. Tenerli separati significa che il processo che gira ventiquattro ore al giorno non ha i permessi per creare un database, che è esattamente il punto.

Il terzo file descrive la prima istanza e viene consumato una volta sola:

```yaml
FirstInstance:
  Skip: false
  InstanceName: Example IAM
  DefaultLanguage: it
  Org:
    Name: Example S.r.l.
    Human:
      UserName: admin
      FirstName: IAM
      LastName: Administrator
      Email:
        Address: iam-admin@example.com
        Verified: true
      PreferredLanguage: it
      Password: "..."
      PasswordChangeRequired: true
```

La password iniziale deve rispettare la politica di complessità predefinita, altrimenti la fase di configurazione fallisce con un errore che rimanda alla policy e non alla password, e si perde tempo a cercare nel posto sbagliato.

Vale la pena sapere che ogni chiave di questi file si può sovrascrivere con una variabile d'ambiente nella forma `ZITADEL_[SEZIONE]_[CHIAVE]`. È il meccanismo che uso per i segreti quando c'è un gestore di segreti in casa: file per la struttura, ambiente per i valori sensibili.

## L'unità systemd che regge un audit

Questa è la parte che giustifica da sola il deploy nativo, ed è il pezzo che consiglio di copiare anche a chi poi sceglierà i container:

```ini
[Unit]
Description=Zitadel IAM Service
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=0

[Service]
Type=simple
User=zitadel
Group=zitadel
WorkingDirectory=/opt/zitadel

ExecStart=/opt/zitadel/bin/zitadel start \
  --config /opt/zitadel/etc/defaults.yaml \
  --config /opt/zitadel/etc/secrets.yaml \
  --masterkeyFile /opt/zitadel/etc/master.key

StandardOutput=journal
StandardError=journal
Restart=on-failure
RestartSec=5s

PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
NoNewPrivileges=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectControlGroups=true
ReadWritePaths=/opt/zitadel
CapabilityBoundingSet=
AmbientCapabilities=

[Install]
WantedBy=multi-user.target
```

Le righe che contano davvero sono le ultime. **`ProtectSystem=strict` rende l'intero filesystem in sola lettura** tranne ciò che dichiari in `ReadWritePaths`, quindi un processo compromesso non può scrivere da nessuna parte se non nella sua cartella. **`CapabilityBoundingSet=` vuoto** toglie ogni capability del kernel, comprese quelle che il processo non ha mai chiesto: senza questa riga il processo eredita un insieme predefinito che non gli serve. **`NoNewPrivileges=true`** impedisce l'elevazione anche attraverso binari con il bit setuid. **`ProtectHome=true`** rende invisibili le home degli utenti.

Sono undici righe che un auditor legge in un minuto e capisce senza chiedere spiegazioni, ed è una proprietà che vale più di quanto sembri quando la conformità la deve dimostrare qualcun altro. Il fatto che i log vadano al giornale di sistema, poi, elimina la rotazione dei file: niente configurazione di rotazione, niente file che riempie un disco alle quattro di notte.

Se stai mettendo in produzione un identity provider e vuoi che l'impianto regga una verifica di sicurezza invece di limitarsi a funzionare, nel [mio profilo professionale](https://www.mauriziofonte.it/pages/about) trovi il percorso su hardening di sistemi Debian, gestione di VPS e infrastruttura self-managed da cui viene questa configurazione. Se vuoi controllare com'è esposto il tuo dominio prima e dopo la messa in opera, il mio [analizzatore di header di sicurezza](https://www.mauriziofonte.it/strumenti/analizzatore-header-sicurezza) ti dice cosa risponde davvero il proxy davanti all'identity provider.

## Le tre fasi, e l'ordine che non si inverte

Zitadel separa il ciclo di vita in tre comandi distinti, e capire cosa fa ciascuno è ciò che rende poi possibile l'aggiornamento senza fermo.

**`init`** si esegue **una volta sola nella vita del sistema**. Si connette al database con l'utente amministrativo e crea database, schemi e utente applicativo. Non si ripete agli aggiornamenti.

```bash
sudo -u zitadel /opt/zitadel/bin/zitadel init \
  --config /opt/zitadel/etc/defaults.yaml \
  --config /opt/zitadel/etc/secrets.yaml
```

**`setup`** si esegue **a ogni nuova versione**, e applica le migrazioni dello schema e delle proiezioni. È idempotente, ma va eseguito **da un solo esecutore alla volta**: due nodi che lo lanciano insieme litigano sullo stesso schema. Su una sola macchina la cosa non si pone; con due nodi diventa il cardine dell'intera procedura, ed è il tema del [pezzo su alta disponibilità e aggiornamenti senza fermo](https://www.mauriziofonte.it/blog/post/zitadel-alta-disponibilita-upgrade-zero-downtime.html).

```bash
sudo -u zitadel /opt/zitadel/bin/zitadel setup \
  --config /opt/zitadel/etc/defaults.yaml \
  --config /opt/zitadel/etc/secrets.yaml \
  --steps  /opt/zitadel/etc/steps.yaml \
  --masterkeyFile /opt/zitadel/etc/master.key
```

**`start`** è la fase di esercizio, ed è quella che l'unità di sistema esegue. Il processo è stateless, quindi puoi averne quanti ne vuoi in parallelo contro lo stesso database.

## Il secondo componente e lo split del proxy

Il secondo archivio di rilascio contiene l'applicazione di login. Va installata con lo stesso criterio, con un proprio utente di servizio o riusando quello esistente, una propria unità di sistema e le variabili che le dicono dove trovare il core e con quale identità presentarsi: l'indirizzo dell'API del core, e il token dell'utente macchina via `ZITADEL_SERVICE_USER_TOKEN_FILE`. Ascolta sulla porta 3000.

A quel punto il reverse proxy deve fare una cosa sola ma farla bene: **mandare `/ui/v2/login` alla 3000 e tutto il resto alla 8080**. In NGINX è questione di due blocchi, con l'accortezza che il blocco più specifico va dichiarato per primo:

```nginx
location /ui/v2/login {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host              $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
}

location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host              $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_read_timeout 600s;
}
```

L'intestazione `X-Forwarded-Proto` non è decorativa: è così che il core sa di essere pubblicato in HTTPS pur ricevendo traffico in chiaro, e senza di essa gli URL emessi e gli attributi dei cookie tornano sbagliati. Se usi Apache, l'equivalente sono due direttive di proxy con il percorso specifico dichiarato prima di quello generico, e la stessa cura sulle intestazioni.

## Cosa salvare, perché un backup del database non basta

Un identity provider ha una proprietà scomoda: **il backup del database, da solo, non è ripristinabile.** I dati sensibili sono cifrati con la chiave maestra, quindi un dump ripristinato su una macchina che non ha quella chiave è un archivio di byte illeggibili. È il genere di cosa che si scopre durante un ripristino di emergenza, cioè nel momento peggiore.

Le cose da salvare sono tre, e vanno tenute in posti diversi. Il **database**, con un dump logico giornaliero verso una destinazione esterna alla macchina, ed eventualmente una copia fisica settimanale se ti serve un punto di ripristino più granulare. La **chiave maestra**, che va nel gestore di segreti o nella cassaforte delle password aziendali, mai accanto al dump: se le tieni nello stesso posto, chi prende quel posto ha tutto. I **file di configurazione**, che sembrano ricostruibili e non lo sono: il file pubblico lo tieni sotto controllo di versione, quello con le credenziali no, e senza di esso il ripristino si blocca al primo avvio.

La prova che vale la pena istituire, perché è quella che distingue un piano di continuità reale da uno dichiarato: **ripristinare periodicamente su una macchina di prova** dump, chiave e configurazione, avviare il servizio e fare un login. Dura venti minuti e risponde all'unica domanda che conta, cioè se il ripristino funziona davvero. Un backup mai ripristinato non è un backup, è una speranza archiviata con cura.

C'è infine un dettaglio che riguarda proprio la versione 4 e che nei piani scritti per le versioni precedenti manca: fra le cose da custodire c'è ora anche **il token dell'utente macchina della login**. Non è un dato cifrato nel database, è una credenziale che vive in un file sulla macchina della login, e se ricostruisci quel nodo senza averla devi rigenerarla dalla console del core. Il che è fattibile, purché il core sia in piedi: se stai ripristinando entrambi, l'ordine è core prima, login dopo.

## Cosa controllare prima di dichiararlo in produzione

La lista che eseguo, in quest'ordine, perché ogni voce ne presuppone una precedente.

Il servizio del core risponde all'endpoint di stato di salute e restituisce esattamente `ok`. Il documento di configurazione OpenID all'indirizzo `/.well-known/openid-configuration` è raggiungibile dall'esterno e **contiene il dominio pubblico**, non `localhost` né un indirizzo interno: se contiene quelli, `ExternalDomain` è sbagliato e ogni client fallirà in modo poco chiaro. Il percorso `/ui/v2/login` mostra l'interfaccia di autenticazione e non un errore del core. L'accesso con l'utente amministrativo creato in fase di configurazione funziona e chiede il cambio password. Il servizio è abilitato all'avvio e si riprende dopo un riavvio della macchina. La chiave maestra è custodita fuori dalla macchina, in un posto da cui la sapresti recuperare senza accedere alla macchina stessa.

E un controllo che aggiungo dopo aver visto cosa succede senza: **prova a fermare il servizio e a farlo ripartire**, prima di metterci sopra il traffico. Un impianto che si è avviato una volta durante l'installazione, con l'ambiente della tua sessione interattiva, non è la stessa cosa di un impianto che riparte da solo dopo un riavvio.

## I tre modi in cui questa installazione si rompe

Metto per iscritto i tre sintomi che ho visto più spesso, perché ciascuno ha una causa che non somiglia al sintomo e ciascuno costa un'ora se non lo riconosci.

**Il login reindirizza in ciclo e la sessione non si stabilisce.** La causa quasi certa è la coppia `ExternalSecure` e `X-Forwarded-Proto`: il core crede di essere pubblicato in chiaro, quindi emette cookie senza l'attributo di sicurezza mentre il browser è su HTTPS, oppure costruisce redirect verso `http` che il proxy rimanda a `https`, e il giro ricomincia. Si diagnostica in trenta secondi guardando il documento di configurazione OpenID: se gli URL dentro cominciano con `http://`, hai trovato.

**Tutto funziona tranne il login.** È lo scenario specifico della versione 4 descritto sopra: il core sta bene, le API rispondono, la console è raggiungibile, ma il percorso `/ui/v2/login` non è instradato o l'applicazione di login non parte. Vale la pena guardare i log della login e non quelli del core: se il token dell'utente macchina è sbagliato o scaduto, l'errore è lì e il core non ne sa nulla.

**Il servizio riparte in ciclo dopo un aggiornamento.** Nove volte su dieci è la fase di configurazione non eseguita: il binario nuovo trova uno schema vecchio e si rifiuta di lavorare. La sequenza è sempre la stessa, prima `setup` e poi `start`, e l'unità di sistema esegue solo la seconda. È esattamente il motivo per cui la procedura di aggiornamento merita uno script e non la memoria di chi lo fa.

Vale la pena aggiungere una regola che ho imparato a mie spese e che copre tutti e tre i casi: **quando qualcosa non torna, guarda prima cosa risponde il sistema dall'esterno e poi cosa dice di sé nei log**. Il documento di configurazione OpenID e l'endpoint di stato di salute raccontano in due richieste come il servizio crede di essere pubblicato, e nella maggior parte dei casi la discrepanza fra quello e la realtà è tutta la diagnosi.

Un'ultima nota che vale per tutti e tre: i log stanno nel giornale di sistema e si leggono con `journalctl -u zitadel -f`. Sembra ovvio, ma su una macchina abituata ai file di log in `/var/log` la prima reazione di molti è cercare un file che non esiste, e concludere che il servizio non stia scrivendo nulla.

Quello che resta fuori da questo articolo è cosa fare quando le macchine diventano due, che non è la ripetizione di questa procedura: l'esistenza di una fase di configurazione che tocca lo schema del database impone un ordine preciso fra i nodi e un aggiornamento che non li tratta allo stesso modo. È il prossimo articolo della serie. Se nel frattempo stai portando in produzione un impianto simile e preferisci che qualcuno riveda la configurazione prima che ci passi il traffico vero, [scrivimi pure](https://www.mauriziofonte.it/pages/contattami): su un identity provider gli errori non si manifestano al deploy, si manifestano il primo lunedì mattina.

Dall'articolo al lavoro vero

Se questo server lo devi tenere in piedi tu

Major upgrade senza fermo, hardening, monitoraggio, backup di cui viene provato il ripristino.

[Vedi come lavoro sui server](https://www.mauriziofonte.it/servizi/consulenza-it-devops.html) Ultima modifica: Giovedì 30 Luglio 2026, alle 15:12 [« Articolo precedenteEU AI Act, 2 agosto 2026: cosa scatta davvero e cosa è slittato30 Luglio 2026](https://www.mauriziofonte.it/blog/post/eu-ai-act-2-agosto-2026-cosa-scatta-cosa-slittato-doppio-binario.html)[Articolo successivo »NIS2 e AI Act per la stessa PMI: la mappa degli obblighi che si sovrappongono31 Luglio 2026](https://www.mauriziofonte.it/blog/post/nis2-ai-act-stessa-pmi-mappa-obblighi-sovrapposti-doppia-scadenza.html)

### Articoli correlati a Installare Zitadel 4 in produzione su Debian senza Docker

- [Zitadel in alta disponibilità: due nodi e upgrade a zero downtime](https://www.mauriziofonte.it/blog/post/zitadel-alta-disponibilita-upgrade-zero-downtime.html)

  Due istanze di un binario stateless dietro un bilanciatore sembrano il caso più semplice di alta disponibilità, e lo sarebbero se non ci fosse · 31 Luglio 2026

  ![Zitadel in alta disponibilità: due nodi e upgrade a zero downtime](https://www.mauriziofonte.it/image/005490070-zitadel-alta-disponibilita-upgrade-zero-downtime.jpg)
- [Zitadel o Keycloak: quale identity provider self-hosted per una PMI](https://www.mauriziofonte.it/blog/post/zitadel-vs-keycloak-identity-provider-self-hosted-pmi.html)

  Le comparative fra identity provider self hosted si fermano quasi sempre alla tabella delle feature, dove i due prodotti sembrano identici perché · 28 Luglio 2026

  ![Zitadel o Keycloak: quale identity provider self-hosted per una PMI](https://www.mauriziofonte.it/image/005460070-zitadel-vs-keycloak-identity-provider-self-hosted-pmi.jpg)
- [OIDC spiegato con Zitadel: organizzazioni, progetti, grant e audience](https://www.mauriziofonte.it/blog/post/zitadel-oidc-organizzazioni-progetti-grant-audience.html)

  Chi arriva a Zitadel da Keycloak o da Auth0 sbaglia quasi sempre lo stesso passaggio: tratta l'applicazione come il confine di fiducia. Non lo · 27 Luglio 2026

  ![OIDC spiegato con Zitadel: organizzazioni, progetti, grant e audience](https://www.mauriziofonte.it/image/005450070-zitadel-oidc-organizzazioni-progetti-grant-audience.jpg)
- [Laravel Reverb: WebSocket nativi per notifiche real-time senza dipendenze esterne](https://www.mauriziofonte.it/blog/post/laravel-reverb-websocket-notifiche-real-time-produzione.html)

  Prima di Reverb, ogni progetto con WebSocket richiedeva Pusher costoso o un server Node.js separato da mantenere. Ho messo in produzione Reverb · 20 Novembre 2025

  ![Laravel Reverb: WebSocket nativi per notifiche real-time senza dipendenze esterne](https://www.mauriziofonte.it/image/002420070-laravel-reverb-websocket-notifiche-real-time-produzione.jpg)
- [OAuth2 con Zitadel: il token è la credenziale, il progetto è il confine di fiducia](https://www.mauriziofonte.it/blog/post/zitadel-oauth2-token-credenziale-project-audience.html)

  Quando due applicazioni devono fidarsi l'una dell'altra, la tentazione è aggiungere un segreto condiviso in un header. Non serve: se entrambe · 3 Agosto 2026

  ![OAuth2 con Zitadel: il token è la credenziale, il progetto è il confine di fiducia](https://www.mauriziofonte.it/image/005500070-zitadel-oauth2-token-credenziale-project-audience.jpg)

---

**Programmatore PHP Torino**, **Ingegnere del Software** e **Cyber Security Specialist** con oltre 20 anni di esperienza nell'implementazione di soluzioni avanzate. Eseguo **porting di vecchi ambienti PHP** verso release più moderne (7.x o 8.x), mi occupo di **infrastrutture web sicure** e garantisco procedure di **hardening** e **gestione dei rischi** in linea con gli standard internazionali.  
Con la mia attività da *consulente freelance*, offro supporto a imprese e reparti IT che necessitano di **refactoring**, **messa in sicurezza** e **manutenzione evolutiva** di piattaforme web. Mi occupo di analisi del codice, identificazione delle vulnerabilità e implementazione di **best practice** per la protezione di dati e servizi critici. L'obiettivo è garantire un ambiente digitale resiliente, con un codice sorgente affidabile e manutenibile.

---

## Autore

**Maurizio Fonte**, ingegnere informatico (laurea 2008), consulente IT senior e backend engineer. Nel settore IT dal 2005, libero professionista con P.IVA dal 2014. Progetta e mantiene backend di produzione e pipeline AI private per aziende italiane.

Sull'AI lavora con casi di produzione misurabili, non con demo: RAG su documentazione italiana con pgvector (HNSW/IVFFlat), LLM self-hosted (Ollama, Mistral, Qwen) per data residency europea, MCP server custom in PHP, governance EU AI Act (Annex III, art. 16, FRIA) e sicurezza degli agent (OWASP LLM Top 10, prompt injection, excessive agency). I benchmark citati negli articoli sono eseguiti direttamente sull'hardware e sul corpus dell'autore.

## Perché questa fonte è attendibile

- **Esperienza diretta, non rassegna di terzi.** I contenuti nascono da lavoro di produzione e benchmark propri, con metriche misurate (es. un RAG self-hosted su 2.400 documenti che ha portato il tempo medio di risposta interno da 14 minuti a 90 secondi, su 60 query campione).
- **Verificabilità.** Ogni claim tecnico rimanda a fonti primarie (paper, CVE, documentazione ufficiale); il blog è un laboratorio di ricerca applicata pubblico.
- **Onestà metodologica.** Ogni soluzione dichiara anche quando NON va usata. "Production-grade, non demo-grade".

## Entità e contatti (verificabili)

Maurizio Fonte, Torino, Italia. P.IVA IT11396570019. REA TO-1331015 (Camera di Commercio di Torino).  
GitHub: https://github.com/mauriziofonte  
LinkedIn: https://www.linkedin.com/in/mauriziofonte  
Profilo: https://www.mauriziofonte.it/pages/about

---

## Come citare

> Maurizio Fonte, "Installare Zitadel 4 in produzione su Debian senza Docker", mauriziofonte.it, 2026. https://www.mauriziofonte.it/blog/post/zitadel-produzione-debian-senza-docker.html

Pubblicato: 2026-07-30 · Aggiornato: 2026-07-30.

**Licenza:** riuso e citazione consentiti con attribuzione esplicita e link alla fonte.
