Add readme
This commit is contained in:
parent
846e853a99
commit
0e08632bce
294
README.md
Normal file
294
README.md
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
# 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 ce guide. Ils sont dans `.secrets.env` (généré aléatoirement)
|
||||||
|
> et injectés dans `.env`, `config.yml`, `garage.toml`, `cantaloupe.properties`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` + `<bucket>.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 <<EOF
|
||||||
|
PG_PASS=$(openssl rand -hex 16)
|
||||||
|
SECRET_KEY=$(openssl rand -base64 48 | tr -d '\n/+=' | head -c 50)
|
||||||
|
GARAGE_RPC=$(openssl rand -hex 32)
|
||||||
|
GARAGE_ADMIN_TOKEN=$(openssl rand -hex 24)
|
||||||
|
GK_ID=GK$(openssl rand -hex 12)
|
||||||
|
GK_SECRET=$(openssl rand -hex 32)
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
Puis créer `.env` (PG_PASS, GK_ID, GK_SECRET), `garage.toml`, `cantaloupe.properties`, `config.yml`
|
||||||
|
à partir des valeurs de `.secrets.env` (cf. fichiers du dépôt).
|
||||||
|
|
||||||
|
### 4.2 IMPORTANT — permissions des fichiers montés
|
||||||
|
Le backend tourne en **uid 2000 (`arkindex`)**. Avec `umask 077`, les fichiers sont en `600` (uid 1000)
|
||||||
|
→ le conteneur ne peut pas les lire. **Les passer en 644** :
|
||||||
|
```bash
|
||||||
|
chmod 644 config.yml cantaloupe.properties garage.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Démarrer l'infra de base + initialiser Garage
|
||||||
|
```bash
|
||||||
|
docker compose up -d db redis solr garage
|
||||||
|
```
|
||||||
|
Garage doit être initialisé (layout + import de la clé S3 + création des 9 buckets). `bootstrap` étant
|
||||||
|
dev-only (cf. §5), on le fait à la main :
|
||||||
|
```bash
|
||||||
|
set -a; . ./.secrets.env; set +a
|
||||||
|
NODE=$(docker exec ark-garage /garage status | awk '/NO ROLE ASSIGNED/{print $1}')
|
||||||
|
docker exec ark-garage /garage layout assign -z dev -c 1G "$NODE"
|
||||||
|
docker exec ark-garage /garage layout apply --version 1
|
||||||
|
docker exec ark-garage /garage key import --yes -n arkindex "$GK_ID" "$GK_SECRET"
|
||||||
|
for b in uploads ingest export iiif-cache ponos-artifacts ponos-logs staging thumbnails training; do
|
||||||
|
docker exec ark-garage /garage bucket create "$b"
|
||||||
|
docker exec ark-garage /garage bucket allow --read --write --owner --key "$GK_ID" "$b"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Démarrer le backend + migrations
|
||||||
|
```bash
|
||||||
|
docker compose up -d backend
|
||||||
|
docker exec ark-backend arkindex migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 Équivalent de `bootstrap` (ImageServers + farm Ponos), avec le VRAI domaine
|
||||||
|
```bash
|
||||||
|
docker exec -i ark-backend arkindex shell <<'PYEOF'
|
||||||
|
from arkindex.images.models import ImageServer
|
||||||
|
from arkindex.ponos.models import Farm
|
||||||
|
for sid, url, bucket, name in [
|
||||||
|
(12345, "https://uploads.iiif.ark.nak0x.dev/iiif/2", "uploads", "Local uploads IIIF server"),
|
||||||
|
(67890, "https://ingest.iiif.ark.nak0x.dev/iiif/2", "ingest", "Ingest IIIF server"),
|
||||||
|
]:
|
||||||
|
ImageServer.objects.update_or_create(id=sid, defaults=dict(url=url, s3_bucket=bucket, s3_region="local", display_name=name))
|
||||||
|
Farm.objects.update_or_create(
|
||||||
|
id="001e411a-1111-2222-3333-444455556666",
|
||||||
|
defaults=dict(name="Bootstrap farm", seed="b12868101dab84984481741663d809d2393784894d6e807ceee0bd95051bf971"))
|
||||||
|
print("OK")
|
||||||
|
PYEOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.6 Démarrer le reste
|
||||||
|
```bash
|
||||||
|
docker compose up -d cantaloupe front worker
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.7 Créer le compte admin
|
||||||
|
```bash
|
||||||
|
docker exec -i -e E=ton@mail -e P="$(openssl rand -base64 18 | tr -d '/+=' | head -c 20)" ark-backend arkindex shell <<'PYEOF'
|
||||||
|
import os; from django.contrib.auth import get_user_model
|
||||||
|
U = get_user_model()
|
||||||
|
u = U.objects.create_superuser(email=os.environ["E"], display_name="admin", password=os.environ["P"])
|
||||||
|
u.verified_email = True
|
||||||
|
for f in ["can_upload_s3_image","can_create_iiif_image","can_ingest","can_manage_workers","can_create_worker_version","can_validate_images"]:
|
||||||
|
setattr(u, f, True)
|
||||||
|
u.save(); print("admin créé:", u.email)
|
||||||
|
PYEOF
|
||||||
|
```
|
||||||
|
> 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 `<bucket>.s3.ark…` (pas
|
||||||
|
`s3.ark…/<bucket>`). 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-<uuid>`) à 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 `<service>-<uuid>` 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/<key>/info.json"
|
||||||
|
# RAM
|
||||||
|
free -h; docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}' | grep ark-
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Adoption dans l'UI Coolify (étape en attente)
|
||||||
|
|
||||||
|
Utiliser `docker-compose.coolify.yml`. Deux voies :
|
||||||
|
- **API Coolify** : `POST /api/v1/services` avec `docker_compose_raw` (nécessite un token API
|
||||||
|
read/write) — créer un projet « arkindex », y coller le compose, fournir les secrets en variables
|
||||||
|
d'environnement (`PG_PASS`, `GK_ID`, `GK_SECRET`), puis déployer.
|
||||||
|
- **UI** : New Resource → Docker Compose → coller `docker-compose.coolify.yml`.
|
||||||
|
|
||||||
|
Bascule sans perte : arrêter la stack CLI (`docker compose down` **sans `-v`** → les volumes
|
||||||
|
persistent), puis déployer via Coolify qui réutilise les volumes `external`. Prévoir une **coupure de
|
||||||
|
quelques secondes**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
Loading…
Reference in New Issue
Block a user