Attempt to deploy Arkindex as standalone in coolify.
Go to file
nak0x 18064be674 Repo complet : scripts, templates, docs et durcissement secrets
- Ajout scripts/ reproduisant chaque étape (secrets, render config, up CLI,
  init Garage, bootstrap prod, admin, vérif E2E, adoption Coolify, backup)
- Templates envsubst (config.yml, garage.toml, cantaloupe.properties) + .example
- deploy.conf (config non-secrète : domaine, versions, uuids Coolify)
- docs/ : ARCHITECTURE, TROUBLESHOOTING (11 problèmes/fixes), RUNBOOK, SECURITY
- Makefile (raccourcis)
- .gitignore + retrait du suivi de .env/.secrets.env/config.yml/garage.toml
  (fichiers conservés sur disque ; secrets restent dans l'historique -> voir SECURITY.md)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 16:26:29 +02:00
docs Repo complet : scripts, templates, docs et durcissement secrets 2026-07-09 16:26:29 +02:00
scripts Repo complet : scripts, templates, docs et durcissement secrets 2026-07-09 16:26:29 +02:00
templates Repo complet : scripts, templates, docs et durcissement secrets 2026-07-09 16:26:29 +02:00
.env.example Repo complet : scripts, templates, docs et durcissement secrets 2026-07-09 16:26:29 +02:00
.gitignore Repo complet : scripts, templates, docs et durcissement secrets 2026-07-09 16:26:29 +02:00
.secrets.env.example Repo complet : scripts, templates, docs et durcissement secrets 2026-07-09 16:26:29 +02:00
cantaloupe.properties Claude work 2026-07-09 15:57:13 +02:00
deploy.conf Repo complet : scripts, templates, docs et durcissement secrets 2026-07-09 16:26:29 +02:00
docker-compose.coolify.yml Claude work 2026-07-09 15:57:13 +02:00
docker-compose.yml Claude work 2026-07-09 15:57:13 +02:00
Makefile Repo complet : scripts, templates, docs et durcissement secrets 2026-07-09 16:26:29 +02:00
README.md Repo complet : scripts, templates, docs et durcissement secrets 2026-07-09 16:26:29 +02:00

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

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-proxys3.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

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 :

chmod 644 config.yml cantaloupe.properties garage.toml

4.3 Démarrer l'infra de base + initialiser Garage

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 :

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

docker compose up -d backend
docker exec ark-backend arkindex migrate

4.5 Équivalent de bootstrap (ImageServers + farm Ponos), avec le VRAI domaine

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

docker compose up -d cantaloupe front worker

4.7 Créer le compte admin

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.0manifest 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.

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 077config.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 :

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)

# 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 — 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)

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 <uuid>_* 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-<uuid> …). 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 <service>-<uuid>.
  • Ne déclencher le deploy qu'une fois (POST ou GET, pas les deux) — sinon double déploiement.

8. Commandes de maintenance

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.