From 653d7b472987c3e06e3b53eb893a383cb279ef94 Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Wed, 18 Mar 2026 20:12:09 +0100 Subject: [PATCH] Add Docker infrastructure for dev and prod environments - Separate PHP Dockerfiles (dev/prod) with extensions and prod opcache/php.ini optimization - docker-compose-dev: PHP, PostgreSQL, Redis, Messenger, Mailpit, RedisInsight - docker-compose-prod: 2x PHP replicas, PgSQL master/slave with PgBouncer, 2x Messenger, Redis - Makefile with build/start/stop/purge commands - AGENT.md to restrict AI access to the repository Co-Authored-By: Claude Opus 4.6 (1M context) --- AGENT.md | 31 ++++++++++ Makefile | 27 +++++++++ assets/app.scss | 0 docker-compose-dev.yml | 82 +++++++++++++++++++++++++ docker-compose-prod.yml | 114 +++++++++++++++++++++++++++++++++++ docker/pgsql/init-master.sql | 2 + docker/pgsql/init-slave.sh | 12 ++++ docker/pgsql/pgbouncer.ini | 19 ++++++ docker/pgsql/userlist.txt | 1 + docker/php/dev/Dockerfile | 28 +++++++++ docker/php/prod/Dockerfile | 32 ++++++++++ docker/php/prod/opcache.ini | 9 +++ docker/php/prod/php.ini | 21 +++++++ public/logo.png | Bin 0 -> 15189 bytes 14 files changed, 378 insertions(+) create mode 100644 AGENT.md create mode 100644 Makefile create mode 100644 assets/app.scss create mode 100644 docker-compose-dev.yml create mode 100644 docker-compose-prod.yml create mode 100644 docker/pgsql/init-master.sql create mode 100755 docker/pgsql/init-slave.sh create mode 100644 docker/pgsql/pgbouncer.ini create mode 100644 docker/pgsql/userlist.txt create mode 100644 docker/php/dev/Dockerfile create mode 100644 docker/php/prod/Dockerfile create mode 100644 docker/php/prod/opcache.ini create mode 100644 docker/php/prod/php.ini create mode 100644 public/logo.png diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..08c7dbb --- /dev/null +++ b/AGENT.md @@ -0,0 +1,31 @@ +# AGENT.md + +## Interdiction generale + +**Aucun agent IA, assistant de code, ou outil automatise n'est autorise a modifier, committer, pousser, supprimer ou alterer de quelque maniere que ce soit les fichiers de ce depot.** + +Cela inclut, sans s'y limiter : + +- La modification de fichiers existants +- La creation de nouveaux fichiers +- La suppression de fichiers +- Les commits et pushs Git +- L'ouverture de pull requests ou d'issues +- La modification de la configuration du projet +- L'execution de commandes destructrices + +## Portee + +Cette interdiction s'applique a tous les agents IA et outils automatises, y compris mais sans s'y limiter : + +- Claude Code / Claude +- GitHub Copilot +- Cursor +- Windsurf +- Devin +- OpenAI Codex +- Tout autre assistant IA ou bot + +## Exceptions + +Aucune exception. Toute intervention IA sur ce depot est strictement interdite sauf autorisation explicite et ecrite du proprietaire du projet. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7addc73 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.DEFAULT_GOAL := help + +## —— Help ———————————————————————————————————————— +help: ## Affiche la liste des commandes disponibles + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +## —— Docker —————————————————————————————————————— +build_dev: ## Build les images Docker pour le dev + docker compose -f docker-compose-dev.yml build + +build_prod: ## Build les images Docker pour la prod + docker compose -f docker-compose-prod.yml build + +start_dev: ## Lance les containers dev + docker compose -f docker-compose-dev.yml up + +start_prod: ## Lance les containers prod en background + docker compose -f docker-compose-prod.yml up -d + +stop_dev: ## Arrete les containers dev + docker compose -f docker-compose-dev.yml down + +purge_dev: ## Arrete et purge les containers dev (volumes inclus) + docker compose -f docker-compose-dev.yml down -v --rmi all --remove-orphans + +stop_prod: ## Arrete les containers prod + docker compose -f docker-compose-prod.yml down diff --git a/assets/app.scss b/assets/app.scss new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 0000000..810d9dd --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,82 @@ +services: + php: + build: + context: ./docker/php/dev + dockerfile: Dockerfile + container_name: e-ticket_php + restart: unless-stopped + volumes: + - .:/app + ports: + - "9000:9000" + depends_on: + database: + condition: service_healthy + redis: + condition: service_healthy + + database: + image: postgres:16-alpine + container_name: e-ticket_database + environment: + POSTGRES_USER: app + POSTGRES_PASSWORD: secret + POSTGRES_DB: e-ticket + ports: + - "5432:5432" + volumes: + - db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U app -d e-ticket"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + container_name: e-ticket_redis + command: redis-server --requirepass e-ticket + ports: + - "6379:6379" + volumes: + - redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "e-ticket", "ping"] + interval: 5s + timeout: 5s + retries: 5 + + messenger: + build: + context: ./docker/php/dev + dockerfile: Dockerfile + container_name: e-ticket_messenger + command: php bin/console messenger:consume async -vv + restart: unless-stopped + volumes: + - .:/app + depends_on: + database: + condition: service_healthy + redis: + condition: service_healthy + + mailpit: + image: axllent/mailpit + container_name: e-ticket_mailpit + ports: + - "1025:1025" + - "8025:8025" + + redisinsight: + image: redis/redisinsight:latest + container_name: e-ticket_redisinsight + ports: + - "5540:5540" + depends_on: + redis: + condition: service_healthy + +volumes: + db-data: + redis-data: diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml new file mode 100644 index 0000000..c9894f3 --- /dev/null +++ b/docker-compose-prod.yml @@ -0,0 +1,114 @@ +name: e-ticket + +services: + php: + build: + context: ./docker/php/prod + dockerfile: Dockerfile + deploy: + replicas: 2 + restart: unless-stopped + volumes: + - .:/app + ports: + - "9000-9001:9000" + depends_on: + pgbouncer: + condition: service_started + redis: + condition: service_healthy + + db-master: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: e-ticket + POSTGRES_PASSWORD: e-ticket + POSTGRES_DB: e-ticket + command: + - postgres + - -c + - wal_level=replica + - -c + - max_wal_senders=3 + - -c + - wal_keep_size=64MB + - -c + - hot_standby=on + volumes: + - db-master-data:/var/lib/postgresql/data + - ./docker/pgsql/init-master.sql:/docker-entrypoint-initdb.d/init-master.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U e-ticket -d e-ticket"] + interval: 5s + timeout: 5s + retries: 5 + + db-slave: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: e-ticket + POSTGRES_PASSWORD: e-ticket + POSTGRES_DB: e-ticket + PGDATA: /var/lib/postgresql/data + volumes: + - db-slave-data:/var/lib/postgresql/data + - ./docker/pgsql/init-slave.sh:/init-slave.sh + entrypoint: ["/bin/bash", "/init-slave.sh"] + command: ["postgres"] + depends_on: + db-master: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "pg_isready -U e-ticket -d e-ticket"] + interval: 5s + timeout: 5s + retries: 5 + + pgbouncer: + image: edoburu/pgbouncer + restart: unless-stopped + volumes: + - ./docker/pgsql/pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini + - ./docker/pgsql/userlist.txt:/etc/pgbouncer/userlist.txt + ports: + - "6432:6432" + depends_on: + db-master: + condition: service_healthy + db-slave: + condition: service_healthy + + messenger: + build: + context: ./docker/php/prod + dockerfile: Dockerfile + command: php bin/console messenger:consume async --time-limit=3600 --memory-limit=256M --limit=500 -vv + deploy: + replicas: 2 + restart: unless-stopped + volumes: + - .:/app + depends_on: + pgbouncer: + condition: service_started + redis: + condition: service_healthy + + redis: + image: redis:7-alpine + restart: unless-stopped + command: redis-server --requirepass e-ticket + volumes: + - redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "e-ticket", "ping"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + db-master-data: + db-slave-data: + redis-data: diff --git a/docker/pgsql/init-master.sql b/docker/pgsql/init-master.sql new file mode 100644 index 0000000..9649421 --- /dev/null +++ b/docker/pgsql/init-master.sql @@ -0,0 +1,2 @@ +CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'e-ticket'; +SELECT pg_create_physical_replication_slot('slave_slot'); diff --git a/docker/pgsql/init-slave.sh b/docker/pgsql/init-slave.sh new file mode 100755 index 0000000..bc258e4 --- /dev/null +++ b/docker/pgsql/init-slave.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +until pg_isready -h db-master -U e-ticket; do + echo "Waiting for master..." + sleep 2 +done + +rm -rf /var/lib/postgresql/data/* +pg_basebackup -h db-master -D /var/lib/postgresql/data -U replicator -Fp -Xs -P -R + +echo "hot_standby = on" >> /var/lib/postgresql/data/postgresql.conf diff --git a/docker/pgsql/pgbouncer.ini b/docker/pgsql/pgbouncer.ini new file mode 100644 index 0000000..3eeb851 --- /dev/null +++ b/docker/pgsql/pgbouncer.ini @@ -0,0 +1,19 @@ +[databases] +e-ticket = host=db-master port=5432 dbname=e-ticket +e-ticket_readonly = host=db-slave port=5432 dbname=e-ticket + +[pgbouncer] +listen_addr = 0.0.0.0 +listen_port = 6432 +auth_type = md5 +auth_file = /etc/pgbouncer/userlist.txt +pool_mode = transaction +max_client_conn = 200 +default_pool_size = 20 +min_pool_size = 5 +reserve_pool_size = 5 +reserve_pool_timeout = 3 +server_lifetime = 3600 +server_idle_timeout = 600 +log_connections = 0 +log_disconnections = 0 diff --git a/docker/pgsql/userlist.txt b/docker/pgsql/userlist.txt new file mode 100644 index 0000000..bb3436a --- /dev/null +++ b/docker/pgsql/userlist.txt @@ -0,0 +1 @@ +"e-ticket" "md5f79275ff8ae69fcb9e6218d88e699961" diff --git a/docker/php/dev/Dockerfile b/docker/php/dev/Dockerfile new file mode 100644 index 0000000..0139a1c --- /dev/null +++ b/docker/php/dev/Dockerfile @@ -0,0 +1,28 @@ +FROM php:8.4-fpm + +RUN apt-get update && apt-get install -y \ + libpq-dev \ + libsqlite3-dev \ + libzip-dev \ + libxml2-dev \ + libicu-dev \ + libpng-dev \ + libjpeg-dev \ + libfreetype6-dev \ + libmagickwand-dev \ + unzip \ + && rm -rf /var/lib/apt/lists/* + +RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install \ + pdo_mysql \ + pdo_pgsql \ + pdo_sqlite \ + zip \ + xml \ + intl \ + mbstring \ + gd + +RUN pecl install redis imagick \ + && docker-php-ext-enable redis imagick diff --git a/docker/php/prod/Dockerfile b/docker/php/prod/Dockerfile new file mode 100644 index 0000000..cf1044b --- /dev/null +++ b/docker/php/prod/Dockerfile @@ -0,0 +1,32 @@ +FROM php:8.4-fpm + +RUN apt-get update && apt-get install -y \ + libpq-dev \ + libsqlite3-dev \ + libzip-dev \ + libxml2-dev \ + libicu-dev \ + libpng-dev \ + libjpeg-dev \ + libfreetype6-dev \ + libmagickwand-dev \ + unzip \ + && rm -rf /var/lib/apt/lists/* + +RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install \ + pdo_mysql \ + pdo_pgsql \ + pdo_sqlite \ + zip \ + xml \ + intl \ + mbstring \ + gd \ + opcache + +RUN pecl install redis imagick \ + && docker-php-ext-enable redis imagick + +COPY php.ini /usr/local/etc/php/conf.d/app.ini +COPY opcache.ini /usr/local/etc/php/conf.d/opcache.ini diff --git a/docker/php/prod/opcache.ini b/docker/php/prod/opcache.ini new file mode 100644 index 0000000..0f7044e --- /dev/null +++ b/docker/php/prod/opcache.ini @@ -0,0 +1,9 @@ +opcache.enable=1 +opcache.memory_consumption=256 +opcache.interned_strings_buffer=16 +opcache.max_accelerated_files=20000 +opcache.validate_timestamps=0 +opcache.save_comments=0 +opcache.enable_cli=1 +opcache.jit=tracing +opcache.jit_buffer_size=128M diff --git a/docker/php/prod/php.ini b/docker/php/prod/php.ini new file mode 100644 index 0000000..6f3d3a7 --- /dev/null +++ b/docker/php/prod/php.ini @@ -0,0 +1,21 @@ +date.timezone = Europe/Paris + +memory_limit = 256M +upload_max_filesize = 100M +post_max_size = 150M +max_execution_time = 30 +max_input_time = 30 + +expose_php = Off +display_errors = Off +display_startup_errors = Off +log_errors = On +error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT + +realpath_cache_size = 4096K +realpath_cache_ttl = 600 + +session.cookie_secure = On +session.cookie_httponly = On +session.cookie_samesite = Lax +session.use_strict_mode = 1 diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..27bff6ec198319f388cef6a17546540d3f8505f3 GIT binary patch literal 15189 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