Le domaine se pose désormais dans l'interface, sur le service `php`.
`SERVICE_FQDN_PHP_80` demandait à Coolify de GÉNÉRER un domaine en …sslip.io.
Utile pour un modèle en un clic, mais en concurrence avec un domaine renseigné
à la main : deux routes pour un même service, dont une seule correspond au DNS
réel. Le proxy répond alors 403 sur le domaine qu'il ne connaît pas — un code
qui n'apparaît nulle part dans l'application, laquelle sert le back-office sur
`/` sans jamais rien refuser.
`expose: ['80']` ajouté. L'image exposait déjà 80, mais aussi 443 et 2019
(l'endpoint d'administration de Caddy) : rien n'obligeait un proxy à deviner
lequel des trois sert l'application.
Vérifié depuis le réseau interne, avec l'en-tête Host public : `/`, `/login` et
`/api/v1/health` répondent 200, et les variables d'environnement arrivent bien
dans le conteneur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le premier déploiement Coolify a échoué sur « Lazy loading proxies require
PHP 8.4 or higher ». La cause n'était pas dans le dépôt — Coolify avait
construit avec Nixpacks au lieu du compose — mais l'échec a mis au jour une
inexactitude réelle.
composer.json annonçait « php >=8.3 » alors que l'application exige 8.4 :
config.platform le pinçait déjà à 8.4.0, et les proxies de chargement paresseux
de Symfony ne fonctionnent pas en dessous. Toute chaîne de packaging qui lit la
contrainte pouvait donc choisir 8.3 en toute bonne foi, et échouer bien plus
loin sur un message sans rapport apparent. Contrainte remise à >=8.4.
compose.prod.yaml n'utilise plus `${VAR:?message}` : la forme arrête
correctement `docker compose`, mais le parseur de Coolify ne la comprend pas
partout. Le refus reste porté par docker/entrypoint.prod.sh, où il est testé et
où le message dit quoi faire plutôt que de citer un nom de variable.
Ajout au passage du garde-fou manquant : MARIADB_PASSWORD oubliée produisait
« mysql://tqslator:@database:… », un DSN valide au mot de passe vide, dont
l'échec remonte en erreur de droits d'accès sans jamais nommer la variable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'image de production existait mais n'était jamais partie. Quatre manques la
rendaient inexploitable, tous silencieux — l'application démarrait dans les
quatre cas.
Le front n'était pas dans l'image
public/build et public/index.html sont gitignorés, et le Dockerfile n'avait
pas d'étage Node. L'API répondait, chaque URL de l'interface renvoyait 503 :
une panne partielle, donc plus déroutante qu'une panne franche. Étage
app_frontend ajouté, avec une vérification au build qui échoue si le front
manque.
Aucune migration n'était appliquée
Seul l'entrypoint de dev le faisait. docker/entrypoint.prod.sh applique les
migrations sous garde RUN_MIGRATIONS et refuse de démarrer sur un APP_SECRET
vide, un DATABASE_URL absent ou un BACK_OFFICE_URL sur localhost — cette
dernière valeur ne casse rien de visible, elle casse la réception des
invitations, ailleurs, plusieurs jours plus tard.
Aucun moyen de créer le premier compte
Les comptes s'obtiennent par invitation, et inviter demande d'être connecté.
Les fixtures sont une dépendance de dev. Il ne restait que l'INSERT à la
main. app:create-super-admin crée l'organisation et le premier compte, refuse
une adresse connue, exige douze caractères.
Le référentiel de langues était vide
Créé par les fixtures, donc absent en production. Sans langue source à
choisir, aucun projet ne pouvait être créé. Passé en migration : c'est une
donnée de référence, pas un jeu d'exemple.
Au passage : l'expéditeur des courriels était en dur sur no-reply@tq-slator.local,
qu'un relais SMTP réel rejette. Devenu MAILER_FROM.
compose.prod.yaml est un fichier distinct, et c'est le point qui compte : Docker
fusionne compose.override.yaml dès que le principal s'appelle compose.yaml, si
bien que Mailpit et les ports publiés seraient partis en production sans que
personne ne l'ait demandé.
Parcours vérifié sur une pile neuve : connexion, création de projet, plateforme,
push de clés, publication, clé API, fichier livré.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Système de gestion de traductions pensé en CMS headless : un back-office
pour ceux qui traduisent, une API pour ce qui consomme.
Architecture
- Symfony 7.4 / API Platform 4.3 / MariaDB 11.4, SPA React 19 servie en
même origine — ce qui rend viable le cookie de session plutôt qu'un
jeton en localStorage.
- Deux APIs séparées : Management (session ou clé) et Delivery
(stateless, clé seule). Les fusionner ferait porter à chaque lecture de
bundle le coût de la session.
- Stockage canonique en ICU MessageFormat, sérialisation par plateforme.
Le format d'une plateforme ne contamine pas la base.
- Publication par releases immuables ; le déploiement est un déplacement
de pointeur, donc le rollback aussi.
- Isolation multi-organisation par filtre Doctrine, avec un test
d'architecture qui casse la CI si une entité échappe à l'invariant.
Éditeur, deux vues
- Par langue : source et cible, jamais douze colonnes. Grille virtualisée,
saisie sans bouton « Enregistrer », panneau de contexte permanent.
- Par clé : une clé, toutes ses langues empilées et repliées. Répond à
« ce libellé est-il prêt partout ? ».
- Mode Focus dans les deux : une file à vider, ⌘↵ pour enchaîner.
- Le traducteur ne voit jamais d'ICU : pastilles de variables, un champ
par catégorie CLDR de la langue cible.
Administration
- Deux niveaux : projet (membres, clés API, plateformes) et organisation
(annuaire des comptes, création de projets).
- Invitations par e-mail, jeton 256 bits stocké haché.
- Désactiver un compte coupe les sessions en cours, pas seulement les
connexions suivantes.
- Les plateformes s'archivent ; ni elles ni les environnements ne se
suppriment — la trace explique pourquoi telle clé existe.
CLI tqs
- PHAR autonome de 3 Mo, autoloader généré : le dépôt client ne dépend
ni de Composer ni de la disponibilité de TQ-Slator.
- init / push / pull / status ; le sync est non destructif par défaut et
son prune est scopé plateforme.
126 tests, PHPStan niveau 8.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>