diff --git a/.env.example b/.env.example index bb2fe02..5241fd7 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,10 @@ # Copier vers .env et renseigner. Ne jamais committer .env. # --- Base de données -------------------------------------------------------- +# Port hôte publié pour PostgreSQL. Non commun à dessein : plusieurs instances +# PostgreSQL coexistent sur le même serveur, et 5432 (voire 5433) est souvent +# déjà pris. Le conteneur app écoute toujours sur 5432 en interne. +POSTGRES_PORT=55432 POSTGRES_USER=planflow # Une valeur par défaut existe dans docker-compose.yml : la base n'étant jamais # publiée ni attachée au réseau du reverse-proxy, ce mot de passe protège d'un diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2840beb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto + +# Les scripts shell doivent rester en LF : le CRLF casse le shebang dans une +# image Linux (conteneur « .../entrypoint.sh: not found »). +*.sh text eol=lf \ No newline at end of file diff --git a/README.md b/README.md index 2e4b206..15ee6dc 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,9 @@ pnpm test:e2e # build, serveur standalone, tests de bout en bout **L'application ne doit pas se connecter en superutilisateur PostgreSQL.** -En docker-compose c'est déjà réglé : `docker/init-app-role.sh` crée au premier démarrage un rôle `planflow_app`, `NOSUPERUSER NOBYPASSRLS`, propriétaire de la base — il lui faut ce droit pour appliquer les migrations, et les politiques sont déclarées en `FORCE` précisément pour s'appliquer aussi au propriétaire. +En docker-compose c'est déjà réglé : d'abord `docker/init-app-role.sh` au premier démarrage, puis le service `db-init` qui le rejoue **à chaque `docker compose up`** — et seulement ensuite l'application. Le rôle `planflow_app`, `NOSUPERUSER NOBYPASSRLS`, propriétaire de la base — il lui faut ce droit pour appliquer les migrations, et les politiques sont déclarées en `FORCE` précisément pour s'appliquer aussi au propriétaire. + +L'idempotence n'est pas un luxe : une base déjà en place dont le rôle manque (ou dont le mot de passe a changé) ne doit pas exiger de SQL à la main. Le service `db-init` corrige les deux cas à chaque relance, et l'application n'est démarrée qu'une fois sa tâche terminée. Le script ne s'exécute qu'à la **première** initialisation du volume. Sur une installation déjà en place, jouer le même SQL à la main puis basculer `DATABASE_URL` sur ce rôle. diff --git a/docker-compose.yml b/docker-compose.yml index 67059d1..5d723a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,18 +34,26 @@ services: # vrai problème. networks: - interne - # Jamais publiée : rien hors de la pile n'a besoin de la base, et un jeu de - # données RH ne doit pas être à une règle de pare-feu du monde entier. - expose: - - '5432' + # Le conteneur reste sur 5432 pour l'application, et le port hôte est publié + # sur un port non commun, configurable : plusieurs instances PostgreSQL + # coexistent sur le même serveur, et s'en tenir au 5432 par défaut les + # ferait entrer en collision. + # + # **Lié à la boucle locale**, et c'est important : la publication sert à se + # connecter depuis le serveur (sauvegarde, psql), pas depuis le réseau. Sans + # `127.0.0.1`, Docker ouvre le port sur toutes les interfaces — un jeu de + # données RH derrière un mot de passe par défaut, joignable depuis + # l'extérieur. Pour un accès distant, passer par un tunnel. + ports: + - '${POSTGRES_BIND:-127.0.0.1}:${POSTGRES_PORT:-55432}:5432' # Pose le rôle applicatif à **chaque** démarrage de la pile. # # `/docker-entrypoint-initdb.d` ne s'exécute qu'à la toute première - # initialisation du volume : un volume créé par une tentative antérieure - # n'aurait jamais vu passer ce rôle, et l'application échouerait à - # s'authentifier sans que rien n'explique pourquoi. Le script est rejouable, - # ce service le rejoue. + # initialisation du volume : une base dont le volume existait déjà — ou dont + # le mot de passe a changé — laisserait l'application en échec + # d'authentification P1000, sans recours autre que du SQL à la main. Le script + # est rejouable, ce service le rejoue. # # Les identifiants du superutilisateur restent ici : les confier au conteneur # applicatif lui donnerait de quoi contourner la row-level security, ce que @@ -65,6 +73,8 @@ services: APP_DB_PASSWORD: ${APP_DB_PASSWORD:-planflow-app-interne} volumes: - ./docker/init-app-role.sh:/init-app-role.sh:ro + # `entrypoint` et non `command` : l'image postgres a son propre point + # d'entrée, qui tenterait de démarrer un serveur. entrypoint: ['sh', '/init-app-role.sh'] networks: - interne diff --git a/docker/init-app-role.sh b/docker/init-app-role.sh index 4a8210b..e61b914 100755 --- a/docker/init-app-role.sh +++ b/docker/init-app-role.sh @@ -14,22 +14,26 @@ set -eu # ni superutilisateur ni BYPASSRLS. C'est précisément pourquoi les politiques # sont déclarées en FORCE : elles s'appliquent aussi au propriétaire. # -# Ce script est **rejouable**, et il le doit : `/docker-entrypoint-initdb.d` ne -# s'exécute qu'à la toute première initialisation du volume. Une installation -# déjà en place — un volume créé par une tentative antérieure, par exemple — -# n'aurait jamais vu passer ce rôle, et l'application échouerait à se connecter -# sans que rien n'explique pourquoi. Il est donc aussi joué à chaque démarrage -# de la pile, par le service `db-init`. +# Joué par le conteneur `db` à la **première** initialisation du volume, puis +# par le service `db-init` à chaque `docker compose up`. Les deux sont +# nécessaires : `/docker-entrypoint-initdb.d` ne s'exécute qu'une fois, et une +# installation dont le volume existait déjà n'aurait jamais vu passer ce rôle. +# Le script est donc rejouable de bout en bout. APP_ROLE="${APP_DB_USER:-planflow_app}" APP_PASSWORD="${APP_DB_PASSWORD:-planflow-app-interne}" DB_NAME="${POSTGRES_DB:-planflow}" DB_USER="${POSTGRES_USER:-planflow}" -# Sans PGHOST, psql passe par la socket locale — le cas quand le script est -# joué par l'image postgres à l'initialisation. Avec, il passe par le réseau — -# le cas du service qui le rejoue à chaque démarrage. -psql -v ON_ERROR_STOP=1 --username "$DB_USER" --dbname "$DB_NAME" <