Ce dont on part
Ce guide part d'une situation simple : un VPS Ubuntu tout neuf (22.04 ou 24.04), un nom de domaine acheté chez un registrar quelconque, et un projet Symfony prêt à être mis en ligne. L'objectif : arriver à https://mon-domaine.com qui répond, avec un certificat valide, une base de données, et des permissions de fichiers qui ne cassent pas au premier upload.
J'utilise Apache + PHP-FPM (mon setup habituel sur les VPS que j'administre), mais les étapes DNS, SSH, permissions et SSL sont identiques avec Nginx.

1. Pointer le nom de domaine vers le VPS

Avant même de toucher au serveur, chez le registrar du domaine (OVH, Gandi, Namecheap...), on crée un enregistrement DNS de type A qui pointe vers l'IP publique du VPS :
Type    Nom     Valeur              TTL
A       @       203.0.113.42        3600
A       www     203.0.113.42        3600
La propagation DNS peut prendre de quelques minutes à 24h. On vérifie qu'elle est effective avant de continuer :
dig +short mon-domaine.com
# doit renvoyer 203.0.113.42

2. Sécuriser l'accès SSH

On ne travaille jamais en root au quotidien. Première connexion avec l'utilisateur root fourni par l'hébergeur, création d'un utilisateur dédié avec les droits sudo :

adduser amar
usermod -aG sudo amar

# copier sa cle publique pour l'authentification sans mot de passe
rsync --archive --chown=amar:amar ~/.ssh /home/amar

Puis on durcit la config SSH pour interdire la connexion root et l'authentification par mot de passe :

# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
sudo systemctl restart sshd
# on garde la session root ouverte tant qu'on n'a pas confirme
# qu'on peut se reconnecter avec le nouvel utilisateur, dans un AUTRE terminal
ssh amar@mon-domaine.com

3. Mettre à jour le système et installer les paquets nécessaires

sudo apt update && sudo apt upgrade -y

# PHP 8.4 et les extensions dont Symfony a besoin
sudo apt install -y php8.4 php8.4-fpm php8.4-cli php8.4-common \
    php8.4-mysql php8.4-xml php8.4-curl php8.4-mbstring \
    php8.4-zip php8.4-intl php8.4-opcache php8.4-gd

# Apache, MySQL, Git, outils
sudo apt install -y apache2 mysql-server git unzip curl

# Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

On vérifie les versions installées avant d'aller plus loin :

php -v
composer --version
apache2 -v

4. Créer la structure de dossiers et les bonnes permissions

Chaque application vit dans son propre dossier sous /var/www, avec un utilisateur système dédié plutôt que le www-data générique — ça isole les applications les unes des autres si jamais l'une d'elles est compromise :
sudo mkdir -p /var/www/mon-projet
sudo adduser --system --group --no-create-home --shell /usr/sbin/nologin www-monprojet

# le code appartient a l'utilisateur dedie, le groupe www-data
# (celui du serveur web) doit pouvoir le lire
sudo chown -R www-monprojet:www-data /var/www/mon-projet

Le point qui casse le plus souvent une mise en prod Symfony : les dossiers var/cache et var/log doivent être inscriptibles par le processus PHP-FPM, sans pour autant ouvrir tout le projet en écriture :

cd /var/www/mon-projet
sudo chmod -R 755 .
sudo chmod -R 775 var/cache var/log public/uploads

# le bit setgid garde le groupe www-data sur les nouveaux fichiers/dossiers
# crees dans var/ et uploads/, meme si un autre utilisateur les cree
sudo chmod g+s var/cache var/log public/uploads

Si les permissions posent encore problème (erreur Unable to write in the "var/cache" directory), la solution la plus propre est setfacl plutôt que du chmod 777 — jamais 777 en prod :

sudo apt install -y acl
sudo setfacl -R -m u:www-monprojet:rwX -m u:www-data:rwX var/cache var/log public/uploads
sudo setfacl -dR -m u:www-monprojet:rwX -m u:www-data:rwX var/cache var/log public/uploads

5. Déployer le code


sudo -u www-monprojet git clone git@github.com:moi/mon-projet.git /var/www/mon-projet
cd /var/www/mon-projet

sudo -u www-monprojet composer install --no-dev --optimize-autoloader --no-interaction
Le fichier .env.local ne doit jamais être versionné — on le crée directement sur le serveur avec les vraies valeurs de prod :
sudo -u www-monprojet tee .env.local > /dev/null << 'EOF'
APP_ENV=prod
APP_SECRET=change_moi_avec_une_vraie_valeur_aleatoire
DATABASE_URL="mysql://mon_user:mon_mdp@127.0.0.1:3306/mon_projet?serverVersion=8.0"
EOF

sudo chmod 640 .env.local
sudo chown www-monprojet:www-monprojet .env.local
sudo -u www-monprojet php bin/console cache:clear --env=prod --no-debug
sudo -u www-monprojet php bin/console cache:warmup --env=prod

