Compatibilité MySQL + suppression de Redis comme dépendance requise

DbCompat (app/Support/DbCompat.php) :
- like()           → ilike (pgsql) ou like (mysql)
- jsonRegexRaw()   → data::text ~* ? (pgsql) ou CAST(data AS CHAR) REGEXP ? (mysql)
- ftsRaw()         → to_tsvector/plainto_tsquery (pgsql) ou null/fallback LIKE (mysql)
- generatedJsonCol()       → syntaxe colonne générée JSON selon le SGBD
- generatedJsonNestedCol() → idem pour champs imbriqués

Migrations :
- create_releves_table : JSON/JSONB selon SGBD, colonnes générées adaptées,
  index GIN uniquement pour PostgreSQL

Controllers :
- LieuController (search + index) : ilike → DbCompat::like()
- Admin\UserController (index)     : ilike → DbCompat::like()
- RechercheController              : FTS + regex → DbCompat, fallback LIKE MySQL
- ExportController                 : regex → DbCompat::jsonRegexRaw()

UpdateService :
- backupDatabase()  : pg_dump (pgsql) ou mysqldump (mysql)
- restoreBackup()   : psql (pgsql) ou mysql (mysql)

Docker :
- docker-compose.yml       : suppression Redis (plus requis)
- docker-compose.mysql.yml : nouveau fichier pour dev MySQL
- docker-compose.prod.yml  : suppression Redis, DB_IMAGE configurable,
  CACHE_STORE/SESSION_DRIVER/QUEUE_CONNECTION → database par défaut

