Mettre en ligne une application Symfony sur un VPS, étape par étape
Par Mendel · 01/10/2026 à 00:32
Développer une application Symfony en local, c'est confortable : symfony serve, une base de données, et tout fonctionne. Mais le jour de la mise en ligne, beaucoup de questions arrivent d'un coup : quel serveur web ? Quelles permissions ? Comment faire tourner les workers ? Comment mettre à jour sans tout casser ? Voici, étape par étape, comment mettre une application Symfony en production sur un simple VPS sous Ubuntu.
La stack choisie
- Ubuntu 26.04 LTS, une version à support long ;
- Nginx comme serveur web ;
- PHP 8.5 avec PHP-FPM, directement fourni par Ubuntu ;
- MySQL 8.4 pour la base de données ;
- Supervisor pour les processus qui tournent en permanence ;
- Git pour récupérer le code, et un script pour automatiser les mises à jour.
Pas de Docker, pas de plateforme spécialisée : l'objectif est de comprendre chaque brique.
Avant de commencer : où taper les commandes ?
Tout au long de cet article, deux endroits différents entrent en jeu :
- Sur votre poste : le terminal de votre ordinateur, dans le dossier de votre projet ;
- Sur le serveur : un terminal connecté au VPS en SSH, avec
ssh deploy@ADRESSE_DU_SERVEUR.
Chaque bloc de code indique où il s'exécute. Pour modifier un fichier sur le serveur, on utilise l'éditeur nano, installé par défaut sur Ubuntu :
nano chemin/du/fichier
Une fois le texte saisi ou collé, Ctrl+O puis Entrée pour enregistrer, et Ctrl+X pour quitter. Pour un fichier système (dans /etc/), il faut les droits administrateur : on ouvre alors le fichier avec sudo nano.
Dans les exemples, remplacez mon-app par le nom de votre application, et ADRESSE_DU_SERVEUR par l'adresse IP ou le nom de domaine de votre VPS.
1. Sécuriser le serveur
On évite de travailler en root. Sur le serveur, connecté en root pour la première et dernière fois, on crée un utilisateur dédié :
adduser deploy
usermod -aG sudo deploy
Sur votre poste, on envoie sa clé SSH au serveur, pour se connecter sans mot de passe :
ssh-copy-id deploy@ADRESSE_DU_SERVEUR
Si la commande indique qu'aucune clé n'existe, créez-en une d'abord avec ssh-keygen -t ed25519.
Puis sur le serveur, connecté cette fois en deploy, on active le pare-feu :
sudo ufw allow OpenSSH
sudo ufw enable
Un piège classique : autoriser SSH avant d'activer le pare-feu, sinon on se coupe soi-même l'accès au serveur.
2. Installer PHP, Nginx et MySQL
Sur le serveur :
sudo apt update
sudo apt install -y php8.5-fpm php8.5-cli php8.5-mysql php8.5-intl \
php8.5-mbstring php8.5-xml php8.5-curl php8.5-zip
sudo apt install -y nginx mysql-server git unzip acl
sudo ufw allow 'Nginx Full'
Composer s'installe avec son installateur officiel, toujours sur le serveur :
curl -sS https://getcomposer.org/installer -o composer-setup.php
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm composer-setup.php
Côté MySQL, on crée un utilisateur dédié, qui n'a accès qu'à la base de l'application. Sur le serveur, ouvrez la console MySQL avec sudo mysql, puis tapez :
CREATE DATABASE mon_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'mon_app'@'localhost' IDENTIFIED BY 'un-mot-de-passe-solide';
GRANT ALL PRIVILEGES ON mon_app.* TO 'mon_app'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Si l'application était un jour compromise, l'attaquant n'aurait accès qu'à cette base, jamais aux autres.
3. Récupérer le code
Le serveur récupère le code depuis votre dépôt Git. Pour un dépôt privé, on utilise une deploy key : une clé SSH qui donne accès en lecture seule à ce seul dépôt. Sur le serveur :
ssh-keygen -t ed25519 -C "deploy@mon-app"
cat ~/.ssh/id_ed25519.pub
Copiez la clé affichée, et ajoutez-la dans les paramètres de votre dépôt (sur GitHub : Settings → Deploy keys), sans cocher l'accès en écriture. Puis, toujours sur le serveur :
sudo mkdir -p /var/www/mon-app
sudo chown deploy:deploy /var/www/mon-app
git clone git@github.com:mon-compte/mon-app.git /var/www/mon-app
4. Configurer la production
Les secrets ne sont jamais dans Git. Sur le serveur, générez d'abord une clé secrète :
php -r 'echo bin2hex(random_bytes(32)), PHP_EOL;'
Puis créez le fichier .env.local à la racine du projet :
nano /var/www/mon-app/.env.local
Et écrivez-y :
APP_ENV=prod
APP_DEBUG=0
APP_SECRET=la-cle-generee-juste-avant
DATABASE_URL="mysql://mon_app:un-mot-de-passe-solide@127.0.0.1:3306/mon_app?serverVersion=8.4.11&charset=utf8mb4"
APP_DEBUG=0 est essentiel : en mode debug, une erreur afficherait des détails techniques à n'importe quel visiteur. La valeur de serverVersion correspond à la version de MySQL installée : vous la trouverez avec mysql --version.
On installe ensuite les dépendances sans les outils de développement, et on applique les migrations. Sur le serveur, dans le dossier du projet :
cd /var/www/mon-app
composer install --no-dev --optimize-autoloader
php bin/console doctrine:migrations:migrate -n
Si votre application utilise AssetMapper, les assets doivent être compilés dans public/assets/, avec des noms versionnés pour le cache des navigateurs :
php bin/console importmap:install
php bin/console asset-map:compile
5. Les permissions
Nginx et PHP-FPM tournent avec l'utilisateur système www-data. Il doit pouvoir écrire dans var/ (cache et logs), ainsi que dans le dossier des fichiers envoyés par les utilisateurs si votre application en a un (ici public/uploads). Les ACL permettent de donner les droits à www-data et à deploy, sans conflit. Sur le serveur, dans le dossier du projet :
mkdir -p public/uploads
sudo setfacl -dR -m u:www-data:rwX -m u:deploy:rwX var public/uploads
sudo setfacl -R -m u:www-data:rwX -m u:deploy:rwX var public/uploads
La première ligne concerne les fichiers qui seront créés plus tard, la seconde ceux qui existent déjà.
6. Nginx
Sur le serveur, créez le fichier de configuration du site :
sudo nano /etc/nginx/sites-available/mon-app
Et écrivez-y cette configuration, basée sur celle recommandée par la documentation de Symfony :
server {
listen 80;
server_name mon-domaine.fr;
root /var/www/mon-app/public;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
internal;
}
location ~ \.php$ {
return 404;
}
error_log /var/log/nginx/mon-app_error.log;
access_log /var/log/nginx/mon-app_access.log;
}
Deux règles de sécurité importantes :
- seul le dossier
public/est exposé : le code, le.env.localet les mots de passe restent hors d'atteinte ; - seul
index.phpest exécuté : si quelqu'un parvenait à déposer un fichier.phpdans le dossier des uploads, Nginx refuserait de l'exécuter.
Il reste à activer le site, désactiver la page par défaut de Nginx, vérifier la configuration et recharger :
sudo ln -s /etc/nginx/sites-available/mon-app /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
nginx -t doit afficher syntax is ok : sinon, il indique la ligne en erreur, et Nginx continue de tourner avec l'ancienne configuration.
7. Deux réglages PHP à ne pas oublier
PHP a deux fichiers de configuration distincts : un pour le serveur web, un pour la ligne de commande. Sur le serveur, ouvrez le premier :
sudo nano /etc/php/8.5/fpm/php.ini
Cherchez la ligne avec Ctrl+W, retirez le ; au début si elle est commentée, et indiquez votre fuseau horaire :
date.timezone = Europe/Paris
Faites de même dans /etc/php/8.5/cli/php.ini, puis redémarrez PHP-FPM pour appliquer :
sudo systemctl restart php8.5-fpm
Sans fuseau horaire, les dates enregistrées et les tâches planifiées peuvent avoir plusieurs heures de décalage. Et si l'application accepte des envois de fichiers, pensez aussi à upload_max_filesize dans le fichier de PHP-FPM : sa valeur par défaut de 2 Mo bloque les fichiers avant même la validation de Symfony, avec un message d'erreur peu parlant.
8. Les workers avec Supervisor
De nombreuses applications Symfony utilisent le composant Messenger pour traiter des tâches en arrière-plan : envoi d'e-mails, génération de PDF, redimensionnement d'images, appels à des API externes… Ces messages sont placés dans une file d'attente, puis traités par un worker : la commande messenger:consume, qui doit tourner en permanence.
En développement, on la lance à la main dans un terminal. En production, il faut qu'elle démarre avec le serveur et redémarre si elle s'arrête : c'est le rôle de Supervisor. Sur le serveur :
sudo apt install -y supervisor
Chaque processus à surveiller se décrit dans un fichier du dossier /etc/supervisor/conf.d/. Créez celui du worker :
sudo nano /etc/supervisor/conf.d/mon-app-worker.conf
Et écrivez-y :
[program:mon-app-worker]
command=php /var/www/mon-app/bin/console messenger:consume async --time-limit=3600 --memory-limit=128M
user=www-data
numprocs=2
process_name=%(program_name)s_%(process_num)02d
autostart=true
autorestart=true
startsecs=0
redirect_stderr=true
stdout_logfile=/var/log/mon-app-worker.log
Chaque ligne a son rôle :
asyncest le nom du transport configuré dansconfig/packages/messenger.yamlde votre projet. Adaptez-le si le vôtre porte un autre nom ;user=www-data: le worker tourne avec le même utilisateur que le site, il a donc les mêmes droits sur les fichiers ;numprocs=2lance deux workers en parallèle, pour traiter deux messages en même temps.process_nameleur donne un nom distinct à chacun ;--time-limit=3600et--memory-limit=128M: le worker s'arrête proprement au bout d'une heure, ou s'il consomme trop de mémoire, etautorestartle relance aussitôt. C'est une bonne pratique pour tout processus PHP de longue durée, qui évite les fuites de mémoire ;stdout_logfileenregistre tout ce qu'affiche le worker, bien utile pour comprendre un problème.
Si votre application utilise aussi le composant Scheduler pour des tâches planifiées, créez un second programme dans un autre fichier, avec messenger:consume scheduler_default et surtout numprocs=1 : avec plusieurs processus, chaque tâche planifiée s'exécuterait plusieurs fois.
Demandez ensuite à Supervisor de lire la nouvelle configuration et de démarrer les workers :
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status
La dernière commande doit afficher vos deux workers avec l'état RUNNING.
9. Automatiser les mises à jour
Dernière étape : un script qui déploie une nouvelle version, dans le bon ordre. Sur votre poste, créez le fichier deploy.sh à la racine de votre projet, avec votre éditeur habituel :
#!/usr/bin/env bash
set -euo pipefail
cd /var/www/mon-app
echo "→ Récupération du code"
git pull --ff-only
echo "→ Dépendances"
composer install --no-dev --optimize-autoloader --no-interaction
echo "→ Base de données"
php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration
echo "→ Assets"
php bin/console importmap:install
php bin/console asset-map:compile
echo "→ Cache"
php bin/console cache:clear
echo "→ Redémarrage des workers"
php bin/console messenger:stop-workers
echo "✓ Déploiement terminé"
set -euo pipefailarrête le script à la première erreur : une migration qui échoue ne passe pas inaperçue ;git pull --ff-onlyrefuse de mettre à jour si le code du serveur a été modifié à la main. Toute modification doit passer par Git ;messenger:stop-workersdemande aux workers de s'arrêter proprement, une fois le message en cours terminé. Supervisor les relance aussitôt, avec le nouveau code. Sans cette étape, les workers continueraient d'exécuter l'ancienne version, qu'ils gardent en mémoire. Et comme elle passe par la console Symfony, elle ne nécessite aucun droit administrateur.
Si votre application n'utilise pas AssetMapper, supprimez les deux lignes de la partie « Assets ».
Toujours sur votre poste, rendez le script exécutable et enregistrez-le dans Git :
chmod +x deploy.sh
git add deploy.sh
git commit -m "Ajout du script de déploiement"
git push
Git conserve le droit d'exécution : le script sera directement utilisable sur le serveur. Sur le serveur, récupérez-le une première fois :
cd /var/www/mon-app
git pull
Désormais, chaque mise en ligne se fait depuis votre poste, en deux commandes :
git push
ssh deploy@ADRESSE_DU_SERVEUR /var/www/mon-app/deploy.sh
La seconde se connecte au serveur, lance le script, et affiche chaque étape dans votre terminal.
Et ensuite ?
Il reste une étape indispensable pour un vrai site en production : le HTTPS. Avec un nom de domaine pointant vers le serveur, Certbot obtient et renouvelle automatiquement un certificat gratuit Let's Encrypt, et adapte la configuration Nginx en une seule commande.
Au-delà, on peut ajouter des sauvegardes automatiques de la base, une surveillance du serveur, ou un déploiement sans interruption. Mais avec ces étapes, vous avez déjà une application en production sécurisée, maintenable, et que vous comprenez de bout en bout.
Et vous, où hébergez-vous vos applications Symfony ? VPS, PaaS, conteneurs ? Dites-le en commentaire !
Commentaires (0)
Aucun commentaire pour le moment.