· 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.

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).

Back to Blog

Comments (0)

Loading comments...

Leave a Comment