.env.example :
- DB_PORT commenté avec les deux valeurs (5432/3306)
- CACHE_STORE et QUEUE_CONNECTION commentés (database par défaut)
- Redis marqué optionnel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 18:13:42 +02:00
parent f57ae068b9
commit 236d37976c
11 changed files with 243 additions and 108 deletions
+7 -3
View File
@@ -20,13 +20,15 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
# ── Base de données — choisir pgsql ou mysql ──────────────────────────────────
DB_CONNECTION=pgsql DB_CONNECTION=pgsql
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
DB_PORT=5432 DB_PORT=5432 # PostgreSQL : 5432 | MySQL : 3306
DB_DATABASE=mesreleves DB_DATABASE=mesreleves
DB_USERNAME=mesreleves DB_USERNAME=mesreleves
DB_PASSWORD=secret DB_PASSWORD=secret
# ── Sessions / Cache / Queue — driver "database" (pas de Redis requis) ────────
SESSION_DRIVER=database SESSION_DRIVER=database
SESSION_LIFETIME=120 SESSION_LIFETIME=120
SESSION_ENCRYPT=false SESSION_ENCRYPT=false
@@ -35,13 +37,15 @@ SESSION_DOMAIN=null
BROADCAST_CONNECTION=log BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local FILESYSTEM_DISK=local
QUEUE_CONNECTION=database QUEUE_CONNECTION=database # database (défaut) | redis (optionnel)
CACHE_STORE=database CACHE_STORE=database # database (défaut) | redis (optionnel)
# CACHE_PREFIX= # CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1 MEMCACHED_HOST=127.0.0.1
# ── Redis (optionnel — non requis par défaut) ─────────────────────────────────
# Pour l'activer : changer CACHE_STORE et SESSION_DRIVER en "redis"
REDIS_CLIENT=phpredis REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1 REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null REDIS_PASSWORD=null
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
use App\Enums\UserRole; use App\Enums\UserRole;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\User; use App\Models\User;
use App\Support\DbCompat;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\Rules\Enum; use Illuminate\Validation\Rules\Enum;
@@ -22,9 +23,10 @@ class UserController extends Controller
if ($request->filled('q')) { if ($request->filled('q')) {
$q = trim($request->get('q')); $q = trim($request->get('q'));
$like = DbCompat::like();
$query->where(fn ($wq) => $wq $query->where(fn ($wq) => $wq
->where('name', 'ilike', "%{$q}%") ->where('name', $like, "%{$q}%")
->orWhere('email', 'ilike', "%{$q}%") ->orWhere('email', $like, "%{$q}%")
); );
} }
+13 -10
View File
@@ -6,6 +6,7 @@ use App\Enums\SourceStatus;
use App\Models\Releve; use App\Models\Releve;
use App\Models\Source; use App\Models\Source;
use App\Services\GedcomExportService; use App\Services\GedcomExportService;
use App\Support\DbCompat;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -50,15 +51,17 @@ class ExportController extends Controller
}); // $request déjà dans le use() }); // $request déjà dans le use()
if ($request->filled('q')) { if ($request->filled('q')) {
$q = trim($request->get('q')); $q = trim($request->get('q'));
$query->where(function ($wq) use ($q) { $like = DbCompat::like();
$wq->where('nom', 'ilike', "%{$q}%") $fts = DbCompat::ftsRaw();
->orWhere('prenom','ilike', "%{$q}%")
->orWhere('date_evenement', 'ilike', "%{$q}%") $query->where(function ($wq) use ($q, $like, $fts) {
->orWhereRaw( $wq->where('nom', $like, "%{$q}%")
"to_tsvector('french', data::text) @@ plainto_tsquery('french', ?)", ->orWhere('prenom', $like, "%{$q}%")
[$q] ->orWhere('date_evenement', $like, "%{$q}%");
); if ($fts) {
$wq->orWhereRaw($fts, [$q]);
}
}); });
} }
@@ -76,7 +79,7 @@ class ExportController extends Controller
$noms = collect($rows)->pluck('nom')->filter(); $noms = collect($rows)->pluck('nom')->filter();
if ($noms->isNotEmpty()) { if ($noms->isNotEmpty()) {
$pattern = $noms->map(fn ($n) => preg_quote($n, '/'))->join('|'); $pattern = $noms->map(fn ($n) => preg_quote($n, '/'))->join('|');
$query->whereRaw("data::text ~* ?", [$pattern]); $query->whereRaw(DbCompat::jsonRegexRaw('data'), [$pattern]);
} }
} }
+7 -6
View File
@@ -6,6 +6,7 @@ use App\Http\Requests\StoreLieuRequest;
use App\Http\Requests\UpdateLieuRequest; use App\Http\Requests\UpdateLieuRequest;
use App\Models\Lieu; use App\Models\Lieu;
use App\Models\LieuType; use App\Models\LieuType;
use App\Support\DbCompat;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -20,9 +21,9 @@ class LieuController extends Controller
$lieux = Lieu::with('lieuType') $lieux = Lieu::with('lieuType')
->where(function ($query) use ($q) { ->where(function ($query) use ($q) {
$query->where('nom_long', 'ilike', "%{$q}%") $query->where('nom_long', DbCompat::like(), "%{$q}%")
->orWhere('nom', 'ilike', "%{$q}%") ->orWhere('nom', DbCompat::like(), "%{$q}%")
->orWhere('code', 'ilike', "%{$q}%"); ->orWhere('code', DbCompat::like(), "%{$q}%");
}) })
->orderBy('nom_long') ->orderBy('nom_long')
->limit(25) ->limit(25)
@@ -49,9 +50,9 @@ class LieuController extends Controller
if ($request->filled('q')) { if ($request->filled('q')) {
$q = trim($request->get('q')); $q = trim($request->get('q'));
$query->where(function ($wq) use ($q) { $query->where(function ($wq) use ($q) {
$wq->where('nom_long', 'ilike', "%{$q}%") $wq->where('nom_long', DbCompat::like(), "%{$q}%")
->orWhere('nom', 'ilike', "%{$q}%") ->orWhere('nom', DbCompat::like(), "%{$q}%")
->orWhere('code', 'ilike', "%{$q}%"); ->orWhere('code', DbCompat::like(), "%{$q}%");
}); });
} }
+14 -11
View File
@@ -6,6 +6,7 @@ use App\Enums\SourceStatus;
use App\Models\Lieu; use App\Models\Lieu;
use App\Models\Releve; use App\Models\Releve;
use App\Models\SourceType; use App\Models\SourceType;
use App\Support\DbCompat;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\View\View; use Illuminate\View\View;
@@ -50,15 +51,18 @@ class RechercheController extends Controller
// ── Recherche textuelle ────────────────────────────────────────────── // ── Recherche textuelle ──────────────────────────────────────────────
if ($request->filled('q')) { if ($request->filled('q')) {
$q = trim($request->get('q')); $q = trim($request->get('q'));
$query->where(function ($wq) use ($q) { $like = DbCompat::like();
$wq->where('nom', 'ilike', "%{$q}%") $fts = DbCompat::ftsRaw();
->orWhere('prenom','ilike', "%{$q}%")
->orWhere('date_evenement', 'ilike', "%{$q}%") $query->where(function ($wq) use ($q, $like, $fts) {
->orWhereRaw( $wq->where('nom', $like, "%{$q}%")
"to_tsvector('french', data::text) @@ plainto_tsquery('french', ?)", ->orWhere('prenom', $like, "%{$q}%")
[$q] ->orWhere('date_evenement', $like, "%{$q}%");
);
if ($fts) {
$wq->orWhereRaw($fts, [$q]);
}
}); });
} }
@@ -66,11 +70,10 @@ class RechercheController extends Controller
if ($request->filled('lieu_id')) { if ($request->filled('lieu_id')) {
$lieuNoms = $this->getLieuNoms($request->integer('lieu_id')); $lieuNoms = $this->getLieuNoms($request->integer('lieu_id'));
if ($lieuNoms->isNotEmpty()) { if ($lieuNoms->isNotEmpty()) {
// Recherche regex case-insensitive dans le JSONB text
$pattern = $lieuNoms $pattern = $lieuNoms
->map(fn ($n) => preg_quote($n, '/')) ->map(fn ($n) => preg_quote($n, '/'))
->join('|'); ->join('|');
$query->whereRaw("data::text ~* ?", [$pattern]); $query->whereRaw(DbCompat::jsonRegexRaw('data'), [$pattern]);
} }
} }
+47 -30
View File
@@ -206,21 +206,30 @@ class UpdateService
throw new RuntimeException("Fichier introuvable : {$path}"); throw new RuntimeException("Fichier introuvable : {$path}");
} }
$host = config('database.connections.pgsql.host'); $driver = config('database.default');
$port = config('database.connections.pgsql.port', 5432); $conn = config("database.connections.{$driver}");
$db = config('database.connections.pgsql.database');
$user = config('database.connections.pgsql.username');
$password = config('database.connections.pgsql.password');
$cmd = sprintf( if ($driver === 'pgsql') {
'PGPASSWORD=%s psql -h %s -p %s -U %s %s < %s', $cmd = sprintf(
escapeshellarg($password), 'PGPASSWORD=%s psql -h %s -p %s -U %s %s < %s',
escapeshellarg($host), escapeshellarg($conn['password']),
(int) $port, escapeshellarg($conn['host']),
escapeshellarg($user), (int) ($conn['port'] ?? 5432),
escapeshellarg($db), escapeshellarg($conn['username']),
escapeshellarg($path), escapeshellarg($conn['database']),
); escapeshellarg($path),
);
} else {
$cmd = sprintf(
'mysql -h %s -P %s -u %s %s %s < %s',
escapeshellarg($conn['host']),
(int) ($conn['port'] ?? 3306),
escapeshellarg($conn['username']),
$conn['password'] ? '-p' . escapeshellarg($conn['password']) : '',
escapeshellarg($conn['database']),
escapeshellarg($path),
);
}
Artisan::call('down'); Artisan::call('down');
$this->exec($cmd); $this->exec($cmd);
@@ -238,27 +247,35 @@ class UpdateService
$filename = "db-before-{$version}-" . date('Ymd-His') . ".sql"; $filename = "db-before-{$version}-" . date('Ymd-His') . ".sql";
$path = "{$dir}/{$filename}"; $path = "{$dir}/{$filename}";
$driver = config('database.default');
$conn = config("database.connections.{$driver}");
$host = config('database.connections.pgsql.host'); if ($driver === 'pgsql') {
$port = config('database.connections.pgsql.port', 5432); $cmd = sprintf(
$db = config('database.connections.pgsql.database'); 'PGPASSWORD=%s pg_dump -h %s -p %s -U %s %s > %s',
$user = config('database.connections.pgsql.username'); escapeshellarg($conn['password']),
$password = config('database.connections.pgsql.password'); escapeshellarg($conn['host']),
(int) ($conn['port'] ?? 5432),
$cmd = sprintf( escapeshellarg($conn['username']),
'PGPASSWORD=%s pg_dump -h %s -p %s -U %s %s > %s', escapeshellarg($conn['database']),
escapeshellarg($password), escapeshellarg($path),
escapeshellarg($host), );
(int) $port, } else {
escapeshellarg($user), $cmd = sprintf(
escapeshellarg($db), 'mysqldump -h %s -P %s -u %s %s %s > %s',
escapeshellarg($path), escapeshellarg($conn['host']),
); (int) ($conn['port'] ?? 3306),
escapeshellarg($conn['username']),
$conn['password'] ? '-p' . escapeshellarg($conn['password']) : '',
escapeshellarg($conn['database']),
escapeshellarg($path),
);
}
$this->exec($cmd); $this->exec($cmd);
if (! file_exists($path) || filesize($path) === 0) { if (! file_exists($path) || filesize($path) === 0) {
throw new RuntimeException("La sauvegarde PostgreSQL a échoué (pg_dump disponible dans le container ?)"); throw new RuntimeException("La sauvegarde de la base de données a échoué ({$driver}).");
} }
return $path; return $path;
+74
View File
@@ -0,0 +1,74 @@
<?php
namespace App\Support;
use Illuminate\Support\Facades\DB;
/**
* Helpers pour les différences de syntaxe SQL entre PostgreSQL et MySQL.
*/
class DbCompat
{
public static function isPgsql(): bool
{
return DB::connection()->getDriverName() === 'pgsql';
}
/** Opérateur LIKE insensible à la casse */
public static function like(): string
{
return self::isPgsql() ? 'ilike' : 'like';
}
/** Caste une colonne JSON en texte brut */
public static function jsonToText(string $column): string
{
return self::isPgsql()
? "{$column}::text"
: "CAST({$column} AS CHAR)";
}
/**
* Condition de correspondance regex (insensible à la casse) sur une colonne JSON.
* Retourne la chaîne à passer à whereRaw() avec un seul binding `?`.
*/
public static function jsonRegexRaw(string $column = 'data'): string
{
return self::isPgsql()
? "{$column}::text ~* ?"
: "CAST({$column} AS CHAR) REGEXP ?";
}
/**
* Condition de recherche plein-texte.
* Retourne null pour MySQL (le caller doit prévoir un fallback LIKE).
*/
public static function ftsRaw(): ?string
{
return self::isPgsql()
? "to_tsvector('french', data::text) @@ plainto_tsquery('french', ?)"
: null;
}
/** Syntaxe de la colonne générée stockée pour extraire un champ JSON de premier niveau */
public static function generatedJsonCol(string $jsonKey): string
{
return self::isPgsql()
? "data->>'$jsonKey'"
: "JSON_UNQUOTE(JSON_EXTRACT(data, '$.$jsonKey'))";
}
/** Syntaxe de la colonne générée stockée pour un champ JSON imbriqué (ex: date_evenement.valeur) */
public static function generatedJsonNestedCol(string $jsonPath): string
{
if (self::isPgsql()) {
// data->'date_evenement'->>'valeur'
$parts = explode('.', $jsonPath);
$last = array_pop($parts);
$chain = implode('', array_map(fn ($p) => "->'{$p}'", $parts));
return "data{$chain}->>'{$last}'";
}
// MySQL: $.date_evenement.valeur
return "JSON_UNQUOTE(JSON_EXTRACT(data, '$.{$jsonPath}'))";
}
}
@@ -1,35 +1,56 @@
<?php <?php
use App\Support\DbCompat;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
Schema::create('releves', function (Blueprint $table) { $isPgsql = DbCompat::isPgsql();
Schema::create('releves', function (Blueprint $table) use ($isPgsql) {
$table->id(); $table->id();
$table->foreignId('source_id')->constrained()->cascadeOnDelete(); $table->foreignId('source_id')->constrained()->cascadeOnDelete();
$table->jsonb('data'); // champs variables selon source_type // PostgreSQL : JSONB (binary JSON, indexable par GIN)
// MySQL : JSON
if ($isPgsql) {
$table->jsonb('data');
} else {
$table->json('data');
}
$table->foreignId('created_by')->constrained('users')->restrictOnDelete(); $table->foreignId('created_by')->constrained('users')->restrictOnDelete();
$table->foreignId('updated_by')->constrained('users')->restrictOnDelete(); $table->foreignId('updated_by')->constrained('users')->restrictOnDelete();
$table->timestamps(); $table->timestamps();
}); });
// Colonnes générées stockées pour les champs les plus filtrés // ── Colonnes générées stockées (syntaxe différente selon le SGBD) ──────
DB::statement("ALTER TABLE releves ADD COLUMN nom TEXT GENERATED ALWAYS AS (data->>'nom') STORED"); $nomExpr = DbCompat::generatedJsonCol('nom');
DB::statement("ALTER TABLE releves ADD COLUMN prenom TEXT GENERATED ALWAYS AS (data->>'prenom') STORED"); $prenomExpr = DbCompat::generatedJsonCol('prenom');
DB::statement("ALTER TABLE releves ADD COLUMN date_evenement TEXT GENERATED ALWAYS AS (data->'date_evenement'->>'valeur') STORED"); $dateEvtExpr = DbCompat::generatedJsonNestedCol('date_evenement.valeur');
// Index pour les colonnes générées if ($isPgsql) {
DB::statement("ALTER TABLE releves ADD COLUMN nom TEXT GENERATED ALWAYS AS ({$nomExpr}) STORED");
DB::statement("ALTER TABLE releves ADD COLUMN prenom TEXT GENERATED ALWAYS AS ({$prenomExpr}) STORED");
DB::statement("ALTER TABLE releves ADD COLUMN date_evenement TEXT GENERATED ALWAYS AS ({$dateEvtExpr}) STORED");
} else {
DB::statement("ALTER TABLE releves ADD COLUMN nom VARCHAR(255) GENERATED ALWAYS AS ({$nomExpr}) STORED");
DB::statement("ALTER TABLE releves ADD COLUMN prenom VARCHAR(255) GENERATED ALWAYS AS ({$prenomExpr}) STORED");
DB::statement("ALTER TABLE releves ADD COLUMN date_evenement VARCHAR(255) GENERATED ALWAYS AS ({$dateEvtExpr}) STORED");
}
// ── Index B-tree sur les colonnes générées ───────────────────────────
DB::statement('CREATE INDEX releves_nom_idx ON releves (nom)'); DB::statement('CREATE INDEX releves_nom_idx ON releves (nom)');
DB::statement('CREATE INDEX releves_prenom_idx ON releves (prenom)'); DB::statement('CREATE INDEX releves_prenom_idx ON releves (prenom)');
DB::statement('CREATE INDEX releves_date_evenement_idx ON releves (date_evenement)'); DB::statement('CREATE INDEX releves_date_evenement_idx ON releves (date_evenement)');
// Index GIN pour la recherche fulltext sur l'ensemble du JSONB // ── Index GIN sur le JSON complet (PostgreSQL uniquement) ─────────────
DB::statement('CREATE INDEX releves_data_gin_idx ON releves USING gin (data)'); if ($isPgsql) {
DB::statement('CREATE INDEX releves_data_gin_idx ON releves USING gin (data)');
}
} }
public function down(): void public function down(): void
+27
View File
@@ -0,0 +1,27 @@
# Développement local — MySQL 8 (alternative à PostgreSQL)
# Usage : docker compose -f docker-compose.mysql.yml up -d
# .env : DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306
services:
db:
image: mysql:8
container_name: mesreleves_db_mysql
restart: unless-stopped
environment:
MYSQL_DATABASE: mesreleves
MYSQL_USER: mesreleves
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: secret
MYSQL_CHARSET: utf8mb4
MYSQL_COLLATION: utf8mb4_unicode_ci
ports:
- "3306:3306"
volumes:
- mysqldata:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-umesreleves", "-psecret"]
interval: 10s
timeout: 5s
retries: 5
volumes:
mysqldata:
+18 -25
View File
@@ -1,4 +1,5 @@
# Déploiement production — stack complète (app + db + redis + nginx) # Déploiement production — stack complète (app + db + nginx)
# Pas de Redis — cache/sessions/queue utilisent la base de données
# Usage : docker compose -f docker-compose.prod.yml up -d # Usage : docker compose -f docker-compose.prod.yml up -d
services: services:
app: app:
@@ -10,25 +11,20 @@ services:
environment: environment:
APP_ENV: production APP_ENV: production
APP_KEY: ${APP_KEY} APP_KEY: ${APP_KEY}
DB_CONNECTION: pgsql DB_CONNECTION: ${DB_CONNECTION:-pgsql}
DB_HOST: db DB_HOST: db
DB_PORT: 5432 DB_PORT: ${DB_PORT:-5432}
DB_DATABASE: ${DB_DATABASE:-mesreleves} DB_DATABASE: ${DB_DATABASE:-mesreleves}
DB_USERNAME: ${DB_USERNAME:-mesreleves} DB_USERNAME: ${DB_USERNAME:-mesreleves}
DB_PASSWORD: ${DB_PASSWORD} DB_PASSWORD: ${DB_PASSWORD}
CACHE_STORE: redis CACHE_STORE: database
SESSION_DRIVER: redis SESSION_DRIVER: database
QUEUE_CONNECTION: redis QUEUE_CONNECTION: database
REDIS_HOST: redis
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
redis:
condition: service_started
volumes: volumes:
# Le code est monté depuis l'hôte : les mises à jour ne nécessitent pas de rebuild image
- .:/var/www/html - .:/var/www/html
# Le storage est un volume nommé (uploads, logs, sessions — persisté indépendamment du code)
- app_storage:/var/www/html/storage - app_storage:/var/www/html/storage
networks: networks:
- internal - internal
@@ -50,35 +46,32 @@ services:
- internal - internal
db: db:
image: postgres:18-alpine image: ${DB_IMAGE:-postgres:18-alpine}
container_name: mesreleves_db container_name: mesreleves_db
restart: unless-stopped restart: unless-stopped
environment: environment:
# PostgreSQL
POSTGRES_DB: ${DB_DATABASE:-mesreleves} POSTGRES_DB: ${DB_DATABASE:-mesreleves}
POSTGRES_USER: ${DB_USERNAME:-mesreleves} POSTGRES_USER: ${DB_USERNAME:-mesreleves}
POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_PASSWORD: ${DB_PASSWORD}
# MySQL (ignoré par PostgreSQL)
MYSQL_DATABASE: ${DB_DATABASE:-mesreleves}
MYSQL_USER: ${DB_USERNAME:-mesreleves}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes: volumes:
- pgdata:/var/lib/postgresql - dbdata:/var/lib/postgresql # PostgreSQL
# Pour MySQL, remplacer par : - dbdata:/var/lib/mysql
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-mesreleves}"] test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-mesreleves} || mysqladmin ping -h localhost -u${DB_USERNAME:-mesreleves} -p${DB_PASSWORD}"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
networks: networks:
- internal - internal
redis:
image: redis:7-alpine
container_name: mesreleves_redis
restart: unless-stopped
volumes:
- redisdata:/data
networks:
- internal
volumes: volumes:
pgdata: dbdata:
redisdata:
app_storage: app_storage:
networks: networks:
+1 -11
View File
@@ -1,4 +1,4 @@
# Développement local — PostgreSQL + Redis uniquement # Développement local — PostgreSQL uniquement (pas de Redis requis)
# L'application PHP tourne avec `php artisan serve` en dehors de Docker # L'application PHP tourne avec `php artisan serve` en dehors de Docker
services: services:
db: db:
@@ -19,15 +19,5 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
redis:
image: redis:7-alpine
container_name: mesreleves_redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redisdata:/data
volumes: volumes:
pgdata: pgdata:
redisdata: