Laravel 13 en prod : pipeline CI/CD complet avec GitHub Actions
Pierre
22 min de lecture
Je viens de mettre en ligne ce petit blog alors quoi de mieux pour un premier article que de détailler du mieux possible la procédure que j'utilise ?
Ce que j'utilise :
- développement local sur une machine Ubuntu (25.10)
- Phpstorm
- un terminal (j'utilise Warp Terminal)
- un vps que je loue chez OVH sur lequel je me connecte en ssh
- des jobs Github Actions
On zappe la partie développement local, ça fera peut-être l'objet d'un futur article, ici on push en prod, même le vendredi soir.
Je vais considérer aussi que vous disposez déjà d'un serveur (vps, dédié) avec les services web nécessaires (Php, MySQL/MariaDB, Nginx/Apache, ...)
La stack logicielle utilisée :
- Laravel 13
- Php 8.4
- SQLite en local, MariaDB en prod
- Nginx en prod
- Composer
- Livewire 4 via le starter kit Laravel
- Alpine JS
- Tailwind 4
- Vite 8
- Flux UI
C'est ce qu'on appelle la stack TALL (Tailwind Alpine JS Laravel Livewire).
Créer le dépot Github (via Phpstorm)
Premièrement on va partir de notre projet local et le partager sur Github. On peut faire ça directement dans Phpstorm :
Version Control -> Share Project On -> GitHub...

On choisit le Repository name, si il est public ou privé et on clic sur Share.

Phpstorm va nous proposer notre premier commit : il va cocher tous les fichiers et respectant les .gitignore. Vérifiez, personnalisez votre message de commit ou gardez "Initial commit", puis Add.

Laissez faire le job. Si c'est la première fois Phpstorm va vous demander de vous connecter via GitHub. Une fois terminé rendez-vous sur GitHub et vérifiez le dépot fraichement crée avec vos fichiers.

Les workflows GitHub
Ici on va écrire des jobs Github Actions "deploy.yml" "lint.yml" et "tests.yml". L'idée c'est quoi ? Développement local -> commit -> push sur une branche (dev ou autre) -> envoi sur le repo Github -> Pull Request -> Merge sur la branche principale (main ou master pour moi) -> le job se lance automatiquement -> Lint -> Tests -> si Lint et Tests passent alors -> deploiement sur le vps.
Je vous l'accorde ça paraît obscur dit comme ça, et je ne suis pas encore le champion de la pédagogie, mais on va détailler cela pas à pas.
Si il n'existe pas encore on va créer un dossier ".github" à la racine de notre projet (le . est important) puis "workflows" et dedans nos fichiers de tests, lint et deploy dont voici les contenus :
.github/workflows/deploy.yml
name: deploy
on:
push:
branches:
- master
jobs:
tests:
uses: ./.github/workflows/tests.yml
secrets: inherit
deploy:
runs-on: ubuntu-latest
needs: tests
environment: Production
steps:
- name: Deploy to VPS
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.SSH_PORT }}
script: |
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
cd ${{ secrets.DEPLOY_PATH }}
php artisan down --refresh=15
git pull origin master
chmod -R 775 storage bootstrap/cache
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
npm ci
npm run build
php artisan migrate --force
php artisan optimize
php artisan view:cache
php artisan event:cache
php artisan up
.github/workflows/tests.yml
name: tests
on:
workflow_call:
push:
branches:
- develop
- master
pull_request:
branches:
- develop
- master
jobs:
ci:
runs-on: ubuntu-latest
environment: Testing
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
tools: composer:v2
coverage: xdebug
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '25'
- name: Install Node Dependencies
run: npm i
- name: Copy Environment File
run: cp .env.example .env
- name: Create Storage Directories
run: mkdir -p storage/framework/{cache,sessions,views,testing} storage/logs bootstrap/cache
- name: Install Dependencies
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Generate Application Key
run: php artisan key:generate
- name: Build Assets
run: npm run build
- name: Run Tests
run: ./vendor/bin/pest
.github/workflows/lint.yml
name: linter
on:
push:
branches:
- develop
- master
pull_request:
branches:
- develop
- master
permissions:
contents: write
jobs:
quality:
runs-on: ubuntu-latest
environment: Testing
steps:
- uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- name: Add Flux Credentials Loaded From ENV
run: composer config http-basic.composer.fluxui.dev "${{ secrets.FLUX_USERNAME }}" "${{ secrets.FLUX_LICENSE_KEY }}"
- name: Install Dependencies
run: |
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
npm install
- name: Run Pint
run: composer lint
Que dit le deploy.yml ?
Quand on push (ou merge) sur la branche master (à vous d'adapter selon votre branche principale) alors tu lances les tests (via tests.yml). Si les tests passent alors "Deploy to VPS" de la manière suivante :
- on se connecte en ssh sur le serveur de prod grâce à :
- host : adresse ip du vps fournie via le secret du repo Github (on verra après comment les créer), l'idée c'est de ne pas mettre en clair sur votre repo vos données (très) sensibles.
- username : nom d'utilisateur autorisé à se connecter au vps
- key : la clé privée ssh pour vous connecter sans mot de passe (la création des clés ssh pourra faire l'objet d'un futur article)
- port : par defaut ssh utilise le port 22 et github aussi, si vous n'avez pas changé votre port par défaut sur votre vps (je vous conseille de le faire pour améliorer la sécurité) alors vous pouvez supprimer cette ligne
- on lance le script :
export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
Quand GitHub Actions se connecte en SSH pour exécuter le script, il ouvre un shell non-interactif : les fichiers comme .bashrc ou .bash_profile ne sont pas chargés. NVM (Node Version Manager) est normalement sourcé depuis .bashrc, donc sans ces deux lignes, les commandes npm seraient introuvables. On le source manuellement avant de continuer.
cd ${{ secrets.DEPLOY_PATH }}
On se met dans le dossier du projet (toujours via un secret github)
php artisan down --refresh=15
On met le site en maintenance avec un header HTTP Retry-After: 15s — les navigateurs/crawlers savent qu'il faut revenir bientôt. php artisan up à la fin le remet en ligne.
git pull origin master
On télécharge le code source mis à jour depuis la branche master
chmod -R 775 storage bootstrap/cache
On ajuste les permissions sur les dossiers storage et bootstrap/cache. Le git pull peut réinitialiser les permissions des fichiers. 775 donne lecture/écriture/exécution au propriétaire et au groupe (typiquement www-data pour Nginx/Apache), et lecture/exécution aux autres. Laravel a besoin d'écrire dans storage/ (logs, cache, sessions, vues compilées) et bootstrap/cache/ (config/routes cachés).
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
--no-interaction empêche Composer de bloquer le déploiement en posant des questions dans le terminal (vital pour l'automatisation). --prefer-dist accélère le téléchargement en récupérant des archives zip plutôt qu'en clonant des dépôts. --optimize-autoloader accélère grandement le chargement des classes en production. Enfin, --no-dev exclut les paquets inutiles en prod (Pest, Pint, Sail) pour des raisons de sécurité et d'espace disque.
npm ci
À la différence de npm install, npm ci (pour clean install) installe les dépendances exactement comme définies dans le fichier package-lock.json, sans jamais le modifier. C'est ce qu'on veut en CI/CD et en production : des builds reproductibles et prévisibles.
npm run build
Lance le build des assets frontend
php artisan migrate --force
Lance les migrations de la base de données
php artisan optimize
Cette commande regroupe plusieurs mises en cache en une seule : la configuration (config:cache), les routes (route:cache) et les services. Laravel lira alors des fichiers PHP compilés plutôt que de reconstruire tout ça à chaque requête. Gain de performance immédiat.
php artisan view:cache
Compile toutes les vues Blade en PHP pur et les met en cache. Sans ça, Blade recompile chaque vue à la première requête qui la demande. En prod, on préfère que tout soit prêt à l'avance.
php artisan event:cache
Scanne et met en cache la correspondance entre les Events et leurs Listeners. Sans cache, Laravel parcourt tous tes fichiers pour les découvrir automatiquement à chaque requête. Avec, c'est instantané.
php artisan up
Sort de maintenance le site
Voilà un job simple, propre et solide de déploiement grâce aux Github Actions.
Que dit le tests.yml ?
On ne va pas ici rentrer en détail sur chaque ligne, je pense que vous avez saisi le principe. En gros voici ce que fait ce job :
- quand on push ou fait une PR sur la branche "develop" ou "master"
- github crée un serveur jetable qui va reproduire notre environnement de prod sur la dernière version de Ubuntu dans notre cas
- installe php 8.4
- installe Node JS 25
- installe les dépendances frontend
- crée un fichier .env en copiant celui d'example
- crée les dossiers de stockage
- installe les dépendances php
- génère une app key grâce à la commande artisan
- build les assets
- lance les tests Pest
Que dit le lint.yml ?
Pareil que pour les tests on ne va pas trop détailler ici :
- quand on push ou fait une PR sur la branche "develop" ou "master"
- github crée un serveur jetable qui va reproduire notre environnement de prod sur la dernière version de Ubuntu dans notre cas
- installe les dépendances
- lance le linter
Bon ça en fait des informations à digérer mais il est important de comprendre le fonctionnement plutôt que de faire des simples copier/coller. Passons à la suite.
Ce qu'il nous reste à faire :
- Créer une clé ssh
- Configurer les Github secrets
- Créer la base de données de prod
- Préparer le vps (une seule fois)
- Paramétrer le fichier .env
- Activer le site Nginx
- Faire pointer le domaine sur le vps via la zone dns
- Générer un certificat ssl
- Tester le site (vous aurez droit à un repos bien mérité si tout passe du 1er coup :D )
Créer une clé ssh sur notre machine locale. Cette commande va nous générer un duo clé publique (.pub) et clé privée
ssh-keygen -t ed25519 -C "github-deploy" -f ~/.ssh/votre_site
Puis copier la clé publique sur le VPS :
ssh-copy-id -i ~/.ssh/votre_site.pub votre_user@votre_ip_vps
Configurer les Github secrets
Dans notre repo GitHub on va aller dans Settings → Secrets and variables → Actions et on va créer ces 5 secrets :
- SSH_HOST -> l'adresse ip de notre vps
- SSH_USERNAME -> le nom d'utilisateur autorisé à se connecter en ssh au serveur
- SSH_PORT -> le port ssh (si vous n'avez pas gardé 22 par défaut, sinon pas besoin de ce secret comme vu plus haut dans le job)
- SSH_KEY -> la clé privée qu'on à crée précédemment. Pour l'afficher utilisez la commande
cat ~/.ssh/votre_siteet collez tout à partir de -----BEGIN OPENSSH PRIVATE KEY----- jusque -----END OPENSSH PRIVATE KEY----- - DEPLOY_PATH -> Le chemin absolu du projet sur le VPS (ex: /var/www/votre-site)



Créer la base de données de prod
On se connecte sur notre vps en ssh avec le terminal :
ssh -p 1234 user@ip-du-vps
On se connecte à MariaDB en root sans mot de passe (on considère ici que le vps est déjà configuré avec les différents services installés comme php, mariadb, nginx, ... la stack classique) :
sudo mysql
CREATE DATABASE le_nom_de_votre_db DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;
CREATE USER 'le_nom_de_l_user_pour_cette_db'@'localhost' IDENTIFIED BY 'unmotdepassebiensolide';
GRANT ALL ON le_nom_de_votre_db.* TO 'le_nom_de_l_user_pour_cette_db'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Préparer le vps (une seule fois)
L'idée c'est d'installer manuellement le projet la première fois, et après les jobs mettrons à jour votre site à chaque push sur github.
Sur le VPS on se met dans le dossier web :
cd /var/www
On clone le repo github (notez ici qu'on utilise la version https du repo github et non la version ssh à des fins de simplicité) :
sudo git clone https://github.com/votre-compte/votre-repo.git votre-site
Si on voudrait utiliser la version ssh pour le git clone le VPS doit avoir sa propre clé SSH publique enregistrée dans GitHub (en tant que "Deploy Key" sur le repo). Si non GitHub rejettera le clone. Ceci pourra faire l'objet d'un article séparé.
On ajuste la propriété du dossier :
sudo chown -R votre-utilisateur:www-data /var/www/votre-site
On se met dans le dossier crée :
cd votre-site
On crée les dossiers nécessaires au fonctionnement de Laravel :
mkdir -p storage/framework/{cache,sessions,views,testing} storage/logs bootstrap/cache
On attribue les droits d'écriture pour Nginx (www-data) et notre utilisateur :
chown -R $USER:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache
On installe les dépendances et lance un build :
composer install --no-dev --optimize-autoloader
npm ci && npm run build
On configure l'environnement en copiant le fichier d'exemple vers un vrai fichier .env
cp .env.example .env
On édite le fichier .env avec les vraies valeurs de prod (base de données, ...) :
nano .env
Ce qu'il faut éditer (vous adaptez selon votre projet) :
- APP_NAME="Nom de votre app"
- APP_ENV=production
- APP_DEBUG=false
- APP_URL=https://url-de-votre-site.com
- DB_CONNECTION=mysql
- DB_HOST=127.0.0.1
- DB_PORT=3306
- DB_DATABASE=le nom de votre db
- DB_USERNAME=l'utilisateur que vous avez crée
- DB_PASSWORD=le mot de passe de votre db
Ctrl + X -> Y -> Entrée pour quitter nano en sauvegardant les modifications.
On génère une clé d'application, lance les migrations de base de données et les optimisations
php artisan key:generate
php artisan migrate
php artisan optimize
Activer le site Nginx
Pour activer un site avec Nginx il faut créer un lien symbolique du fichier de configuration de notre site (on va le créer) depuis /etc/nginx/sites-available dans /etc/nginx/sites-enabled
cd /etc/nginx/sites-available
sudo touch le-nom-du-fichier-de-config-de-votre-site-sans-extension par exemple touch blog
sudo nano le-nom-du-fichier-de-config-de-votre-site-sans-extension
server {
server_name url-de-votre-site.com www.url-de-votre-site.com;
root /var/www/dossier-de-votre-site/public;
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Ctrl + X -> Y pour sauvegarder -> Entrée
On crée le lien symbolique, il est important ici d'utiliser les noms de chemins complets même si nous sommes déjà dans le dossier sites-available afin d'éviter des soucis :
sudo ln -s /etc/nginx/sites-available/le-nom-du-fichier-de-config-de-votre-site-sans-extension /etc/nginx/sites-enabled/
On test la config nginx :
sudo nginx -t
On recharge Nginx :
sudo systemctl reload nginx
On redémarre le service :
sudo systemctl restart php8.4-fpm
Faire pointer le domaine sur le vps via la zone dns
Selon l'hébergeur la façon de faire va être différente mais l'idée restera la même. Perso je suis chez OVH.
Ce qu'on doit faire :
- ajouter une entrée de type A du domaine ou sous-domaine vers l'adresse ip de votre vps
- ajouter une seconde entrée de type CNAME de la version www de votre domaine vers sa version non www
Nginx s'occupera de servir le bon dossier.

Générer un certificat ssl
Pour ceci on va utiliser certbot qui va nous générer et s'occuper de renouveler notre certificat ssl gratuit Let's Encrypt. Je pars du principe qu'on a déjà certbot d'installé, sinon veuillez suivre les instructions ici.
Sur notre vps :
sudo certbot --nginx
Puis il suffit de se laisser guider c'est très simple. Assurez-vous que rien n'échoue.
Derniers ajustements et test
Vu que certbot va éditer le fichier de config du site Nginx, on relance un test et redémarre les services :
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl restart php8.4-fpm
On se rend sur l'url de notre site. Normalement si notre plan s'est déroulé sans accrocs vous devriez voir votre site en ligne.
Pipeline CI/CD
Alors toute cette procédure peut paraitre complexe, mais je vous rassure en pratique ça ne l'est pas tant que cela. Le gros avantage c'est que maintenant on a mis en place tout notre pipeline. On va le tester.
Pour ceci on va créer/modifier le fichier README.md sur notre projet local. On retourne alors sur notre ide, voici un exemple de fichier de démonstration qui sera du plus bel effet sur votre GitHub :
# 🍔 BurgerDrop
> *"La perfection entre deux pains."*





---
## 🧐 C'est quoi ce truc ?
**BurgerDrop** est une application web de commande en ligne pour le meilleur (fictif) fast food de la région. Commandez, personnalisez, et recevez votre burger avant même que votre faim ne soit insupportable.
Construite avec amour, sueur et une quantité déraisonnable de café sur la stack **TALL** :
- 🎨 **Tailwind CSS 4** — parce que le CSS à la main c'est pour les héros
- 🏔️ **Alpine.js** — la légèreté incarnée
- ⚡ **Laravel 13** — le boss final du backend PHP
- 🔥 **Livewire 4** — du temps réel sans avoir à écrire une seule API
---
## ✨ Fonctionnalités
- 🍔 **Menu interactif** avec personnalisation des burgers (pain, steak, sauces, toppings)
- 🛒 **Panier en temps réel** sans rechargement de page (merci Livewire)
- 📅 **Réservation de créneaux** de retrait ou livraison
- 💳 **Paiement en ligne** via Stripe
- 📊 **Back-office** pour gérer les commandes, le menu et les stocks
- 🔔 **Notifications** par email et SMS à chaque étape de la commande
- 🌙 **Mode sombre** parce que manger à 2h du matin ça arrive
---
## 🚀 Installation locale
### Prérequis
Avant de te lancer, assure-toi d'avoir :
- PHP **8.4+**
- Composer **2+**
- Node.js **22 LTS**
- Une base de données (SQLite en local, ça suffit largement)
- L'envie irrésistible de manger un burger
### Clone & go
```bash
# Clone le repo
git clone https://github.com/ton-compte/burgerdrop.git
cd burgerdrop
# Installe les dépendances PHP
composer install
# Installe les dépendances JS
npm install
# Copie le fichier d'environnement
cp .env.example .env
# Génère la clé d'application
php artisan key:generate
# Lance les migrations et les seeders (avec de la fausse data 😋)
php artisan migrate --seed
# Build les assets
npm run dev
```
Ensuite ouvre [http://localhost:8000](http://localhost:8000) et commande-toi un burger (fictif, désolé).
---
## 🧪 Tests
Les tests c'est la vie. On utilise **Pest** 🐛
```bash
# Lancer tous les tests
./vendor/bin/pest
# Avec coverage
./vendor/bin/pest --coverage
# Un test en particulier
./vendor/bin/pest --filter=OrderTest
```
> 💡 Les tests tournent sur une base SQLite en mémoire. Rapide comme l'éclair, propre comme un coup de Kärcher.
---
## 🏗️ Structure du projet
```
burgerdrop/
├── app/
│ ├── Http/
│ │ └── Controllers/ # Les chefs d'orchestre
│ ├── Livewire/ # Les composants magiques
│ ├── Models/ # Burger, Order, User...
│ └── Services/ # La logique métier
├── resources/
│ ├── views/
│ │ └── livewire/ # Les templates Blade + Livewire
│ └── js/ # Alpine.js components
├── routes/
│ ├── web.php # Les routes classiques
│ └── console.php # Les commandes artisan customs
├── tests/
│ ├── Feature/ # Tests d'intégration
│ └── Unit/ # Tests unitaires
└── .github/
└── workflows/ # CI/CD GitHub Actions
```
---
## ⚙️ Variables d'environnement
Les variables importantes à configurer dans ton `.env` :
| Variable | Description | Exemple |
|----------|-------------|---------|
| `APP_URL` | URL de l'application | `https://burgerdrop.fr` |
| `DB_CONNECTION` | Driver de base de données | `mysql` |
| `DB_DATABASE` | Nom de la base | `burgerdrop` |
| `STRIPE_KEY` | Clé publique Stripe | `pk_live_...` |
| `STRIPE_SECRET` | Clé secrète Stripe | `sk_live_...` |
| `MAIL_MAILER` | Driver email | `smtp` |
| `TWILIO_SID` | SID Twilio (SMS) | `AC...` |
---
## 🚢 Déploiement
Le déploiement est entièrement automatisé via **GitHub Actions**.
```
push sur develop → tests + lint
merge sur master → tests + lint + déploiement sur le VPS
```
Pour les détails complets de la procédure de déploiement, va lire [cet article de blog](https://ton-blog.fr) qui explique tout ça proprement. 😏
---
## 🤝 Contribution
Les contributions sont les bienvenues ! Voici comment participer :
1. **Fork** le projet
2. Crée ta branche (`git checkout -b feature/super-burger`)
3. Commit tes modifs (`git commit -m 'feat: add extra cheese option'`)
4. Push sur ta branche (`git push origin feature/super-burger`)
5. Ouvre une **Pull Request**
On suit les conventions [Conventional Commits](https://www.conventionalcommits.org/fr/) et [PSR-12](https://www.php-fig.org/psr/psr-12/) pour le PHP.
---
## 🐛 Signaler un bug
Tu as trouvé un bug ? Ouvre une [issue](https://github.com/ton-compte/burgerdrop/issues) avec :
- Une description claire du problème
- Les étapes pour reproduire
- Le comportement attendu vs observé
- Des captures d'écran si pertinent
> ⚠️ **Note importante** : "Le site ne marche pas" n'est pas une description de bug acceptable. Merci. 🙃
---
## 📄 Licence
Ce projet est sous licence **MIT**. Voir le fichier [LICENSE](LICENSE) pour plus de détails.
En gros : fais-en ce que tu veux, mais cite la source et ne me tiens pas responsable si tu prends 5 kilos.
---
## 👨💻 Auteur
Fait avec ❤️ (et beaucoup de 🍔) par **Pierre Bultez**
[](https://github.com/PierreBultez)
---
<div align="center">
<sub>🍟 <em>Aucun burger n'a été blessé lors du développement de cette application.</em> 🍟</sub>
</div>
Toujours dans Phpstorm on va dans "Commit", on coche notre fichier README.md et on ajoute un message de commit (ou on demande à notre IA préférée). Pour terminer le bouton "Commit and Push".

Ça va faire une vérif, si il y a des avertissements non bloquants vous faites "Commit anyway and Push" et si tout va bien "Pushed 1 commit to origin/master".



Si on retourne sur GitHub on peut constater que notre fichier README.md est bien pris en compte.

On constate aussi que nos jobs se sont lancés grâce à ce petit indicateur :

On se rend dans l'onglet "Actions" pour y retrouver nos jobs en cours et suivre leur avancement :

On constate que le job "deploy" attend bien que les tests passent pour se lancer. Si ils échouent alors il échoue aussi et ne se lance pas.

Quelques instants après tous nos jobs sont passés au vert.

Vos modifs viennent d'être poussées en prod en quelques clics, bravo ! Alors ok ici il ne s'agit que d'un fichier README.md sans incidence sur le site mais pour vos modifs sur le site (correctifs, nouvelles fonctionnalités, ...) ce sera pareil. On fait les modifs -> commit -> push -> la prod se met à jour.
Elle n'est pas belle la vie ?
