Da cosa partiamo
Questa guida parte da una situazione semplice: un VPS Ubuntu appena installato (22.04 o 24.04), un nome di dominio acquistato presso un registrar qualsiasi e un progetto Symfony pronto per essere messo online. L'obiettivo: arrivare a https://mon-domaine.com che risponde, con un certificato valido, un database e permessi sui file che non si rompono al primo upload.
Uso Apache + PHP-FPM (la mia configurazione abituale sui VPS che amministro), ma i passaggi DNS, SSH, permessi e SSL sono identici con Nginx.

1. Puntare il nome di dominio verso il VPS

Prima ancora di toccare il server, presso il registrar del dominio (OVH, Gandi, Namecheap...) si crea un record DNS di tipo A che punta all'IP pubblico del VPS:
Type    Nom     Valeur              TTL
A       @       203.0.113.42        3600
A       www     203.0.113.42        3600
La propagazione DNS può richiedere da qualche minuto a 24 ore. Verifichiamo che sia effettiva prima di continuare:
dig +short mon-domaine.com
# doit renvoyer 203.0.113.42

2. Proteggere l'accesso SSH

Non si lavora mai come root quotidianamente. Prima connessione con l'utente root fornito dall'hosting provider, creazione di un utente dedicato con i diritti 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

Poi si rafforza la configurazione SSH per vietare la connessione root e l'autenticazione tramite password:

# /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. Aggiornare il sistema e installare i pacchetti necessari

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

Verifichiamo le versioni installate prima di andare avanti:

php -v
composer --version
apache2 -v

4. Creare la struttura di cartelle e i permessi corretti

Ogni applicazione vive nella propria cartella sotto /var/www, con un utente di sistema dedicato invece del generico www-data — questo isola le applicazioni l'una dall'altra se mai una di esse venisse compromessa:
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

Il punto che più spesso fa fallire una messa in produzione di Symfony: le cartelle var/cache e var/log devono essere scrivibili dal processo PHP-FPM, senza però aprire tutto il progetto in scrittura:

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

Se i permessi danno ancora problemi (errore Unable to write in the "var/cache" directory), la soluzione più pulita è setfacl invece di un chmod 777 — mai 777 in produzione:

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. Distribuire il codice


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
Il file .env.local non deve mai essere versionato — lo si crea direttamente sul server con i veri valori di produzione:
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. Creare il database

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 dedicato

Invece di condividere il pool www predefinito tra tutte le applicazioni del VPS, un pool dedicato isola i processi PHP di ogni sito e permette di regolare finemente la memoria allocata:

; /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. Il 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

A questo punto, http://mon-domaine.com (in HTTP, senza la S) deve già mostrare l'applicazione.

9. Il certificato SSL con Let's Encrypt

Certbot genera il certificato e modifica automaticamente il vhost Apache per reindirizzare l'HTTP verso l'HTTPS:

sudo apt install -y certbot python3-certbot-apache

sudo certbot --apache -d mon-domaine.com -d www.mon-domaine.com
Certbot pone alcune domande (email di contatto, accettazione dei termini di servizio, reindirizzamento forzato verso HTTPS — rispondere sì). Il rinnovo è automatico tramite un timer systemd installato dal pacchetto, ma vale la pena verificare che funzioni:
sudo systemctl status certbot.timer
sudo certbot renew --dry-run
https://mon-domaine.com ora risponde con un certificato valido, e il vhost `:80` originale reindirizza automaticamente verso `:443`.

10. Uno script di deploy per il futuro

Per non rifare tutti questi passaggi a mano a ogni aggiornamento, uno script di deploy minimale e idempotente, da lanciare dalla cartella del progetto:
#!/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 restituisce correttamente l'IP del VPS.
  • SSH: connessione possibile solo tramite chiave, con un utente non-root.
  • var/cache, var/log e le cartelle di upload sono scrivibili da PHP-FPM, senza chmod 777.
  • .env.local esiste sul server, non è mai versionato ed è leggibile solo dall'utente applicativo.
  • Il pool PHP-FPM è dedicato all'applicazione, non condiviso con altri siti del VPS.
  • https://mon-domaine.com risponde con un certificato valido, e l'HTTP reindirizza verso l'HTTPS.
  • Esiste uno script di deploy per il prossimo aggiornamento, per non rifare questi passaggi a mano.

Cosa mi porto a casa

  • Non usare mai chmod 777 per risolvere un problema di permessi — setfacl o un buon chown verso un utente dedicato risolvono il problema senza aprire il progetto a tutti.
  • Un utente di sistema e un pool PHP-FPM per applicazione isolano in modo pulito ogni sito, anche su un VPS che ne ospita diversi (vedi il mio articolo sull'hosting di 15+ applicazioni su un solo VPS).
  • Certbot con il plugin Apache fa tutto il lavoro (certificato + reindirizzamento HTTPS) con un solo comando — non serve configurare nulla a mano.
  • Testare sempre la connessione SSH con il nuovo utente in un secondo terminale prima di chiudere la sessione root, per non ritrovarsi mai chiusi fuori.
  • Uno script di deploy, anche basilare, evita di dimenticare un passaggio (migrazione, cache) tre mesi dopo, quando avremo dimenticato tutti i dettagli.