· 13 min read
Déployer en prod votre app Symfony avec Kamal, FrankenPHP et de simples VPS
Perdu dans la jungle des solutions de déploiement pour votre application Symfony ? Vous avez un budget limité ? Vous êtes au bon endroit ! Découvrez comment déployer votre application en production avec Kamal, FrankenPHP et de simples VPS.
Présentation de Kamal
Kamal est un outil innovant conçu pour rendre le déploiement d’applications web, notamment celles développées avec Symfony, rapide et accessible. À une époque où les solutions commerciales comme Heroku et Fly.io dominent le marché, Kamal se distingue par sa flexibilité et sa simplicité, sans vous enfermer dans un écosystème coûteux.
Avec Kamal, il vous suffit d’un serveur Ubuntu et d’une clé SSH pour déployer votre application Symfony en quelques minutes. Pas besoin de préparations complexes : l’outil se charge d’installer Docker automatiquement. Cela permet de bénéficier de la puissance de la conteneurisation tout en gardant une portabilité entre différents fournisseurs de services cloud ou votre propre matériel.
Kamal se veut une alternative aux solutions lourdes comme Kubernetes, en offrant une approche directe et simple, parfaite pour les développeurs Symfony souhaitant déployer sans se compliquer la vie. Avec Kamal, concentrez-vous sur le développement de votre application, et laissez le déploiement entre les mains de cet outil malin et efficace.
Prérequis : Conteneurisation de votre application Symfony
Bien que Kamal soit simple à utiliser, il est important de comprendre les principes de base de la conteneurisation. En effet, Kamal repose sur Docker pour isoler votre application Symfony et ses dépendances. Si vous n’êtes pas familier avec Docker, je vous recommande de suivre un tutoriel pour vous familiariser avec les concepts de base (Vous pouvez consulter cet article pour débuter : FrankenPHP et Symfony : Une solution d’avenir ?).
Notre laboratoire
Pour tester Kamal, nous allons :
- Créer une application Symfony simple
- Conteneuriser cette application avec Docker avec FrankenPHP
- Commander un VPS chez un fournisseur de services cloud (Chez Hidora)
- Appliquer les bonnes pratiques de sécurité pour notre serveur (avec un playbook Ansible)
- Créer une entrée DNS pointant vers notre VPS
- Utiliser Kamal pour déployer notre application Symfony
Dans une seconde partie, nous verrons comment rendre notre application plus robuste en la déployant sur plusieurs serveurs, et comment mettre en place un équilibrage de charge pour répartir la charge entre ces serveurs (avec Scaleway).
Le concept de notre application de démonstration
Pour notre démonstration, nous allons créer une application Symfony simple, qui affiche une page d’accueil, nous propose de choisir une année, et nous renvoie la liste des jours fériés de cette année.
Nous allons utiliser une API publique pour récupérer les jours fériés, et nous allons afficher le résultat dans une page web : API des Jours fériés.
Création de l’application Symfony
- Créons un nouveau projet Symfony avec la commande suivante :
symfony new jours-feries --webapp
- Ajoutons une route pour afficher la page d’accueil :
symfony console make:controller Home
- Modifions le contrôleur pour récupérer les jours fériés de l’année choisie (dans la vraie vie, on utiliserait un service pour cela) et pour créer une route pour un health check sur
/up
(nécessaire pour Kamal) :
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class HomeController extends AbstractController
{
private const API_URL = 'https://calendrier.api.gouv.fr/jours-feries/';
private const ZONE = 'metropole';
private const FORMAT = 'json';
#[Route('/{year}', name: 'app_home', requirements: ['year' => '\d{4}'])]
public function index(HttpClientInterface $client, int $year = null): Response
{
$year = $year ?? date('Y');
$response = $client->request('GET', self::API_URL.self::ZONE.'/'.$year.'.'. self::FORMAT);
$data = $response->toArray();
return $this->render('home/index.html.twig', [
'year' => $year,
'data' => $data,
]);
}
#[Route('/up', name: 'app_up')]
public function up(): JsonResponse
{
return new JsonResponse(['status' => 'ok']);
}
}
-
Vidons le contenu du fichier
assets/styles/app.css
-
Modifions notre
base.html.twig
pour utiliser PicoCSS :
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.min.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% block stylesheets %}
{% endblock %}
{% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
- Créons le template
templates/home/index.html.twig
:
{% extends 'base.html.twig' %}
{% block title %}Jours Fériés {{ year }}{% endblock %}
{% block body %}
<main class="container">
<h1>Jours Fériés {{ year }}</h1>
<table>
<thead>
<tr>
<th>Date</th>
<th>Nom</th>
</tr>
</thead>
<tbody>
{% for date, name in data %}
<tr>
<td>{{ date }}</td>
<td>{{ name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</main>
{% endblock %}
Conteneurisation de l’application Symfony
Pour conteneuriser notre application Symfony, nous allons utiliser FrankenPHP, un outil simple et efficace pour créer des images Docker de vos applications Symfony.
- Mode worker : FrankenPHP propose un mode worker pour charger en mémoire votre application Symfony et la rendre plus performante. Pour cela, il nous faut ajouter une dépendance à notre projet (un nouveau
runtime
).
composer require runtime/frankenphp-symfony
- Créons un fichier
Dockerfile
à la racine de notre projet :
Nous réalisons une image multi-stage pour optimiser la taille de notre image Docker.
FROM composer:2 AS composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-progress --prefer-dist
FROM dunglas/frankenphp
ENV SERVER_NAME=:80
ENV APP_RUNTIME=Runtime\\FrankenPhpSymfony\\Runtime
ENV APP_ENV=prod
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
COPY . /app/
COPY --from=composer /app/vendor /app/vendor
- Buildons et testons notre image Docker :
# Construction de l'image
docker build -t app-frankenphp .
# Lancement du conteneur sur le port 8080
docker run -p 8080:80 --name app-frankenphp app-frankenphp
Un déploiement simple avec Kamal sur un seul hôte
Dans cette première partie, nous allons déployer notre application Symfony sur un seul serveur VPS. Cette solution est idéale pour les petites applications ou les projets personnels, et permet d’utiliser des VPS peu coûteux pour héberger votre application.
Dans le cadre d’une application en production, il est recommandé de déployer votre application sur plusieurs serveurs pour garantir une haute disponibilité et une meilleure tolérance aux pannes, on en parlera dans la seconde partie de la démonstration.
Commande de notre VPS chez Hidora
Pour notre première démonstration, nous allons commander un VPS chez Hidora, un fournisseur de services cloud suisse.
Perso, je prends une VM Ubuntu 22.04 avec 4 vCPU, 8 Go de RAM (je suis gourmand 😀) et une adresse IP V4.
Ma clé SSH est déjà configurée sur mon compte Hidora, je peux donc me connecter en SSH sans mot de passe sur ce nouveau serveur VPS : C’est un prérequis pour utiliser Kamal.
Sécurisation de notre serveur avec Ansible
-
Pour sécuriser notre serveur, nous allons utiliser un playbook Ansible minimal qui va :
- Mettre à jour les paquets du système
- Installer les paquets de base (fail2ban, ufw, etc.)
- Configurer un pare-feu avec UFW
- Configurer Fail2Ban pour protéger notre serveur contre les attaques par force brute
- Empêcher l’accès SSH par login/password
-
Créons un fichier
playbook.yml
à la racine de notre projet :
---
- name: Sécurisation Minimale Ubuntu 22.04
hosts: all
become: yes
tasks:
- name: Mettre à jour tous les paquets
apt:
update_cache: yes
upgrade: full
- name: Installer les outils nécessaires
apt:
name:
- ufw
- fail2ban
- unattended-upgrades
- curl
state: present
- name: Installer UFW
ansible.builtin.apt:
name: ufw
state: present
update_cache: yes
- name: Par défaut, refuser toutes les connexions entrantes
ansible.builtin.shell: ufw default deny incoming
- name: Par défaut, autoriser toutes les connexions sortantes
ansible.builtin.shell: ufw default allow outgoing
- name: Autoriser les connexions SSH
ansible.builtin.shell: ufw allow OpenSSH
- name: Autoriser les connexions HTTP et HTTPS
ansible.builtin.shell:
cmd: >
ufw allow 80/tcp &&
ufw allow 443/tcp
- name: Activer UFW avec force
ansible.builtin.shell: ufw --force enable
- name: Configurer Fail2ban
copy:
dest: /etc/fail2ban/jail.local
content: |
[DEFAULT]
bantime = 10m
findtime = 10m
maxretry = 5
destemail = root@localhost
action = %(action_mwl)s
[sshd]
enabled = true
port = ssh
- name: Redémarrer Fail2ban pour appliquer la configuration
service:
name: fail2ban
state: restarted
- name: Empecher la connexion par login/password en SSH
copy:
dest: /etc/ssh/sshd_config
content: |
PasswordAuthentication no
- name: Redémarrer le service SSH
service:
name: ssh
state: restarted
- Créons un fichier
hosts.yml
à la racine de notre projet :
Ici, c’est un exemple spécifique à Hidora, vous devrez adapter en fonction de votre fournisseur de services cloud.
all:
hosts:
hidora:
ansible_host: gate.hidora.com
ansible_user: votre-user
ansible_ssh_private_key_file: ~/.ssh/id_rsa
ansible_port: 3022
- Exécutons notre playbook Ansible :
ansible-playbook -i hosts.yml playbook.yml
Déclaration DNS
Pour cette première démonstration, nous allons utiliser un premier nom de domaine pour notre application Symfony.
demo1.votre-domaine.com. 3600 IN A XX.XX.XX.XX
Installation de Kamal
Pour installer Kamal, il existe plusieurs méthodes. La plus simple (je trouve) est créer un alias vers la version docker de Kamal, je vous laisse consulter la documentation officielle pour plus d’informations : Installation de Kamal.
Initialisation de Kamal dans notre projet Symfony
Pour initialiser Kamal dans notre projet Symfony, il suffit de lancer la commande suivante :
kamal init
Cette commande va créer un fichier config/deploy.yaml
dans notre projet Symfony, qui contient la configuration de déploiement de notre application, nous allons le personnaliser pour notre cas d’utilisation.
Modifions le fichier config/deploy.yaml
pour une configuration simple et minimaliste :
# Name of your application. Used to uniquely configure containers.
service: jours-feries
# Name of the container image.
image: votre-user/jours-feries
# Deploy to these servers.
servers:
web:
- gate.hidora.com
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
#
# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
proxy:
ssl: true
host: demo1.votre-domaine.com
# Credentials for your image host.
registry:
username: votre-user
password:
- KAMAL_REGISTRY_PASSWORD
# Configure builder setup.
builder:
arch: amd64
context: .
# Use a different ssh user than root
#
ssh:
user: votre-user
port: 3022
Quelques explications sur ce fichier de configuration :
service
: Nom de votre application, utilisé pour configurer les conteneurs.image
: Nom de l’image Docker de votre application.servers
: Liste des serveurs sur lesquels déployer votre application.proxy
: Configuration du proxy pour gérer le SSL.registry
: Informations d’authentification pour votre registre Docker. (ici, nous utilisons le registre public Docker Hub)builder
: Configuration du builder pour construire l’image Docker.ssh
: Configuration de l’utilisateur SSH et du port.
Configurer notre serveur avec Kamal
Avant de lancer les commandes Kamal, nous devons configurer une variable d’environnement avec notre token Docker Hub (en lecture/écriture), n’hésitez pas à consulter la documentation officielle pour plus d’informations : Token Docker Hub.
export KAMAL_REGISTRY_PASSWORD="dckr_********************"
Pour configurer notre serveur avec Kamal, il suffit de lancer la commande suivante :
kamal setup
Cette commande va installer Docker sur notre serveur, déployer notre application Symfony et configurer le proxy pour gérer le SSL (et diriger le trafic vers notre application).
À ce stade, notre application Symfony est déployée et accessible à l’adresse https://demo1.votre-domaine.com
. Magique, non ?
Déployer une nouvelle version de notre application
Pour déployer une nouvelle version de notre application, il suffit de lancer la commande suivante :
kamal deploy
Cette commande va construire une nouvelle image Docker de notre application, la pousser sur Docker Hub, démarrer un nouveau conteneur avec cette image, et rediriger le trafic vers ce nouveau conteneur, puis supprimer l’ancien conteneur, le tout sans interruption de service.
Un déploiement robuste avec Kamal sur plusieurs hôtes et un Load Balancer
Pour notre seconde démonstration, nous allons déployer notre application Symfony sur plusieurs serveurs, et mettre en place un équilibrage de charge pour répartir la charge entre ces serveurs.
Cette solution est idéale pour les applications en production, qui nécessitent une haute disponibilité et une meilleure tolérance aux pannes.
Commande de nos VPS chez Scaleway
Pour notre seconde démonstration, nous allons commander deux VPS (instance
) chez Scaleway, un fournisseur de services cloud français.
Perso, je prends deux VM Ubuntu 22.04 DEV1-MCost-Optimized- 3 - 4 GB Mémoire
et une adresse IP V4 chacune (au passage, j’y ajoute ma clé SSH).
Appliquons nos règles de sécurité avec Ansible
Pour sécuriser nos serveurs, nous allons utiliser le même playbook Ansible que précédemment, en adaptant les adresses IP et les noms de domaine.
Modifions le fichier hosts.yml
pour ajouter nos nouveaux serveurs (Oui, c’est mal, on ne devrait pas utiliser root
, mais c’est pour la démo) :
all:
hosts:
back1:
ansible_host: XX.XX.XX.XX
ansible_user: root
ansible_ssh_private_key_file: ~/.ssh/id_rsa
ansible_port: 22
back2:
ansible_host: XX.XX.XX.X
ansible_user: root
ansible_ssh_private_key_file: ~/.ssh/id_rsa
ansible_port: 22
Exécutons notre playbook Ansible :
ansible-playbook -i hosts.yml playbook.yml
Configuration de Kamal pour un déploiement sur plusieurs hôtes
Nous allons modifier notre fichier config/deploy.yaml
pour déployer notre application Symfony sur plusieurs serveurs, et demander à Kamal de ne pas s’occuper du SSL et du proxy, car nous allons mettre en place un Load Balancer pour gérer cela.
# Name of your application. Used to uniquely configure containers.
service: jours-feries
# Name of the container image.
image: votre-user/jours-feries
# Deploy to these servers.
servers:
web:
- xx.xx.xx.xx # Adresse IP du premier serveur
- xx.xx.xx.xx # Adresse IP du second serveur
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
#
# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
proxy:
app_port: 80
# Credentials for your image host.
registry:
username: votre-user
password:
- KAMAL_REGISTRY_PASSWORD
# Configure builder setup.
builder:
arch: amd64
context: .
# Use a different ssh user than root
#
ssh:
user: root
port: 22
Il suffit de lancer la commande kamal setup
pour déployer notre application Symfony sur nos deux serveurs.
(On peut d’ailleurs vérifier que notre application est bien déployée sur nos deux serveurs en se rendant sur les adresses IP de ces derniers.)
Mise en place d’un Load Balancer
Dans la section Network > Load Balancer
de notre console Scaleway, nous allons créer un nouveau Load Balancer pour répartir la charge entre nos deux serveurs.
- Ajouter vos deux backends dans la section
Backends
:- Protocole : HTTP
- Port : 80
- Liste des serveurs : les deux adresses IP de vos serveurs
- Méthode de répartition : Round Robin
- Health Check : HTTP,
/up
, 200, port 80
Récupérez l’adresse IP de votre Load Balancer, et configurez votre DNS pour pointer vers cette adresse IP.
demo.votre-domaine.com. 3600 IN A XX.XX.XX.XX
- Dans la section
Certificats SSL
:- Type : Let’s Encrypt
- Nom : demo.votre-domaine.com
- Dans la section
Frontends
:- Un frontend HTTP sur le port 80, sans certificat SSL, et avec notre backend configuré.
- Un frontend HTTPS sur le port 443, avec notre certificat SSL, et avec notre backend configuré.
Vous pouvez maintenant accéder à votre application Symfony via l’adresse https://demo.votre-domaine.com
, et vérifier que le Load Balancer répartit bien la charge entre vos deux serveurs. Yeah !
Améliorations possibles
Pour aller plus loin, il y aurait plusieurs améliorations possibles :
- Vous pourriez mettre en place des règles pour n’autoriser que le trafic provenant du Load Balancer sur vos serveurs
- Déployer vos nouvelles versions depuis un pipeline CI/CD
- Etc.
Conclusion
Nous venons de voir deux cas d’utilisation de Kamal pour déployer une application Symfony en production :
-
Pour les petites applications ou les projets sans contraintes de haute disponibilité, Kamal permet de déployer rapidement et simplement votre application sur un seul serveur, en s’occupant de tout pour vous : de l’installation de Docker à la configuration du proxy, à la mise en place du SSL et à la gestion des mises à jour sans interruption de service.
-
Pour les applications en production nécessitant une haute disponibilité et une meilleure tolérance aux pannes, Kamal permet de déployer votre application sur plusieurs serveurs, et de mettre en place un Load Balancer externe pour répartir la charge entre ces serveurs, le tout sans interruption de service également.
Kamal se veut une solution simple et efficace pour déployer vos applications en production, sans vous compliquer la vie et sans devoire soit dépendre d’un écosystème coûteux, soit devoir mettre en place des solutions complexes comme Kubernetes (qui reste une excellente solution pour les applications nécessitant une grande échelle ET avec une équipe formée pour cela).
Loading comments...