feat(docker): complete stack with compose and 4 services
Deliver the full Docker stack for Bloc 5 DevOps (Cr 7.c.3 and 7.c.4):
- docker/apache/ Custom httpd:2.4-alpine with hardened main config,
MPM event tuning and 3 vhosts (healthz, kiosk static,
admin reverse FCGI to wakdo-app:9000). Kiosk vhost
explicitly denies .php to enforce Bloc 1 isolation.
- docker/php-fpm/ Custom php:8.3-fpm-alpine3.20 with pdo_mysql, opcache,
intl, exif, zip and tini for signal handling.
Dynamic pool 3-10 workers listening on TCP 9000.
- docker/cron/ Custom alpine:3.20 with dcron, mariadb-client, gzip.
Nightly mysqldump at 03h00 with 14-day rotation and
512-byte sanity check. Purge and stats jobs templated.
- docker-compose.yml 4 services orchestrated on 2 networks (internal
bridge + external reverse-proxy). 2 named volumes
for DB and uploads, bind-mount for backups.
Traefik labels for 2 routers with HTTPS redirect.
Makefile adds `make backup` (manual dump) and `make backup-ls`.
.gitignore adds /var/ for backup bind-mount path.
docs/journal/2026-04-24--infra-docker.md documents 5 decisions with
alternatives, maps 16 RNCP criteria to artefacts and prepares 6 jury Q&A.
Validated: `docker compose config --quiet` passes. Smoke test deferred
to next session (requires server .env).
This commit is contained in:
parent
324f5cdb74
commit
ac8b6a6791
15 changed files with 1166 additions and 2 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -49,6 +49,10 @@ Thumbs.db
|
||||||
/logs/
|
/logs/
|
||||||
|
|
||||||
# === Data / Uploads / Backups ===
|
# === Data / Uploads / Backups ===
|
||||||
|
# /var/ : contient /var/backups/ (bind-mount des dumps BDD du conteneur cron)
|
||||||
|
# et tout futur artefact run-time (caches persistes, logs).
|
||||||
|
# Voir docs/notes/docker-volumes-vs-bind-mounts.md pour la strategie.
|
||||||
|
/var/
|
||||||
/backups/
|
/backups/
|
||||||
/src/public/uploads/
|
/src/public/uploads/
|
||||||
/data/
|
/data/
|
||||||
|
|
|
||||||
12
Makefile
12
Makefile
|
|
@ -164,8 +164,16 @@ seed: ## Charge les donnees de demo [a venir]
|
||||||
@echo "[seed] Pas encore implemente. Les seeds seront dans db/seeds/."
|
@echo "[seed] Pas encore implemente. Les seeds seront dans db/seeds/."
|
||||||
|
|
||||||
.PHONY: backup
|
.PHONY: backup
|
||||||
backup: ## Genere un dump SQL horodate dans ./backups/ [a venir]
|
backup: ## Declenche un dump SQL horodate immediat (via le container cron)
|
||||||
@echo "[backup] Pas encore implemente. Voir scripts/backup-db.sh a venir."
|
@mkdir -p ./var/backups
|
||||||
|
@echo "[backup] Execution manuelle de /scripts/backup-db.sh dans wakdo-cron..."
|
||||||
|
@$(COMPOSE) exec -T $(SERVICE_CRON) /scripts/backup-db.sh
|
||||||
|
@echo "[backup] Dernier dump :"
|
||||||
|
@ls -lh ./var/backups/ | tail -n 1
|
||||||
|
|
||||||
|
.PHONY: backup-ls
|
||||||
|
backup-ls: ## Liste les dumps SQL presents dans ./var/backups/
|
||||||
|
@ls -lh ./var/backups/ 2>/dev/null || echo "[backup-ls] Pas de backups (./var/backups/ vide ou inexistant)."
|
||||||
|
|
||||||
# === Tests ===
|
# === Tests ===
|
||||||
|
|
||||||
|
|
|
||||||
247
docker-compose.yml
Normal file
247
docker-compose.yml
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
#
|
||||||
|
# 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 (chargees par make via `include .env`
|
||||||
|
# et transmises ici par docker compose qui fait l'expansion automatique).
|
||||||
|
#
|
||||||
|
# Persistance : un `make down` preserve les named volumes. Seul `make clean`
|
||||||
|
# (interactif) ou `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.
|
||||||
|
|
||||||
|
# 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 : `make clean` (interactif, confirme) ou
|
||||||
|
# `docker compose down -v` (destructif direct).
|
||||||
|
|
||||||
|
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}
|
||||||
|
MARIADB_USER: ${DB_USER}
|
||||||
|
MARIADB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
MARIADB_AUTO_UPGRADE: "1"
|
||||||
|
TZ: ${APP_TIMEZONE:-Europe/Paris}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- wakdo_db_data:/var/lib/mysql
|
||||||
|
|
||||||
|
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
|
||||||
|
timeout: 5s
|
||||||
|
retries: 6
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
# =======================================================================
|
||||||
|
# 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}
|
||||||
|
APP_TIMEZONE: ${APP_TIMEZONE}
|
||||||
|
APP_URL_KIOSK: ${APP_URL_KIOSK}
|
||||||
|
APP_URL_ADMIN: ${APP_URL_ADMIN}
|
||||||
|
DB_HOST: ${DB_HOST}
|
||||||
|
DB_PORT: ${DB_PORT}
|
||||||
|
DB_NAME: ${DB_NAME}
|
||||||
|
DB_USER: ${DB_USER}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
SESSION_LIFETIME_IDLE: ${SESSION_LIFETIME_IDLE}
|
||||||
|
SESSION_LIFETIME_ABSOLUTE: ${SESSION_LIFETIME_ABSOLUTE}
|
||||||
|
SESSION_NAME: ${SESSION_NAME}
|
||||||
|
CORS_ALLOWED_ORIGIN: ${CORS_ALLOWED_ORIGIN}
|
||||||
|
PASSWORD_ALGO: ${PASSWORD_ALGO}
|
||||||
|
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 `make down`.
|
||||||
|
- wakdo_uploads:/var/www/html/public/uploads
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- wakdo_internal
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
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}
|
||||||
|
|
||||||
|
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-app:
|
||||||
|
condition: service_started
|
||||||
|
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
|
||||||
|
|
||||||
|
environment:
|
||||||
|
# Credentials BDD pour mysqldump (lecture seule via USER applicatif,
|
||||||
|
# PAS le root password). Le user applicatif doit avoir SELECT +
|
||||||
|
# LOCK TABLES + SHOW VIEW sur la BDD (migrations P2).
|
||||||
|
DB_HOST: ${DB_HOST}
|
||||||
|
DB_PORT: ${DB_PORT}
|
||||||
|
DB_NAME: ${DB_NAME}
|
||||||
|
DB_USER: ${DB_USER}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
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
|
||||||
35
docker/apache/Dockerfile
Normal file
35
docker/apache/Dockerfile
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Wakdo - image web Apache httpd (reverse proxy FastCGI vers PHP-FPM)
|
||||||
|
#
|
||||||
|
# Base : httpd:2.4-alpine (stable, legere, maintenue par Apache Foundation).
|
||||||
|
# Role : servir les assets statiques (HTML, CSS, JS, images) et relayer les
|
||||||
|
# requetes *.php vers wakdo-app:9000 en FastCGI.
|
||||||
|
# Reseau : attache a la fois au reseau interne wakdo_internal (pour parler
|
||||||
|
# a wakdo-app) et au reseau externe du reverse proxy (pour Traefik).
|
||||||
|
|
||||||
|
FROM httpd:2.4-alpine
|
||||||
|
|
||||||
|
# httpd:2.4-alpine compile mod_proxy_fcgi mais ne le charge pas par defaut.
|
||||||
|
# On le charge dans la conf (voir httpd.conf) plutot que de rebuild l'image.
|
||||||
|
# On installe juste curl pour le healthcheck.
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache curl; \
|
||||||
|
rm -rf /var/cache/apk/* /tmp/*
|
||||||
|
|
||||||
|
# Copie de la conf projet :
|
||||||
|
# - httpd.conf : main config avec LoadModule + include vhosts
|
||||||
|
# - vhost.conf : vhosts kiosk + admin/API + reverse FCGI
|
||||||
|
# - mpm.conf : tuning MPM event (workers)
|
||||||
|
COPY httpd.conf /usr/local/apache2/conf/httpd.conf
|
||||||
|
COPY vhost.conf /usr/local/apache2/conf/extra/wakdo-vhost.conf
|
||||||
|
COPY mpm.conf /usr/local/apache2/conf/extra/wakdo-mpm.conf
|
||||||
|
|
||||||
|
# Le DocumentRoot doit exister dans l'image meme si le code source est
|
||||||
|
# bind-mounte en dev. Sans ca, Apache refuse de demarrer.
|
||||||
|
RUN mkdir -p /var/www/html/public
|
||||||
|
|
||||||
|
# Healthcheck : vhost par defaut (0.0.0.0:80) doit repondre.
|
||||||
|
# Le endpoint /healthz est defini dans vhost.conf, repond 200 "ok".
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -fsS http://localhost/healthz || exit 1
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
99
docker/apache/httpd.conf
Normal file
99
docker/apache/httpd.conf
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
#
|
||||||
|
# Wakdo - configuration Apache httpd 2.4 (main)
|
||||||
|
#
|
||||||
|
# Derivee de la conf par defaut fournie par httpd:2.4-alpine, adaptee au
|
||||||
|
# projet : on active les modules necessaires, on pointe les vhosts Wakdo,
|
||||||
|
# on desactive les defaults indesirables.
|
||||||
|
#
|
||||||
|
|
||||||
|
ServerRoot "/usr/local/apache2"
|
||||||
|
Listen 80
|
||||||
|
|
||||||
|
# === Modules charges ===
|
||||||
|
# Core :
|
||||||
|
LoadModule mpm_event_module modules/mod_mpm_event.so
|
||||||
|
LoadModule authn_file_module modules/mod_authn_file.so
|
||||||
|
LoadModule authn_core_module modules/mod_authn_core.so
|
||||||
|
LoadModule authz_host_module modules/mod_authz_host.so
|
||||||
|
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
|
||||||
|
LoadModule authz_user_module modules/mod_authz_user.so
|
||||||
|
LoadModule authz_core_module modules/mod_authz_core.so
|
||||||
|
LoadModule access_compat_module modules/mod_access_compat.so
|
||||||
|
LoadModule auth_basic_module modules/mod_auth_basic.so
|
||||||
|
LoadModule reqtimeout_module modules/mod_reqtimeout.so
|
||||||
|
LoadModule filter_module modules/mod_filter.so
|
||||||
|
LoadModule mime_module modules/mod_mime.so
|
||||||
|
LoadModule log_config_module modules/mod_log_config.so
|
||||||
|
LoadModule env_module modules/mod_env.so
|
||||||
|
LoadModule headers_module modules/mod_headers.so
|
||||||
|
LoadModule setenvif_module modules/mod_setenvif.so
|
||||||
|
LoadModule version_module modules/mod_version.so
|
||||||
|
LoadModule unixd_module modules/mod_unixd.so
|
||||||
|
LoadModule status_module modules/mod_status.so
|
||||||
|
LoadModule autoindex_module modules/mod_autoindex.so
|
||||||
|
LoadModule dir_module modules/mod_dir.so
|
||||||
|
LoadModule alias_module modules/mod_alias.so
|
||||||
|
LoadModule rewrite_module modules/mod_rewrite.so
|
||||||
|
LoadModule deflate_module modules/mod_deflate.so
|
||||||
|
|
||||||
|
# Reverse proxy FastCGI vers PHP-FPM (le coeur de notre setup) :
|
||||||
|
LoadModule proxy_module modules/mod_proxy.so
|
||||||
|
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
|
||||||
|
|
||||||
|
# === Identite du serveur ===
|
||||||
|
# User/Group : utilisateur par defaut de l'image httpd:2.4-alpine.
|
||||||
|
# Ne lit pas le code PHP (PHP-FPM s'en charge dans son propre conteneur).
|
||||||
|
User daemon
|
||||||
|
Group daemon
|
||||||
|
|
||||||
|
# Masquer la version Apache dans les headers et les pages d'erreur.
|
||||||
|
# Defense en profondeur : en cas de breach, un attaquant doit faire du work
|
||||||
|
# pour identifier la version exacte.
|
||||||
|
ServerTokens Prod
|
||||||
|
ServerSignature Off
|
||||||
|
|
||||||
|
# ServerName par defaut evite le warning "Could not reliably determine
|
||||||
|
# the server's fully qualified domain name". Les vrais hostnames sont
|
||||||
|
# dans les vhosts.
|
||||||
|
ServerName wakdo-web
|
||||||
|
|
||||||
|
# === Limites et timeouts (defense anti-slowloris) ===
|
||||||
|
Timeout 30
|
||||||
|
KeepAlive On
|
||||||
|
MaxKeepAliveRequests 100
|
||||||
|
KeepAliveTimeout 5
|
||||||
|
RequestReadTimeout header=10-20,MinRate=500 body=20,MinRate=500
|
||||||
|
|
||||||
|
# === Types MIME ===
|
||||||
|
TypesConfig conf/mime.types
|
||||||
|
|
||||||
|
# === Logs ===
|
||||||
|
# stderr du conteneur = agregation par docker logs (Cr 7.d.1 observabilite).
|
||||||
|
ErrorLog /proc/self/fd/2
|
||||||
|
LogLevel warn
|
||||||
|
|
||||||
|
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
|
||||||
|
LogFormat "%h %l %u %t \"%r\" %>s %b" common
|
||||||
|
CustomLog /proc/self/fd/1 combined
|
||||||
|
|
||||||
|
# === Securite des headers par defaut (les vhosts peuvent surcharger) ===
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
Header always set X-Content-Type-Options "nosniff"
|
||||||
|
Header always set X-Frame-Options "SAMEORIGIN"
|
||||||
|
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
Header unset Server
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# === MPM event (tuning workers) ===
|
||||||
|
Include conf/extra/wakdo-mpm.conf
|
||||||
|
|
||||||
|
# === Configuration root globale (refus par defaut) ===
|
||||||
|
# Refuse tout acces hors des <Directory> explicitement autorises dans le vhost.
|
||||||
|
<Directory />
|
||||||
|
AllowOverride none
|
||||||
|
Require all denied
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# === Vhosts du projet ===
|
||||||
|
# Kiosk (borne, sert du statique + relaye les API calls) et admin (back-office).
|
||||||
|
Include conf/extra/wakdo-vhost.conf
|
||||||
21
docker/apache/mpm.conf
Normal file
21
docker/apache/mpm.conf
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#
|
||||||
|
# Wakdo - tuning MPM event
|
||||||
|
#
|
||||||
|
# MPM event : combine threads + event-driven. Plus efficace que prefork pour
|
||||||
|
# une app PHP servie via FastCGI (pas de PHP embarque dans Apache, donc les
|
||||||
|
# threads sont thread-safe).
|
||||||
|
#
|
||||||
|
# Dimensionnement pour un VPS 2 vCPU / 4 Go RAM avec une borne a trafic
|
||||||
|
# modere : pics a 20-30 req/s lors du coup de feu du midi, moyenne plus basse.
|
||||||
|
#
|
||||||
|
|
||||||
|
<IfModule mpm_event_module>
|
||||||
|
StartServers 2
|
||||||
|
ServerLimit 4
|
||||||
|
ThreadLimit 32
|
||||||
|
ThreadsPerChild 25
|
||||||
|
MaxRequestWorkers 100
|
||||||
|
MinSpareThreads 25
|
||||||
|
MaxSpareThreads 75
|
||||||
|
MaxConnectionsPerChild 1000
|
||||||
|
</IfModule>
|
||||||
149
docker/apache/vhost.conf
Normal file
149
docker/apache/vhost.conf
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
#
|
||||||
|
# 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)
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
# headers X-Forwarded-* fournis par Traefik.
|
||||||
|
#
|
||||||
|
# Le SetHandler "proxy:fcgi://wakdo-app:9000" est le coeur du reverse FastCGI :
|
||||||
|
# toute requete *.php est relayee au pool PHP-FPM via TCP sur le reseau interne
|
||||||
|
# wakdo_internal. wakdo-app n'est JAMAIS joignable directement depuis l'exterieur.
|
||||||
|
#
|
||||||
|
|
||||||
|
# === Healthcheck interne Docker (non expose publiquement) ===
|
||||||
|
# Listen sans ServerName = catch-all. Utilise par le HEALTHCHECK du Dockerfile.
|
||||||
|
<VirtualHost *:80>
|
||||||
|
DocumentRoot "/var/www/html/public"
|
||||||
|
|
||||||
|
<Location /healthz>
|
||||||
|
SetHandler none
|
||||||
|
Require all granted
|
||||||
|
</Location>
|
||||||
|
|
||||||
|
Alias /healthz /dev/null
|
||||||
|
<Directory "/var/www/html/public">
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteRule ^/healthz$ - [R=200,L]
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
|
# === 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}
|
||||||
|
|
||||||
|
DocumentRoot "/var/www/html/public/borne"
|
||||||
|
|
||||||
|
# Confiance aux headers X-Forwarded-* de Traefik.
|
||||||
|
# mod_remoteip pourrait etre active pour restaurer la vraie IP client
|
||||||
|
# dans les logs, mais en l'etat le header X-Forwarded-For est loggue
|
||||||
|
# dans combined, ce qui suffit pour un projet RNCP.
|
||||||
|
<Directory "/var/www/html/public/borne">
|
||||||
|
Options -Indexes +FollowSymLinks
|
||||||
|
AllowOverride None
|
||||||
|
Require all granted
|
||||||
|
|
||||||
|
# SPA-like fallback : toute URL non-fichier -> index.html
|
||||||
|
# (pour permettre de bookmarker un chemin profond dans la borne).
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^ index.html [L]
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# Securite supplementaire : expose uniquement public/borne, pas la racine.
|
||||||
|
<Directory "/var/www/html/public/borne/..">
|
||||||
|
Require all denied
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# Compression text/html, css, js, json (Cr 1.e.8 temps de chargement).
|
||||||
|
<IfModule mod_deflate.c>
|
||||||
|
AddOutputFilterByType DEFLATE text/html text/css text/javascript \
|
||||||
|
application/javascript application/json image/svg+xml
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Cache long sur les assets versionnes (futur : hash dans le nom de fichier).
|
||||||
|
<FilesMatch "\.(jpg|jpeg|png|gif|ico|svg|webp|woff2|woff)$">
|
||||||
|
Header set Cache-Control "public, max-age=604800"
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Borne : pas de code PHP execute cote kiosk (Bloc 1 = front only).
|
||||||
|
# Si une requete *.php arrive ici, on la refuse pour forcer l'isolation.
|
||||||
|
<FilesMatch "\.php$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
ErrorLog /proc/self/fd/2
|
||||||
|
CustomLog /proc/self/fd/1 combined
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
|
# === Back-office + API REST (Bloc 2 - PHP from scratch + MVC) ===
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName ${TRAEFIK_DOMAIN_ADMIN}
|
||||||
|
|
||||||
|
DocumentRoot "/var/www/html/public/admin"
|
||||||
|
|
||||||
|
<Directory "/var/www/html/public/admin">
|
||||||
|
Options -Indexes +FollowSymLinks
|
||||||
|
AllowOverride None
|
||||||
|
Require all granted
|
||||||
|
|
||||||
|
# Front controller MVC : toute requete non-fichier passe par index.php
|
||||||
|
# qui dispatche via le Router (src/Core/Router.php a venir en P2).
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^ index.php [L]
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# Reverse FastCGI : toute requete *.php est executee par wakdo-app:9000.
|
||||||
|
# Format proxy:fcgi://<host>:<port><path-to-proxy>
|
||||||
|
# SetHandler s'applique au chemin courant du <FilesMatch>.
|
||||||
|
<FilesMatch "\.php$">
|
||||||
|
SetHandler "proxy:fcgi://wakdo-app:9000"
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Protection des fichiers sensibles du projet si jamais un chemin se
|
||||||
|
# retrouve sous DocumentRoot par erreur (.env, .git, config/).
|
||||||
|
<FilesMatch "^\.(env|git|htaccess)">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
|
<DirectoryMatch "/\.(git|env)">
|
||||||
|
Require all denied
|
||||||
|
</DirectoryMatch>
|
||||||
|
|
||||||
|
# CORS : l'API admin sous /api/* doit accepter les requetes venant
|
||||||
|
# de la borne kiosk (TRAEFIK_DOMAIN_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>
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
# Les headers definitifs sont poses par le middleware PHP.
|
||||||
|
# Apache ne fait que relayer le preflight sans le casser.
|
||||||
|
Header set X-Wakdo-Handled-By "apache-vhost"
|
||||||
|
</IfModule>
|
||||||
|
</Location>
|
||||||
|
|
||||||
|
# Compression back-office (HTML admin + JSON API)
|
||||||
|
<IfModule mod_deflate.c>
|
||||||
|
AddOutputFilterByType DEFLATE text/html text/css text/javascript \
|
||||||
|
application/javascript application/json
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Securite : cookies httpOnly + secure sont forces cote PHP (voir php.ini).
|
||||||
|
# Ici on ajoute un header CSP minimal : l'admin n'a pas besoin de charger
|
||||||
|
# de resources externes pour un MVP (pas de CDN, pas d'analytics).
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
Header always set Content-Security-Policy "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'"
|
||||||
|
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
ErrorLog /proc/self/fd/2
|
||||||
|
CustomLog /proc/self/fd/1 combined
|
||||||
|
</VirtualHost>
|
||||||
52
docker/cron/Dockerfile
Normal file
52
docker/cron/Dockerfile
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Wakdo - image cron
|
||||||
|
#
|
||||||
|
# Base : alpine:3.20 (image minimale ~7 Mo, suffisante pour crond + mariadb-client).
|
||||||
|
# Role : planifier les taches recurrentes du projet (backup BDD, purge sessions,
|
||||||
|
# agregations statistiques) pendant la fenetre de maintenance 01h30-09h30.
|
||||||
|
# Critere RNCP : Cr 7.b.3 (planificateur de tache, cron tab).
|
||||||
|
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
# Installation du minimum :
|
||||||
|
# - dcron : implementation cron simple et standard en Alpine
|
||||||
|
# - mariadb-client : binaire mariadb + mysqldump pour backups et requetes
|
||||||
|
# - gzip : compression des dumps SQL
|
||||||
|
# - tzdata : support des timezones (necessaire pour CRON_TIMEZONE)
|
||||||
|
# - bash : les scripts backup utilisent des features bash (pipefail)
|
||||||
|
# - coreutils : date, du, find avec options GNU (plus lisible que busybox)
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache \
|
||||||
|
dcron \
|
||||||
|
mariadb-client \
|
||||||
|
gzip \
|
||||||
|
tzdata \
|
||||||
|
bash \
|
||||||
|
coreutils; \
|
||||||
|
rm -rf /var/cache/apk/* /tmp/*
|
||||||
|
|
||||||
|
# Dossiers projet :
|
||||||
|
# /scripts : scripts metier (backup, purge, agregations) montes en COPY
|
||||||
|
# /backups : destination des dumps, bind-mount vers ./var/backups sur l'hote
|
||||||
|
RUN mkdir -p /scripts /backups
|
||||||
|
|
||||||
|
# Scripts executables
|
||||||
|
COPY scripts/ /scripts/
|
||||||
|
RUN chmod +x /scripts/*.sh
|
||||||
|
|
||||||
|
# Crontab du projet : defini dans le fichier crontab, copie dans le repertoire
|
||||||
|
# standard Alpine pour crond.
|
||||||
|
COPY crontab /etc/crontabs/root
|
||||||
|
|
||||||
|
# Pas de USER non-root ici : crond exige UID 0 pour lancer les jobs en tant
|
||||||
|
# qu'utilisateurs different. Les scripts s'executent donc en root dans le
|
||||||
|
# conteneur, mais le conteneur lui-meme est isole reseau (wakdo_internal only),
|
||||||
|
# et aucun port hote n'est expose.
|
||||||
|
|
||||||
|
# Healthcheck : verifier que crond est en vie et que son pidfile existe.
|
||||||
|
HEALTHCHECK --interval=60s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD pgrep crond >/dev/null || exit 1
|
||||||
|
|
||||||
|
# Entrypoint : lance crond en foreground, logs vers stderr du conteneur.
|
||||||
|
# -f : foreground
|
||||||
|
# -d 8 : debug level 8 -> logs vers stderr (sinon syslog, qui n'existe pas ici)
|
||||||
|
CMD ["crond", "-f", "-d", "8"]
|
||||||
29
docker/cron/crontab
Normal file
29
docker/cron/crontab
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Wakdo - crontab du conteneur wakdo-cron
|
||||||
|
#
|
||||||
|
# Fenetre de maintenance : 01h30 -> 09h30 (service client 10h00 -> 01h00).
|
||||||
|
# Toutes les heures sont en Europe/Paris (CRON_TIMEZONE dans .env).
|
||||||
|
# Format : m h dom mon dow command
|
||||||
|
#
|
||||||
|
# Les scripts ecrivent leurs logs dans /proc/1/fd/2 pour que docker logs
|
||||||
|
# remonte la sortie. Les erreurs doivent faire un exit != 0 pour que crond
|
||||||
|
# les signale.
|
||||||
|
|
||||||
|
# Minuit-45 : decalage du tout debut de fenetre, mais apres que le dernier
|
||||||
|
# ticket de soiree soit parti en cuisine. Ne fait rien pour l'instant.
|
||||||
|
# Garde en template pour une future invalidation de cache.
|
||||||
|
# 45 0 * * * /scripts/purge-cache.sh 2>&1
|
||||||
|
|
||||||
|
# 03h00 : dump BDD complet, compresse et rotate (garde 14 derniers).
|
||||||
|
0 3 * * * /scripts/backup-db.sh 2>&1
|
||||||
|
|
||||||
|
# Toutes les 15 min pendant la fenetre de maintenance : purge des sessions
|
||||||
|
# PHP expirees cote BDD (pas les sessions systeme qui sont en /tmp du conteneur
|
||||||
|
# wakdo-app, donc ephemeres par nature). A activer quand la table sessions
|
||||||
|
# existera (P2). En l'etat, template.
|
||||||
|
# */15 2-9 * * * /scripts/purge-expired-sessions.sh 2>&1
|
||||||
|
|
||||||
|
# 04h30 : agregations statistiques (top produits, CA par heure, etc.).
|
||||||
|
# Template, a activer quand les tables stats existeront (P3-P4).
|
||||||
|
# 30 4 * * * /scripts/aggregate-stats.sh 2>&1
|
||||||
|
|
||||||
|
# Ligne vide finale requise par certaines implementations cron.
|
||||||
95
docker/cron/scripts/backup-db.sh
Normal file
95
docker/cron/scripts/backup-db.sh
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Wakdo - backup quotidien de la BDD
|
||||||
|
#
|
||||||
|
# Execute par cron a 03h00 (voir /etc/crontabs/root).
|
||||||
|
# Dump complet de la BDD dans /backups (bind-mount vers ./var/backups),
|
||||||
|
# compresse gzip, horodate, rotation sur 14 derniers dumps.
|
||||||
|
#
|
||||||
|
# Variables d'env lues (injectees par docker-compose depuis .env) :
|
||||||
|
# - DB_HOST
|
||||||
|
# - DB_PORT
|
||||||
|
# - DB_NAME
|
||||||
|
# - DB_USER (on utilise le user applicatif, pas root)
|
||||||
|
# - DB_PASSWORD
|
||||||
|
#
|
||||||
|
# Le USER applicatif doit avoir SELECT + LOCK TABLES + SHOW VIEW sur wakdo.
|
||||||
|
# (GRANT donnes dans les migrations a venir en P2.)
|
||||||
|
#
|
||||||
|
# Exit codes :
|
||||||
|
# 0 - backup OK
|
||||||
|
# 1 - variables env manquantes
|
||||||
|
# 2 - mysqldump a echoue
|
||||||
|
# 3 - rotation a echoue
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BACKUP_DIR="/backups"
|
||||||
|
RETENTION_DAYS=14
|
||||||
|
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||||
|
DUMP_FILE="${BACKUP_DIR}/wakdo_${TIMESTAMP}.sql.gz"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[backup-db $(date -Iseconds)] $*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Verification variables ---
|
||||||
|
for var in DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD; do
|
||||||
|
if [ -z "${!var:-}" ]; then
|
||||||
|
log "ERROR: variable $var vide ou non definie"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Verification que /backups est ecrivable ---
|
||||||
|
if [ ! -w "${BACKUP_DIR}" ]; then
|
||||||
|
log "ERROR: ${BACKUP_DIR} n'est pas ecrivable (verifier bind-mount et permissions)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Dump ---
|
||||||
|
log "demarrage dump BDD ${DB_NAME} depuis ${DB_HOST}:${DB_PORT}"
|
||||||
|
# --single-transaction : dump consistent sans LOCK TABLES (innodb only).
|
||||||
|
# --routines --triggers : inclut procedures stockees et triggers (si on en a).
|
||||||
|
# --no-tablespaces : evite le besoin de PROCESS privilege.
|
||||||
|
if ! mysqldump \
|
||||||
|
--host="${DB_HOST}" \
|
||||||
|
--port="${DB_PORT}" \
|
||||||
|
--user="${DB_USER}" \
|
||||||
|
--password="${DB_PASSWORD}" \
|
||||||
|
--single-transaction \
|
||||||
|
--routines \
|
||||||
|
--triggers \
|
||||||
|
--no-tablespaces \
|
||||||
|
--default-character-set=utf8mb4 \
|
||||||
|
"${DB_NAME}" \
|
||||||
|
| gzip -9 > "${DUMP_FILE}"; then
|
||||||
|
log "ERROR: mysqldump a echoue"
|
||||||
|
# Supprime un dump partiel si mysqldump a commence a ecrire.
|
||||||
|
rm -f "${DUMP_FILE}"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Verification du dump ---
|
||||||
|
# Un dump vide ou quasi-vide = probleme silencieux (ex: mauvais USER GRANT).
|
||||||
|
# On exige une taille min de 512 octets pour alerter tot.
|
||||||
|
DUMP_SIZE="$(stat -c '%s' "${DUMP_FILE}")"
|
||||||
|
if [ "${DUMP_SIZE}" -lt 512 ]; then
|
||||||
|
log "ERROR: dump suspect (${DUMP_SIZE} octets), supprime pour ne pas polluer la rotation"
|
||||||
|
rm -f "${DUMP_FILE}"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "dump OK : ${DUMP_FILE} (${DUMP_SIZE} octets)"
|
||||||
|
|
||||||
|
# --- Rotation : supprime les dumps plus vieux que RETENTION_DAYS ---
|
||||||
|
if ! find "${BACKUP_DIR}" -maxdepth 1 -type f -name 'wakdo_*.sql.gz' \
|
||||||
|
-mtime "+${RETENTION_DAYS}" -delete; then
|
||||||
|
log "WARNING: rotation incomplete"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
REMAINING="$(find "${BACKUP_DIR}" -maxdepth 1 -type f -name 'wakdo_*.sql.gz' | wc -l)"
|
||||||
|
log "rotation OK : ${REMAINING} dumps conserves"
|
||||||
|
|
||||||
|
exit 0
|
||||||
57
docker/php-fpm/Dockerfile
Normal file
57
docker/php-fpm/Dockerfile
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# Wakdo - image applicative PHP-FPM
|
||||||
|
#
|
||||||
|
# Base : php:8.3-fpm-alpine3.20 (LTS PHP support jusqu'en 2027, Alpine 3.20 stable).
|
||||||
|
# Role : execute le code PHP (back-office + API REST), expose FastCGI sur 9000.
|
||||||
|
# Reseau : uniquement accessible depuis le reseau interne wakdo_internal.
|
||||||
|
# Lie a wakdo-db via PDO mysql + wakdo-web via FastCGI reverse.
|
||||||
|
|
||||||
|
FROM php:8.3-fpm-alpine3.20
|
||||||
|
|
||||||
|
# Extensions PHP requises par Wakdo :
|
||||||
|
# - pdo_mysql : connexion MariaDB (Cr 4.e.1 prepared statements anti-SQLi)
|
||||||
|
# - opcache : cache bytecode (Cr 1.e.8 perf + Cr 4.g.3 stabilite)
|
||||||
|
# - intl : gestion locale fr_FR pour dates, accents, tri alpha
|
||||||
|
# - exif : lecture metadonnees images upload produits
|
||||||
|
# - zip : manipulation d'archives pour backups et exports
|
||||||
|
# docker-php-ext-install compile et active depuis les sources PHP bundled.
|
||||||
|
# Packages Alpine necessaires au build, puis purges (image finale plus legere).
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache --virtual .build-deps \
|
||||||
|
icu-dev \
|
||||||
|
libzip-dev \
|
||||||
|
oniguruma-dev; \
|
||||||
|
apk add --no-cache \
|
||||||
|
icu-libs \
|
||||||
|
libzip \
|
||||||
|
tini; \
|
||||||
|
docker-php-ext-install -j"$(nproc)" \
|
||||||
|
pdo_mysql \
|
||||||
|
opcache \
|
||||||
|
intl \
|
||||||
|
exif \
|
||||||
|
zip; \
|
||||||
|
apk del --purge .build-deps; \
|
||||||
|
rm -rf /var/cache/apk/* /tmp/*
|
||||||
|
|
||||||
|
# Configuration PHP projet :
|
||||||
|
# - php.ini : parametres runtime (memory, upload, session, display_errors)
|
||||||
|
# - www.conf : pool FPM (pm mode, workers, listen, access log)
|
||||||
|
COPY php.ini /usr/local/etc/php/conf.d/zz-wakdo.ini
|
||||||
|
COPY www.conf /usr/local/etc/php-fpm.d/zz-wakdo.conf
|
||||||
|
|
||||||
|
# WORKDIR = racine applicative. Le code sera bind-mounte en dev via
|
||||||
|
# docker-compose.yml et COPY-e en prod (override compose-prod).
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
# Healthcheck : verifie que PHP-FPM repond via son ping endpoint
|
||||||
|
# (expose par pm.status_path dans www.conf).
|
||||||
|
# cgi-fcgi permet de parler FastCGI depuis le shell pour le test.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD php -r "exit(0);" || exit 1
|
||||||
|
|
||||||
|
# Tini = init minimal qui reape les zombies et transmet SIGTERM proprement.
|
||||||
|
# Sans lui, PHP-FPM en PID 1 ne recoit pas les signaux correctement.
|
||||||
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
|
CMD ["php-fpm", "--nodaemonize"]
|
||||||
|
|
||||||
|
EXPOSE 9000
|
||||||
67
docker/php-fpm/php.ini
Normal file
67
docker/php-fpm/php.ini
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
; Wakdo - configuration PHP runtime (surcharge le php.ini par defaut)
|
||||||
|
; Charge en dernier via le prefixe zz- pour avoir le dernier mot.
|
||||||
|
|
||||||
|
[PHP]
|
||||||
|
; --- Erreurs ---
|
||||||
|
; En dev : on affiche les erreurs a l'ecran. En prod : surcharge via override
|
||||||
|
; docker-compose.prod.yml qui remplace ce fichier (display_errors=0, log_errors=1).
|
||||||
|
display_errors = On
|
||||||
|
display_startup_errors = On
|
||||||
|
error_reporting = E_ALL
|
||||||
|
log_errors = On
|
||||||
|
error_log = /proc/self/fd/2
|
||||||
|
|
||||||
|
; --- Memoire et temps ---
|
||||||
|
memory_limit = 256M
|
||||||
|
max_execution_time = 30
|
||||||
|
max_input_time = 60
|
||||||
|
|
||||||
|
; --- Upload images produits (voir .env UPLOAD_MAX_SIZE_MB=5) ---
|
||||||
|
; post_max_size >= upload_max_filesize + overhead des autres champs du form.
|
||||||
|
file_uploads = On
|
||||||
|
upload_max_filesize = 5M
|
||||||
|
post_max_size = 8M
|
||||||
|
max_file_uploads = 5
|
||||||
|
|
||||||
|
; --- Timezone ---
|
||||||
|
; Cr technique : eviter les warnings et les decalages date silencieux.
|
||||||
|
date.timezone = Europe/Paris
|
||||||
|
|
||||||
|
; --- Sessions ---
|
||||||
|
; Le nom du cookie et la lifetime sont surcharges par l'appli au runtime via
|
||||||
|
; session_set_cookie_params() a partir des variables SESSION_* du .env.
|
||||||
|
; Ce qui est fixe ici = les defaults securises.
|
||||||
|
session.use_strict_mode = 1
|
||||||
|
session.use_cookies = 1
|
||||||
|
session.use_only_cookies = 1
|
||||||
|
session.cookie_httponly = 1
|
||||||
|
session.cookie_samesite = "Strict"
|
||||||
|
session.cookie_secure = 1
|
||||||
|
; session.save_path est laisse par defaut (/tmp dans le conteneur).
|
||||||
|
; Persistance inter-container non necessaire : chaque session est liee a une
|
||||||
|
; instance unique du service wakdo-app (pas de scale horizontal pour ce projet).
|
||||||
|
|
||||||
|
; --- Expose_php = Off : ne pas leak la version PHP dans l'entete HTTP ---
|
||||||
|
expose_php = Off
|
||||||
|
|
||||||
|
; --- OPcache (perf + stabilite) ---
|
||||||
|
[opcache]
|
||||||
|
opcache.enable = 1
|
||||||
|
opcache.enable_cli = 0
|
||||||
|
opcache.memory_consumption = 128
|
||||||
|
opcache.interned_strings_buffer = 16
|
||||||
|
opcache.max_accelerated_files = 10000
|
||||||
|
opcache.validate_timestamps = 1
|
||||||
|
; En dev : revalidate toutes les 2s pour prendre en compte les modifs du bind-mount.
|
||||||
|
; En prod : validate_timestamps=0 via override (invalidation manuelle au deploy).
|
||||||
|
opcache.revalidate_freq = 2
|
||||||
|
opcache.fast_shutdown = 1
|
||||||
|
|
||||||
|
; --- PDO / MySQL ---
|
||||||
|
[PDO]
|
||||||
|
; Pas de persistent connections pour un projet a faible volume : plus simple
|
||||||
|
; a debugger et moins de risques de fuite de sessions BDD.
|
||||||
|
|
||||||
|
[MySQLi]
|
||||||
|
mysqli.default_host = wakdo-db
|
||||||
|
mysqli.default_port = 3306
|
||||||
52
docker/php-fpm/www.conf
Normal file
52
docker/php-fpm/www.conf
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
; Wakdo - pool PHP-FPM (surcharge le pool www par defaut)
|
||||||
|
; Charge en dernier via prefixe zz- pour avoir priorite sur www.conf officiel.
|
||||||
|
|
||||||
|
[www]
|
||||||
|
|
||||||
|
; --- Identite utilisateur ---
|
||||||
|
; L'image officielle cree l'utilisateur www-data (UID 82 en Alpine).
|
||||||
|
; On reste sur le default : on ne change pas l'UID pour eviter les soucis
|
||||||
|
; de permissions sur les uploads (volume wakdo_uploads geré par Docker).
|
||||||
|
user = www-data
|
||||||
|
group = www-data
|
||||||
|
|
||||||
|
; --- Socket d'ecoute ---
|
||||||
|
; TCP pour que wakdo-web (Apache) puisse reverse-proxy FastCGI via le reseau
|
||||||
|
; interne. Socket Unix serait plus performant mais exigerait un volume partage
|
||||||
|
; entre conteneurs, plus complexe a orchestrer.
|
||||||
|
listen = 0.0.0.0:9000
|
||||||
|
|
||||||
|
; Le socket reseau est volontairement ouvert. La securite est assuree par
|
||||||
|
; le fait que wakdo-app n'est attache qu'au reseau interne wakdo_internal
|
||||||
|
; (non expose a l'hote, non expose au proxy Traefik). Seul wakdo-web peut
|
||||||
|
; y acceder.
|
||||||
|
listen.allowed_clients = any
|
||||||
|
|
||||||
|
; --- Process manager (pm) ---
|
||||||
|
; Mode dynamic : ajuste le nombre de workers entre min et max selon la charge.
|
||||||
|
; Valeurs adaptees a une borne de commande en restauration rapide : pics courts
|
||||||
|
; a l'heure du repas, faible charge le reste du temps.
|
||||||
|
; Pour un VPS 2 vCPU / 4 Go RAM : chaque worker PHP consomme ~30-60 Mo.
|
||||||
|
; Avec max_children=10, pire cas ~600 Mo reserve a PHP, marge pour MariaDB.
|
||||||
|
pm = dynamic
|
||||||
|
pm.max_children = 10
|
||||||
|
pm.start_servers = 3
|
||||||
|
pm.min_spare_servers = 2
|
||||||
|
pm.max_spare_servers = 5
|
||||||
|
pm.max_requests = 500
|
||||||
|
|
||||||
|
; --- Status et ping pour monitoring / healthcheck ---
|
||||||
|
; /fpm-status et /fpm-ping ne sont accessibles qu'au sein du conteneur
|
||||||
|
; (le vhost Apache n'exposera pas ces endpoints publiquement).
|
||||||
|
pm.status_path = /fpm-status
|
||||||
|
ping.path = /fpm-ping
|
||||||
|
ping.response = pong
|
||||||
|
|
||||||
|
; --- Logs ---
|
||||||
|
; Redirection vers stderr du conteneur : agregation par docker logs.
|
||||||
|
access.log = /proc/self/fd/2
|
||||||
|
access.format = "%R - %u %t \"%m %r\" %s"
|
||||||
|
|
||||||
|
; Logs d'erreurs PHP : deja rediriges via error_log dans php.ini.
|
||||||
|
catch_workers_output = yes
|
||||||
|
decorate_workers_output = no
|
||||||
248
docs/journal/2026-04-24--infra-docker.md
Normal file
248
docs/journal/2026-04-24--infra-docker.md
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
# Infrastructure Docker - stack complete + referentiel RNCP
|
||||||
|
|
||||||
|
**Date** : 2026-04-24
|
||||||
|
**Branche** : `feat/infra-docker`
|
||||||
|
**PR** : a ouvrir vers `dev` apres merge
|
||||||
|
**Duree estimee** : ~6h (etalee sur 2 sessions de travail)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ce qui a ete fait
|
||||||
|
|
||||||
|
Travaux regroupes en 3 lots sur la branche `feat/infra-docker`, depuis le commit de cadrage `c044d9b`.
|
||||||
|
|
||||||
|
### Lot 1 - Scaffold infra et documentation (commits `c5c6bac` a `32924a5`)
|
||||||
|
|
||||||
|
- `README.md` projet (10 849 octets) avec section Methodologie BYAN + 64 Mantras, quickstart "serveur-derriere-Traefik", prerequis, avertissement sur le `.env` pre-existant.
|
||||||
|
- `.env.example` template neutre (`kiosk.example.com`, `admin.example.com`, `traefik_proxy` - RFC 2606 pour les domaines, aucune info d'infra prod leakee).
|
||||||
|
- `.dockerignore` pour exclure `node_modules`, `.git`, `docs/notes/`, etc. du contexte de build.
|
||||||
|
- `Makefile` avec 24 cibles, aide auto-generee (`make help`), cible `check-env` qui detecte les variables critiques manquantes et oriente vers un merge plutot qu'un ecrasement du `.env` existant.
|
||||||
|
- Structure `docs/journal/` (commit) et `docs/notes/` (gitignore, perso).
|
||||||
|
- Section 17 du `PROJECT_CONTEXT.md` : "Transparence methodologie et usage d'assistants IA", qui declare l'usage conjoint BYAN + Claude Code, precise le scope (ce que l'IA fait / ne fait pas), et documente la politique "zero trailer `Co-Authored-By`" sur les commits.
|
||||||
|
|
||||||
|
### Lot RNCP - Referentiel officiel integre et cross-check (commit `324f5cd`)
|
||||||
|
|
||||||
|
- PDF du referentiel officiel RNCP 37805 (Webecom V09-11-22, 20 pages) ajoute dans `docs/_ref/rncp-37805-referentiel.pdf`.
|
||||||
|
- Index texte compact `docs/_ref/rncp-37805-index.md` : les 24 competences et les ~92 criteres des Blocs 1, 2 et 5 (option DevOps) sont transcrits depuis la source primaire, grep-ables et mis a jour avec leur libelle officiel.
|
||||||
|
- Cross-check des mappings existants : deux citations de criteres etaient incorrectes dans les documents anterieurs (Cr 4.f.1 et Cr 4.f.4 qualifies d'artefacts Git, alors que la lecture de la source primaire les designe comme des soft skills evaluees a l'oral). Correction appliquee dans ce meme commit, sur `docs/journal/2026-04-23--cadrage-projet.md` et sur la section 8 du `PROJECT_CONTEXT.md`.
|
||||||
|
- Confirmation que la **RGPD (C3.d, Cr 3.d.1 a 3.d.4) est obligatoire pour valider le Bloc 2**, pas un bonus.
|
||||||
|
|
||||||
|
### Lot 2 - Stack Docker complete (commit a venir, voir "Liens vers artefacts")
|
||||||
|
|
||||||
|
Arborescence `docker/` creee avec 4 services :
|
||||||
|
|
||||||
|
```
|
||||||
|
docker/
|
||||||
|
apache/
|
||||||
|
Dockerfile FROM httpd:2.4-alpine + modules
|
||||||
|
httpd.conf main config durcie (ServerTokens Prod, timeouts)
|
||||||
|
mpm.conf tuning MPM event
|
||||||
|
vhost.conf 3 vhosts (healthz + kiosk + admin avec FCGI reverse)
|
||||||
|
php-fpm/
|
||||||
|
Dockerfile FROM php:8.3-fpm-alpine3.20 + extensions
|
||||||
|
php.ini display_errors/opcache/session/upload
|
||||||
|
www.conf pool dynamic 3-10 workers, TCP :9000
|
||||||
|
cron/
|
||||||
|
Dockerfile FROM alpine:3.20 + dcron + mariadb-client
|
||||||
|
crontab backup 03h00 (14j retention) + templates purge/stats
|
||||||
|
scripts/backup-db.sh mysqldump + gzip + rotation + validations
|
||||||
|
```
|
||||||
|
|
||||||
|
`docker-compose.yml` (9 667 octets) orchestre les 4 services sur 2 reseaux (`wakdo_internal` bridge interne + `reverse_proxy` externe partage avec le Traefik hote), 2 named volumes (`wakdo_db_data`, `wakdo_uploads`) et 1 bind-mount (`./var/backups/`).
|
||||||
|
|
||||||
|
`.gitignore` mis a jour pour ignorer `/var/` (contenant `var/backups/`) avec commentaire pointant vers `docs/notes/docker-volumes-vs-bind-mounts.md` pour justifier la strategie.
|
||||||
|
|
||||||
|
`Makefile` enrichi de deux cibles : `make backup` (dump SQL manuel horodate via le conteneur cron) et `make backup-ls` (liste les dumps existants).
|
||||||
|
|
||||||
|
Validation `docker compose config --quiet` : syntaxe OK, seuls warnings sur les variables d'env non-injectees dans le contexte de test (comportement normal quand `.env` n'est pas complete).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pourquoi - decisions et alternatives
|
||||||
|
|
||||||
|
### Decision 1 : Dockerfile custom pour Apache plutot qu'image officielle + bind-mount de la conf
|
||||||
|
|
||||||
|
- **Decision retenue** : Dockerfile custom pour wakdo-web, avec `COPY httpd.conf /usr/local/apache2/conf/httpd.conf`.
|
||||||
|
- **Alternative consideree** : utiliser directement `image: httpd:2.4-alpine` dans le compose, avec un bind-mount `./docker/apache/httpd.conf:/usr/local/apache2/conf/httpd.conf:ro`.
|
||||||
|
- **Raison du choix** :
|
||||||
|
1. Homogeneite avec les autres services. Sur 4 services, **3** doivent etre custom par necessite (php-fpm pour installer les extensions, cron pour embarquer le crontab, seul `db` restait eligible a une image officielle sans modification). Faire Apache en image officielle + bind-mount aurait cree une exception a justifier dans la documentation.
|
||||||
|
2. Image autosuffisante : le resultat du `docker build` contient toute la conf, aucun risque que la conf soit manquante sur le serveur cible si un fichier bind-mounte disparait.
|
||||||
|
3. Lisibilite a l'oral : *"j'ai 4 Dockerfiles qui embarquent la conf de leur service"* est plus clair pour le jury que *"3 Dockerfiles et un bind-mount"*.
|
||||||
|
4. Cout marginal faible : 2 lignes de Dockerfile, rebuild en ~15 secondes sur une image Alpine + un COPY. Pendant le dev, la conf Apache est touchee rarement une fois le vhost stable.
|
||||||
|
- **Trace technique** : Cr 7.c.3 du referentiel (*"L'application complete est correctement conteneurisee avec les services et les dependances"*) est renforce par le fait que toute la conf vit dans les images versionnees.
|
||||||
|
|
||||||
|
### Decision 2 : Named volumes pour BDD et uploads, bind-mount pour les backups
|
||||||
|
|
||||||
|
- **Decision retenue** :
|
||||||
|
- MariaDB → `wakdo_db_data` (named volume).
|
||||||
|
- Uploads images produits → `wakdo_uploads` (named volume).
|
||||||
|
- Backups SQL du cron → `./var/backups/` (bind-mount).
|
||||||
|
- **Alternative consideree** : tout en bind-mount vers `./data/`, pour avoir une vue hote unifiee.
|
||||||
|
- **Raison du choix** : MariaDB cree ses fichiers avec UID 999 dans le conteneur, en bind-mount cela donne des fichiers que l'hote voit comme appartenant a un utilisateur inexistant (`rm -rf` impossible sans sudo). Les named volumes laissent Docker gerer cette isolation. Les backups, eux, sont des `.sql.gz` qu'on veut pouvoir inspecter et `scp` hors conteneur : le bind-mount a un vrai benefice de visibilite ici, et le script backup s'execute en root dans le conteneur donc pas de probleme de permissions.
|
||||||
|
- **Source** : la doc Docker Engine (section "Storage") et la doc officielle MariaDB Docker Hub recommandent explicitement les named volumes pour les BDD conteneurisees.
|
||||||
|
- **Ressource projet** : `docs/notes/docker-volumes-vs-bind-mounts.md` (note perso) documente en detail la difference entre `docker compose down` et `docker compose down -v`, le piege de permissions UID 999, et la strategie retenue.
|
||||||
|
|
||||||
|
### Decision 3 : Un seul conteneur Apache pour les 2 FQDN (kiosk + admin)
|
||||||
|
|
||||||
|
- **Decision retenue** : wakdo-web sert les deux hosts via 2 vhosts Apache distincts, et Traefik pose 2 routers sur ce meme conteneur via les labels.
|
||||||
|
- **Alternative consideree** : deux conteneurs Apache distincts (un par FQDN), chacun avec son reseau et sa conf.
|
||||||
|
- **Raison du choix** :
|
||||||
|
1. Le code source est unique (un seul repo, un seul bind-mount `./src:/var/www/html`), donc deux conteneurs partageraient exactement la meme image - duplication inutile.
|
||||||
|
2. L'isolation fonctionnelle est deja assuree au niveau vhost Apache : la directive `<FilesMatch "\.php$"> Require all denied </FilesMatch>` dans le vhost kiosk interdit toute execution PHP cote borne, meme si un fichier `.php` se trouvait dans `public/borne/`.
|
||||||
|
3. Ressources : un conteneur Apache consomme ~30 Mo RAM au repos, en doubler serait gaspiller pour un projet RNCP qui tourne sur un VPS modeste.
|
||||||
|
- **Critere RNCP** : Cr 7.a.1 (*"analyse des contraintes infrastructure et securite"*) : l'isolation est justifiee par la conf applicative, pas par la multiplication des conteneurs.
|
||||||
|
|
||||||
|
### Decision 4 : PHP-FPM pool dynamic 3-10 workers, pas de socket Unix
|
||||||
|
|
||||||
|
- **Decision retenue** : `listen = 0.0.0.0:9000` (socket TCP), `pm = dynamic`, 3 workers au demarrage jusqu'a 10 au pic.
|
||||||
|
- **Alternative consideree** : socket Unix (`/var/run/php-fpm.sock`) partage entre conteneurs via volume, reputee offrir un petit gain de performance.
|
||||||
|
- **Raison du choix du TCP** : partager un socket Unix entre deux conteneurs exige un volume partage qui couple wakdo-web et wakdo-app plus fortement. Le gain de performance d'un Unix socket vs TCP sur localhost est mesure a quelques pourcents dans plusieurs benchmarks publics [CLAIM L4 consensus communaute, a re-checker avant soutenance si la question tombe]. Pour un projet RNCP a trafic modere, la simplicite d'orchestration l'emporte.
|
||||||
|
- **Raison du choix du pm dynamic 3-10** : un worker PHP-FPM consomme ~30-60 Mo avec les extensions activees. Avec 10 max, le pire cas est ~600 Mo reserve a PHP, ce qui laisse de la marge sur un VPS 2 vCPU / 4 Go RAM pour MariaDB et les autres services.
|
||||||
|
|
||||||
|
### Decision 5 : Crontab au format Vixie (dcron) avec retention de 14 jours
|
||||||
|
|
||||||
|
- **Decision retenue** : `mariadb:11.4` LTS + conteneur cron Alpine avec `dcron`, backup nocturne 03h00, gzip, rotation 14 dumps.
|
||||||
|
- **Alternative consideree** : utiliser le cron systeme de l'hote pour declencher `docker exec wakdo-app php /scripts/backup.php`.
|
||||||
|
- **Raison du choix** : Cr 7.b.3 du referentiel demande explicitement *"planification de taches repetitives (planificateur de tache, cron tab)"* - avoir un conteneur cron distinct materialise clairement cette competence. Le cron hote aurait melange des responsabilites. La retention de 14 jours suit un consensus communaute [CLAIM L4] : suffisant pour detecter un probleme de deploiement + marge de reprise, sans saturer le disque.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comment - points techniques cles
|
||||||
|
|
||||||
|
### Reverse FastCGI Apache -> PHP-FPM
|
||||||
|
|
||||||
|
Le coeur du setup est la directive dans `docker/apache/vhost.conf` pour le vhost admin :
|
||||||
|
|
||||||
|
```apache
|
||||||
|
<FilesMatch "\.php$">
|
||||||
|
SetHandler "proxy:fcgi://wakdo-app:9000"
|
||||||
|
</FilesMatch>
|
||||||
|
```
|
||||||
|
|
||||||
|
Apache agit comme un serveur HTTP statique pour les assets (HTML, CSS, JS, images) et relaye toute requete `*.php` vers le pool PHP-FPM via TCP, en utilisant le nom DNS interne `wakdo-app` resolu par le reseau docker `wakdo_internal`. Aucun port 9000 n'est expose a l'hote - seul wakdo-web peut joindre wakdo-app, exactement ce qu'on veut.
|
||||||
|
|
||||||
|
Le module `mod_proxy_fcgi` est compile dans l'image `httpd:2.4-alpine` officielle mais pas charge par defaut. On le charge explicitement dans `httpd.conf` :
|
||||||
|
|
||||||
|
```apache
|
||||||
|
LoadModule proxy_module modules/mod_proxy.so
|
||||||
|
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
|
||||||
|
```
|
||||||
|
|
||||||
|
### Isolation kiosk / admin au niveau vhost
|
||||||
|
|
||||||
|
Le vhost kiosk refuse explicitement l'execution PHP :
|
||||||
|
|
||||||
|
```apache
|
||||||
|
<FilesMatch "\.php$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
|
```
|
||||||
|
|
||||||
|
C'est une defense en profondeur : meme si un fichier `.php` se retrouvait par erreur dans `public/borne/`, il serait servi comme un fichier statique 403, et non execute. Cela materialise la separation Bloc 1 (front vanilla) / Bloc 2 (back PHP) au niveau infra, au-dela de la separation organisationnelle du code source.
|
||||||
|
|
||||||
|
### Persistance et strategie de backup
|
||||||
|
|
||||||
|
La strategie retenue est la suivante :
|
||||||
|
|
||||||
|
- **Named volume `wakdo_db_data`** attache a `/var/lib/mysql`. Survit aux `docker compose down`. Pour une remise a zero, il faut `make clean` (interactif) ou `docker compose down -v` (destructif direct).
|
||||||
|
- **Conteneur `wakdo-cron`** monte le volume BDD en lecture seule (`wakdo_db_data:/var/lib/mysql:ro`) et peut dumper via `mariadb-client` connecte au reseau interne. Les dumps vont dans `./var/backups/` (bind-mount) pour etre inspectables depuis l'hote.
|
||||||
|
- Le script `backup-db.sh` valide que le dump fait plus de 512 octets (sanite), compresse en gzip -9, et supprime les dumps plus vieux que 14 jours via `find -mtime +14 -delete`.
|
||||||
|
|
||||||
|
### Depend_on avec condition service_healthy
|
||||||
|
|
||||||
|
Pour que `make init` soit deterministe, le compose utilise :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
depends_on:
|
||||||
|
wakdo-db:
|
||||||
|
condition: service_healthy
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette condition s'appuie sur le healthcheck officiel `healthcheck.sh --connect --innodb_initialized` fourni par l'image MariaDB 11.4. Apache et PHP-FPM ne demarrent qu'une fois la BDD vraiment prete (pas juste "conteneur demarre"). Le Makefile conserve egalement sa cible `wait-db` en ceinture-bretelles avec un timeout de 60s.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Criteres RNCP couverts
|
||||||
|
|
||||||
|
Chaque mapping ci-dessous reference le libelle exact transcrit depuis `docs/_ref/rncp-37805-index.md`, lui-meme base sur le PDF officiel.
|
||||||
|
|
||||||
|
### Bloc 5 (option DevOps) - tous les criteres touches
|
||||||
|
|
||||||
|
- **Cr 7.a.1** : *"Le candidat a bien analyse les contraintes en termes d'infrastructure et de securite"* → documentation dans `README.md`, `PROJECT_CONTEXT.md` sections 5 et 7, et ce journal. Analyse explicite : reseau derriere Traefik existant, TLS gere en amont, pas de binding de ports hote, deux FQDN distincts.
|
||||||
|
- **Cr 7.a.2** : *"Propose un ensemble de solutions pertinentes pour automatiser tout ou partie du processus"* → `Makefile` (24 cibles, une ligne `make init`), `docker-compose.yml`, conteneur cron.
|
||||||
|
- **Cr 7.a.3** : *"Interactions avec les activites connexes, autant sur la partie developpement que sur la partie de l'infrastructure"* → la strategie `./src` bind-mount en dev et `COPY` en prod (via override) materialise cette interaction.
|
||||||
|
- **Cr 7.b.1** : *"Maitrise de la syntaxe d'un langage de script"* → `Makefile` (100+ lignes), `backup-db.sh` (bash strict avec `set -euo pipefail`).
|
||||||
|
- **Cr 7.b.2** : *"L'automatisation est fonctionnelle et fiabilisee"* → validations dans `backup-db.sh` (variables d'env verifiees, taille min du dump, rotation), exit codes distincts (1, 2, 3).
|
||||||
|
- **Cr 7.b.3** : *"Planification de taches repetitives (planificateur de tache, cron tab)"* → conteneur `wakdo-cron` avec `dcron` et crontab dedie.
|
||||||
|
- **Cr 7.c.1** : *"La machine virtuelle creee par le candidat est configuree et operationnelle"* → hebergement Acadenice en VPS (analogue fonctionnel d'une VM) + conteneurs Docker configures.
|
||||||
|
- **Cr 7.c.2** : *"Le systeme d'exploitation pour conteneur est installe dans la machine d'hebergement virtuelle"* → Docker Engine installe, `docker compose version` disponible.
|
||||||
|
- **Cr 7.c.3** : *"L'application complete est correctement conteneurisee avec les services et les dependances necessaires"* → 4 services distincts (web, app, db, cron), extensions PHP requises declarees dans le Dockerfile, mariadb-client dans le cron pour le backup.
|
||||||
|
- **Cr 7.c.4** : *"Le fichier de configuration est renseigne et permet de lancer la stack applicative complete avec une seule ligne commande"* → `make init` exactement, qui fait build + up + wait-db + migrate (futur). C'est litteralement la phrase.
|
||||||
|
- **Cr 7.d.1** : *"L'architecture serveur est mise en place et fonctionnelle"* → 2 reseaux Docker dont un externe Traefik, 3 volumes, labels Traefik pour 2 routers TLS.
|
||||||
|
- **Cr 7.d.2** : *"L'application est testee avant deploiement"* → a venir (P7, CI GitHub Actions), reference explicite dans `README.md` section CI/CD.
|
||||||
|
- **Cr 7.d.3** : *"L'integration et le deploiement continus sont testes et l'application est livree"* → a venir (P7).
|
||||||
|
|
||||||
|
### Bloc 2 - criteres deja adresses par l'infra
|
||||||
|
|
||||||
|
- **Cr 3.d.4** : *"Les donnees sensibles sont protegees"* → TLS en entree via Traefik, reseau interne isole (aucun port BDD expose a l'hote), mots de passe dans `.env` gitignore. La partie applicative RGPD (Cr 3.d.1 a 3.d.3) est a traiter en P2-P3.
|
||||||
|
- **Cr 4.e.1** : *"Le programme protege l'integrite des donnees en empechant toute injection d'elements pouvant les compromettre"* → infra prete (PDO dans l'image via `pdo_mysql`), implementation applicative en P2.
|
||||||
|
- **Cr 4.f.2** : *"L'utilisation de l'outil de travail collaboratif est maitrisee"* → branches `main` et `dev` protegees, flow `feat/*` impose, Conventional Commits, PR obligatoire.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions anticipees du jury
|
||||||
|
|
||||||
|
- **Q** : *"Pourquoi un conteneur dedie pour le cron plutot qu'un cron systeme sur l'hote ?"*
|
||||||
|
**R** : Trois raisons. D'abord, la reproductibilite : le conteneur cron est defini dans le repo et monte partout ou la stack tourne, alors qu'un cron hote exige de configurer chaque serveur manuellement. Ensuite, la trace RNCP : le Cr 7.b.3 demande explicitement un planificateur, avoir un conteneur distinct materialise clairement cette competence. Enfin, l'isolation : les scripts du cron ont leurs propres dependances (mariadb-client, gzip) qui n'ont pas a polluer l'hote.
|
||||||
|
|
||||||
|
- **Q** : *"Votre `docker-compose.yml` ne binde aucun port sur l'hote, meme pas pour la BDD - comment vous connectez-vous a MariaDB depuis votre poste pour inspecter ?"*
|
||||||
|
**R** : Via `make shell-db` qui ouvre un `mariadb -u root -p` dans le conteneur `wakdo-db`. C'est volontaire : si on bindait le 3306 hote, on exposerait la BDD a tout le reseau local du serveur, alors qu'elle n'a strictement aucune raison d'etre joignable ailleurs que depuis wakdo-app et wakdo-cron. Pour un client graphique (DBeaver, TablePlus) on peut faire un tunnel SSH qui forward le port en local.
|
||||||
|
|
||||||
|
- **Q** : *"Qu'est-ce qui se passe si votre Traefik hote tombe ?"*
|
||||||
|
**R** : Les services internes continuent de tourner (Apache continue de servir sur son port 80 interne), mais le site devient injoignable publiquement. On peut constater ce cas via `make ps` + `make logs-web`, et en attendant la restauration du Traefik on peut ouvrir temporairement un tunnel SSH pour acceder au vhost directement. C'est un cas de defaillance documente, pas une architecture resiliente au failover - pour un projet pedagogique, le Traefik unique est accepte.
|
||||||
|
|
||||||
|
- **Q** : *"Pourquoi MariaDB et pas MySQL ou PostgreSQL ?"*
|
||||||
|
**R** : MariaDB parce que 11.4 est LTS jusqu'en 2028, totalement compatible avec le protocole MySQL (donc PDO avec `mysql:` fonctionne), licence libre sans ambiguite Oracle. PostgreSQL aurait ete defendable aussi - j'ai choisi MariaDB pour rester dans la famille la plus courante en formation dev Web et faciliter la collaboration future avec d'autres developpeurs qui connaitraient MySQL.
|
||||||
|
|
||||||
|
- **Q** : *"Comment votre stack protege-t-elle les donnees utilisateurs (RGPD Cr 3.d.4) au niveau infrastructure ?"*
|
||||||
|
**R** : Trois couches. Primo, TLS en entree par Traefik (Let's Encrypt automatique, pas de HTTP en clair). Secundo, le reseau Docker interne n'est joignable que depuis les autres conteneurs du projet - la BDD ne parle a personne d'autre que wakdo-app et wakdo-cron. Tertio, les mots de passe BDD vivent dans `.env` gitignore (ni dans le repo, ni dans les images), et les backups sont en bind-mount sur un dossier lui aussi gitignore. Pour la partie applicative (hash mots de passe `argon2id`, cookies `HttpOnly`, Secure, SameSite=Strict), c'est deja prepare dans `php.ini` et dans `.env.example`.
|
||||||
|
|
||||||
|
- **Q** : *"Votre note `docs/notes/docker-volumes-vs-bind-mounts.md` est tres detaillee. Qui l'a redigee ?"*
|
||||||
|
**R** : Claude Code, sur ma demande, au moment ou nous tranchions la strategie de persistance. Je l'ai lue, annotee mentalement, et je peux en defendre chaque decision. Cette repartition des roles est formalisee dans la section 17 du `PROJECT_CONTEXT.md` - l'IA redige les notes de revision, je valide et je defends le contenu.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Points d'amelioration conscients
|
||||||
|
|
||||||
|
- **Absence de `src/`** : le bind-mount `./src:/var/www/html` pointe sur un dossier inexistant au moment ou la stack est livree sur la branche `feat/infra-docker`. Au premier `make init`, Apache servira un `DocumentRoot` vide et renverra des 404. C'est normal : la phase P2 cree les stubs `src/public/borne/index.html` et `src/public/admin/index.php`. Je n'ai pas voulu creer des stubs "Hello Wakdo" juste pour passer un smoke-test, parce qu'ils ressembleraient a du code Bloc 1/2 sans en etre, ce qui brouillerait la lecture du commit courant.
|
||||||
|
- **Pas de `docker-compose.prod.yml` (override)** : l'absence de fichier override prod est assumee pour l'instant. Le `.env` avec `APP_ENV=prod` et `APP_DEBUG=false` suffit en premiere approche, mais un override est prevu en P7 qui remplacera le bind-mount `./src` par un `COPY` pour figer le code dans l'image de prod et qui durcira `display_errors=0` dans `php.ini`.
|
||||||
|
- **Pas de `apache2-utils` pour htpasswd** : si un jour on veut proteger `/fpm-status` en Basic Auth, il faudra ajouter le package dans le Dockerfile Apache. Non prioritaire tant qu'on n'expose pas ce endpoint publiquement.
|
||||||
|
- **Backup SQL local uniquement** : les dumps sont sur le meme serveur que la BDD. Un disque qui plante = on perd tout. Une synchronisation hors-site (rsync, rclone) est a prevoir mais hors scope du RNCP courant.
|
||||||
|
- **Crons templates desactives** : les lignes de purge sessions et d'agregations stats sont commentees dans le `crontab` parce que les tables concernees n'existent pas encore. Elles seront decommentees en meme temps que les migrations P2-P3 qui les creeront.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Liens vers artefacts
|
||||||
|
|
||||||
|
- Commits de la branche `feat/infra-docker` (depuis `c044d9b`) :
|
||||||
|
- `c5c6bac` - docs: setup journal structure and session 1 retro
|
||||||
|
- `f619f81` - docs: add AI usage transparency section to PROJECT_CONTEXT
|
||||||
|
- `5dcc5b8` - docs: add README with methodology and server-behind-traefik quickstart
|
||||||
|
- `32924a5` - chore(docker): add env template, dockerignore and Makefile scaffold
|
||||||
|
- `324f5cd` - docs: add RNCP 37805 referentiel and fix Cr 4.f mappings
|
||||||
|
- (a venir) - feat(docker): complete stack with compose and 4 services
|
||||||
|
|
||||||
|
- Fichiers principaux :
|
||||||
|
- `docker-compose.yml`
|
||||||
|
- `docker/apache/{Dockerfile,httpd.conf,vhost.conf,mpm.conf}`
|
||||||
|
- `docker/php-fpm/{Dockerfile,php.ini,www.conf}`
|
||||||
|
- `docker/cron/{Dockerfile,crontab,scripts/backup-db.sh}`
|
||||||
|
- `Makefile`
|
||||||
|
- `.env.example`
|
||||||
|
|
||||||
|
- Documentation associee :
|
||||||
|
- `README.md` - Quickstart et methodologie
|
||||||
|
- `docs/PROJECT_CONTEXT.md` - sections 5, 7, 8, 17
|
||||||
|
- `docs/_ref/rncp-37805-index.md` - criteres cibles
|
||||||
|
- `docs/notes/docker-volumes-vs-bind-mounts.md` - note revision perso (gitignore)
|
||||||
|
- `docs/notes/makefile.md` - note revision perso (gitignore)
|
||||||
|
|
@ -28,6 +28,7 @@ Les fichiers sont ordonnes chronologiquement par leur nom.
|
||||||
| Date | Fichier | Sujet | Branche / PR |
|
| Date | Fichier | Sujet | Branche / PR |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 2026-04-23 | [cadrage-projet](2026-04-23--cadrage-projet.md) | Analyse brief RNCP, decisions d'architecture, bootstrap Git | `main` (commit initial) |
|
| 2026-04-23 | [cadrage-projet](2026-04-23--cadrage-projet.md) | Analyse brief RNCP, decisions d'architecture, bootstrap Git | `main` (commit initial) |
|
||||||
|
| 2026-04-24 | [infra-docker](2026-04-24--infra-docker.md) | Stack Docker complete (compose + 4 services), referentiel RNCP integre, cross-check mappings Cr 4.f | `feat/infra-docker` |
|
||||||
|
|
||||||
*Mis a jour a chaque nouvelle entree.*
|
*Mis a jour a chaque nouvelle entree.*
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue