# Installer Inbox Zero sur Dokploy — Guide complet

Déploiement self-hosted d'Inbox Zero sur un serveur **Dokploy**, avec Postgres
et Redis hébergés dans le même Docker Compose, et **Claude (Anthropic)** comme
moteur IA.

> 🧭 **Vue d'ensemble** : on installe sur le serveur **Dokploy** (pas sur le
> serveur seo-insight prod, qu'on laisse isolé). Dokploy déploie nativement le
> `docker-compose.yml` fourni par le repo et gère le reverse-proxy Traefik +
> le SSL Let's Encrypt automatiquement.

---

## Sommaire

1. [Architecture déployée](#1-architecture-déployée)
2. [Prérequis](#2-prérequis)
3. [Étape A — DNS](#étape-a--dns)
4. [Étape B — Générer les secrets](#étape-b--générer-les-secrets)
5. [Étape C — Google OAuth (Console)](#étape-c--google-oauth-console)
6. [Étape D — Google Pub/Sub (gcloud CLI)](#étape-d--google-pubsub-gcloud-cli)
7. [Étape E — Clé Anthropic](#étape-e--clé-anthropic)
8. [Étape F — Déploiement sur Dokploy](#étape-f--déploiement-sur-dokploy)
9. [Étape G — Domaine + SSL](#étape-g--domaine--ssl)
10. [Étape H — Finaliser le push Pub/Sub](#étape-h--finaliser-le-push-pubsub)
11. [Étape I — Premier login + vérifs](#étape-i--premier-login--vérifications)
12. [Backups](#12-backups)
13. [Maintenance & mises à jour](#13-maintenance--mises-à-jour)
14. [Dépannage](#14-dépannage)

---

## 1. Architecture déployée

Le `docker-compose.yml` lance 6 services :

| Service | Rôle | Image |
|---|---|---|
| `web` | App Next.js (UI + API) | `ghcr.io/elie222/inbox-zero:latest` |
| `worker` | Traitement des jobs en file (profil `queue-worker`) | idem |
| `cron` | Déclenche les tâches planifiées (watch Gmail, digests, follow-ups…) | `alpine` |
| `db` | PostgreSQL 16 (profil `all`) | `postgres:16` |
| `redis` | Redis 7 (profil `all`) | `redis:7` |
| `serverless-redis-http` | Couche HTTP compat Upstash (profil `all`) | `hiett/serverless-redis-http` |

Activés via `COMPOSE_PROFILES=all,queue-worker`. `web` et `cron` n'ont pas de
profil → ils tournent toujours.

---

## 2. Prérequis

- Un serveur avec **Dokploy installé** et accessible.
- Un **nom de domaine** dont tu contrôles le DNS (`elevaseo.app`), pour le
  sous-domaine dédié `inbox.elevaseo.app`.
- Le **`gcloud` CLI** installé et authentifié (`gcloud auth login`).
- Un **compte Google Cloud** (projet GCP) — gratuit.
- Une **clé API Anthropic** (`sk-ant-...`) — https://console.anthropic.com
- *(Optionnel mais recommandé)* un compte **Resend** pour les emails (digests, notifs).

---

## Étape A — DNS

Dans **Cloudflare** (le domaine `elevaseo.app` y est géré), crée un
enregistrement **A** vers l'IP publique du serveur Dokploy, **proxy activé
(orange cloud)** :

```
Type   Nom      Valeur                    Proxy
A      inbox    <IP_DU_SERVEUR_DOKPLOY>   Proxied (orange)
```

Vérifie la propagation :

```bash
dig +short inbox.elevaseo.app
```

Avec le proxy Cloudflare, `dig` renvoie des **IP Cloudflare** (pas celle du
serveur) — c'est normal. Le TLS public est géré par Cloudflare (voir étape G).

---

## Étape B — Générer les secrets

Sur ta machine (ou le serveur), génère tous les secrets d'un coup. Tailles
renforcées (48 octets pour les secrets, 24 pour le salt, 32 pour Postgres).

**Shell fish** (substitution `(...)`, boucle `for ... end`) :

```fish
for v in AUTH_SECRET EMAIL_ENCRYPT_SECRET INTERNAL_API_KEY CRON_SECRET UPSTASH_REDIS_TOKEN GOOGLE_PUBSUB_VERIFICATION_TOKEN
    echo "$v="(openssl rand -hex 48)
end
echo "EMAIL_ENCRYPT_SALT="(openssl rand -hex 24)
echo "POSTGRES_PASSWORD="(openssl rand -hex 32)
```

**Shell bash/zsh** :

```bash
for v in AUTH_SECRET EMAIL_ENCRYPT_SECRET INTERNAL_API_KEY CRON_SECRET \
         UPSTASH_REDIS_TOKEN GOOGLE_PUBSUB_VERIFICATION_TOKEN; do
  echo "$v=$(openssl rand -hex 48)"
done
echo "EMAIL_ENCRYPT_SALT=$(openssl rand -hex 24)"
echo "POSTGRES_PASSWORD=$(openssl rand -hex 32)"
```

Garde précieusement cette sortie : tu colleras ces valeurs dans Dokploy à
l'étape F. **Ne les commit jamais.**

> 💡 `openssl rand -hex N` produit `2 × N` caractères hexadécimaux. `-hex 48`
> = 384 bits d'entropie : largement au-delà du nécessaire, mais demandé pour
> être « vraiment vraiment sécurisé ».

---

## Étape C — Google OAuth (Console)

⚠️ Cette partie ne se fait **pas** en CLI (gcloud ne gère pas les clients OAuth
web standard). Tout se passe dans la **Console Google Cloud**.

> 🔑 **Active d'abord la People API** — Better Auth s'en sert pour récupérer le
> profil Google au login. Sans elle, le login échoue avec `unable_to_create_user`.
> ```fish
> gcloud services enable people.googleapis.com --project=inbox-zero-499901
> ```
> (ou Console → APIs & Services → Enable APIs → *People API*.)

1. Va sur https://console.cloud.google.com et sélectionne (ou crée) ton projet.
2. **APIs & Services → OAuth consent screen** :
   - User type : **External**.
   - Renseigne nom de l'app, email de support, domaine.
   - **Scopes** : ajoute les scopes Gmail (lecture/modification/envoi selon les
     fonctions voulues). Au minimum les scopes Gmail demandés par l'app au login.
   - **Test users** : ajoute `elevaseo99@gmail.com` (le compte qui se connectera).
     En mode *Testing*, pas besoin de validation Google → suffisant pour un usage
     perso/équipe.
3. **APIs & Services → Credentials → Create Credentials → OAuth client ID** :
   - Application type : **Web application**.
   - **Authorized redirect URIs** :
     ```
     https://inbox.elevaseo.app/api/auth/callback/google
     ```
   - Crée, puis copie le **Client ID** et le **Client secret** →
     `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`.

---

## Étape D — Google Pub/Sub

> Projet GCP de référence dans ce guide : **`inbox-zero-499901`**. Remplace-le
> par le tien si différent (visible sous « Nom du sujet » à la création du topic).

Les notifications Gmail (push) passent par un topic Pub/Sub. Deux façons de le
créer : Console ou CLI. **Crée le topic + le binding Publisher maintenant ;
l'abonnement push se fait plus tard (étape H), après le SSL.**

### Option 1 — Console

1. **Pub/Sub → Sujets → Créer un sujet** :
   - **ID du sujet** : `inbox-zero`
   - ❌ **Décoche « Ajouter un abonnement par défaut »** — c'est un abonnement
     *pull* qu'Inbox Zero n'utilise pas (lui a besoin d'un *push*). Le laisser
     coché créerait un abonnement inutile qui accumule des messages.
   - Laisse décoché : schéma, ingestion, conservation, export BigQuery/Cloud
     Storage.
   - **Chiffrement** : garde **Clé gérée par Google** (le défaut). Inutile de
     passer en Cloud KMS : les messages ne contiennent **aucun contenu d'email**,
     juste une enveloppe `{emailAddress, historyId}`.
2. Tu verras un avertissement *« Les messages publiés dans ce sujet seront
   perdus… »* → **normal et sans gravité** : le topic n'a pas encore d'abonné, et
   Gmail ne publie rien tant qu'aucun compte n'est connecté. L'avertissement
   disparaît dès que l'abonnement push existe (étape H). **Ne crée pas** de règle
   de conservation pour le faire taire (rétention inutile + facturée). Clique OK.
3. **Autoriser Gmail à publier** : sur le topic → onglet **Autorisations** →
   *Ajouter un compte principal* :
   - Principal : `gmail-api-push@system.gserviceaccount.com`
   - Rôle : **Pub/Sub Publisher**

### Option 2 — gcloud CLI (équivalent, shell fish)

```fish
gcloud config set project inbox-zero-499901
gcloud services enable gmail.googleapis.com pubsub.googleapis.com people.googleapis.com

# Topic
gcloud pubsub topics create inbox-zero

# Autoriser Gmail à publier (compte de service Google fixe)
gcloud pubsub topics add-iam-policy-binding inbox-zero \
  --member="serviceAccount:gmail-api-push@system.gserviceaccount.com" \
  --role="roles/pubsub.publisher"
```

> ⏸️ **Ne crée pas encore l'abonnement push** : l'endpoint HTTPS doit déjà
> répondre, sinon Google rejette la validation. On le fait à l'**étape H**.

La valeur à mettre dans l'env :

```
GOOGLE_PUBSUB_TOPIC_NAME=projects/inbox-zero-499901/topics/inbox-zero
```

---

## Étape E — Clé Anthropic

1. https://console.anthropic.com → **API Keys** → crée une clé.
2. Note-la (`sk-ant-...`) pour `ANTHROPIC_API_KEY`.
3. Modèles utilisés dans la config :
   - `DEFAULT_LLMS=anthropic:claude-sonnet-4-6` (tri/rédaction)
   - `ECONOMY_LLMS=anthropic:claude-haiku-4-5` (tâches simples, moins cher)

Assure-toi d'avoir des crédits sur le compte Anthropic.

---

## Étape F — Déploiement sur Dokploy

1. Dans Dokploy : **Create Project** → puis **Create Service → Compose**.
2. **Source** : **Raw** → colle le contenu de **`docker-compose.dokploy.yml`**
   (PAS le `docker-compose.yml` du repo).
   > Ce compose adapté Dokploy : (a) met `web` sur `dokploy-network` — sinon
   > **502 Bad Gateway**, Traefik ne joint pas le conteneur ; (b) injecte les
   > variables via interpolation `${VAR}` (anchor `x-app-env`) au lieu d'un
   > `env_file` au chemin non garanti sous Dokploy.
3. **Environment** : colle ton `.env` complet (généré par `generate-env.sh`).
   Ces variables alimentent les `${VAR}` du compose. Points clés :

   ```bash
   COMPOSE_PROFILES=all,queue-worker
   NEXT_PUBLIC_BASE_URL=https://inbox.elevaseo.app

   POSTGRES_USER=postgres
   POSTGRES_DB=inboxzero
   POSTGRES_PASSWORD=<étape B>

   UPSTASH_REDIS_URL=http://serverless-redis-http:80
   UPSTASH_REDIS_TOKEN=<étape B>

   AUTH_SECRET=<étape B>
   EMAIL_ENCRYPT_SECRET=<étape B>
   EMAIL_ENCRYPT_SALT=<étape B>
   INTERNAL_API_KEY=<étape B>
   CRON_SECRET=<étape B>

   GOOGLE_CLIENT_ID=<étape C>
   GOOGLE_CLIENT_SECRET=<étape C>
   GOOGLE_PUBSUB_TOPIC_NAME=projects/inbox-zero-499901/topics/inbox-zero
   GOOGLE_PUBSUB_VERIFICATION_TOKEN=<étape B>

   DEFAULT_LLMS=anthropic:claude-sonnet-4-6
   ANTHROPIC_API_KEY=<étape E>
   ECONOMY_LLMS=anthropic:claude-haiku-4-5

   AUTH_ALLOWED_EMAILS=elevaseo99@gmail.com,bastien@elevaseo.com
   ADMINS=bastien@elevaseo.com

   RESEND_API_KEY=<resend, optionnel>
   RESEND_FROM_EMAIL=noreply@elevaseo.app
   NEXT_PUBLIC_IS_RESEND_CONFIGURED=true
   ```

4. **Deploy**. Au premier démarrage du conteneur `web`, les migrations Prisma
   s'appliquent automatiquement sur la base.
5. Surveille les logs du service `web` jusqu'à voir le serveur Next.js prêt, puis
   vérifie que les variables sont bien injectées (point sensible Dokploy) :
   ```bash
   docker exec $(docker ps -qf "name=web") printenv \
     | grep -E "AUTH_SECRET|GOOGLE_CLIENT_ID|ANTHROPIC_API_KEY|DATABASE_URL"
   ```
   Si ces lignes apparaissent → l'interpolation `${VAR}` fonctionne.

> 🔐 **Restriction des inscriptions** : on utilise `AUTH_ALLOWED_EMAILS` (liste
> d'adresses exactes) plutôt que `AUTH_ALLOWED_EMAIL_DOMAINS`, car le compte de
> connexion est `elevaseo99@gmail.com` — restreindre par domaine `elevaseo.com`
> le bloquerait. **Ne saute pas cette ligne** sur une instance exposée.

---

## Étape G — Domaine + SSL (via Cloudflare)

Le domaine est **proxifié par Cloudflare** (orange cloud). Cloudflare gère le
certificat public ; Dokploy/Traefik sert un **Origin Certificate Cloudflare** à
l'origine. Mode SSL/TLS retenu : **Full (Strict)**.

### G.1 — Origin Certificate Cloudflare
1. Cloudflare → **SSL/TLS → Origin Server → Create Certificate** (couvre
   `inbox.elevaseo.app` ou `*.elevaseo.app`). Copie le **certificat** + la **clé
   privée**.
2. Dokploy → section **Certificates** → ajoute un certificat custom (colle cert +
   clé). Traefik le servira par SNI.
3. Cloudflare → **SSL/TLS → Overview** → mode **Full (Strict)**.

### G.2 — Domaine dans Dokploy
Sur le service **`web`** → onglet **Domains** → **Add Domain** :

| Champ | Valeur |
|---|---|
| Service Name | `web` |
| Host | `inbox.elevaseo.app` |
| Path | `/` |
| Internal Path | `/` |
| Strip Path | OFF |
| Container Port | `3000` |
| Custom Entrypoint | OFF |
| HTTPS | **ON** |
| **Certificate Provider** | **`None`** (c'est Cloudflare qui fournit le cert, pas Let's Encrypt) |
| Middlewares | (vide) |

Puis **Create** et **redeploie le compose**.

### G.3 — Ne pas bloquer Google côté Cloudflare ⚠️
Le webhook Pub/Sub (`POST /api/google/webhook`) transite par Cloudflare. Si
**Bot Fight Mode** / WAF est actif, Google peut être bloqué → notifications Gmail
perdues silencieusement. Crée une **WAF skip rule** sur `/api/google/webhook`
(idéalement `/api/*`) ou désactive Bot Fight Mode.

### G.4 — Vérifications
```bash
# Servi par Cloudflare ?
curl -sI https://inbox.elevaseo.app | grep -i server          # -> cloudflare
# Le webhook répond bien l'app (401/400), pas un challenge CF (403) ?
curl -s -o /dev/null -w '%{http_code}\n' \
  "https://inbox.elevaseo.app/api/google/webhook?token=bad"   # -> 401/400 attendu
```

> *Alternative sans Cloudflare* : passer le DNS en « DNS only » (gris), mettre
> `Certificate Provider: Let's Encrypt` sur le domaine, configurer l'email
> Let's Encrypt dans Dokploy (*Web Server / Settings*) et ouvrir les ports 80/443.

---

## Étape H — Finaliser le push Pub/Sub

Maintenant que `https://inbox.elevaseo.app` répond, crée l'abonnement push
(shell fish) :

```fish
set DOMAIN "inbox.elevaseo.app"
set VERIFICATION_TOKEN "<ton GOOGLE_PUBSUB_VERIFICATION_TOKEN, étape B>"

gcloud pubsub subscriptions create inbox-zero-push \
  --project=inbox-zero-499901 \
  --topic=inbox-zero \
  --push-endpoint="https://$DOMAIN/api/google/webhook?token=$VERIFICATION_TOKEN" \
  --ack-deadline=10
```

Le webhook Inbox Zero (`/api/google/webhook`) valide le `?token=` contre
`GOOGLE_PUBSUB_VERIFICATION_TOKEN`. Vérifie que les deux correspondent.

Vérifie l'abonnement :

```fish
gcloud pubsub subscriptions describe inbox-zero-push --project=inbox-zero-499901
```

Dans la sortie, `pushConfig.pushEndpoint` doit afficher ton URL
`.../api/google/webhook?token=...`.

---

## Étape I — Premier login + vérifications

1. Ouvre `https://inbox.elevaseo.app`.
2. Connecte-toi avec `elevaseo99@gmail.com` (**Test user** étape C, et listé dans
   `AUTH_ALLOWED_EMAILS`).
3. Accepte les permissions Gmail.
4. Vérifie côté serveur :
   - Logs `web` : pas d'erreur de DB / d'auth.
   - Logs `worker` : il consomme bien la file.
   - Logs `cron` : messages `[cron] Processing ...` toutes les 15 min.
5. Envoie-toi un email de test → il doit apparaître / être traité par l'IA.

---

## 12. Backups

La base tourne **dans le compose** (volume Docker `database-data`). **Sans
backup, une perte du serveur = perte des données.**

- Dans Dokploy, configure un **backup planifié** de la base Postgres vers un
  stockage S3 (fonction native de Dokploy), ou
- Mets en place un `pg_dump` cron exportant hors-serveur :
  ```bash
  docker exec <conteneur_db> pg_dump -U postgres inboxzero | gzip > backup_$(date +%F).sql.gz
  ```

Teste au moins une **restauration** avant de considérer le backup fiable.

---

## 13. Maintenance & mises à jour

- **Mise à jour de l'app** : les images sont en `:latest` avec
  `pull_policy: always`. Un redéploiement Dokploy (ou `Redeploy`) récupère la
  dernière version. Les migrations Prisma s'appliquent au démarrage de `web`.
- **Watches Gmail** : renouvelées automatiquement par le service `cron`
  (`/api/watch/all` toutes les 6 h). Si les notifications s'arrêtent, vérifie ce
  cron et la subscription Pub/Sub.
- **Rotation des secrets** : si tu changes `EMAIL_ENCRYPT_SECRET` /
  `EMAIL_ENCRYPT_SALT`, les tokens OAuth chiffrés en base deviennent illisibles
  → les comptes devront se reconnecter. À éviter sauf incident.

---

## 14. Dépannage

| Symptôme | Piste |
|---|---|
| `Invalid verification token` dans les logs `web` | Le `?token=` du push subscription ≠ `GOOGLE_PUBSUB_VERIFICATION_TOKEN`. Recrée la subscription. |
| Push subscription refusée par Google | L'URL HTTPS ne répond pas encore (SSL pas prêt). Termine l'étape G d'abord. |
| Pas de notifications Gmail | Vérifie le binding `roles/pubsub.publisher` sur `gmail-api-push@system.gserviceaccount.com` + le cron `/api/watch/all`. |
| Boucle de redirection / OAuth & webhook KO | Mode Cloudflare en **Flexible**. Passe en **Full (Strict)**. |
| Erreur 526 (invalid SSL cert) Cloudflare | Mode **Full (Strict)** mais Origin Certificate absent/incorrect côté Dokploy. Vérifie le certificat custom dans Dokploy → Certificates. |
| Webhook renvoie un challenge CF (403) | Bot Fight Mode / WAF bloque Google. Ajoute une skip rule sur `/api/google/webhook`. |
| **502 Bad Gateway** | Traefik ne joint pas `web` : il n'est pas sur `dokploy-network` (utilise `docker-compose.dokploy.yml`), ou `web` a crashé (variables manquantes → `printenv` vide). |
| `REDIS_URL is required for the BullMQ worker` | `REDIS_URL` absent. Le compose Dokploy le définit (`redis://redis:6379`) ; vérifie que tu utilises bien `docker-compose.dokploy.yml`. |
| Worker actif mais ne traite rien | `QUEUE_BACKEND` non aligné : web enfile en `internal`, worker écoute BullMQ. Force `QUEUE_BACKEND=bullmq` (défaut du compose Dokploy). |
| Domaine renvoie 404 / bad gateway | Le service `web` n'est pas sur `dokploy-network`, ou le compose n'a pas été redéployé après ajout du domaine. |
| `redirect_uri_mismatch` au login | L'URI dans la Console OAuth doit être **exactement** `https://inbox.elevaseo.app/api/auth/callback/google`. |
| `unable_to_create_user` au login | **People API désactivée**. `gcloud services enable people.googleapis.com` (Better Auth lit le profil Google via cette API). Attends 1-2 min de propagation. |
| `access_denied` / "has not completed Google verification" | Compte non ajouté en **Test user** (OAuth consent screen en mode Testing). |
| Accès refusé au login | Email non listé dans `AUTH_ALLOWED_EMAIL_DOMAINS`/`AUTH_ALLOWED_EMAILS`, ou pas en Test user GCP. |
| `web` redémarre en boucle | Vérifie `DATABASE_URL`/Postgres up, et que les secrets requis sont tous présents. |
| Worker ne traite rien | `COMPOSE_PROFILES` doit inclure `queue-worker` ; vérifie la connexion Redis. |

---

### Fichiers liés

- `.env.dokploy.example` — template d'environnement prêt à remplir (racine du repo).
- `docker-compose.yml` — la stack déployée (racine du repo).