6. Créer la base de données

CREATE DATABASE mon_projet CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
CREATE USER 'mon_user'@'localhost' IDENTIFIED BY 'mon_mdp';
GRANT ALL PRIVILEGES ON mon_projet.* TO 'mon_user'@'localhost';
FLUSH PRIVILEGES;
sudo -u www-monprojet php bin/console doctrine:migrations:migrate --no-interaction

7. Un pool PHP-FPM dédié

Plutôt que de partager le pool www par défaut entre toutes les applications du VPS, un pool dédié isole les process PHP de chaque site et permet de régler finement la mémoire allouée :

; /etc/php/8.4/fpm/pool.d/mon-projet.conf
[mon-projet]
user = www-monprojet
group = www-data
listen = /run/php/mon-projet.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
sudo systemctl restart php8.4-fpm

8. Le virtual host Apache

<VirtualHost *:80>
    ServerName mon-domaine.com
    ServerAlias www.mon-domaine.com
    DocumentRoot /var/www/mon-projet/public

    <Directory /var/www/mon-projet/public>
        AllowOverride All
        Require all granted
        FallbackResource /index.php
    </Directory>

    <FilesMatch \.php$>
        SetHandler "proxy:unix:/run/php/mon-projet.sock|fcgi://localhost"
    </FilesMatch>

    ErrorLog ${APACHE_LOG_DIR}/mon-projet-error.log
    CustomLog ${APACHE_LOG_DIR}/mon-projet-access.log combined
</VirtualHost>
sudo a2enmod proxy_fcgi setenvif rewrite
sudo a2ensite mon-projet.conf
sudo apache2ctl configtest
sudo systemctl reload apache2

À ce stade, http://mon-domaine.com (en HTTP, sans le S) doit déjà afficher l'application.

9. Le certificat SSL avec Let's Encrypt

Certbot génère le certificat et modifie automatiquement le vhost Apache pour rediriger le HTTP vers le HTTPS :

sudo apt install -y certbot python3-certbot-apache

sudo certbot --apache -d mon-domaine.com -d www.mon-domaine.com
Certbot pose quelques questions (email de contact, acceptation des CGU, redirection forcée vers HTTPS — répondre oui). Le renouvellement est automatique via un timer systemd installé par le paquet, mais autant vérifier qu'il fonctionne :
sudo systemctl status certbot.timer
sudo certbot renew --dry-run
https://mon-domaine.com répond maintenant avec un certificat valide, et le vhost `:80` d'origine redirige automatiquement vers `:443`.

10. Un script de déploiement pour la suite

Pour ne pas refaire toutes ces étapes à la main à chaque mise à jour, un script de déploiement minimal, idempotent, à lancer depuis le dossier du projet :
#!/usr/bin/env bash
# deploy.sh — a lancer en tant qu'utilisateur www-monprojet
set -euo pipefail

cd /var/www/mon-projet

git pull origin main
composer install --no-dev --optimize-autoloader --no-interaction

php bin/console doctrine:migrations:migrate --no-interaction
php bin/console cache:clear --env=prod --no-debug
php bin/console cache:warmup --env=prod

echo "Déploiement terminé : $(date)"
chmod +x deploy.sh
sudo -u www-monprojet ./deploy.sh

Checklist finale

  • DNS : dig +short mon-domaine.com renvoie bien l'IP du VPS.
  • SSH : connexion possible uniquement par clé, avec un utilisateur non-root.
  • var/cache, var/log et les dossiers d'upload sont inscriptibles par PHP-FPM, sans chmod 777.
  • .env.local existe sur le serveur, n'est jamais versionné, et n'est lisible que par l'utilisateur applicatif.
  • Le pool PHP-FPM est dédié à l'application, pas partagé avec d'autres sites du VPS.
  • https://mon-domaine.com répond avec un certificat valide, et le HTTP redirige vers le HTTPS.
  • Un script de déploiement existe pour la prochaine mise à jour, pour ne pas refaire ces étapes à la main.

Ce que je retiens

  • Ne jamais utiliser chmod 777 pour résoudre un problème de permissions — setfacl ou un bon chown vers un utilisateur dédié règle le problème sans ouvrir le projet à tout le monde.
  • Un utilisateur système et un pool PHP-FPM par application isolent proprement chaque site, même sur un VPS qui en héberge plusieurs (voir mon article sur l'hébergement de 15+ applications sur un seul VPS).
  • Certbot avec le plugin Apache fait tout le travail (certificat + redirection HTTPS) en une seule commande — pas besoin de configurer ça à la main.
  • Toujours tester la connexion SSH avec le nouvel utilisateur dans un second terminal avant de fermer la session root, pour ne jamais se retrouver enfermé dehors.
  • Un script de déploiement, même basique, évite d'oublier une étape (migration, cache) trois mois plus tard quand on aura tout oublié des détails.