chore(docker): docker-compose.yml standalone portable + .env local-first (#41)
All checks were successful
CI / secret-scan (push) Successful in 9s
CI / js-tests (push) Successful in 22s
CI / php-lint (push) Successful in 22s
CI / static-tests (push) Successful in 50s
CI / auto-merge (push) Has been skipped

This commit is contained in:
Corentin JOGUET 2026-06-17 15:31:57 +02:00
parent e613adc24a
commit 9d75fabdca
5 changed files with 78 additions and 323 deletions

View file

@ -3,17 +3,13 @@
#
# Usage :
# cp .env.example .env
# Editer .env (gitignore) avec les valeurs reelles.
# docker compose up -d
#
# Audience :
# Destine a l'auteur, au jury et aux contributeurs futurs.
#
# Modele de deploiement :
# Ce projet tourne sur serveur derriere un reverse proxy Traefik. Il n'y a
# pas de binding de ports hote : l'acces se fait uniquement via les FQDN
# configures ci-dessous et routes par Traefik (reseau admin_proxy).
# Les distinctions dev / staging / prod se font par FQDN distincts
# (ex : .dev.acadenice.fr vs .acadenice.fr) et par .env dedie.
# Ce template fonctionne EN LOCAL tel quel (valeurs dev) : la stack est joignable
# sur http://kiosk.localhost:8080 (borne) et http://admin.localhost:8080 (admin).
# Le deploiement derriere un reverse proxy (Traefik) se fait via l'overlay
# docker-compose.prod.yml + les variables du bloc "Deploiement prod" en fin de
# fichier. En prod : changer les mots de passe, APP_DEBUG=false, vrais FQDN.
#
# ===================================================================
@ -24,27 +20,30 @@ APP_ENV=dev # dev | staging | prod
APP_DEBUG=true # true en dev, false en prod
APP_TIMEZONE=Europe/Paris
# URL publique de la borne (Bloc 1), doit pointer vers le FQDN Traefik.
# Placeholder example.com (RFC 2606) - a remplacer par le FQDN reel.
APP_URL_KIOSK=https://kiosk.example.com
# Port hote publie par wakdo-web (acces local). Change si 8080 est pris.
HTTP_PORT=8080
# URL publique du back-office + API (Bloc 2).
# Placeholder example.com (RFC 2606) - a remplacer par le FQDN reel.
APP_URL_ADMIN=https://admin.example.com
# Hostnames des deux vhosts Apache (ServerName). En local : *.localhost resout
# vers 127.0.0.1 nativement. En prod : les vrais FQDN (voir bloc prod en bas).
APP_HOST_KIOSK=kiosk.localhost
APP_HOST_ADMIN=admin.localhost
# URLs publiques (consommees par l'app). En local = les hostnames sur HTTP_PORT.
APP_URL_KIOSK=http://kiosk.localhost:8080
APP_URL_ADMIN=http://admin.localhost:8080
# ===================================================================
# Base de donnees (MariaDB)
# ===================================================================
# Valeurs ci-dessous = PLACEHOLDERS. Remplacer par des mots de passe forts.
# Pas accessible depuis l'exterieur : le service wakdo-db est sur le reseau
# interne uniquement, aucun port exposé a l'hote.
# Valeurs dev ci-dessous : OK en local. EN PROD : mots de passe forts.
# wakdo-db est sur le reseau interne, aucun port expose a l'hote.
DB_HOST=wakdo-db # nom du service docker-compose
DB_PORT=3306
DB_NAME=wakdo
DB_USER=wakdo
DB_PASSWORD=change_me_strong_password
DB_ROOT_PASSWORD=change_me_root_password
DB_PASSWORD=wakdo_dev_password
DB_ROOT_PASSWORD=wakdo_dev_root_password
# ===================================================================
# Sessions
@ -58,18 +57,15 @@ SESSION_NAME=WAKDO_SID # nom du cookie (evite PHPSESSID)
# Securite
# ===================================================================
# Origine autorisee pour les requetes CORS de l'API.
# Doit correspondre exactement a APP_URL_KIOSK (pas de wildcard).
CORS_ALLOWED_ORIGIN=https://kiosk.example.com
# Origine autorisee pour les requetes CORS de l'API. Doit correspondre
# exactement a APP_URL_KIOSK (pas de wildcard).
CORS_ALLOWED_ORIGIN=http://kiosk.localhost:8080
# Algorithme de hashage : argon2id, FIXE dans le code (App\Auth\PasswordHasher),
# choix security-by-design non configurable (pas de bascule runtime vers un algo
# plus faible). Seuls les COUTS ci-dessous sont reglables.
# choix security-by-design non configurable. Seuls les COUTS ci-dessous sont reglables.
# Parametres de cout argon2id (password_hash options).
# Defauts alignes sur les recommandations OWASP Password Storage Cheat Sheet
# (memoire >= 19 MiB, >= 2 iterations). 65536 KiB = 64 MiB, marge confortable.
# Ces valeurs servent aussi au hash du PIN equipier (pin_hash, actions sensibles).
# Parametres de cout argon2id (password_hash options). Defauts alignes OWASP
# (memoire >= 19 MiB, >= 2 iterations). Servent aussi au hash du PIN equipier.
ARGON2_MEMORY_COST=65536 # KiB (64 MiB)
ARGON2_TIME_COST=4 # nombre d'iterations
ARGON2_THREADS=1 # parallelisme (1 = portable, deterministe)
@ -77,49 +73,38 @@ ARGON2_THREADS=1 # parallelisme (1 = portable, determini
# ===================================================================
# Anti brute-force - throttling de connexion (security-by-design)
# ===================================================================
# Deux gardes complementaires (cf. docs/merise/mld.md 4.21 + PROJECT_CONTEXT 19) :
# 1. par compte : colonnes user.failed_login_attempts / user.lockout_until
# 2. par IP source : table login_throttle (entite 21)
# Backoff degressif (pas de lock definitif) : evite le DoS sur compte legitime.
# Deux gardes : par compte (user.failed_login_attempts / lockout_until) et par IP
# (table login_throttle). Backoff degressif, pas de lock definitif.
# Compteur par compte : nombre d'echecs avant declenchement du backoff.
ACCOUNT_LOCKOUT_THRESHOLD=5
# Duree de base du backoff (secondes). Croit de facon degressive a chaque
# palier d'echecs supplementaires (ex : 60 -> 300 -> 900).
ACCOUNT_LOCKOUT_BASE_SECONDS=60
ACCOUNT_LOCKOUT_MAX_SECONDS=900 # plafond du backoff (15 min)
# Gate par IP : fenetre glissante et plafond de tentatives sur cette fenetre.
IP_THROTTLE_WINDOW_SECONDS=900 # 15 min
IP_THROTTLE_MAX_ATTEMPTS=20 # par IP sur la fenetre
# PIN equipier pour actions sensibles (annulation, override). Chiffres uniquement,
# bornes min ET max (RG-T18 : validation serveur + longueur bornee).
# PIN equipier pour actions sensibles. Chiffres, bornes min ET max (RG-T18).
STAFF_PIN_MIN_LENGTH=4
STAFF_PIN_MAX_LENGTH=12
# Throttle du PIN d'action sensible (RG-T22) - compteurs SEPARES du login : la
# dimension est l'utilisateur AGISSANT (session), pas l'email cible ni l'IP. Bornes
# volontairement plus permissives que le login (controle de dissuasion) : ne pas
# bloquer un manager en plein rush sur quelques fautes de frappe.
PIN_THROTTLE_THRESHOLD=5 # echecs avant le backoff (par acteur)
PIN_THROTTLE_BASE_SECONDS=30 # 1er palier (vs 60 au login)
PIN_THROTTLE_MAX_SECONDS=300 # plafond du backoff (5 min, vs 900 au login)
PIN_THROTTLE_WINDOW_SECONDS=900 # fenetre glissante (15 min)
# Throttle du PIN d'action sensible (RG-T22) - compteurs SEPARES du login,
# dimension = utilisateur agissant. Bornes plus permissives que le login.
PIN_THROTTLE_THRESHOLD=5
PIN_THROTTLE_BASE_SECONDS=30
PIN_THROTTLE_MAX_SECONDS=300
PIN_THROTTLE_WINDOW_SECONDS=900
# Expiration du token de reinitialisation de mot de passe (secondes).
PASSWORD_RESET_TTL=3600 # 1h
# ===================================================================
# Retention des donnees (RGPD - minimisation et limitation de conservation)
# Retention des donnees (RGPD)
# ===================================================================
# Purges executees par le service cron (docker/cron/crontab).
# Justification documentee : interet legitime / obligations probatoires.
AUDIT_LOG_RETENTION_DAYS=365 # journal d'audit ~12 mois
THROTTLE_PURGE_AFTER_HOURS=24 # login_throttle : lignes sans lockout actif > 24h
ORDER_RETENTION_DAYS=1095 # commandes (historique/stats) ~3 ans
# Purge des sessions expirees : deja geree par le cron */15 (voir crontab).
# ===================================================================
# Upload images produits
@ -131,34 +116,18 @@ UPLOAD_ALLOWED_MIME=image/jpeg,image/png,image/webp
# ===================================================================
# Cron (fenetre de maintenance 01h30 - 09h30)
# ===================================================================
# Les jobs sont definis dans docker/cron/crontab. Ici uniquement le TZ.
CRON_TIMEZONE=Europe/Paris
# ===================================================================
# Exposition via Traefik
# Deploiement prod (overlay docker-compose.prod.yml) - OPTIONNEL
# ===================================================================
# FQDN consommes par les labels docker-compose.yml pour generer les routes
# Traefik et les certificats TLS (Traefik gere le resolver par defaut).
# Le Traefik de l'hote prend en charge Let's Encrypt automatiquement.
TRAEFIK_DOMAIN_KIOSK=kiosk.example.com
TRAEFIK_DOMAIN_ADMIN=admin.example.com
# ===================================================================
# Reseau Docker externe du reverse proxy
# ===================================================================
# Nom du reseau Docker externe auquel le service web doit se connecter
# pour etre expose par le reverse proxy de l'hote.
# A ignorer pour un usage local. Necessaire UNIQUEMENT derriere un reverse proxy
# Traefik, avec : docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
#
# Adapter selon votre infrastructure. Valeurs courantes :
# traefik_proxy - convention neutre (placeholder)
# traefik_public - convention doc Traefik
# traefik - setups simples
# proxy - autre convention frequente
# En prod, surcharger aussi : APP_ENV=prod, APP_DEBUG=false, mots de passe forts,
# et APP_HOST_*/APP_URL_*/CORS_ALLOWED_ORIGIN avec les vrais FQDN HTTPS.
#
# Le reseau doit exister AVANT 'docker compose up' (cree par votre stack de
# reverse proxy, ou manuellement : docker network create <nom>). Sinon
# 'docker compose up' echoue avec une erreur sur le reseau externe introuvable.
REVERSE_PROXY_NETWORK=traefik_proxy
# Nom du reseau Docker externe partage avec le Traefik de l'hote (doit exister
# AVANT le up : cree par la stack Traefik, ou `docker network create <nom>`).
REVERSE_PROXY_NETWORK=admin_proxy

5
.gitignore vendored
View file

@ -5,6 +5,11 @@
*.pem
*.key
# Compose de production (propre a chaque hote : Traefik/reverse proxy, FQDN).
# Le repo ne ship que docker-compose.yml (standalone, local). Chaque hote derriere
# un proxy maintient son propre docker-compose.prod.yml (hors versionnement).
docker-compose.prod.yml
# === BYAN — plateforme (moteur), masquee ===
# Le code moteur des agents n'est pas part du rendu RNCP.
# La methodologie appliquee (CLAUDE.md + rules + hooks) reste dans .claude/

View file

@ -89,80 +89,41 @@ Reseaux, volumes, services et decoupage reseau interne / reseau proxy : voir `do
---
## Quickstart
Ce projet tourne **sur serveur derriere un reverse proxy Traefik** : pas de binding de ports hote, pas d'acces `localhost`. L'acces public se fait par FQDN HTTPS (TLS gere automatiquement par Traefik). Les environnements `dev`, `staging` et `prod` se distinguent par des FQDN et des fichiers `.env` separes.
### Prerequis sur l'hote
1. Docker Engine + docker compose v2 (voir ci-dessous)
2. Un reverse proxy Traefik deja en place, avec un reseau Docker externe dedie. Le **nom du reseau** est configurable via la variable `REVERSE_PROXY_NETWORK` du `.env` (defaut : `admin_proxy` — convention de l'auteur). A adapter a votre infrastructure.
3. Les FQDN cibles pointent en DNS vers l'hote
### Sur un hote deja equipe (Docker + Traefik)
## Quickstart (local)
```bash
git clone git@github.com:AcadeNice/wakdo_corentin.git
cd wakdo_corentin
cp .env.example .env
# Editer .env : DB_PASSWORD, DB_ROOT_PASSWORD, APP_URL_*, TRAEFIK_DOMAIN_*
docker compose up -d
```
> **Attention au `.env` pre-existant.** Si un fichier `.env` existe deja a la racine (tooling externe, autre plateforme installee dans le meme repertoire), **ne pas faire** `cp .env.example .env` — cela ecraserait les variables existantes. Faire un **merge manuel** a la place : ajouter les variables manquantes du template dans le `.env` actuel. Les prefixes de variables de ce projet (`APP_`, `DB_`, `SESSION_`, `CORS_`, `UPLOAD_`, `CRON_`, `TRAEFIK_`, `REVERSE_PROXY_`) sont disjoints de ceux utilises par des outils tiers courants, donc la cohabitation est safe.
Une seule commande lance la stack complete (Cr 7.c.4) : le service one-shot
`wakdo-migrate` applique les migrations puis le seed (idempotents, tables de suivi
`schema_migrations` / `seeds_applied`) avant que l'app ne serve. Ensuite :
Critere RNCP Cr 7.c.4 couvert : une seule commande (`docker compose up -d`) lance la
stack complete. Le service one-shot `wakdo-migrate` applique le schema (migrations)
puis les donnees de reference (seed) avant que l'app ne serve ; `wakdo-app`/`wakdo-web`
attendent sa completion (`depends_on: service_completed_successfully`). Migrations et
seed sont idempotents (tables de suivi `schema_migrations` / `seeds_applied`).
Detail : `docs/journal/2026-06-17--makefile-to-compose-migrate.md`.
- Borne : http://kiosk.localhost:8080
- Admin + API : http://admin.localhost:8080
Services accessibles apres `docker compose up -d` :
- Borne : la valeur de `TRAEFIK_DOMAIN_KIOSK` dans `.env`
- Admin + API : la valeur de `TRAEFIK_DOMAIN_ADMIN` dans `.env`
`*.localhost` resout vers `127.0.0.1` nativement ; changer le port via `HTTP_PORT`
dans `.env`. Le `.env.example` fonctionne tel quel en local (valeurs dev).
### Installation Docker sur un hote neuf (Debian / Ubuntu)
Docker non installe ? Voir https://docs.docker.com/engine/install/
Procedure officielle detaillee : `https://docs.docker.com/engine/install/` (selectionner la distribution). Resume pour Debian stable :
### Deploiement prod (derriere un reverse proxy Traefik)
Le repo ne ship que `docker-compose.yml` (standalone). En production derriere un
reverse proxy, chaque hote maintient son **propre `docker-compose.prod.yml`**
(gitignore, hors repo, comme `.env`) : meme stack, mais exposee via Traefik (reseau
externe + labels TLS) au lieu d'un port hote.
```bash
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/debian \
$(. /etc/os-release && echo $VERSION_CODENAME) stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
# Fermer et rouvrir la session pour activer le groupe docker
docker compose -f docker-compose.prod.yml up -d
```
### Reseau externe du reverse proxy
Le `docker-compose.yml` attend un reseau Docker externe deja existant sur l'hote, dont le nom est donne par la variable `REVERSE_PROXY_NETWORK` (defaut : `admin_proxy`).
Si vous avez deja un Traefik en place, ce reseau a generalement ete cree par son propre stack. Adaptez la variable `REVERSE_PROXY_NETWORK` dans votre `.env` au nom utilise par votre proxy. Sinon, creez-le manuellement :
```bash
docker network create mon_reseau_proxy
# puis dans .env :
# REVERSE_PROXY_NETWORK=mon_reseau_proxy
```
Avant le premier `docker compose up`, s'assurer que le reseau existe. Verification rapide :
```bash
docker network inspect "$(grep ^REVERSE_PROXY_NETWORK .env | cut -d= -f2)"
```
Si la commande retourne une erreur, soit adapter `REVERSE_PROXY_NETWORK` au nom du reseau utilise par votre proxy, soit creer le reseau manuellement (le reseau externe doit exister avant `docker compose up`).
Avec un `.env` adapte : `APP_ENV=prod`, `APP_DEBUG=false`, mots de passe forts,
`APP_HOST_*` / `APP_URL_*` / `CORS_ALLOWED_ORIGIN` en vrais FQDN HTTPS, et
`REVERSE_PROXY_NETWORK` = reseau Docker du Traefik de l'hote (doit exister avant le up).
*Section mise a jour au fil de l'implementation (migrations reelles, seed, CI/CD deploiement).*

View file

@ -1,87 +1,19 @@
#
# Wakdo - orchestration des 4 services de la stack (Bloc 5 DevOps, Cr 7.c.3 / 7.c.4).
#
# Services :
# wakdo-web : Apache httpd, reverse proxy FastCGI -> wakdo-app, expose 80 via Traefik
# wakdo-app : PHP-FPM 8.3, execute le code PHP back-office + API
# wakdo-db : MariaDB 11.4, persistance des donnees metier
# wakdo-cron : Alpine + dcron, backups nocturnes et taches planifiees
#
# Reseaux :
# default (nom = wakdo_internal) : bridge interne pour l'inter-service,
# non expose a l'hote. app/db/cron ne sont PAS joignables publiquement.
# <REVERSE_PROXY_NETWORK> : reseau externe (ex. traefik_proxy) partage
# avec le Traefik de l'hote. Seul wakdo-web y est attache.
#
# Volumes :
# wakdo_db_data : named volume pour /var/lib/mysql (persistance BDD)
# wakdo_uploads : named volume pour les uploads produits back-office
# ./var/backups : bind-mount lecture-ecriture pour les dumps SQL
#
# Variables d'env consommees depuis .env : docker compose lit automatiquement le
# fichier .env a la racine et fait l'expansion des ${...} ci-dessous.
#
# Persistance : `docker compose down` preserve les named volumes ; seul
# `docker compose down -v` supprime les donnees.
#
name: wakdo
networks:
# Reseau interne : isolement des services applicatifs.
# wakdo-app, wakdo-db et wakdo-cron n'y sont accessibles que via les autres
# conteneurs de la stack. Aucun port hote expose.
wakdo_internal:
driver: bridge
internal: false
# internal: false (par defaut) car wakdo-app et wakdo-cron ont besoin
# de sortir sur internet (pour telecharger des packages au build et pour
# de potentiels appels API externes futurs). L'isolation vient du fait
# qu'aucun port hote n'est binde ici.
#
# Subnet explicite (RFC 1918) : l'auto-allocateur Docker du daemon hote
# est sature (15 /16 + 15 /20 deja alloues par d'autres stacks), il ne
# peut plus creer de reseau bridge sans subnet explicite. 192.168.148.0/24
# est dans le gap libre 192.168.144-159 (256 IP, largement suffisant pour
# 4 services), aucune collision avec les /24 acquagest voisins (150/154/
# 155/157). Choix defendable : right-sizing + isolation des fluctuations
# d'allocation auto sur cet hote mutualise.
ipam:
driver: default
config:
- subnet: 192.168.148.0/24
# Reseau du reverse proxy (Traefik) pre-existant sur l'hote.
# Son nom est configurable via REVERSE_PROXY_NETWORK dans .env pour
# supporter differents setups (traefik_proxy, traefik_public, proxy, ...).
reverse_proxy:
name: ${REVERSE_PROXY_NETWORK}
external: true
volumes:
wakdo_db_data:
# Named volume MariaDB. Permissions gerees par Docker (UID mysql=999
# dans le conteneur), zero souci cote hote. Survit a `docker compose down`.
# Pour remise a zero : `docker compose down -v` (destructif : supprime les volumes).
wakdo_uploads:
# Images produits uploadees par les equipiers depuis le back-office.
# Named volume pour les memes raisons que wakdo_db_data : permissions
# propres (www-data UID 82 en Alpine) et pas de pollution du repo git
# par des binaires.
services:
# =======================================================================
# wakdo-db : MariaDB 11.4 LTS
# =======================================================================
wakdo-db:
image: mariadb:11.4
container_name: wakdo-db
restart: unless-stopped
# Variables d'env d'initialisation (ne s'appliquent qu'au premier demarrage
# sur volume vide - voir docs/notes/docker-volumes-vs-bind-mounts.md).
environment:
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MARIADB_DATABASE: ${DB_NAME}
@ -89,22 +21,11 @@ services:
MARIADB_PASSWORD: ${DB_PASSWORD}
MARIADB_AUTO_UPGRADE: "1"
TZ: ${APP_TIMEZONE:-Europe/Paris}
volumes:
- wakdo_db_data:/var/lib/mysql
# Scripts d'init MariaDB, executes UNIQUEMENT au premier demarrage sur
# volume vierge : durcit le privilege du user applicatif (moindre
# privilege, db/init/10-scope-app-user.sh). Lecture seule.
- ./db/init:/docker-entrypoint-initdb.d:ro
networks:
- wakdo_internal
# Pas de ports exposes a l'hote : seuls wakdo-app et wakdo-cron peuvent
# joindre wakdo-db:3306 via le reseau interne.
# Healthcheck officiel fourni par l'image mariadb : le script bundled
# healthcheck.sh teste la connexion et l'init innodb.
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
@ -112,48 +33,30 @@ services:
retries: 6
start_period: 30s
# =======================================================================
# wakdo-migrate : service ONE-SHOT (migrations + seed idempotents)
# =======================================================================
# Tourne une fois apres que wakdo-db soit healthy, applique le schema puis les
# donnees de reference (idempotent via schema_migrations / seeds_applied), et
# sort. app/web attendent sa COMPLETION : `docker compose up` rend ainsi la stack
# complete + utilisable en UNE commande, sans dependance a l'hote (Cr 7.c.4).
wakdo-migrate:
image: mariadb:11.4
container_name: wakdo-migrate
restart: "no"
environment:
DB_HOST: ${DB_HOST}
DB_PORT: ${DB_PORT}
DB_NAME: ${DB_NAME}
# Root : migrations = DDL, seeds = INSERT de reference.
DB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
# migrations + seeds + le runner, en lecture seule.
- ./db:/db:ro
networks:
- wakdo_internal
depends_on:
wakdo-db:
condition: service_healthy
entrypoint: ["bash", "/db/migrate-container.sh"]
# =======================================================================
# wakdo-app : PHP-FPM 8.3 (execute le code back-office + API)
# =======================================================================
wakdo-app:
build:
context: ./docker/php-fpm
dockerfile: Dockerfile
container_name: wakdo-app
restart: unless-stopped
environment:
APP_ENV: ${APP_ENV}
APP_DEBUG: ${APP_DEBUG}
@ -169,80 +72,50 @@ services:
SESSION_LIFETIME_ABSOLUTE: ${SESSION_LIFETIME_ABSOLUTE}
SESSION_NAME: ${SESSION_NAME}
CORS_ALLOWED_ORIGIN: ${CORS_ALLOWED_ORIGIN}
# Cout argon2id (password_hash) : aligne sur .env.example / OWASP. Sert au
# hash du mot de passe ET du PIN equipier (actions sensibles, P3).
ARGON2_MEMORY_COST: ${ARGON2_MEMORY_COST}
ARGON2_TIME_COST: ${ARGON2_TIME_COST}
ARGON2_THREADS: ${ARGON2_THREADS}
# Anti brute-force : backoff degressif par compte (user.lockout_until) et
# par IP source (table login_throttle). Voir mlt.md 12.1 RG-8/RG-9.
ACCOUNT_LOCKOUT_THRESHOLD: ${ACCOUNT_LOCKOUT_THRESHOLD}
ACCOUNT_LOCKOUT_BASE_SECONDS: ${ACCOUNT_LOCKOUT_BASE_SECONDS}
ACCOUNT_LOCKOUT_MAX_SECONDS: ${ACCOUNT_LOCKOUT_MAX_SECONDS}
IP_THROTTLE_WINDOW_SECONDS: ${IP_THROTTLE_WINDOW_SECONDS}
IP_THROTTLE_MAX_ATTEMPTS: ${IP_THROTTLE_MAX_ATTEMPTS}
# Bornes du PIN equipier (actions sensibles, P3) : longueur min ET max.
STAFF_PIN_MIN_LENGTH: ${STAFF_PIN_MIN_LENGTH}
STAFF_PIN_MAX_LENGTH: ${STAFF_PIN_MAX_LENGTH}
# Throttle du PIN d'action sensible (RG-T22) : compteurs SEPARES du login.
PIN_THROTTLE_THRESHOLD: ${PIN_THROTTLE_THRESHOLD}
PIN_THROTTLE_BASE_SECONDS: ${PIN_THROTTLE_BASE_SECONDS}
PIN_THROTTLE_MAX_SECONDS: ${PIN_THROTTLE_MAX_SECONDS}
PIN_THROTTLE_WINDOW_SECONDS: ${PIN_THROTTLE_WINDOW_SECONDS}
# Expiration du token de reinitialisation de mot de passe (mlt.md 12.3).
PASSWORD_RESET_TTL: ${PASSWORD_RESET_TTL}
UPLOAD_MAX_SIZE_MB: ${UPLOAD_MAX_SIZE_MB}
UPLOAD_ALLOWED_MIME: ${UPLOAD_ALLOWED_MIME}
volumes:
# Bind-mount du code source pour le hot-reload en dev.
# En prod, cette ligne est remplacee par un COPY dans l'image
# via docker-compose.prod.yml (override a venir en P7).
- ./src:/var/www/html
# Named volume pour les uploads, plus specifique que le bind-mount
# parent : les uploads ne vont pas dans ./src, ils restent dans le
# volume Docker et survivent aux `docker compose down`.
- wakdo_uploads:/var/www/html/public/uploads
networks:
- wakdo_internal
depends_on:
# Le schema + les donnees de reference sont en place AVANT que l'app serve.
wakdo-migrate:
condition: service_completed_successfully
wakdo-db:
condition: service_healthy
# Healthcheck defini dans le Dockerfile (php -r exit 0).
# =======================================================================
# wakdo-web : Apache httpd (reverse FastCGI vers wakdo-app, expose via Traefik)
# =======================================================================
wakdo-web:
build:
context: ./docker/apache
dockerfile: Dockerfile
container_name: wakdo-web
restart: unless-stopped
environment:
# Noms de domaine injectes dans les vhosts (ServerName ${TRAEFIK_DOMAIN_*}).
TRAEFIK_DOMAIN_KIOSK: ${TRAEFIK_DOMAIN_KIOSK}
TRAEFIK_DOMAIN_ADMIN: ${TRAEFIK_DOMAIN_ADMIN}
APP_HOST_KIOSK: ${APP_HOST_KIOSK}
APP_HOST_ADMIN: ${APP_HOST_ADMIN}
ports:
- "${HTTP_PORT:-8080}:80"
volumes:
# Meme bind-mount que wakdo-app : Apache sert les fichiers statiques
# (HTML, CSS, JS, images) depuis ce dossier. PHP ne passe pas par
# ce chemin ici, il passe par le proxy FastCGI vers wakdo-app.
- ./src:/var/www/html
- wakdo_uploads:/var/www/html/public/uploads
networks:
- wakdo_internal
- reverse_proxy
depends_on:
wakdo-migrate:
condition: service_completed_successfully
@ -251,79 +124,26 @@ services:
wakdo-db:
condition: service_healthy
# === Labels Traefik : deux routers (kiosk + admin) sur le meme conteneur ===
# Le Traefik de l'hote decouvre ces labels automatiquement (provider docker).
# On ne configure PAS le certresolver ici : le Traefik hote le gere via
# sa propre config (acme.json, resolver par defaut).
labels:
- "traefik.enable=true"
- "traefik.docker.network=${REVERSE_PROXY_NETWORK}"
# --- Router kiosk (borne client) ---
- "traefik.http.routers.wakdo-kiosk.rule=Host(`${TRAEFIK_DOMAIN_KIOSK}`)"
- "traefik.http.routers.wakdo-kiosk.entrypoints=websecure"
- "traefik.http.routers.wakdo-kiosk.tls=true"
- "traefik.http.routers.wakdo-kiosk.tls.certresolver=letsencrypt"
- "traefik.http.routers.wakdo-kiosk.service=wakdo-kiosk-svc"
- "traefik.http.services.wakdo-kiosk-svc.loadbalancer.server.port=80"
# --- Router admin (back-office + API) ---
- "traefik.http.routers.wakdo-admin.rule=Host(`${TRAEFIK_DOMAIN_ADMIN}`)"
- "traefik.http.routers.wakdo-admin.entrypoints=websecure"
- "traefik.http.routers.wakdo-admin.tls=true"
- "traefik.http.routers.wakdo-admin.tls.certresolver=letsencrypt"
- "traefik.http.routers.wakdo-admin.service=wakdo-admin-svc"
- "traefik.http.services.wakdo-admin-svc.loadbalancer.server.port=80"
# --- Middleware : redirection HTTP -> HTTPS ---
# Applique aux 2 hosts via un router "catch-all" sur entrypoints=web.
- "traefik.http.routers.wakdo-kiosk-http.rule=Host(`${TRAEFIK_DOMAIN_KIOSK}`)"
- "traefik.http.routers.wakdo-kiosk-http.entrypoints=web"
- "traefik.http.routers.wakdo-kiosk-http.middlewares=wakdo-to-https"
- "traefik.http.routers.wakdo-admin-http.rule=Host(`${TRAEFIK_DOMAIN_ADMIN}`)"
- "traefik.http.routers.wakdo-admin-http.entrypoints=web"
- "traefik.http.routers.wakdo-admin-http.middlewares=wakdo-to-https"
- "traefik.http.middlewares.wakdo-to-https.redirectscheme.scheme=https"
- "traefik.http.middlewares.wakdo-to-https.redirectscheme.permanent=true"
# =======================================================================
# wakdo-cron : taches planifiees (backup, purge, agregations)
# =======================================================================
wakdo-cron:
build:
context: ./docker/cron
dockerfile: Dockerfile
container_name: wakdo-cron
restart: unless-stopped
# init: true -> Docker injecte tini comme PID 1. dcron exige un init
# parent pour pouvoir setpgid() sur ses jobs (sinon "Operation not
# permitted" en boucle car un PID 1 sans init ne peut pas changer les
# groupes de processus). Cf. busybox-utils issue tracker.
init: true
environment:
# Credentials BDD pour mysqldump et les purges. Le user applicatif est en
# moindre privilege (DML + SELECT/SHOW VIEW/TRIGGER/LOCK TABLES, jamais le
# root password ; cf. db/init/10-scope-app-user.sh).
DB_HOST: ${DB_HOST}
DB_PORT: ${DB_PORT}
DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
# Retention des donnees (mlt.md 13.4/13.5). Defaut applique par les scripts
# ET ici, pour rester coherent si la var manque du .env.
AUDIT_LOG_RETENTION_DAYS: ${AUDIT_LOG_RETENTION_DAYS:-365}
THROTTLE_PURGE_AFTER_HOURS: ${THROTTLE_PURGE_AFTER_HOURS:-24}
TZ: ${CRON_TIMEZONE:-Europe/Paris}
volumes:
# Bind-mount vers l'hote pour les dumps : inspectables par ls, scp-able
# hors docker. Le dossier ./var/backups est gitignore.
- ./var/backups:/backups
networks:
- wakdo_internal
depends_on:
wakdo-db:
condition: service_healthy

View file

@ -2,8 +2,8 @@
# Wakdo - vhosts applicatifs
#
# Un seul conteneur Apache derriere Traefik sert **les deux** FQDN :
# - TRAEFIK_DOMAIN_KIOSK -> /var/www/html/public/borne (borne client, Bloc 1)
# - TRAEFIK_DOMAIN_ADMIN -> /var/www/html/public/admin (back-office + API, Bloc 2)
# - APP_HOST_KIOSK -> /var/www/html/public/borne (borne client, Bloc 1)
# - APP_HOST_ADMIN -> /var/www/html/public/admin (back-office + API, Bloc 2)
#
# Comme Traefik termine TLS en amont et communique en HTTP clair avec Apache
# sur le reseau docker, les vhosts ecoutent sur :80 et font confiance aux
@ -39,8 +39,8 @@
# === Borne client (Bloc 1 - front vanilla HTML/CSS/JS) ===
<VirtualHost *:80>
# Hostname injecte par la var d'env TRAEFIK_DOMAIN_KIOSK au runtime.
ServerName ${TRAEFIK_DOMAIN_KIOSK}
# Hostname injecte par la var d'env APP_HOST_KIOSK au runtime.
ServerName ${APP_HOST_KIOSK}
DocumentRoot "/var/www/html/public/borne"
@ -89,7 +89,7 @@
# === Back-office + API REST (Bloc 2 - PHP from scratch + MVC) ===
<VirtualHost *:80>
ServerName ${TRAEFIK_DOMAIN_ADMIN}
ServerName ${APP_HOST_ADMIN}
DocumentRoot "/var/www/html/public/admin"
@ -128,7 +128,7 @@
</DirectoryMatch>
# CORS : l'API admin sous /api/* doit accepter les requetes venant
# de la borne kiosk (TRAEFIK_DOMAIN_KIOSK). Wildcard interdit.
# de la borne kiosk (APP_HOST_KIOSK). Wildcard interdit.
# La vraie valeur vient de CORS_ALLOWED_ORIGIN dans .env, lue cote PHP.
# Ici on pose juste les headers de prealable OPTIONS.
<Location /api>