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>
This commit is contained in:
parent
0e08632bce
commit
18064be674
5
.env.example
Normal file
5
.env.example
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Consommé par `docker compose` pour la substitution ${...} dans les compose.
|
||||||
|
# Généré à partir de .secrets.env par scripts/01-render-config.sh. NE PAS committer .env.
|
||||||
|
PG_PASS=CHANGE_ME
|
||||||
|
GK_ID=CHANGE_ME
|
||||||
|
GK_SECRET=CHANGE_ME
|
||||||
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# ---- Secrets : NE JAMAIS committer ----
|
||||||
|
.secrets.env
|
||||||
|
.env
|
||||||
|
config.yml
|
||||||
|
garage.toml
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
.ark_coolify_token
|
||||||
|
|
||||||
|
# ---- Généré / local ----
|
||||||
|
*.log
|
||||||
|
tmp/
|
||||||
|
backups/
|
||||||
15
.secrets.env.example
Normal file
15
.secrets.env.example
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Copie ce fichier en .secrets.env puis génère de VRAIES valeurs (scripts/00-gen-secrets.sh).
|
||||||
|
# NE JAMAIS committer .secrets.env (il est dans .gitignore).
|
||||||
|
|
||||||
|
# Mot de passe PostgreSQL -> openssl rand -hex 16
|
||||||
|
PG_PASS=CHANGE_ME
|
||||||
|
# Django SECRET_KEY (50 chars) -> openssl rand -base64 48 | tr -d '\n/+=' | head -c 50
|
||||||
|
SECRET_KEY=CHANGE_ME
|
||||||
|
# Secret RPC Garage (64 hex) -> openssl rand -hex 32
|
||||||
|
GARAGE_RPC=CHANGE_ME
|
||||||
|
# Token admin Garage -> openssl rand -hex 24
|
||||||
|
GARAGE_ADMIN_TOKEN=CHANGE_ME
|
||||||
|
# Clé d'accès S3 Garage (GK + 24 hex) -> echo GK$(openssl rand -hex 12)
|
||||||
|
GK_ID=CHANGE_ME
|
||||||
|
# Clé secrète S3 Garage (64 hex) -> openssl rand -hex 32
|
||||||
|
GK_SECRET=CHANGE_ME
|
||||||
44
Makefile
Normal file
44
Makefile
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Arkindex sur Coolify — cibles pratiques.
|
||||||
|
# Les scripts détaillés sont dans scripts/. Voir README.md et docs/.
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
SHELL := /bin/bash
|
||||||
|
|
||||||
|
help: ## Affiche cette aide
|
||||||
|
@grep -hE '^[a-zA-Z0-9_-]+:.*?## ' $(MAKEFILE_LIST) | \
|
||||||
|
awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-16s\033[0m %s\n",$$1,$$2}'
|
||||||
|
|
||||||
|
secrets: ## Génère .secrets.env (si absent)
|
||||||
|
@scripts/00-gen-secrets.sh
|
||||||
|
|
||||||
|
config: ## Rend config.yml / garage.toml / cantaloupe.properties / .env
|
||||||
|
@scripts/01-render-config.sh
|
||||||
|
|
||||||
|
deploy-cli: ## Déploiement complet en docker compose (CLI)
|
||||||
|
@scripts/10-up-cli.sh
|
||||||
|
|
||||||
|
garage: ## (Ré)initialise Garage (layout, clé, buckets)
|
||||||
|
@scripts/20-garage-init.sh
|
||||||
|
|
||||||
|
init: ## Migrations + ImageServers + farm Ponos
|
||||||
|
@scripts/30-arkindex-init.sh
|
||||||
|
|
||||||
|
admin: ## Crée/maj le compte admin (ADMIN_PASS=... optionnel)
|
||||||
|
@scripts/40-create-admin.sh
|
||||||
|
|
||||||
|
verify: ## Vérification end-to-end (ADMIN_PASS=... pour tester le login)
|
||||||
|
@scripts/50-verify.sh
|
||||||
|
|
||||||
|
coolify-adopt: ## Crée la ressource Coolify (API) — voir sortie pour la bascule
|
||||||
|
@scripts/90-coolify-adopt.sh
|
||||||
|
|
||||||
|
backup: ## Dump PostgreSQL + inventaire Garage -> backups/
|
||||||
|
@scripts/99-backup-db.sh
|
||||||
|
|
||||||
|
ps: ## Liste les conteneurs Arkindex
|
||||||
|
@docker ps --filter name=ark- --format 'table {{.Names}}\t{{.Status}}'
|
||||||
|
|
||||||
|
logs: ## Logs backend (SVC=ark-worker pour un autre service)
|
||||||
|
@docker logs -f $${SVC:-ark-backend}$${CONTAINER_SUFFIX:-}
|
||||||
|
|
||||||
|
.PHONY: help secrets config deploy-cli garage init admin verify coolify-adopt backup ps logs
|
||||||
69
README.md
69
README.md
@ -8,8 +8,36 @@ Déploiement d'**Arkindex** (plateforme Teklia d'analyse de documents, édition
|
|||||||
- **Date** : 2026-07-09.
|
- **Date** : 2026-07-09.
|
||||||
- **Dossier** : `~/arkindex-deploy/`.
|
- **Dossier** : `~/arkindex-deploy/`.
|
||||||
|
|
||||||
> ⚠️ Les secrets ne figurent PAS dans ce guide. Ils sont dans `.secrets.env` (généré aléatoirement)
|
> ⚠️ Les secrets ne figurent PAS dans le dépôt. Ils sont dans `.secrets.env` (git-ignoré, généré
|
||||||
> et injectés dans `.env`, `config.yml`, `garage.toml`, `cantaloupe.properties`.
|
> 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
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -260,17 +288,36 @@ free -h; docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}' | gr
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Adoption dans l'UI Coolify (étape en attente)
|
## 7. Adoption dans l'UI Coolify — ✅ FAIT
|
||||||
|
|
||||||
Utiliser `docker-compose.coolify.yml`. Deux voies :
|
La stack est désormais une ressource **gérée par Coolify** : projet **arkindex**, service **arkindex**
|
||||||
- **API Coolify** : `POST /api/v1/services` avec `docker_compose_raw` (nécessite un token API
|
(uuid `tdquwauzxsq0985fk9kjqqys`), conteneurs `ark-*-tdquwauzxsq0985fk9kjqqys`. Source =
|
||||||
read/write) — créer un projet « arkindex », y coller le compose, fournir les secrets en variables
|
`docker-compose.coolify.yml`. Pilotage (deploy/logs/restart) via le dashboard Coolify.
|
||||||
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
|
### Procédure suivie (API Coolify)
|
||||||
persistent), puis déployer via Coolify qui réutilise les volumes `external`. Prévoir une **coupure de
|
```bash
|
||||||
quelques secondes**.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
40
deploy.conf
Normal file
40
deploy.conf
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Configuration NON-secrète du déploiement Arkindex/Coolify.
|
||||||
|
# Sourcé par les scripts (scripts/lib.sh). Les SECRETS sont dans .secrets.env (git-ignoré).
|
||||||
|
|
||||||
|
# --- Domaine ---
|
||||||
|
# Hostname racine ; les sous-domaines s3.<DOMAIN>, iiif.<DOMAIN>, uploads.iiif.<DOMAIN>,
|
||||||
|
# ingest.iiif.<DOMAIN> et <bucket>.s3.<DOMAIN> en découlent.
|
||||||
|
DOMAIN="ark.nak0x.dev"
|
||||||
|
|
||||||
|
# --- Versions d'images (dernières finales stables, pas de -rc/-beta) ---
|
||||||
|
BACKEND_TAG="1.12.2"
|
||||||
|
FRONTEND_TAG="1.12.2"
|
||||||
|
TASKS_TAG="0.6.2"
|
||||||
|
CANTALOUPE_TAG="5.0.7"
|
||||||
|
POSTGIS_TAG="17-3.5"
|
||||||
|
SOLR_TAG="9"
|
||||||
|
GARAGE_TAG="v2.1.0"
|
||||||
|
REDIS_TAG="alpine"
|
||||||
|
|
||||||
|
# --- Compte admin ---
|
||||||
|
ADMIN_EMAIL="theolesage38@gmail.com"
|
||||||
|
ADMIN_DISPLAY_NAME="nak0x"
|
||||||
|
|
||||||
|
# --- Registry Teklia (images publiques, pull anonyme) ---
|
||||||
|
REGISTRY="registry.gitlab.teklia.com"
|
||||||
|
|
||||||
|
# --- Buckets S3 Garage (fixes) ---
|
||||||
|
GARAGE_BUCKETS="uploads ingest export iiif-cache ponos-artifacts ponos-logs staging thumbnails training"
|
||||||
|
|
||||||
|
# --- Coolify (identifiants de la ressource déployée ; le TOKEN est hors dépôt) ---
|
||||||
|
COOLIFY_API="http://localhost:8000/api/v1"
|
||||||
|
COOLIFY_TOKEN_FILE="$HOME/.ark_coolify_token"
|
||||||
|
COOLIFY_SERVER_UUID="vedsaj5e96wtmu8jcd3nkxi2"
|
||||||
|
COOLIFY_PROJECT_UUID="h1s06qu51vf7x33axjy94d0e"
|
||||||
|
COOLIFY_ENV_UUID="gqje671dsr1ulvb7y9fw80t2"
|
||||||
|
COOLIFY_ENV_NAME="production"
|
||||||
|
COOLIFY_SERVICE_UUID="tdquwauzxsq0985fk9kjqqys"
|
||||||
|
|
||||||
|
# Suffixe des conteneurs gérés par Coolify (= <service>-<COOLIFY_SERVICE_UUID>).
|
||||||
|
# Vide = mode CLI (conteneurs ark-* sans suffixe).
|
||||||
|
CONTAINER_SUFFIX="-tdquwauzxsq0985fk9kjqqys"
|
||||||
76
docs/ARCHITECTURE.md
Normal file
76
docs/ARCHITECTURE.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# Architecture — Arkindex sur Coolify
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet (navigateur)
|
||||||
|
│ HTTPS (Let's Encrypt, HTTP-01)
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ coolify-proxy (Traefik v3) │ :80/:443
|
||||||
|
│ routage par labels Docker │
|
||||||
|
└───────┬───────────┬──────────┘
|
||||||
|
ark.nak0x.dev │ │ s3.nak0x… / iiif.nak0x…
|
||||||
|
(split par chemin) │ │
|
||||||
|
┌───────────────┬────────┘ └──────────┬──────────────┐
|
||||||
|
▼ ▼ ▼ ▼
|
||||||
|
ark-front ark-backend ark-garage ark-cantaloupe
|
||||||
|
(SPA nginx) (API gunicorn) (S3) (IIIF)
|
||||||
|
│ │ \ ▲ │
|
||||||
|
│ │ \ hairpin S3 │ http interne│ http interne
|
||||||
|
│ │ └──► https://s3.<DOMAIN> ───┘ :3900 │ :3900
|
||||||
|
│ ▼ │
|
||||||
|
│ ark-worker (RQ/Ponos) │
|
||||||
|
│ │ ▼
|
||||||
|
│ ┌──────┴──────┬──────────┐ (lit les images
|
||||||
|
▼ ▼ ▼ ▼ dans les buckets)
|
||||||
|
[réseau "ark"] ark-database ark-redis ark-solr
|
||||||
|
(Postgres) (RQ+cache) (recherche)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Réseaux Docker
|
||||||
|
- **`ark`** (interne) : tous les services ; communication par **nom de service** (`ark-database`,
|
||||||
|
`ark-redis`, …). Ces noms sont **uniques** exprès (cf. Troubleshooting P4).
|
||||||
|
- **`coolify`** (externe, partagé) : uniquement `ark-garage`, `ark-cantaloupe`, `ark-backend`,
|
||||||
|
`ark-front`, `ark-worker` — pour être joignables par le proxy Traefik et pour le hairpin S3.
|
||||||
|
- En mode Coolify, un 3ᵉ réseau par ressource (`<uuid>`) est ajouté automatiquement.
|
||||||
|
|
||||||
|
## TLS & hostnames (contrainte centrale)
|
||||||
|
Coolify n'a que le challenge **HTTP-01** → **pas de certificat wildcard**. Or Arkindex sert S3 et
|
||||||
|
IIIF en **virtual-hosted** (`<bucket>.s3.<DOMAIN>`, `<x>.iiif.<DOMAIN>`). Comme l'ensemble des
|
||||||
|
hostnames est **fini et connu**, on les énumère dans les règles `Host(...)` Traefik → Traefik émet
|
||||||
|
un certificat HTTP-01 par hostname. Le **wildcard DNS** `*.nak0x.dev` (résolution, pas TLS) fait
|
||||||
|
pointer tous ces noms vers le serveur, donc **zéro entrée DNS à créer**.
|
||||||
|
|
||||||
|
Hostnames servis :
|
||||||
|
- `ark.<DOMAIN>` — front (catch-all, priorité 1) + backend (`/api`,`/api-docs`,`/admin`,`/rq`,`/static`, priorité 100)
|
||||||
|
- `s3.<DOMAIN>` + `<bucket>.s3.<DOMAIN>` × 9 buckets — Garage
|
||||||
|
- `iiif.<DOMAIN>`, `uploads.iiif.<DOMAIN>`, `ingest.iiif.<DOMAIN>` — Cantaloupe
|
||||||
|
|
||||||
|
## Stockage S3 (Garage)
|
||||||
|
- Le backend utilise boto3 avec `addressing_style="auto"` (codé en dur) → **virtual-hosted**
|
||||||
|
(`<bucket>.s3.<DOMAIN>`). D'où l'énumération des sous-domaines de bucket + `root_domain` dans
|
||||||
|
`garage.toml` (Garage mappe sous-domaine → bucket).
|
||||||
|
- **Hairpin** : le backend doit joindre `https://s3.<DOMAIN>` (les URLs présignées données au
|
||||||
|
navigateur utilisent ce host) : requête → IP publique → `coolify-proxy` (TLS valide) → Garage.
|
||||||
|
Vérifié fonctionnel ici.
|
||||||
|
- Cantaloupe, lui, parle à Garage **en interne** (`http://ark-garage:3900`, sans TLS).
|
||||||
|
- 9 buckets : `uploads, ingest, export, iiif-cache, ponos-artifacts, ponos-logs, staging, thumbnails, training`.
|
||||||
|
- Clé S3 : importée dans Garage (admin API/CLI) = `GK_ID`/`GK_SECRET` de `.secrets.env`.
|
||||||
|
|
||||||
|
## IIIF (Cantaloupe)
|
||||||
|
- Image custom Teklia : `ScriptLookupStrategy` + `S3Source.top_domain = iiif.<DOMAIN>` → le bucket
|
||||||
|
est déduit du sous-domaine (`uploads.iiif.<DOMAIN>` → bucket `uploads`).
|
||||||
|
- 2 `ImageServer` en base : `12345` (uploads) et `67890` (ingest), URLs `https://…iiif.<DOMAIN>/iiif/2`.
|
||||||
|
|
||||||
|
## Recherche (Solr)
|
||||||
|
- SolrCloud (`solr -f -cloud`). Les collections sont créées **par corpus** (`project-<uuid>`) à la
|
||||||
|
volée lors de l'indexation (`arkindex reindex`). Rien à pré-créer.
|
||||||
|
|
||||||
|
## Images (versions)
|
||||||
|
`backend/frontend 1.12.2`, `tasks 0.6.2`, `cantaloupe 5.0.7`, `postgis 17-3.5`, `solr 9`,
|
||||||
|
`garage v2.1.0`, `redis alpine`. Toutes **publiques** (pull anonyme).
|
||||||
|
|
||||||
|
## Bootstrap : pourquoi on ne l'utilise pas
|
||||||
|
`arkindex bootstrap` est **dev-only** : il refuse si `DEBUG=False` et code les URLs en
|
||||||
|
`*.ark.localhost`. On réplique ses 3 actions avec le vrai domaine (`scripts/20` + `scripts/30`).
|
||||||
76
docs/RUNBOOK.md
Normal file
76
docs/RUNBOOK.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# Runbook — exploitation (day-2)
|
||||||
|
|
||||||
|
> En mode Coolify, les conteneurs ont le suffixe `-tdquwauzxsq0985fk9kjqqys` (cf. `deploy.conf`
|
||||||
|
> `CONTAINER_SUFFIX`). Les helpers `scripts/*.sh` le gèrent automatiquement via `cname`.
|
||||||
|
|
||||||
|
## Consulter / piloter
|
||||||
|
```bash
|
||||||
|
docker ps --filter name=ark- # état
|
||||||
|
docker logs -f ark-backend-<suffix> # logs backend
|
||||||
|
docker logs -f ark-worker-<suffix> # logs worker (tâches)
|
||||||
|
# depuis Coolify : dashboard -> projet arkindex -> service arkindex (Deploy / Logs / Restart)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Redémarrer / redéployer
|
||||||
|
```bash
|
||||||
|
# CLI :
|
||||||
|
cd ~/arkindex-deploy && docker compose restart backend
|
||||||
|
# Coolify (API) :
|
||||||
|
curl -s -X POST -H "Authorization: Bearer $(cat ~/.ark_coolify_token)" \
|
||||||
|
"http://localhost:8000/api/v1/deploy?uuid=tdquwauzxsq0985fk9kjqqys"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mettre à jour Arkindex (nouvelle version d'image)
|
||||||
|
1. Vérifier la dernière finale (cf. Troubleshooting P1).
|
||||||
|
2. Changer les tags dans `docker-compose.coolify.yml` (backend/front) + `deploy.conf`.
|
||||||
|
3. En Coolify : mettre à jour le compose du service (UI ou API) puis redéployer.
|
||||||
|
4. **Toujours** relancer les migrations :
|
||||||
|
```bash
|
||||||
|
docker exec ark-backend-<suffix> arkindex migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shell Django / admin
|
||||||
|
```bash
|
||||||
|
docker exec -it ark-backend-<suffix> arkindex shell
|
||||||
|
docker exec -it ark-backend-<suffix> arkindex createsuperuser # (interactif)
|
||||||
|
scripts/40-create-admin.sh # (scripté, idempotent)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recherche (Solr)
|
||||||
|
```bash
|
||||||
|
# (re)indexer un corpus après import de données
|
||||||
|
docker exec ark-backend-<suffix> arkindex reindex --corpus-id <UUID> --drop
|
||||||
|
# tout réindexer
|
||||||
|
docker exec ark-backend-<suffix> arkindex reindex --all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stockage S3 (Garage)
|
||||||
|
```bash
|
||||||
|
G=ark-garage-<suffix>
|
||||||
|
docker exec $G /garage bucket list
|
||||||
|
docker exec $G /garage bucket info staging
|
||||||
|
docker exec $G /garage stats
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sauvegardes
|
||||||
|
```bash
|
||||||
|
scripts/99-backup-db.sh # pg_dump + inventaire buckets -> backups/
|
||||||
|
# volumes gérés par Coolify : arkindex-deploy_* (anciens, backup) et <uuid>_* (actifs)
|
||||||
|
docker volume ls | grep -E 'arkindex-deploy_|tdquwauzxsq'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nettoyage des anciens volumes CLI (après validation Coolify)
|
||||||
|
```bash
|
||||||
|
docker volume rm arkindex-deploy_pgdata arkindex-deploy_redisdata \
|
||||||
|
arkindex-deploy_solrdata arkindex-deploy_garagedata
|
||||||
|
```
|
||||||
|
|
||||||
|
## Santé / vérification complète
|
||||||
|
```bash
|
||||||
|
ADMIN_PASS='…' scripts/50-verify.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mémoire serrée ?
|
||||||
|
- Soupape : désactiver la recherche → `features.search: no` dans `config.yml`, retirer le service
|
||||||
|
`ark-solr`, redéployer. Solr est le plus gros consommateur (~450 Mo).
|
||||||
|
- Le swap (4 Go) absorbe les pics ; surveiller `free -h` et `docker stats`.
|
||||||
97
docs/TROUBLESHOOTING.md
Normal file
97
docs/TROUBLESHOOTING.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# Troubleshooting — problèmes rencontrés & diagnostics
|
||||||
|
|
||||||
|
Récapitulatif des 11 problèmes du déploiement (symptôme → cause → fix), + commandes de diagnostic
|
||||||
|
réutilisables. Voir aussi `README.md` §5.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1 — `manifest unknown` au pull d'une image
|
||||||
|
**Symptôme** : `docker pull …/backend:1.13.0` → `manifest unknown`.
|
||||||
|
**Cause** : `1.13.0` n'existe qu'en `-rc1` ; l'extraction du cœur numérique inventait une version.
|
||||||
|
**Fix** : n'utiliser que des **finales** (dernière = `1.12.2`). Vérifier :
|
||||||
|
```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 — Pas de certificat wildcard possible
|
||||||
|
**Cause** : Coolify = challenge HTTP-01 seulement.
|
||||||
|
**Fix** : énumérer chaque hostname dans les règles `Host(...)` (Garage, Cantaloupe). Diag certs :
|
||||||
|
```bash
|
||||||
|
for h in ark s3.ark staging.s3.ark uploads.iiif.ark; do
|
||||||
|
curl -sS -o /dev/null -w "$h.nak0x.dev -> %{http_code} tls=%{ssl_verify_result}\n" "https://$h.nak0x.dev/"
|
||||||
|
done # tls=0 = certificat valide
|
||||||
|
```
|
||||||
|
|
||||||
|
## P3 — `PermissionError: /arkindex.yml`
|
||||||
|
**Cause** : fichiers montés en `600` (uid 1000), backend en uid **2000**.
|
||||||
|
**Fix** : `chmod 644 config.yml garage.toml cantaloupe.properties` (fait par `scripts/01`).
|
||||||
|
|
||||||
|
## P4 — Worker : `redis AuthenticationError` ⭐
|
||||||
|
**Symptôme** : « HELLO must be called with the client already authenticated ».
|
||||||
|
**Cause** : sur le réseau `coolify`, le nom générique `redis` résout vers **`coolify-redis`**
|
||||||
|
(protégé par mot de passe), pas le nôtre.
|
||||||
|
**Diagnostic** :
|
||||||
|
```bash
|
||||||
|
docker exec ark-worker-<suffix> getent hosts redis # IP de coolify-redis (mauvais)
|
||||||
|
docker exec ark-worker-<suffix> getent hosts ark-redis # IP correcte
|
||||||
|
```
|
||||||
|
**Fix** : référencer les services par leur **nom unique** `ark-redis`/`ark-database`/`ark-solr`/
|
||||||
|
`ark-garage` (jamais `redis`/`db`). Appliqué dans `config.yml` et `cantaloupe.properties`.
|
||||||
|
|
||||||
|
## P5 — `bootstrap` refuse en prod
|
||||||
|
**Cause** : `You cannot run this script in production.` + URLs `*.ark.localhost` codées en dur.
|
||||||
|
**Fix** : réplique manuelle → `scripts/20-garage-init.sh` + `scripts/30-arkindex-init.sh`.
|
||||||
|
|
||||||
|
## P6 — S3 en virtual-host (et non path-style)
|
||||||
|
**Cause** : backend force `addressing_style="auto"` (boto3) → `<bucket>.s3.<DOMAIN>`.
|
||||||
|
**Fix** : énumérer les 9 sous-domaines de bucket + `root_domain` dans `garage.toml`.
|
||||||
|
**Test** :
|
||||||
|
```bash
|
||||||
|
docker exec -i ark-backend-<suffix> arkindex shell <<'PY'
|
||||||
|
from arkindex.project.aws import s3
|
||||||
|
o=s3.Bucket("staging").Object("t"); o.put(Body=b"x"); print(o.get()["Body"].read()); o.delete()
|
||||||
|
PY
|
||||||
|
```
|
||||||
|
|
||||||
|
## P7 — Trop de workers gunicorn
|
||||||
|
**Cause** : `nproc*2+1` = 9.
|
||||||
|
**Fix** : `MAX_WORKERS: "3"` dans l'env du backend.
|
||||||
|
|
||||||
|
## P8 — Statics admin/swagger non stylés
|
||||||
|
**Cause** : pas de WhiteNoise ; statics servis par CDN.
|
||||||
|
**Fix** : `static.cdn_assets_url: https://assets.teklia.com/arkindex`. Cosmétique (API/SPA OK).
|
||||||
|
|
||||||
|
## P9 — `reindex --setup` demande un corpus
|
||||||
|
**Cause** : collections Solr créées **par corpus** à la volée. Rien à pré-créer.
|
||||||
|
|
||||||
|
## P10 — Backend sans Pillow (tests)
|
||||||
|
**Cause** : traitement image côté workers. Pour un test : générer le JPEG hors backend +
|
||||||
|
uploader via un conteneur `amazon/aws-cli` (cf. `scripts/50-verify.sh`).
|
||||||
|
|
||||||
|
## P11 — Adoption Coolify : volumes vidés + container_name écrasé
|
||||||
|
**Causes** :
|
||||||
|
1. Coolify **remappe** les volumes nommés en `<uuid>_*` et **ignore `external:`** → volumes vides.
|
||||||
|
2. Coolify écrase `container_name` en `<service>-<uuid>` (mais garde le **nom de service** = alias DNS).
|
||||||
|
**Fix** :
|
||||||
|
1. Sur un déploiement neuf : ré-init (`scripts/20/30/40`) contre les conteneurs Coolify. Pour des
|
||||||
|
données existantes : utiliser des **bind-mounts absolus** (dossiers hôte) au lieu de volumes nommés.
|
||||||
|
2. Nommer les **services** `ark-*` (déjà unique → pas de collision P4).
|
||||||
|
**Piège** : ne déclencher le deploy **qu'une fois** (POST *ou* GET `/api/v1/deploy?uuid=`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Diagnostics rapides
|
||||||
|
```bash
|
||||||
|
# logs d'un service
|
||||||
|
docker logs --tail 50 ark-backend-<suffix>
|
||||||
|
# résolution DNS depuis un conteneur
|
||||||
|
docker exec ark-worker-<suffix> getent hosts ark-redis
|
||||||
|
# état santé
|
||||||
|
docker inspect ark-backend-<suffix> --format '{{.State.Health.Status}}'
|
||||||
|
# mémoire par conteneur
|
||||||
|
docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}' | grep ark-
|
||||||
|
# routers Traefik vus par le proxy
|
||||||
|
docker exec coolify-proxy wget -qO- http://localhost:8080/api/http/routers 2>/dev/null | head
|
||||||
|
```
|
||||||
20
scripts/00-gen-secrets.sh
Executable file
20
scripts/00-gen-secrets.sh
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Génère .secrets.env avec des valeurs aléatoires (idempotent : ne réécrit pas si déjà présent).
|
||||||
|
source "$(dirname "$0")/lib.sh"
|
||||||
|
|
||||||
|
if [ -f "$ROOT/.secrets.env" ]; then
|
||||||
|
warn ".secrets.env existe déjà — rien à faire (supprime-le pour régénérer)."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Génération des secrets…"
|
||||||
|
umask 077
|
||||||
|
cat > "$ROOT/.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
|
||||||
|
ok ".secrets.env généré (mode 600)."
|
||||||
27
scripts/01-render-config.sh
Executable file
27
scripts/01-render-config.sh
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Rend config.yml, garage.toml, cantaloupe.properties et .env à partir des templates + secrets.
|
||||||
|
source "$(dirname "$0")/lib.sh"
|
||||||
|
load_secrets
|
||||||
|
|
||||||
|
command -v envsubst >/dev/null || { echo "❌ 'envsubst' requis (paquet gettext)"; exit 1; }
|
||||||
|
|
||||||
|
log "Rendu des fichiers de configuration (DOMAIN=$DOMAIN)…"
|
||||||
|
export DOMAIN SECRET_KEY PG_PASS GK_ID GK_SECRET GARAGE_RPC GARAGE_ADMIN_TOKEN
|
||||||
|
|
||||||
|
envsubst '${DOMAIN} ${SECRET_KEY} ${PG_PASS} ${GK_ID} ${GK_SECRET}' \
|
||||||
|
< "$ROOT/templates/config.yml.tmpl" > "$ROOT/config.yml"
|
||||||
|
envsubst '${DOMAIN} ${GARAGE_RPC} ${GARAGE_ADMIN_TOKEN}' \
|
||||||
|
< "$ROOT/templates/garage.toml.tmpl" > "$ROOT/garage.toml"
|
||||||
|
envsubst '${DOMAIN}' \
|
||||||
|
< "$ROOT/templates/cantaloupe.properties.tmpl" > "$ROOT/cantaloupe.properties"
|
||||||
|
|
||||||
|
cat > "$ROOT/.env" <<EOF
|
||||||
|
PG_PASS=${PG_PASS}
|
||||||
|
GK_ID=${GK_ID}
|
||||||
|
GK_SECRET=${GK_SECRET}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Le backend tourne en uid 2000 : les fichiers montés doivent être lisibles (644).
|
||||||
|
chmod 644 "$ROOT/config.yml" "$ROOT/garage.toml" "$ROOT/cantaloupe.properties"
|
||||||
|
chmod 600 "$ROOT/.env"
|
||||||
|
ok "config.yml, garage.toml, cantaloupe.properties, .env générés."
|
||||||
33
scripts/10-up-cli.sh
Executable file
33
scripts/10-up-cli.sh
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Déploiement complet en mode "docker compose" (CLI), de A à Z.
|
||||||
|
# Prérequis : Docker + réseau externe "coolify" (proxy Traefik) + DNS *.<DOMAIN> -> serveur.
|
||||||
|
# NB : ce script force le mode CLI (conteneurs sans suffixe Coolify).
|
||||||
|
source "$(dirname "$0")/lib.sh"
|
||||||
|
export CONTAINER_SUFFIX="" # mode CLI
|
||||||
|
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
log "1/6 Secrets + configs"
|
||||||
|
scripts/00-gen-secrets.sh
|
||||||
|
scripts/01-render-config.sh
|
||||||
|
|
||||||
|
log "2/6 Infra de base (db, redis, solr, garage)"
|
||||||
|
docker compose up -d db redis solr garage
|
||||||
|
# attendre Postgres
|
||||||
|
until docker inspect ark-database --format '{{.State.Health.Status}}' 2>/dev/null | grep -q healthy; do sleep 3; done
|
||||||
|
|
||||||
|
log "3/6 Init Garage"
|
||||||
|
scripts/20-garage-init.sh
|
||||||
|
|
||||||
|
log "4/6 Backend + migrations + bootstrap prod"
|
||||||
|
docker compose up -d backend
|
||||||
|
until docker inspect ark-backend --format '{{.State.Health.Status}}' 2>/dev/null | grep -q healthy; do sleep 3; done
|
||||||
|
scripts/30-arkindex-init.sh
|
||||||
|
|
||||||
|
log "5/6 Reste de la stack (cantaloupe, front, worker)"
|
||||||
|
docker compose up -d cantaloupe front worker
|
||||||
|
|
||||||
|
log "6/6 Compte admin"
|
||||||
|
scripts/40-create-admin.sh
|
||||||
|
|
||||||
|
ok "Déploiement CLI terminé. Vérifie avec scripts/50-verify.sh"
|
||||||
30
scripts/20-garage-init.sh
Executable file
30
scripts/20-garage-init.sh
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Initialise Garage : layout du cluster (1 nœud), import de la clé S3, création + droits des buckets.
|
||||||
|
# Idempotent. Fonctionne en mode CLI ou Coolify (via CONTAINER_SUFFIX).
|
||||||
|
source "$(dirname "$0")/lib.sh"
|
||||||
|
load_secrets
|
||||||
|
|
||||||
|
G=$(cname ark-garage)
|
||||||
|
log "Init Garage ($G)…"
|
||||||
|
|
||||||
|
NODE=$(docker exec "$G" /garage status 2>/dev/null | awk '/NO ROLE ASSIGNED/{print $1}')
|
||||||
|
if [ -n "${NODE:-}" ]; then
|
||||||
|
docker exec "$G" /garage layout assign -z dev -c 1G "$NODE" >/dev/null
|
||||||
|
# applique la version courante+1 (1 si cluster neuf)
|
||||||
|
VER=$(docker exec "$G" /garage layout show 2>/dev/null | awk '/Current cluster layout version/{print $NF}')
|
||||||
|
docker exec "$G" /garage layout apply --version "$(( ${VER:-0} + 1 ))" >/dev/null
|
||||||
|
ok "Layout appliqué."
|
||||||
|
else
|
||||||
|
warn "Nœud déjà configuré (layout existant)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Import de la clé S3 (même GK_ID/GK_SECRET que config.yml)
|
||||||
|
docker exec "$G" /garage key import --yes -n arkindex "$GK_ID" "$GK_SECRET" >/dev/null 2>&1 \
|
||||||
|
&& ok "Clé S3 importée." || warn "Clé S3 déjà présente."
|
||||||
|
|
||||||
|
for b in $GARAGE_BUCKETS; do
|
||||||
|
docker exec "$G" /garage bucket create "$b" >/dev/null 2>&1 || true
|
||||||
|
docker exec "$G" /garage bucket allow --read --write --owner --key "$GK_ID" "$b" >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
N=$(docker exec "$G" /garage bucket list 2>/dev/null | grep -c '20' || true)
|
||||||
|
ok "Buckets prêts ($N)."
|
||||||
26
scripts/30-arkindex-init.sh
Executable file
26
scripts/30-arkindex-init.sh
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Migrations DB + équivalent PROD de `arkindex bootstrap` (dev-only) :
|
||||||
|
# crée les 2 ImageServers (uploads/ingest) avec le VRAI domaine + la farm Ponos.
|
||||||
|
source "$(dirname "$0")/lib.sh"
|
||||||
|
|
||||||
|
log "Migrations…"
|
||||||
|
dex ark-backend arkindex migrate 2>&1 | tail -2
|
||||||
|
ok "Migrations appliquées."
|
||||||
|
|
||||||
|
log "ImageServers + farm Ponos (domaine=$DOMAIN)…"
|
||||||
|
dexi ark-backend env DOMAIN="$DOMAIN" arkindex shell <<'PYEOF' 2>&1 | tail -3
|
||||||
|
import os
|
||||||
|
from arkindex.images.models import ImageServer
|
||||||
|
from arkindex.ponos.models import Farm
|
||||||
|
d = os.environ["DOMAIN"]
|
||||||
|
for sid, url, bucket, name in [
|
||||||
|
(12345, f"https://uploads.iiif.{d}/iiif/2", "uploads", "Local uploads IIIF server"),
|
||||||
|
(67890, f"https://ingest.iiif.{d}/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("ImageServers + farm OK")
|
||||||
|
PYEOF
|
||||||
|
ok "Bootstrap (prod) terminé."
|
||||||
25
scripts/40-create-admin.sh
Executable file
25
scripts/40-create-admin.sh
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Crée (ou met à jour) le compte admin avec toutes les capacités. Génère un mot de passe si non fourni.
|
||||||
|
# Usage: ADMIN_PASS='monmotdepasse' scripts/40-create-admin.sh (sinon mot de passe aléatoire)
|
||||||
|
source "$(dirname "$0")/lib.sh"
|
||||||
|
|
||||||
|
PASS="${ADMIN_PASS:-$(openssl rand -base64 18 | tr -d '/+=' | head -c 20)}"
|
||||||
|
log "Compte admin $ADMIN_EMAIL…"
|
||||||
|
dexi ark-backend env E="$ADMIN_EMAIL" P="$PASS" N="$ADMIN_DISPLAY_NAME" arkindex shell <<'PYEOF' 2>&1 | tail -2
|
||||||
|
import os
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
U = get_user_model()
|
||||||
|
email, pw, name = os.environ["E"], os.environ["P"], os.environ["N"]
|
||||||
|
u = U.objects.filter(email=email).first() or U.objects.create_superuser(email=email, display_name=name, password=pw)
|
||||||
|
u.set_password(pw); u.is_admin = True; 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 OK:", u.email, "is_admin =", u.is_admin)
|
||||||
|
PYEOF
|
||||||
|
ok "Admin prêt."
|
||||||
|
echo "########################################"
|
||||||
|
echo " Email : $ADMIN_EMAIL"
|
||||||
|
echo " Password : $PASS"
|
||||||
|
echo " (à changer après la 1ère connexion)"
|
||||||
|
echo "########################################"
|
||||||
44
scripts/50-verify.sh
Executable file
44
scripts/50-verify.sh
Executable file
@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Vérifications end-to-end : HTTPS/API, S3 (hairpin), pipeline IIIF, état conteneurs, RAM.
|
||||||
|
# Usage: [ADMIN_PASS=... ] scripts/50-verify.sh
|
||||||
|
source "$(dirname "$0")/lib.sh"
|
||||||
|
load_secrets
|
||||||
|
|
||||||
|
echo "=== 1) HTTPS public + certificat ==="
|
||||||
|
curl -sS -o /dev/null -w " health -> HTTP %{http_code} (tls_verify=%{ssl_verify_result})\n" \
|
||||||
|
"https://${DOMAIN}/api/v1/health/"
|
||||||
|
|
||||||
|
if [ -n "${ADMIN_PASS:-}" ]; then
|
||||||
|
echo "=== 2) Login admin ==="
|
||||||
|
curl -sS -X POST "https://${DOMAIN}/api/v1/user/login/" -H 'Content-Type: application/json' \
|
||||||
|
-d "{\"email\":\"${ADMIN_EMAIL}\",\"password\":\"${ADMIN_PASS}\"}" \
|
||||||
|
-o /dev/null -w " login -> HTTP %{http_code}\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== 3) S3 hairpin (backend write/read/delete via https://s3.${DOMAIN}) ==="
|
||||||
|
dexi ark-backend arkindex shell <<'PY' 2>&1 | tail -2
|
||||||
|
from arkindex.project.aws import s3
|
||||||
|
o=s3.Bucket("staging").Object("healthcheck.txt")
|
||||||
|
o.put(Body=b"ok"); print(" S3 read:", o.get()["Body"].read()); o.delete(); print(" S3 delete: ok")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "=== 4) Pipeline IIIF (upload image de test -> Cantaloupe) ==="
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
docker run --rm -v "$TMP":/out alpine sh -c "apk add -q imagemagick imagemagick-jpeg && magick -size 400x300 xc:'#1e78c8' /out/p.jpg" >/dev/null 2>&1
|
||||||
|
docker run --rm --network ark -v "$TMP/p.jpg":/img.jpg:ro \
|
||||||
|
-e AWS_ACCESS_KEY_ID="$GK_ID" -e AWS_SECRET_ACCESS_KEY="$GK_SECRET" \
|
||||||
|
amazon/aws-cli --endpoint-url http://ark-garage:3900 --region local \
|
||||||
|
s3 cp /img.jpg s3://uploads/_verify/p.jpg >/dev/null 2>&1
|
||||||
|
curl -sS -o /dev/null -w " IIIF info.json -> HTTP %{http_code}\n" \
|
||||||
|
"https://uploads.iiif.${DOMAIN}/iiif/2/_verify%2Fp.jpg/info.json"
|
||||||
|
curl -sS -o /dev/null -w " IIIF render -> HTTP %{http_code}\n" \
|
||||||
|
"https://uploads.iiif.${DOMAIN}/iiif/2/_verify%2Fp.jpg/full/200,/0/default.jpg"
|
||||||
|
docker run --rm --network ark -e AWS_ACCESS_KEY_ID="$GK_ID" -e AWS_SECRET_ACCESS_KEY="$GK_SECRET" \
|
||||||
|
amazon/aws-cli --endpoint-url http://ark-garage:3900 --region local s3 rm s3://uploads/_verify/p.jpg >/dev/null 2>&1
|
||||||
|
rm -rf "$TMP"
|
||||||
|
|
||||||
|
echo "=== 5) Conteneurs ==="
|
||||||
|
docker ps --filter "name=ark-" --format ' {{.Names}} {{.Status}}' | sort
|
||||||
|
|
||||||
|
echo "=== 6) RAM ==="
|
||||||
|
free -h | awk 'NR==2{printf " used=%s free=%s dispo=%s\n",$3,$4,$7}'
|
||||||
53
scripts/90-coolify-adopt.sh
Executable file
53
scripts/90-coolify-adopt.sh
Executable file
@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Adopte la stack comme ressource "Docker Compose" gérée par Coolify (via l'API).
|
||||||
|
# Prérequis : token API Coolify dans $COOLIFY_TOKEN_FILE (Settings -> API Tokens, read/write).
|
||||||
|
#
|
||||||
|
# ⚠️ Coolify remappe les volumes nommés en <uuid>_* et IGNORE `external:` -> la stack repart sur
|
||||||
|
# des volumes VIDES. Sur un déploiement neuf : ré-init ensuite (20/30/40). Sur une stack avec
|
||||||
|
# données : préférer des bind-mounts absolus (host dirs) plutôt que des volumes nommés.
|
||||||
|
source "$(dirname "$0")/lib.sh"
|
||||||
|
load_secrets
|
||||||
|
[ -f "$COOLIFY_TOKEN_FILE" ] || { echo "❌ Token Coolify absent ($COOLIFY_TOKEN_FILE)"; exit 1; }
|
||||||
|
|
||||||
|
COMPOSE_FILE="$ROOT/docker-compose.coolify.yml"
|
||||||
|
|
||||||
|
log "1) Projet 'arkindex' (si absent)"
|
||||||
|
if [ -z "${COOLIFY_PROJECT_UUID:-}" ]; then
|
||||||
|
COOLIFY_PROJECT_UUID=$(capi POST /projects -d '{"name":"arkindex","description":"Arkindex full stack"}' \
|
||||||
|
| python3 -c 'import sys,json;print(json.load(sys.stdin)["uuid"])')
|
||||||
|
echo " project_uuid=$COOLIFY_PROJECT_UUID (à reporter dans deploy.conf)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "2) Création du service (docker_compose_raw en BASE64)"
|
||||||
|
BODY=$(python3 - "$COMPOSE_FILE" <<PY
|
||||||
|
import base64,json,sys
|
||||||
|
raw=open(sys.argv[1]).read()
|
||||||
|
print(json.dumps({
|
||||||
|
"server_uuid":"$COOLIFY_SERVER_UUID","project_uuid":"$COOLIFY_PROJECT_UUID",
|
||||||
|
"environment_name":"$COOLIFY_ENV_NAME","environment_uuid":"$COOLIFY_ENV_UUID",
|
||||||
|
"name":"arkindex","description":"Arkindex full stack",
|
||||||
|
"docker_compose_raw":base64.b64encode(raw.encode()).decode(),"instant_deploy":False}))
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
RESP=$(capi POST /services -d "$BODY")
|
||||||
|
SVC=$(echo "$RESP" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("uuid",""))' 2>/dev/null)
|
||||||
|
[ -n "$SVC" ] || { echo "❌ Échec création service: $RESP"; exit 1; }
|
||||||
|
echo " service_uuid=$SVC (à reporter dans deploy.conf : COOLIFY_SERVICE_UUID + CONTAINER_SUFFIX=-$SVC)"
|
||||||
|
|
||||||
|
log "3) Variables d'env (secrets) sur le service"
|
||||||
|
for K in PG_PASS GK_ID GK_SECRET; do
|
||||||
|
V="${!K}"
|
||||||
|
capi PATCH "/services/$SVC/envs" -d "{\"key\":\"$K\",\"value\":\"$V\"}" >/dev/null \
|
||||||
|
|| capi POST "/services/$SVC/envs" -d "{\"key\":\"$K\",\"value\":\"$V\"}" >/dev/null
|
||||||
|
done
|
||||||
|
ok "Env posées."
|
||||||
|
|
||||||
|
warn "Étape suivante MANUELLE (coupure) :"
|
||||||
|
echo " cd $ROOT && docker compose down # arrête la stack CLI (garde les volumes)"
|
||||||
|
echo " # UNE SEULE fois :"
|
||||||
|
echo " curl -s -X POST -H \"Authorization: Bearer \$(cat $COOLIFY_TOKEN_FILE)\" \\"
|
||||||
|
echo " \"$COOLIFY_API/deploy?uuid=$SVC\""
|
||||||
|
echo " # puis ré-init sur les volumes neufs :"
|
||||||
|
echo " CONTAINER_SUFFIX=-$SVC scripts/20-garage-init.sh"
|
||||||
|
echo " CONTAINER_SUFFIX=-$SVC scripts/30-arkindex-init.sh"
|
||||||
|
echo " CONTAINER_SUFFIX=-$SVC scripts/40-create-admin.sh"
|
||||||
14
scripts/99-backup-db.sh
Executable file
14
scripts/99-backup-db.sh
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Sauvegarde PostgreSQL (pg_dump) + liste des buckets Garage. Sortie dans backups/.
|
||||||
|
source "$(dirname "$0")/lib.sh"
|
||||||
|
mkdir -p "$ROOT/backups"
|
||||||
|
TS=$(date +%Y%m%d-%H%M%S)
|
||||||
|
OUT="$ROOT/backups/arkindex-db-$TS.sql.gz"
|
||||||
|
|
||||||
|
log "Dump PostgreSQL -> $OUT"
|
||||||
|
dex ark-database pg_dump -U arkindex arkindex | gzip > "$OUT"
|
||||||
|
ok "Dump DB : $(du -h "$OUT" | cut -f1)"
|
||||||
|
|
||||||
|
log "Inventaire Garage"
|
||||||
|
dex ark-garage /garage bucket list 2>/dev/null | tee "$ROOT/backups/garage-buckets-$TS.txt"
|
||||||
|
ok "Backup terminé dans backups/"
|
||||||
39
scripts/lib.sh
Executable file
39
scripts/lib.sh
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fonctions et variables partagées par les scripts de déploiement.
|
||||||
|
# Usage : source "$(dirname "$0")/lib.sh"
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
# Config non-secrète
|
||||||
|
[ -f "$ROOT/deploy.conf" ] && . "$ROOT/deploy.conf"
|
||||||
|
|
||||||
|
# Secrets (optionnels selon le script)
|
||||||
|
load_secrets() {
|
||||||
|
if [ ! -f "$ROOT/.secrets.env" ]; then
|
||||||
|
echo "❌ $ROOT/.secrets.env manquant. Lance d'abord scripts/00-gen-secrets.sh" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
set -a; . "$ROOT/.secrets.env"; set +a
|
||||||
|
}
|
||||||
|
|
||||||
|
# Nom réel d'un conteneur (avec suffixe Coolify si défini)
|
||||||
|
# ex: cname ark-backend -> ark-backend-tdquwauzxsq0985fk9kjqqys (mode Coolify)
|
||||||
|
# -> ark-backend (mode CLI)
|
||||||
|
cname() { echo "$1${CONTAINER_SUFFIX:-}"; }
|
||||||
|
|
||||||
|
# docker exec sur un service Arkindex
|
||||||
|
dex() { local svc="$1"; shift; docker exec "$(cname "$svc")" "$@"; }
|
||||||
|
dexi() { local svc="$1"; shift; docker exec -i "$(cname "$svc")" "$@"; }
|
||||||
|
|
||||||
|
log() { printf '\033[1;34m▶ %s\033[0m\n' "$*"; }
|
||||||
|
ok() { printf '\033[1;32m✅ %s\033[0m\n' "$*"; }
|
||||||
|
warn() { printf '\033[1;33m⚠ %s\033[0m\n' "$*"; }
|
||||||
|
|
||||||
|
coolify_token() { cat "${COOLIFY_TOKEN_FILE}"; }
|
||||||
|
capi() { # capi METHOD PATH [curl-args...]
|
||||||
|
local m="$1" p="$2"; shift 2
|
||||||
|
curl -s -X "$m" -H "Authorization: Bearer $(coolify_token)" \
|
||||||
|
-H "Content-Type: application/json" -H "Accept: application/json" \
|
||||||
|
"$@" "${COOLIFY_API}${p}"
|
||||||
|
}
|
||||||
39
templates/cantaloupe.properties.tmpl
Normal file
39
templates/cantaloupe.properties.tmpl
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Template config Cantaloupe (serveur IIIF). Rendu par scripts/01-render-config.sh (envsubst).
|
||||||
|
# Variable attendue : DOMAIN.
|
||||||
|
# - Cantaloupe lit les images dans Garage EN INTERNE (http://ark-garage:3900, sans TLS).
|
||||||
|
# - Le bucket est déduit du sous-domaine de la requête via ScriptLookupStrategy + top_domain
|
||||||
|
# (ex: uploads.iiif.<DOMAIN> -> bucket "uploads"). Le script delegate est fourni par l'image.
|
||||||
|
# - Les identifiants S3 viennent des variables d'env AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY.
|
||||||
|
|
||||||
|
http.enabled = true
|
||||||
|
http.host = 0.0.0.0
|
||||||
|
http.port = 80
|
||||||
|
http.http2.enabled = false
|
||||||
|
https.enabled = false
|
||||||
|
|
||||||
|
endpoint.iiif.1.enabled = false
|
||||||
|
endpoint.iiif.2.enabled = true
|
||||||
|
|
||||||
|
source.static = S3Source
|
||||||
|
S3Source.endpoint = http://ark-garage:3900
|
||||||
|
S3Source.region = local
|
||||||
|
S3Source.top_domain = iiif.${DOMAIN}
|
||||||
|
S3Source.lookup_strategy = ScriptLookupStrategy
|
||||||
|
S3Source.BasicLookupStrategy.bucket.name =
|
||||||
|
|
||||||
|
cache.server.derivative.enabled = true
|
||||||
|
cache.server.derivative = S3Cache
|
||||||
|
cache.server.derivative.ttl_seconds = 604800
|
||||||
|
S3Cache.endpoint = http://ark-garage:3900
|
||||||
|
S3Cache.region = local
|
||||||
|
S3Cache.bucket.name = iiif-cache
|
||||||
|
|
||||||
|
log.application.level = info
|
||||||
|
log.application.ConsoleAppender.enabled = true
|
||||||
|
log_error_responses = true
|
||||||
|
|
||||||
|
HeapCache.target_size = 512M
|
||||||
|
HeapCache.persist = false
|
||||||
|
|
||||||
|
processor.downscale_filter = lanczos3
|
||||||
|
processor.upscale_filter = lanczos3
|
||||||
74
templates/config.yml.tmpl
Normal file
74
templates/config.yml.tmpl
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
# Template de la configuration Arkindex. Rendu par scripts/01-render-config.sh (envsubst).
|
||||||
|
# Variables attendues : DOMAIN, SECRET_KEY, PG_PASS, GK_ID, GK_SECRET.
|
||||||
|
# IMPORTANT : les services internes sont référencés par leur NOM DE CONTENEUR UNIQUE
|
||||||
|
# (ark-database, ark-redis, ark-solr) et jamais par db/redis/solr — sinon collision DNS
|
||||||
|
# avec coolify-redis/coolify-db sur le réseau partagé "coolify".
|
||||||
|
|
||||||
|
arkindex_env: production
|
||||||
|
public_hostname: https://${DOMAIN}
|
||||||
|
local_imageserver_id: 12345
|
||||||
|
secret_key: "${SECRET_KEY}"
|
||||||
|
|
||||||
|
cache:
|
||||||
|
type: redis
|
||||||
|
url: redis://ark-redis:6379/1
|
||||||
|
|
||||||
|
database:
|
||||||
|
host: ark-database
|
||||||
|
port: 5432
|
||||||
|
name: arkindex
|
||||||
|
user: arkindex
|
||||||
|
password: "${PG_PASS}"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
host: ark-redis
|
||||||
|
port: 6379
|
||||||
|
|
||||||
|
s3:
|
||||||
|
access_key_id: "${GK_ID}"
|
||||||
|
secret_access_key: "${GK_SECRET}"
|
||||||
|
endpoint: https://s3.${DOMAIN}
|
||||||
|
region: local
|
||||||
|
|
||||||
|
allowed_hosts:
|
||||||
|
- ${DOMAIN}
|
||||||
|
|
||||||
|
session:
|
||||||
|
cookie_domain: ${DOMAIN}
|
||||||
|
cookie_secure: true
|
||||||
|
|
||||||
|
csrf:
|
||||||
|
cookie_domain: ${DOMAIN}
|
||||||
|
cookie_secure: true
|
||||||
|
trusted_origins:
|
||||||
|
- 'https://${DOMAIN}'
|
||||||
|
|
||||||
|
cors:
|
||||||
|
origin_whitelist:
|
||||||
|
- https://${DOMAIN}
|
||||||
|
|
||||||
|
features:
|
||||||
|
signup: no
|
||||||
|
search: yes
|
||||||
|
|
||||||
|
solr:
|
||||||
|
api_url: http://ark-solr:8983/solr/
|
||||||
|
|
||||||
|
static:
|
||||||
|
cdn_assets_url: https://assets.teklia.com/arkindex
|
||||||
|
frontend_version: 1.12.2
|
||||||
|
|
||||||
|
ponos:
|
||||||
|
default_env:
|
||||||
|
ARKINDEX_API_URL: https://${DOMAIN}/api/v1/
|
||||||
|
|
||||||
|
ingest:
|
||||||
|
access_key_id: "${GK_ID}"
|
||||||
|
secret_access_key: "${GK_SECRET}"
|
||||||
|
endpoint: https://s3.${DOMAIN}
|
||||||
|
region: local
|
||||||
|
imageserver_id: 67890
|
||||||
|
extra_buckets:
|
||||||
|
- ingest
|
||||||
|
prefix_by_bucket_name: false
|
||||||
23
templates/garage.toml.tmpl
Normal file
23
templates/garage.toml.tmpl
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Template de la config Garage (S3). Rendu par scripts/01-render-config.sh (envsubst).
|
||||||
|
# Variables attendues : DOMAIN, GARAGE_RPC, GARAGE_ADMIN_TOKEN.
|
||||||
|
# root_domain permet l'adressage "virtual-hosted" <bucket>.s3.<DOMAIN> (imposé par boto3
|
||||||
|
# addressing_style=auto côté backend).
|
||||||
|
|
||||||
|
metadata_dir = "/var/lib/garage/meta"
|
||||||
|
data_dir = "/var/lib/garage/data"
|
||||||
|
db_engine = "sqlite"
|
||||||
|
|
||||||
|
replication_factor = 1
|
||||||
|
|
||||||
|
rpc_bind_addr = "[::]:3901"
|
||||||
|
rpc_public_addr = "127.0.0.1:3901"
|
||||||
|
rpc_secret = "${GARAGE_RPC}"
|
||||||
|
|
||||||
|
[s3_api]
|
||||||
|
s3_region = "local"
|
||||||
|
api_bind_addr = "[::]:3900"
|
||||||
|
root_domain = ".s3.${DOMAIN}"
|
||||||
|
|
||||||
|
[admin]
|
||||||
|
api_bind_addr = "[::]:3903"
|
||||||
|
admin_token = "${GARAGE_ADMIN_TOKEN}"
|
||||||
Loading…
Reference in New Issue
Block a user