# Déploiement Arkindex sur Coolify — Guide & Runbook Déploiement d'**Arkindex** (plateforme Teklia d'analyse de documents, édition Community/AGPL) en **un seul projet** sur un serveur **Coolify**, exposé via le proxy Traefik de Coolify. - **Serveur** : VPS Contabo, IP publique `167.86.101.155`, Docker 29, Coolify 4.1.2, `coolify-proxy` = Traefik v3.6. - **Domaine** : `ark.nak0x.dev` (wildcard DNS `*.nak0x.dev` déjà en place → aucune modif DNS). - **Date** : 2026-07-09. - **Dossier** : `~/arkindex-deploy/`. > ⚠️ Les secrets ne figurent PAS dans le dépôt. Ils sont dans `.secrets.env` (git-ignoré, généré > aléatoirement) et injectés dans `.env`, `config.yml`, `garage.toml`. Voir `docs/SECURITY.md`. ## Démarrage rapide ```bash make secrets # génère .secrets.env make config # rend config.yml / garage.toml / cantaloupe.properties / .env make deploy-cli # déploiement complet en docker compose (CLI) make verify # vérification end-to-end # adoption dans l'UI Coolify : make coolify-adopt # crée la ressource, puis suivre les instructions affichées make help # toutes les cibles ``` Adapter d'abord le domaine et les versions dans **`deploy.conf`**. ## Structure du dépôt ``` README.md ce guide (architecture, procédure, problèmes/fixes) deploy.conf config NON-secrète (domaine, versions, uuids Coolify) docker-compose.yml stack CLI (docker compose) docker-compose.coolify.yml variante adoptée par Coolify (services ark-*, volumes external) config.yml / garage.toml / fichiers de conf RÉELS (git-ignorés — contiennent des secrets ; cantaloupe.properties montés par la stack en cours) templates/*.tmpl templates rendus par scripts/01 (envsubst) scripts/ 00-gen-secrets, 01-render-config, 10-up-cli, 20-garage-init, 30-arkindex-init, 40-create-admin, 50-verify, 90-coolify-adopt, 99-backup-db, lib.sh docs/ ARCHITECTURE.md, TROUBLESHOOTING.md, RUNBOOK.md, SECURITY.md Makefile raccourcis ``` --- ## 1. Architecture Stack de 8 conteneurs, réseau interne `ark`, + réseau `coolify` (externe) pour les 4 services exposés. Le **Traefik interne d'Arkindex a été supprimé** : routage + TLS délégués au `coolify-proxy` via labels. | Service (conteneur) | Image | Exposé sur | Rôle | |---|---|---|---| | `ark-database` | postgis/postgis:17-3.5 | interne | PostgreSQL + PostGIS | | `ark-redis` | redis:alpine | interne | broker RQ + cache | | `ark-solr` | solr:9 (cloud) | interne | recherche plein-texte | | `ark-garage` | dxflrs/garage:v2.1.0 | `s3.ark.nak0x.dev` (+ 1 host/bucket) | stockage S3 | | `ark-cantaloupe` | registry.gitlab.teklia.com/iiif/cantaloupe:5.0.7 | `iiif.ark.nak0x.dev`, `uploads.iiif.…`, `ingest.iiif.…` | serveur IIIF | | `ark-backend` | registry.gitlab.teklia.com/arkindex/backend:1.12.2 | `ark.nak0x.dev` (+ `/api`,`/admin`,…) | API Django (gunicorn) | | `ark-worker` | …/arkindex/backend:1.12.2 | interne | worker RQ / Ponos | | `ark-front` | registry.gitlab.teklia.com/arkindex/frontend:1.12.2 | `ark.nak0x.dev` (catch-all) | SPA | **Flux réseau clés** - Navigateur → `coolify-proxy` (TLS) → front / backend (split par chemin sur `ark.nak0x.dev`). - Navigateur → `coolify-proxy` → `s3.ark.nak0x.dev` (URLs S3 présignées) et `iiif.ark.nak0x.dev` (images). - Backend → **hairpin** : `https://s3.ark.nak0x.dev` → IP publique → `coolify-proxy` → Garage. - Cantaloupe → Garage en interne : `http://ark-garage:3900` (pas de TLS, réseau `ark`). **Répartition des hostnames** (tous résolus par le wildcard DNS `*.nak0x.dev`) : - `ark.nak0x.dev` - `s3.ark.nak0x.dev` + `.s3.ark.nak0x.dev` (9 buckets : staging, thumbnails, export, training, ponos-logs, ponos-artifacts, ingest, uploads, iiif-cache) - `iiif.ark.nak0x.dev`, `uploads.iiif.ark.nak0x.dev`, `ingest.iiif.ark.nak0x.dev` --- ## 2. Prérequis - Serveur Linux avec Docker + Coolify (proxy Traefik sur 80/443). - Domaine avec **entrées DNS pointant vers l'IP** pour tous les hostnames ci-dessus. Ici, un **wildcard DNS `*.nak0x.dev`** couvre tout, y compris les sous-domaines profonds (`uploads.iiif.ark.nak0x.dev`). - Images Teklia **publiques** (pull anonyme, aucun login registry requis). ### ⚠️ Contrainte structurante : pas de certificat wildcard Le Traefik de Coolify n'utilise que le **challenge HTTP-01** (`acme.httpchallenge=true`), qui **ne peut pas émettre de certificats wildcard** (`*.s3.…`). Or Arkindex, en conf par défaut, sert S3/IIIF via des sous-domaines wildcard. **Solution** : l'ensemble des hostnames d'Arkindex est *fini et connu*, on les **énumère explicitement** dans les règles `Host(...)` Traefik → un cert HTTP-01 par hostname. --- ## 3. Fichiers du dépôt | Fichier | Rôle | |---|---| | `docker-compose.yml` | stack pilotée en `docker compose` (CLI) — **actuellement en service** | | `docker-compose.coolify.yml` | variante pour adoption dans l'UI Coolify (services `ark-*`, volumes `external`) | | `config.yml` | configuration Arkindex (montée sur `/arkindex.yml`) | | `garage.toml` | configuration du stockage S3 Garage | | `cantaloupe.properties` | configuration du serveur IIIF | | `.secrets.env` | secrets générés (PG_PASS, SECRET_KEY, GK_ID/GK_SECRET, tokens Garage) — **mode 600** | | `.env` | variables consommées par `docker compose` (substitution `${...}`) | --- ## 4. Procédure de déploiement (reproductible) ### 4.1 Générer les secrets ```bash mkdir -p ~/arkindex-deploy && cd ~/arkindex-deploy umask 077 cat > .secrets.env < Le modèle User d'Arkindex n'a **pas** `is_superuser` (Django), mais `is_admin`. `create_superuser` > pose `is_admin=True`. Récupérer le mot de passe affiché et le changer. --- ## 5. Problèmes rencontrés → correctifs ### P1 — Version d'image inexistante (`manifest unknown`) **Symptôme** : `docker pull …/backend:1.13.0` → `manifest unknown`, alors que `1.13.0` apparaît dans la liste des tags. **Cause** : le tag listé était `1.13.0-rc1` ; l'extraction du cœur numérique donnait « 1.13.0 » qui n'existe pas en release finale. **Fix** : filtrer les tags `-rc/-beta/-post/-alpha` et vérifier l'existence du manifeste. Dernière **finale** = **1.12.2**. ```bash tok=$(curl -s "https://gitlab.teklia.com/jwt/auth?service=container_registry&scope=repository:arkindex/backend:pull" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') curl -s -H "Authorization: Bearer $tok" https://registry.gitlab.teklia.com/v2/arkindex/backend/tags/list \ | tr ',' '\n' | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"' | sort -uV | tail ``` ### P2 — Certificats wildcard impossibles **Symptôme** : Arkindex veut `*.s3.ark…` et `*.iiif.ark…`, mais Coolify ne fait que du HTTP-01. **Fix** : énumérer explicitement chaque hostname dans une règle `Host(...) || Host(...)` unique par service (Garage, Cantaloupe) → Traefik émet un cert par host. Le wildcard **DNS** `*.nak0x.dev` existant fait résoudre tous ces noms sans config DNS supplémentaire. ### P3 — `PermissionError: /arkindex.yml` **Symptôme** : backend crash au boot, ne peut pas lire le fichier de conf monté. **Cause** : `umask 077` → `config.yml` en `600` (uid 1000) ; backend en uid 2000. **Fix** : `chmod 644 config.yml cantaloupe.properties garage.toml`. ### P4 — `redis.exceptions.AuthenticationError` sur le worker ⭐ (le plus subtil) **Symptôme** : le worker plante avec « HELLO must be called with the client already authenticated ». **Cause** : sur le réseau partagé `coolify`, le nom de service générique `redis` **entrait en collision** avec le conteneur `coolify-redis` (protégé par mot de passe). Le worker se connectait au mauvais Redis. **Diagnostic** : ```bash docker exec ark-worker getent hosts redis # -> IP de coolify-redis (mauvais) docker exec ark-worker getent hosts ark-redis # -> IP correcte ``` **Fix** : dans `config.yml`/`cantaloupe.properties`, référencer les services internes par leur **nom de conteneur unique** `ark-database`, `ark-redis`, `ark-solr`, `ark-garage` (jamais `db`/`redis`). ### P5 — `bootstrap` inutilisable en production **Symptôme** : `arkindex bootstrap` refuse (`You cannot run this script in production.`) et code les URLs en `*.ark.localhost`. **Fix** : répliquer ses 3 actions à la main avec le vrai domaine (cf. §4.3 et §4.5) : init Garage, 2 ImageServers (id 12345 uploads / 67890 ingest), farm Ponos. La clé S3 « placeholder » (`GKAAA…`) de bootstrap est remplacée par une **clé aléatoire** propre. ### P6 — Adressage S3 virtual-host (et non path-style) **Cause** : le backend force `addressing_style="auto"` (boto3) → URLs `.s3.ark…` (pas `s3.ark…/`). Pas d'option pour path-style. **Fix** : énumérer les 9 sous-domaines de bucket dans la règle Traefik de Garage + conserver `root_domain = ".s3.ark.nak0x.dev"` dans `garage.toml` pour que Garage mappe sous-domaine → bucket. Vérifié : le backend écrit/lit/supprime via `https://s3.ark.nak0x.dev` (hairpin + TLS OK). ### P7 — Gunicorn lance trop de workers **Symptôme** : 9 workers gunicorn (= `nproc*2+1`) → trop de RAM sur ce VPS. **Fix** : `MAX_WORKERS: "3"` dans l'environnement du backend. ### P8 — Fichiers statiques admin/swagger non servis **Cause** : pas de WhiteNoise ; en prod les statics viennent d'un **CDN**. **Fix** : `static.cdn_assets_url: https://assets.teklia.com/arkindex` dans `config.yml`. **Note** : purement cosmétique (admin Django/DRF stylés) — l'API et la SPA n'en dépendent pas. Le chemin CDN testé renvoyait 404 → l'admin peut rester sans style, sans impact fonctionnel. ### P9 — Collection Solr `arkindex reindex --setup` exige un corpus : les collections Solr sont créées **par corpus** (`project-`) à la volée lors de l'indexation. **Rien à pré-créer.** ### P10 — Backend sans Pillow (test uniquement) Générer une image de test via le backend échoue (`No module named 'PIL'`, le traitement image est côté workers). Pour tester : générer le JPEG hors backend et l'uploader via un conteneur `amazon/aws-cli`. ### P11 — Coolify écrase `container_name` (pour l'adoption UI) **Cause** : Coolify renomme les conteneurs en `-` mais **garde le nom de service** comme alias DNS. **Fix** : dans `docker-compose.coolify.yml`, **renommer les services** en `ark-*` (uniques → pas de collision, cf. P4) et déclarer les volumes en `external` (noms existants `arkindex-deploy_*`) pour ne rien perdre. --- ## 6. Vérifications (santé de bout en bout) ```bash # API + TLS public curl -sS -o /dev/null -w "%{http_code} tls=%{ssl_verify_result}\n" https://ark.nak0x.dev/api/v1/health/ # Login admin -> 201 + auth_token curl -sS -X POST https://ark.nak0x.dev/api/v1/user/login/ -H 'Content-Type: application/json' \ -d '{"email":"ton@mail","password":"…"}' -w "\n%{http_code}\n" # S3 (backend) — write/read/delete via l'endpoint public (hairpin) docker exec -i ark-backend arkindex shell <<'PY' from arkindex.project.aws import s3 o=s3.Bucket("staging").Object("hc.txt"); o.put(Body=b"ok"); print(o.get()["Body"].read()); o.delete() PY # IIIF : uploader une image dans le bucket 'uploads' puis curl -sS -o /dev/null -w "%{http_code}\n" "https://uploads.iiif.ark.nak0x.dev/iiif/2//info.json" # RAM free -h; docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}' | grep ark- ``` --- ## 7. Adoption dans l'UI Coolify — ✅ FAIT La stack est désormais une ressource **gérée par Coolify** : projet **arkindex**, service **arkindex** (uuid `tdquwauzxsq0985fk9kjqqys`), conteneurs `ark-*-tdquwauzxsq0985fk9kjqqys`. Source = `docker-compose.coolify.yml`. Pilotage (deploy/logs/restart) via le dashboard Coolify. ### Procédure suivie (API Coolify) ```bash TOKEN=$(cat ~/.ark_coolify_token) # 1) projet + récup environnement production curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"name":"arkindex"}' http://localhost:8000/api/v1/projects # 2) créer le service (docker_compose_raw en BASE64 obligatoire), instant_deploy=false # body: server_uuid, project_uuid, environment_name/uuid, name, docker_compose_raw(b64) # 3) renseigner les secrets en variables d'env du service (PATCH .../envs) : PG_PASS, GK_ID, GK_SECRET # 4) arrêter la stack CLI puis déployer UNE SEULE FOIS : cd ~/arkindex-deploy && docker compose down # garde les volumes en backup curl -s -X POST -H "Authorization: Bearer $TOKEN" \ "http://localhost:8000/api/v1/deploy?uuid=tdquwauzxsq0985fk9kjqqys" ``` ### ⚠️ Points d'attention (cf. aussi P11) - **Coolify remappe les volumes nommés** en `_*` et **ignore `external:`** → au premier déploiement la stack repart sur des **volumes vides**. Comme il n'y avait pas encore de données réelles, on a simplement **ré-initialisé** (Garage + `migrate` + image servers + admin) contre les conteneurs Coolify (`docker exec ark-backend- …`). Les anciens volumes `arkindex-deploy_*` restent en backup (supprimables : `docker volume rm arkindex-deploy_pgdata …`). - **Coolify préserve** : réseau `coolify` externe, labels Traefik, montages bind **absolus**, et les **noms de service** (alias DNS `ark-*`). Il écrase `container_name` en `-`. - **Ne déclencher le deploy qu'une fois** (POST *ou* GET, pas les deux) — sinon double déploiement. --- ## 8. Commandes de maintenance ```bash cd ~/arkindex-deploy docker compose ps # état docker compose logs -f backend # logs docker compose restart backend # redémarrer un service docker exec -it ark-backend arkindex shell # shell Django docker exec ark-backend arkindex migrate # migrations (après upgrade d'image) # Mise à jour de version : changer le tag dans le compose puis docker compose pull backend front worker && docker compose up -d ``` ## 9. Réserves / points d'attention - **RAM** : VPS 8 Go déjà chargé ; Arkindex ≈ 1 Go réel (Solr = plus gros). Swap 4 Go présent. Si instable, désactiver Solr (`features.search: no` + retirer le service) est la soupape. - **Socket Docker du worker** : monté + `user: root` (requis pour les tâches Ponos/ML). Point sécurité à garder en tête ; les workers ML tirent de grosses images (hors périmètre de ce déploiement initial). - **Édition Community** (AGPL), pas de licence Enterprise.