Repository navigation
Déploiement
Unraid : voir le Guide Unraid (fichiers prêts à l'emploi dans
deploy/unraid/) et les Procédures : pile Compose, modèles Unraid, export des données, configuration de Zitadel et de Cloudflare. Le site de production esthttps://www.nicephos.art. L'image est publiée surghcr.io/volfieldd/deux-regardsà chaque mise à jour demain(voir Image Docker).
Cette page décrit la pile générique du dépôt (compose.yaml à la racine), qui construit l'image sur place. La pile Unraid, elle, télécharge l'image publiée.
Le site tourne dans Docker. Il est exposé sur Internet par un tunnel Cloudflare (cloudflared) : aucun port n'est ouvert vers l'extérieur et Cloudflare fournit le HTTPS, donc ni Caddy ni certificat à gérer.
Internet ──HTTPS──► Cloudflare ──tunnel──► cloudflared ──http://site:3000──► site
│
/data ◄── dossier de l'hôte (DATA_DIR)
backup (chaque jour) ──► dossier de l'hôte (BACKUP_DIR)
Tout l'état du site (base, originaux, preuves, logos, images servies) est dans un dossier de l'hôte, DATA_DIR, monté dans le conteneur sous /data. Plusieurs protections empêchent de le perdre :
| Risque | Protection |
|---|---|
| Oubli du montage | Le serveur refuse de démarrer si /data n'est pas un montage : « n'est pas un montage : les données seraient perdues ». |
| Dossier absent ou mal orthographié | Compose refuse de démarrer (create_host_path: false) au lieu de créer un dossier vide. |
docker compose down -v, docker system prune
|
Sans effet : un dossier de l'hôte n'est pas un volume Docker. Vérifié : 12 photos et 12 preuves intactes après down -v et une reconstruction. |
| Recréation ou mise à jour de l'image | L'image ne contient aucune donnée, et le système de fichiers du conteneur est en lecture seule. |
| Arrêt brutal pendant un calcul d'image | Écritures atomiques et ménage au démarrage (voir Images et variantes). |
| Panne du disque, fausse manœuvre | Sauvegarde quotidienne dans un second dossier, BACKUP_DIR (idéalement sur un autre disque, puis copié hors de la machine). |
git clone https://github.com/Volfieldd/deux-regards.git && cd deux-regards
sudo mkdir -p /srv/site-photo/data /srv/site-photo/sauvegardes
sudo chown -R 1000:1000 /srv/site-photo # ou l'utilisateur choisi par APP_UID/APP_GID
cp .env.example .env # puis compléter (voir ci-dessous)
docker compose --profile tunnel up -d --buildVariables propres au déploiement, dans .env :
| Variable | Exemple | Rôle |
|---|---|---|
DATA_DIR |
/srv/site-photo/data |
Dossier des données sur l'hôte. Obligatoire. |
BACKUP_DIR |
/srv/site-photo/sauvegardes |
Dossier des sauvegardes sur l'hôte. Obligatoire, et pas à l'intérieur de DATA_DIR. |
APP_UID, APP_GID
|
1000 |
Utilisateur du conteneur, qui doit posséder les deux dossiers. |
BIND_ADDR, HOST_PORT
|
127.0.0.1, 3000
|
Port local du site, utile pour un contrôle ; cloudflared n'en a pas besoin. |
TUNNEL_TOKEN |
— | Jeton du tunnel Cloudflare (profil tunnel). |
BACKUP_KEEP |
14 |
Nombre d'instantanés de base conservés. |
BACKUP_INTERVAL_HOURS |
24 |
Fréquence des sauvegardes. |
BACKUP_DELETED_DAYS |
90 |
Jours pendant lesquels un fichier supprimé (original, logo) reste dans la sauvegarde ; transmise au conteneur de sauvegarde. |
SITE_MEM_LIMIT |
3g (6g pour « Précis ») |
Mémoire du conteneur du site (mem_limit). Les descriptions « Précis » (Florence-2 large, page Séries) demandent 6g ; « Léger » tient dans 3g. Le site lit la limite réelle et refuse un modèle qui ne tient pas. |
Et pour le site en ligne :
NODE_ENV=production
PUBLIC_URL=https://photos.exemple.fr
COOKIE_SECRET=<32+ caractères aléatoires>
OIDC_ISSUER=… OIDC_CLIENT_ID=… OIDC_CLIENT_SECRET=… OIDC_PROJECT_ID=… OIDC_ORG_ID=…
# Pas de LOCAL_AUTH_TOKEN en ligne (le serveur refuserait de démarrer).- Cloudflare Zero Trust → Networks → Tunnels → Create a tunnel (type Cloudflared).
- Copier le jeton affiché dans
TUNNEL_TOKEN(fichier.env). - Dans le tunnel, ajouter un Public hostname : le domaine du site et le service
http://site:3000.siteest le nom du service danscompose.yaml. - Lancer
docker compose --profile tunnel up -d. - Dans Zitadel, déclarer
https://<domaine>/auth/callbackethttps://<domaine>/.
L'adresse réelle des visiteurs arrive par X-Forwarded-For. Le site fait confiance aux adresses privées (TRUST_PROXY par défaut en ligne : loopback,uniquelocal), donc à cloudflared sur le réseau Docker : la limite de connexions s'applique par visiteur, pas au tunnel entier. Si le port du site est ouvert sur le réseau local (BIND_ADDR), un poste de ce réseau peut lui aussi se faire passer pour un autre visiteur : fixez alors TRUST_PROXY à l'adresse exacte de cloudflared, comme le fait la pile Unraid (réseau dédié, TUNNEL_IP).
Le site n'a qu'une adresse canonique, celle de PUBLIC_URL (en production https://www.nicephos.art). Elle sert aux liens absolus, à la connexion Zitadel, à Instagram et aux mentions de droits des images. Toutes les autres adresses (nicephos.art sans www, autres sous-domaines, http://) y sont redirigées par Cloudflare, avant d'atteindre le tunnel.
-
Tunnel : le Public hostname du tunnel est
www.nicephos.art(servicehttp://site:3000, ounicephos:3000sur Unraid). -
DNS : pour que Cloudflare puisse rediriger une adresse, elle doit exister et être proxifiée (nuage orange) : un enregistrement pour
@et un pour*. Leur cible importe peu, puisque la règle répond avant tout contact avec un serveur (par exemple unAvers192.0.2.1, adresse de documentation jamais jointe).wwwgarde l'enregistrementCNAMEcréé par le tunnel. -
Rules → Redirect Rules → Create rule :
-
If incoming requests match… : Custom filter expression, puis Field Hostname, Operator does not equal, Value
www.nicephos.art(sans guillemets). L'Expression Preview doit afficher exactement(http.host ne "www.nicephos.art"); -
Then : Dynamic, expression
concat("https://www.nicephos.art", http.request.uri.path); - code 301, case Preserve query string cochée.
-
If incoming requests match… : Custom filter expression, puis Field Hostname, Operator does not equal, Value
-
SSL/TLS → Edge Certificates → Always Use HTTPS : activé.
http://www.nicephos.artpasse ainsi en HTTPS (la règle ci-dessus ne le traite pas, puisque l'hôte est déjà le bon). -
Network → IP Geolocation : activé (c’est le cas par défaut). Cloudflare transmet alors le pays du visiteur (
CF-IPCountry), utilisé par la carte de la page Statistiques ; sans lui, les visites sont comptées en « Pays inconnu ».
Pièges :
-
Expression dans le champ Value : le champ Value n'attend qu'un nom de domaine. Y coller une expression (
http.host ne …), ou laisser Field sur URI Full, produit une règle qui compare l'URL complète à ce texte : avec l'opérateur wildcard, elle ne correspond jamais (aucune redirection) ; avec does not equal, elle correspond toujours, y compris pourwww.nicephos.art, d'où une boucle de redirection et un site inaccessible. Si cela arrive : désactiver la règle (le site revient aussitôt), corriger l'aperçu comme ci-dessus, et tester en 302 avant de repasser en 301 (un navigateur garde un 301 en mémoire). -
Certificat : le certificat gratuit de Cloudflare (Universal SSL) couvre
nicephos.artet*.nicephos.art, soit un seul niveau de sous-domaine.a.b.nicephos.artprovoquerait une erreur de certificat avant toute redirection : ne créez pas d'adresse à deux niveaux.
Vérifier : curl -sI "https://nicephos.art/serie/rue?x=1" doit répondre 301 avec location: https://www.nicephos.art/serie/rue?x=1, et curl -sI https://www.nicephos.art/ doit répondre 200.
Le service backup fait une sauvegarde au démarrage, puis toutes les 24 h :
BACKUP_DIR/
db/site-2026-10-10T130335.db instantané cohérent de la base, contrôlé (14 derniers gardés)
files/originals/… originaux : ajoutés ; un original supprimé est effacé 90 jours après
files/watermarks/…, files/brand/…
last.json compte rendu de la dernière sauvegarde
- Variantes : elles ne sont pas sauvegardées, puisqu'elles se recalculent.
-
Modèle de « Proposer une série » :
models/n'est pas sauvegardé non plus. S'il manque, il est retéléchargé depuis huggingface.co (empreinte SHA-256 vérifiée), et les empreintes d'images, gardées dans la base, sont conservées. -
Originaux : ceux qu'on supprime dans l'admin restent récupérables dans la sauvegarde pendant 90 jours (
BACKUP_DELETED_DAYS), puis ils en sont effacés (droit à l'effacement). Garde-fou : si le dossier des originaux est vide ou absent (montage raté), rien n'est effacé. -
Hors de la machine : copiez aussi
BACKUP_DIRailleurs (rsync,restic, disque externe). Une sauvegarde sur la même machine ne protège pas d'une panne de celle-ci.
Vérifier : cat $BACKUP_DIR/last.json ou docker compose logs backup.
Sauvegarde manuelle : docker compose run --rm backup node server/db/backup.js.
docker compose down
cp -a "$DATA_DIR" "$DATA_DIR.avant-restauration" # toujours garder l'état actuel
cp "$BACKUP_DIR/db/site-<date>.db" "$DATA_DIR/site.db" && rm -f "$DATA_DIR"/site.db-wal "$DATA_DIR"/site.db-shm
rsync -a "$BACKUP_DIR/files/" "$DATA_DIR/" # originaux, logos
docker compose up -d # les variantes se recalculent seulesLes preuves d'antériorité restent valides : elles portent sur les originaux, restaurés à l'identique.
Le workflow .github/workflows/image.yml construit l'image (linux/amd64) à chaque push sur main, ou à la demande (Run workflow), et la publie sur ghcr.io/volfieldd/deux-regards avec deux étiquettes :
| Étiquette | Usage |
|---|---|
latest |
dernière version de main
|
sha-xxxxxxx |
version d'un commit précis (7 premiers caractères) : pour revenir en arrière ou figer une version |
L'image (Dockerfile) part de node:22-bookworm-slim, tourne sous l'utilisateur node, sans compilateur, et contient seulement le code, les gabarits, les fichiers publics, assets/, seed/ et docs/aide/. Elle fixe NODE_ENV=production, HOST=0.0.0.0, PORT=3000, STORAGE_DIR=/data, DATABASE_PATH=/data/site.db et REQUIRE_PERSISTENT_STORAGE=1, et vérifie sa santé sur /healthz. Elle n'a pas d'instruction VOLUME : sans montage de /data, le serveur refuse de démarrer.
Le récapitulatif d'une rencontre saisie sur le téléphone peut partir tout seul : SMTP_URL (par exemple smtps://adresse%40gmail.com:motdepasse@smtp.gmail.com:465, avec un mot de passe d'application Gmail) et MAIL_FROM (Nicephos <adresse@gmail.com>) dans le .env. Gratuit, par votre propre boîte : aucun service d'envoi tiers. Sans ces variables, l'écran propose d'envoyer le message depuis l'app Mail du téléphone.
Les propositions de traduction de l'admin passent par un serveur LibreTranslate installé à côté du site : docker compose --profile traduction up -d, puis TRANSLATE_URL=http://libretranslate:5000 dans le .env. Il télécharge une fois ses modèles (fr, en, de, ~300 Mo dans le volume traduction-modeles) et travaille ensuite hors ligne : aucun texte ne sort du serveur. Environ 1 Go de mémoire. Sans TRANSLATE_URL, le bouton n'apparaît pas. Sur Unraid, le conteneur fait partie de la pile (voir deploy/unraid/README.md).
Pile générique (image construite sur place) :
git pull
docker compose up -d --build # migrations appliquées au démarragePile Unraid (image publiée) : Compose Pull puis Compose Up. Pour revenir à une version précise, mettez IMAGE=ghcr.io/volfieldd/deux-regards:sha-xxxxxxx dans le .env de la pile. Attention : une migration déjà appliquée par une version plus récente n'est pas défaite ; en cas de doute, restaurez aussi la base d'avant la mise à jour.
Pour plus de sûreté, faites d'abord une sauvegarde manuelle (voir plus haut).
Pour un serveur de test sur le réseau local, sans tunnel ni Zitadel :
NODE_ENV=development
PUBLIC_URL=http://10.21.30.23:3080
BIND_ADDR=10.21.30.23
HOST_PORT=3080
COOKIE_SECRET=<…>
LOCAL_AUTH_TOKEN=<…>Lancement : docker compose up -d --build, sans le profil tunnel.
Utiliser
- Visite de l'admin
- Aide de l'administration · Help (EN) · Hilfe (DE)
- Apparence et thèmes
- Filigrane et EXIF
- Preuves d'antériorité et droits
- Publication sur Instagram
Installer et configurer
Comprendre
Maintenir