From b7079e1e42ed4d4d503ace7ebf3410d602d61a7b Mon Sep 17 00:00:00 2001 From: Leon Morival Date: Fri, 10 Jul 2026 14:46:58 +0200 Subject: [PATCH] feat: ci/cd --- .gitea/workflows/cd.yml | 98 ++++++++++++++++++++++++++++++++++++++ .gitea/workflows/ci.yml | 78 ++++++++++++++++++++++++++++++ compose.prod.yaml | 102 ++++++++++++++++++++++++++++++++++++++++ docker/Dockerfile.prod | 84 +++++++++++++++++++++++++++++++++ docker/nginx.prod.conf | 48 +++++++++++++++++++ docker/php.prod.ini | 27 +++++++++++ 6 files changed, 437 insertions(+) create mode 100644 .gitea/workflows/cd.yml create mode 100644 .gitea/workflows/ci.yml create mode 100644 compose.prod.yaml create mode 100644 docker/Dockerfile.prod create mode 100644 docker/nginx.prod.conf create mode 100644 docker/php.prod.ini diff --git a/.gitea/workflows/cd.yml b/.gitea/workflows/cd.yml new file mode 100644 index 0000000..1ad6414 --- /dev/null +++ b/.gitea/workflows/cd.yml @@ -0,0 +1,98 @@ +# ============================================================ +# CD — Déploiement Continu +# Déclenche un déploiement uniquement sur push main. +# +# Le runner Gitea Actions tourne directement sur le serveur. +# Aucun SSH nécessaire — les commandes s'exécutent localement. +# ============================================================ + +name: CD + +on: + push: + branches: + - main + +jobs: + deploy: + name: 🚀 Déploiement production + runs-on: ubuntu-latest + + steps: + + # ── 1. Mise à jour du code ─────────────────────────────── + - name: 🔄 Mise à jour du code + run: | + set -e + cd /data/docker/landing-api + + echo "🔄 Pull depuis Gitea..." + git fetch origin main + git reset --hard origin/main + + # ── 2. Build de l'image Docker ─────────────────────────── + - name: 🐳 Build image Docker + run: | + set -e + cd /data/docker/landing-api + + echo "🏗️ Build de l'image de production..." + docker compose -f compose.prod.yaml build --no-cache app + + # ── 3. Démarrage / redémarrage des containers ──────────── + - name: 🚀 Déploiement des containers + run: | + set -e + cd /data/docker/landing-api + + echo "🚀 Redémarrage des services..." + docker compose -f compose.prod.yaml up -d --remove-orphans + + # ── 4. Migrations base de données ──────────────────────── + - name: 🗄️ Migrations base de données + run: | + set -e + cd /data/docker/landing-api + + echo "⏳ Attente que la BDD soit prête..." + sleep 5 + + echo "🗄️ Exécution des migrations..." + docker compose -f compose.prod.yaml exec -T app php artisan migrate --force + + # ── 5. Optimisations Laravel ───────────────────────────── + - name: ⚙️ Optimisations Laravel + run: | + set -e + cd /data/docker/landing-api + + echo "⚙️ Mise en cache des configs, routes, vues..." + docker compose -f compose.prod.yaml exec -T app php artisan config:cache + docker compose -f compose.prod.yaml exec -T app php artisan route:cache + docker compose -f compose.prod.yaml exec -T app php artisan view:cache + docker compose -f compose.prod.yaml exec -T app php artisan event:cache + + echo "🔄 Redémarrage des workers..." + docker compose -f compose.prod.yaml exec -T app php artisan queue:restart + + # ── 6. Nettoyage des anciennes images ──────────────────── + - name: 🧹 Nettoyage Docker + run: | + docker image prune -f + + # ── 7. Résultat ────────────────────────────────────────── + - name: 🎉 Déploiement réussi + if: success() + run: | + echo "✅ Application déployée avec succès !" + echo "🔗 Commit : ${{ github.sha }}" + echo "👤 Par : ${{ github.actor }}" + + - name: ❌ Déploiement échoué + if: failure() + run: | + echo "❌ Le déploiement a échoué — rollback recommandé" + echo "🔗 Commit : ${{ github.sha }}" + # Affiche les logs des containers en erreur pour debug + cd /data/docker/landing-api + docker compose -f compose.prod.yaml logs --tail=50 diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..9bd7a11 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,78 @@ +# ============================================================ +# CI — Intégration Continue +# Se déclenche sur chaque push et chaque Pull Request. +# Lance les tests PHPUnit avec SQLite en mémoire (rapide). +# ============================================================ + +name: CI + +on: + push: + branches: + - '**' # tous les branches + pull_request: + branches: + - main + - develop + +jobs: + test: + name: 🧪 Tests Laravel (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: ['8.3'] # Ajoute d'autres versions si tu veux tester plusieurs PHP + + steps: + # ── 1. Récupère le code ───────────────────────────────── + - name: 📥 Checkout du code + uses: actions/checkout@v4 + + # ── 2. Configure PHP avec les extensions nécessaires ──── + - name: 🐘 Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, bcmath, sqlite3, pdo_sqlite, pgsql, pdo_pgsql, xml, curl, zip, gd, intl + coverage: none # met "xdebug" ici si tu veux du coverage + + # ── 3. Cache des dépendances Composer ─────────────────── + - name: 📦 Cache Composer + uses: actions/cache@v4 + with: + path: vendor + key: composer-${{ matrix.php }}-${{ hashFiles('composer.lock') }} + restore-keys: composer-${{ matrix.php }}- + + # ── 4. Installe les dépendances PHP ───────────────────── + - name: 📦 Installation Composer + run: composer install --no-interaction --prefer-dist --optimize-autoloader --no-scripts + + # ── 5. Prépare l'environnement .env de test ───────────── + - name: ⚙️ Configuration .env + run: | + cp .env.example .env + # On écrase les variables critiques pour les tests + echo "APP_ENV=testing" >> .env + echo "APP_KEY=" >> .env + echo "DB_CONNECTION=sqlite" >> .env + echo "DB_DATABASE=:memory:" >> .env + echo "QUEUE_CONNECTION=sync" >> .env + echo "SESSION_DRIVER=array" >> .env + echo "CACHE_STORE=array" >> .env + echo "MAIL_MAILER=array" >> .env + echo "BROADCAST_CONNECTION=null" >> .env + + # ── 6. Génère la clé d'application ────────────────────── + - name: 🔑 Génération de la clé APP_KEY + run: php artisan key:generate --ansi + + # ── 7. Exécute les migrations (SQLite :memory:) ───────── + - name: 🗄️ Migrations + run: php artisan migrate --force --ansi + + # ── 8. Lance les tests ─────────────────────────────────── + - name: ✅ Tests PHPUnit + run: php artisan test --ansi diff --git a/compose.prod.yaml b/compose.prod.yaml new file mode 100644 index 0000000..dfec64e --- /dev/null +++ b/compose.prod.yaml @@ -0,0 +1,102 @@ +# ============================================================ +# Docker Compose — Production +# +# Architecture : +# [Nginx Proxy Manager] ──► [FrankenPHP app:80] +# │ +# [PostgreSQL] +# [Queue Worker] +# +# Le réseau "proxy" est externe (géré par Nginx Proxy Manager). +# Dans NPM, configure un proxy host vers : http://landing-api-app:80 +# +# Usage : +# docker compose -f compose.prod.yaml up -d +# ============================================================ + +services: + + # ── Application FrankenPHP (web + PHP tout-en-un) ─────── + app: + build: + context: . + dockerfile: docker/Dockerfile.prod + image: landing-api:latest + container_name: landing-api-app + restart: unless-stopped + env_file: + - .env + environment: + APP_ENV: production + APP_DEBUG: "false" + SERVER_NAME: ":80" # FrankenPHP écoute sur le port 80 + volumes: + - app_storage:/app/storage/app + - app_logs:/app/storage/logs + networks: + - proxy # réseau NPM pour être accessible depuis l'extérieur + - internal # réseau interne pour parler à PostgreSQL + depends_on: + pgsql: + condition: service_healthy + + # ── Base de données PostgreSQL ────────────────────────── + pgsql: + image: postgres:17-alpine + container_name: landing-api-pgsql + restart: unless-stopped + environment: + POSTGRES_DB: "${DB_DATABASE}" + POSTGRES_USER: "${DB_USERNAME}" + POSTGRES_PASSWORD: "${DB_PASSWORD}" + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - pgsql_data:/var/lib/postgresql/data + networks: + - internal # PostgreSQL n'est PAS exposé sur le réseau proxy + healthcheck: + test: ["CMD-SHELL", "pg_isready -q -d ${DB_DATABASE} -U ${DB_USERNAME}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + # ── Worker Laravel Queue ──────────────────────────────── + queue: + build: + context: . + dockerfile: docker/Dockerfile.prod + image: landing-api:latest + container_name: landing-api-queue + restart: unless-stopped + command: ["php", "artisan", "queue:work", "--sleep=3", "--tries=3", "--max-time=3600"] + env_file: + - .env + environment: + APP_ENV: production + APP_DEBUG: "false" + volumes: + - app_storage:/app/storage/app + - app_logs:/app/storage/logs + networks: + - internal + depends_on: + pgsql: + condition: service_healthy + +networks: + # Réseau externe partagé avec Nginx Proxy Manager + proxy: + external: true + + # Réseau interne isolé (app ↔ pgsql ↔ queue) + internal: + driver: bridge + +volumes: + pgsql_data: + driver: local + app_storage: + driver: local + app_logs: + driver: local diff --git a/docker/Dockerfile.prod b/docker/Dockerfile.prod new file mode 100644 index 0000000..407044c --- /dev/null +++ b/docker/Dockerfile.prod @@ -0,0 +1,84 @@ +# ============================================================ +# Dockerfile — Production (FrankenPHP) +# FrankenPHP = serveur web + PHP runtime tout-en-un +# Remplace PHP-FPM + Nginx en un seul container +# ============================================================ + +FROM dunglas/frankenphp:php8.3-alpine AS base + +# Extensions PHP nécessaires pour Laravel +RUN apk add --no-cache \ + postgresql-dev \ + libpng-dev \ + libjpeg-turbo-dev \ + freetype-dev \ + libzip-dev \ + oniguruma-dev \ + icu-dev \ + curl \ + unzip \ + git \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install \ + pdo \ + pdo_pgsql \ + pgsql \ + mbstring \ + zip \ + gd \ + bcmath \ + intl \ + opcache \ + pcntl + +# Composer +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# ── Stage Build (Node pour compiler les assets Vite) ──────── +FROM base AS builder + +WORKDIR /app + +# Dépendances PHP (sans dev) +COPY composer.json composer.lock ./ +RUN composer install \ + --no-interaction \ + --no-dev \ + --prefer-dist \ + --optimize-autoloader \ + --no-scripts + +# Dépendances Node + build assets +COPY package.json package-lock.json* ./ +RUN apk add --no-cache nodejs npm \ + && npm ci --ignore-scripts + +COPY . . + +# Génère les assets Vite +RUN npm run build + +# Post-scripts Composer +RUN composer run-script post-autoload-dump || true + +# ── Stage Final ────────────────────────────────────────────── +FROM base AS production + +WORKDIR /app + +# Copie tout depuis le builder +COPY --from=builder /app /app + +# Config PHP optimisée prod +COPY docker/php.prod.ini /usr/local/etc/php/conf.d/99-prod.ini + +# Permissions sur storage et bootstrap/cache +RUN mkdir -p storage/logs storage/framework/cache storage/framework/sessions storage/framework/views bootstrap/cache \ + && chown -R www-data:www-data storage bootstrap/cache \ + && chmod -R 775 storage bootstrap/cache + +# FrankenPHP écoute sur le port 80 +EXPOSE 80 + +# FrankenPHP sert le dossier public/ de Laravel +CMD ["frankenphp", "php-server", "--root", "/app/public"] diff --git a/docker/nginx.prod.conf b/docker/nginx.prod.conf new file mode 100644 index 0000000..41a6f3b --- /dev/null +++ b/docker/nginx.prod.conf @@ -0,0 +1,48 @@ +server { + listen 80; + server_name _; + root /var/www/html/public; + index index.php; + + # Sécurité — masque la version Nginx + server_tokens off; + + # Taille max des uploads + client_max_body_size 50M; + + # Logs + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log warn; + + # Gzip + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + gzip_min_length 256; + + # Assets statiques — cache long terme + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # Route principale Laravel + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + # PHP-FPM + location ~ \.php$ { + fastcgi_pass app:9000; + fastcgi_index index.php; + fastcgi_buffers 16 16k; + fastcgi_buffer_size 32k; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + } + + # Bloque l'accès aux fichiers cachés + location ~ /\. { + deny all; + } +} diff --git a/docker/php.prod.ini b/docker/php.prod.ini new file mode 100644 index 0000000..0df5537 --- /dev/null +++ b/docker/php.prod.ini @@ -0,0 +1,27 @@ +; Configuration PHP optimisée pour la production + +[PHP] +; Sécurité +expose_php = Off +display_errors = Off +display_startup_errors = Off +log_errors = On +error_log = /var/www/html/storage/logs/php-errors.log + +; Performance +memory_limit = 256M +max_execution_time = 60 +max_input_time = 60 +post_max_size = 50M +upload_max_filesize = 50M + +; OPcache +opcache.enable = 1 +opcache.enable_cli = 0 +opcache.memory_consumption = 128 +opcache.interned_strings_buffer = 16 +opcache.max_accelerated_files = 10000 +opcache.revalidate_freq = 0 +opcache.validate_timestamps = 0 +opcache.save_comments = 1 +opcache.fast_shutdown = 1