La suite numérique souveraine et open source qui remplace vraiment les géants américains
@ultisuite/deploy (0.19.0)
Installation
@ultisuite:registry=https://gitea.reduav.eu/api/packages/UltiSuite/npm/npm install @ultisuite/deploy@0.19.0"@ultisuite/deploy": "0.19.0"About this package
@ultisuite/deploy
Installeur et orchestrateur en ligne de commande pour la Ultisuite — alternative souveraine à Google Workspace (mail, drive, agenda, contacts, visio, photos, IA). Le CLI tire les images Docker précompilées depuis le registry Gitea (pas de clone du code source app), génère la configuration, gère le TLS et pilote la stack Compose depuis une interface terminal (Ink).
Installation
Le package est publié sur le registry npm privé Gitea (org ultisuite), pas sur npmjs.org.
1. Configurer le registry @ultisuite
npm config set @ultisuite:registry https://gitea.reduav.eu/api/packages/ultisuite/npm/
npm config set -- '//gitea.reduav.eu/api/packages/ultisuite/npm/:_authToken' '<token-read:package>'
Le token est un PAT Gitea avec au minimum read:package (token pull d’instance / licence, ou compte robot dédié).
2. Installer le CLI
# Global
npm install -g @ultisuite/deploy@latest
# Ou sans install globale
npx @ultisuite/deploy --help
3. Lancer l’install (mode images)
export ULTI_LICENSE_KEY='ulti_…' # clé licence instance
# export ULTI_LICENSE_URL='https://license.example.com' # si hors défaut
ulti-deploy install -y \
--domain suite.example.com \
--tls managed-acme \
--acme-email admin@example.com \
--version v1.2.3 \
--license-key "$ULTI_LICENSE_KEY"
Le CLI échange la licence contre un token pull registry, télécharge le deploy-kit, fait docker login + compose pull + up — sans --build et sans déposer le source Go/Next sur le serveur.
Doc closed-source détaillée : docs/closed-source-deploy.md.
Mode interne (clone + build)
Réservé aux machines qui ont déjà accès aux repos source :
ulti-deploy install -y --dev-source --domain localhost.test --tls none
Prérequis
- Linux avec Docker + Docker Compose v2
- Accès réseau sortant vers
gitea.reduav.eu(registry container + npm + API licence) - Node ≥ 20 (pour
npx/ install globale) - Droits suffisants sur le répertoire de travail (par défaut
/opt/ultisuite) - Clé licence
ULTI_LICENSE_KEY(mode images)
Commandes
| Commande | Description |
|---|---|
ulti-deploy install |
Config, TLS, pull images registry, démarrage (ou --dev-source = clone+build) |
ulti-deploy doctor |
Diagnostic complet (Docker, disque, .env, conteneurs, santé backend, pare-feu) |
ulti-deploy verify |
Teste l'accessibilité externe des domaines publics (DNS, TLS, endpoints HTTP) |
ulti-deploy security-check |
Audit sécurité hôte (SSH, pare-feu, sudo, CVE Trivy, outils offensifs) — à lancer sur l'hôte, pas dans un conteneur ; outil partiel, ne remplace pas un audit pro |
ulti-deploy upgrade |
Met à jour images / kit (ou tags git en mode --dev-source) |
ulti-deploy migrate-to-images |
Passe une ancienne install clone+build vers images (supprime le source) |
ulti-deploy backup |
Exporte une archive chiffrée (config, overrides, état, dumps PostgreSQL, object storage) |
ulti-deploy restore <archive> |
Restaure une archive chiffrée puis redémarre la stack |
ulti-deploy start / stop / restart / status / logs |
Cycle de vie de la stack |
ulti-deploy privacy-check |
Smoke RGPD + contrôle anti-fuite source (mode images) |
ulti-deploy modules |
Active ou arrête un module (list, enable, disable) sans recréer le reste de la stack |
ulti-deploy infisical render |
Exporte le coffre, expanse {{VAR}}, écrit le tmpfs (opt-in) |
ulti-deploy infisical import |
Pousse un .env existant vers le coffre (--apply, --cutover) |
ulti-deploy env explain <CLÉ> |
Références {{VAR}} du template et dérivées generate.ts |
Option globale -w, --workspace <dir> pour cibler un autre répertoire de travail.
Scale (workers, PgBouncer, Meilisearch, Hocuspocus Redis, mail bodies S3, backup RustFS) : voir ulti-backend/docs/scale.md. Overlays modules : meilisearch, pgbouncer, matomo (--enable-module …).
Modules (enable / disable scopés)
ulti-deploy modules list -w /opt/ultisuite
ulti-deploy modules enable matomo -y -w /opt/ultisuite
ulti-deploy modules disable matomo -y -w /opt/ultisuite
ulti-deploy modules disable matomo -y --purge-data -w /opt/ultisuite
enable lance compose up -d uniquement sur matomo, matomo-db et matomo-worker. disable fait compose stop puis rm -f de ces services. Les volumes restent, sauf avec --purge-data. Pas de up -d global, pas de --remove-orphans. Le profil cell est refusé (une instance par shard fragmenterait les statistiques).
Mode non-interactif & dry-run
install, upgrade, backup et restore acceptent un mode sans wizard Ink — obligatoire pour les agents et l'automatisation CI :
# Installation prod (ACME + images)
ulti-deploy install -y \
--domain suite.example.com \
--tls managed-acme --acme-email admin@example.com \
--hosts-mode subdomain \
--version v1.2.3 \
--license-key "$ULTI_LICENSE_KEY" \
--enable-module ai --enable-module mailEdge
# Config JSON (partiel) + surcharges CLI
ulti-deploy install -y --config ./deploy.json --enable-module jitsi --license-key "$ULTI_LICENSE_KEY"
# Dry-run : génère .env + overrides, sans Docker
ulti-deploy install -y --dry-run --domain suite.example.com --tls none --license-key "$ULTI_LICENSE_KEY"
# Profil cell (shard hyperscale) — cellId + router URL obligatoires
# Prérequis : control plane déjà up (ulti-router). Voir runbook hyperscale.
# ULTIROUTER_ADMIN_TOKEN : via envOverrides JSON / .env (pas un flag CLI).
ulti-deploy install -y \
--domain cell1.example.com \
--tls managed-acme --acme-email admin@example.com \
--version v1.2.3 \
--license-key "$ULTI_LICENSE_KEY" \
--deployment-profile cell \
--cell-id cell-eu-west-1 \
--router-url https://router.example.com
Profils
| Profile | Défaut | Compose overlay | Control plane |
|---|---|---|---|
standalone |
oui | aucun (stack classique) | Jamais requis |
cell |
non | deploy/cell/docker-compose.cell.yml (ulti-backend) — pin ULTI_DEPLOYMENT_MODE=cell + ids |
Déployé à part : deploy/controlplane/docker-compose.controlplane.yml (ulti-router + Postgres). Pas via ce CLI. |
Runbooks (ulti-backend) :
- Standalone self-host →
docs/hyperscale/09-deploy-standalone.md - Hyperscale CP + cells →
docs/hyperscale/10-deploy-hyperscale.md
install
| Flag | Description |
|---|---|
-y, --non-interactive |
Sans wizard |
-c, --config <file> |
JSON partiel (voir ci-dessous) |
--dry-run |
Pas de démarrage Docker |
--version <tag> |
Tag images / deploy-kit (ex. v1.2.3) |
--license-key <key> |
Clé licence (ou env ULTI_LICENSE_KEY) |
--license-url <url> |
API licence (défaut / env ULTI_LICENSE_URL) |
--dev-source |
Clone git + build local (interne) |
--no-build |
Pas de rebuild (mode --dev-source) |
--domain, --tls, --acme-email, --cert, --key |
Domaine & TLS |
--hosts-mode path|subdomain |
URLs apex vs sous-domaines |
--cookie-domain |
Cookie SSO (ex. .example.com) |
--backend-ref, --client-ref |
Tag/branche git (--dev-source only) |
--enable-module, --disable-module |
Modules (répétables) : jitsi, ultichat, immich, onlyoffice, ai, stt, richtext, mailEdge, sovereign, license, meilisearch, pgbouncer, netdata, matomo |
--deployment-profile standalone|cell |
Défaut standalone. cell merge l'overlay cell (env shard) — ne démarre pas ulti-router |
--cell-id <id> |
Identifiant du shard (requis en profil cell) |
--router-url <url> |
URL de base ulti-router déjà déployé (requis en profil cell) |
--ear |
Active le chiffrement au repos (prod uniquement, opt-in) |
--ear-key-server <url> |
URL ultikeyd (implique --ear) |
--ear-lease-ttl-hours <n> |
Bail de déverrouillage LUKS en heures (défaut 168) |
upgrade
ulti-deploy upgrade --check-only # liste sans appliquer
ulti-deploy upgrade -y --yes --no-backup # applique sans prompt
ulti-deploy upgrade -y --yes --no-backup --version v0.43.4 --frontend-version v0.44.0
| Flag | Description |
|---|---|
-y, --non-interactive |
Sans prompts |
--yes |
Applique (sinon exit 2 si MAJ dispo) |
--check-only |
Détection seule |
--json |
Avec --check-only : une ligne JSON sur stdout (distribution, backend, frontend, cliVersion, checkedAt, ou {"error":"..."} + exit 1), logs sur stderr |
--version <tag> |
Tag backend images / deploy-kit (ULTI_VERSION) |
--frontend-version <tag> |
Tag suite-frontend (ULTI_FRONTEND_VERSION), versionné à part du backend |
--backup-password / --backup-password-env |
Backup pré-upgrade |
--no-backup |
Saute le backup |
En mode images, sans --frontend-version, l'upgrade prend le dernier tag stable vX.Y.Z de suite-frontend dans le registry et l'écrit dans deploy-kit/.env avant le pull. Si le registry ne répond pas, la valeur actuelle est gardée (avertissement, jamais latest). Un tag registry plus ancien que le pin actuel n'est pas appliqué : passez --frontend-version pour downgrader.
backup / restore
export ULTI_DEPLOY_BACKUP_PASSWORD='…'
ulti-deploy backup -y -o ./backups/manual.ultibak
export ULTI_DEPLOY_RESTORE_PASSWORD='…'
ulti-deploy restore -y --yes ./backups/manual.ultibak
| backup | restore |
|---|---|
--password / --password-env (ULTI_DEPLOY_BACKUP_PASSWORD) |
--password / --password-env (ULTI_DEPLOY_RESTORE_PASSWORD) |
--include env,overrides,state,database,objectStorage,… |
--yes requis (destructif) |
--no-restart |
Doc agents détaillée : .cursor/rules/ulti-deploy-cli.mdc (dans ce repo).
Exemple de deploy.json (tous les champs sont optionnels sauf domain) :
{
"domain": "suite.example.com",
"workspaceDir": "/opt/ultisuite",
"deploymentProfile": "standalone",
"modules": { "ai": true, "mailEdge": true },
"tls": { "mode": "managed-acme", "acmeEmail": "admin@example.com" },
"ear": {
"enabled": true,
"keyServerUrl": "https://keys.example.com",
"leaseTtlHours": 168
},
"secrets": { "POSTGRES_PASSWORD": "mon-secret-impose" }
}
Les secrets non fournis sont générés aléatoirement ; ceux présents dans secrets sont conservés.
Encryption at rest (prod)
Chiffrement au repos opt-in, réservé aux installs production (refusé avec --dev-source, domaine local, ou ULTID_ENV ≠ production).
ulti-deploy install -y \
--domain suite.example.com \
--tls managed-acme --acme-email admin@example.com \
--version v1.2.3 \
--license-key "$ULTI_LICENSE_KEY" \
--ear --ear-key-server https://keys.example.com
# ou JSON deploy.json : ear.enabled + ear.keyServerUrl (+ ear.leaseTtlHours optionnel, défaut 168)
Variables hôte (écrites dans le .env backend quand EAR est activé) :
| Variable | Description |
|---|---|
EAR_ENABLED |
true quand opt-in actif |
ULTIKEYD_URL |
URL du serveur de garde de clés |
EAR_LEASE_TTL_HOURS |
Durée du bail de déverrouillage (défaut 168) |
ULTISEALD_SOCKET |
Socket unix ultiseald (défaut /run/ultiseal/ultiseald.sock) |
ULTISEALD_SOCKET_TOKEN |
Bearer pour wrap/unwrap (doit matcher /etc/ultisuite/ultiseald.env côté serve) |
EAR_*_HOST_PATH |
Bind mounts LUKS (postgres / rustfs / keydb — Valkey data) |
Quand EAR_ENABLED=true et ultiseald serve a un bail ouvert : ulti-deploy backup produit ULTIBAK2 (DEK_bak wrappé via socket, sans OTP). Définir ULTISEALD_SOCKET_TOKEN dans l’env du CLI quand serve l’exige. Sans EAR : ULTIBAK1 (mot de passe). Restore V2 exige aussi le lease ouvert.
Détail garde de clés et LUKS : ulti-backend/docs/security/ear-key-custody.md. Rotation DEK (fenêtre maintenance) : ulti-backend/docs/security/ear-rotation.md. Voir aussi .env.example à la racine de ce repo.
Répertoire de travail
Mode images (défaut prod) :
/opt/ultisuite/
├─ deploy-kit/ kit Compose (nginx, overlays, .env.example) — pas de source app
├─ overrides/ fichiers générés (compose TLS, nginx, certs)
├─ backups/ archives .ultibak
└─ state.json état (config + version images déployée)
Mode --dev-source (interne) :
/opt/ultisuite/
├─ ulti-backend/ clone backend (build context Docker)
├─ gmail-interface-clone/ clone frontend (sibling Compose)
├─ overrides/
├─ backups/
└─ state.json
Configuration & secrets
Le CLI lit le .env.example du backend et génère un .env en :
- remplaçant tous les secrets
changemepar des valeurs aléatoires fortes (chaque secret reste overridable dans l'assistant) ; - préservant les placeholders
{{VAR}}(expansés au runtime, comme le fait le backend) ; - basculant les URLs publiques en
https://<domaine>et le frontend sursuite-frontend:3000; - activant/désactivant les modules optionnels (Jitsi, Immich, OnlyOffice, UltiAI, mail edge…).
install --infisical prend les valeurs dans un coffre Infisical au lieu d'un .env de secrets. Runbook : docs/infisical.md.
TLS / certificats
Quatre modes au choix dans l'assistant :
- Let's Encrypt redondant (
managed-acme) : terminateur TLStls-edge+ sidecaracme.shavec bascule automatique entre autorités (Let's Encrypt → ZeroSSL → Buypass). - Certificats custom (
managed-custom) : montage de voscert/key. - Greffe nginx existant (
graft) : génère un server-block pour votre nginx hôte (qui gère le TLS) et bind la stack sur127.0.0.1:<port>. - HTTP seul (
none) : pour le dev ou derrière un autre terminateur TLS.
Sauvegarde & restauration
Les archives .ultibak sont chiffrées en AES-256-GCM :
| Format | Quand | Clé |
|---|---|---|
| ULTIBAK1 | Dev / non-EAR (défaut) | Mot de passe (scrypt) — ULTI_DEPLOY_BACKUP_PASSWORD |
| ULTIBAK2 | EAR_ENABLED=true |
DEK_bak aléatoire, wrappée via socket ultiseald (lease ouvert, sans OTP) |
L'assistant permet de choisir quoi inclure : configuration, overrides, état, dumps PostgreSQL (toutes bases), et optionnellement les volumes object storage.
Object storage (objectStorage, défaut on) : exporte le volume Docker ultisuite_rustfs_data, ou — sous EAR — le bind mount LUKS (EAR_RUSTFS_BIND / EAR_RUSTFS_HOST_PATH). Ce stockage contient tous les buckets RustFS, dont mail-bodies (corps de mails et enveloppes ciphertext Zero-Trust), mail-attachments, et les objets Drive chiffrés UENV. Avec MAIL_BODY_STORAGE=s3 (défaut installs neuves), un dump Postgres seul ne suffit pas — garder objectStorage en prod.
Restore ULTIBAK2 : ultiseald serve doit avoir un lease ouvert (unwrap unattended). Restore ULTIBAK1 : mot de passe inchangé.
Scale / knobs associés : ulti-backend/docs/scale.md (PgBouncer, Meilisearch, workers, Hocuspocus Redis, body backfill).
Checklist RGPD (déploiement)
Après installation ou changement de modules :
- Région d'hébergement —
PRIVACY_HOSTING_PROVIDER(défaut OVH) +PRIVACY_DATA_REGION(ex.FR) pour la disclosure. - Contacts confidentialité — renseigner
NEXT_PUBLIC_PRIVACY_CONTACT_EMAILetNEXT_PUBLIC_DPO_EMAILdans le.envfrontend. - Matomo cookieless — module optionnel, off par défaut.
ulti-deploy modules enable matomo -ydémarre MariaDB et Matomo. Le tracking se règle ensuite dans l'admin vendor. Pas deNEXT_PUBLIC_MATOMO_*(la config est runtime). - Pages légales publiques — vérifier
/privacy,/gdpr,/terms,/subprocessors,/dpa(HTTP 200 sans login). - Sous-traitants dynamiques — après activation d'une intégration externe (IA, OAuth, Stripe, VirusTotal…), contrôler
/subprocessorsetGET /api/v1/privacy/disclosure. - Intégrations org — documenter les webhooks et services tiers configurés par l'organisation auprès des utilisateurs.
- Sauvegardes vs erasure — voir
ulti-backend/docs/compliance/backup-erasure.md: rétention.ultibak, destruction des archives hors délai ; une suppression utilisateur n'efface pas rétroactivement les backups existants. - Restore drill — exercice périodique :
ulti-backend/docs/compliance/restore-drill.md(jamais en prod avec une archive pré-erasure). - Registre incidents — Admin → Confidentialité → registre d'incidents (délai CNIL 72 h) ; procédure
docs/compliance/incident-response.md. - Smoke RGPD —
ulti-deploy privacy-check(pages légales +GET /api/v1/privacy/disclosure). - SOC2 evidence (optionnel B2B) —
ulti-backend/docs/compliance/soc2-evidence.md(export revue d'accès utilisateurs + audit).
SecNumCloud (composition OVH)
- Déployer sur une offre OVH qualifiée SecNumCloud (catalogue ANSSI).
- Activer le module
sovereign(--enable-module sovereign/SOVEREIGN_PROFILE=true) pour dépublier les ports RustFS. - Suivre
ulti-backend/docs/compliance/secnumcloud-3.2-gap.mdetnetwork-segmentation.md. - Accords OVH : checklist
ulti-backend/docs/compliance/ovh-accords-checklist.md. - HDS : guide
ulti-backend/docs/compliance/hds-deploy-ovh.md(profil santé org). - Ne pas revendiquer le Visa SecNumCloud tant que la qualification UltiSuite (composition) n'est pas obtenue.
Smoke test : ulti-deploy verify inclut les endpoints légaux dans PUBLIC_ENDPOINTS.
CDN (prod)
Pour placer un CDN (Cloudflare, Fastly, CloudFront, etc.) devant la suite sans compromettre l’auth :
- Doc complète :
docs/cdn.mddans le deploy-kit / backend ($workspace/deploy-kitou$workspace/ulti-backenden mode source). - Résumé :
NGINX_DEV_NO_CACHE=false,DRIVE_MEDIA_SIGNING_KEYdéfini, cache edge uniquement sur allowlist (/_next/static/*,/api/v1/drive/media/preview?…,/api/v1/fonts/*), bypass sur le reste de/api/v1/*et les pages/mail,/drive.
Développement
pnpm install
pnpm dev -- --help # lance le CLI via tsx
pnpm typecheck
pnpm build # dist/cli.js minifié, sans sourcemaps (tsup)
Publish CI (tag v*) → registry npm Gitea ultisuite (voir .gitea/workflows/publish.yml).
Licence
MIT
Licence et ultid
Si config.license.key est défini, install, upgrade et migrate-to-images écrivent ULTI_LICENSE_KEY, ULTI_LICENSE_INSTANCE_ID et ULTI_LICENSE_URL dans le .env de la suite (l'instance ID est persisté dans state.json). Si l'URL est en loopback et que le conteneur ultilicensed existe, ultid reçoit http://ultilicensed:<port> ; hors réseau ulti-net, overrides/compose.license-net.yml joint ultid et ultid-worker à son réseau. license.containerUrl force l'URL vue des conteneurs. En mode images, upgrade mint les credentials registry via la licence avant de lister les tags et échoue (registry illisible) si la liste est vide sans --version.
upgrade --check-only ne mint pas de credentials tant que l'auth docker existante ($DOCKER_CONFIG/config.json, sinon ~/.docker) ou skopeo liste les tags : le mint côté licence fait tourner le token Gitea et révoquerait celui d'un pull en cours. Si la liste est vide, le CLI mint puis fait un docker login (persisté dans DOCKER_CONFIG).
Sur un workspace installé, le CLI utilise DOCKER_CONFIG=<workspace>/.ulti-docker (sauf si DOCKER_CONFIG est déjà défini), le même dossier que le sidecar ultideployd. Le CLI hôte et la page admin partagent donc un seul token registry au lieu de se révoquer l'un l'autre. ~/.docker/cli-plugins y est lié s'il existe. Un docker pull manuel sur l'hôte doit passer DOCKER_CONFIG=/opt/ultisuite/.ulti-docker.
Dependencies
Dependencies
| ID | Version |
|---|---|
| commander | ^12.1.0 |
| execa | ^9.5.1 |
| ink | ^5.1.0 |
| ink-big-text | ^2.0.0 |
| ink-gradient | ^3.0.0 |
| ink-select-input | ^6.0.0 |
| ink-spinner | ^5.0.0 |
| ink-text-input | ^6.0.0 |
| react | ^18.3.1 |
| semver | ^7.6.3 |
| tar | ^7.4.3 |
| zod | ^3.23.8 |
Development Dependencies
| ID | Version |
|---|---|
| @types/node | ^22.9.0 |
| @types/react | ^18.3.12 |
| @types/semver | ^7.5.8 |
| tsup | ^8.3.5 |
| tsx | ^4.19.2 |
| typescript | ^5.7.2 |