Choisissez votre confort de lecture :
← Retour au blog
Se connecter

Symfony 7 sur hébergeur mutualisé (FTP uniquement) : pattern installer cron-poll

18/05/2026 08:11 — par Miguel Monwoo
Symfony 7 sur hébergeur mutualisé (FTP uniquement) : pattern installer cron-poll

Photo by Safar Safarov on Unsplash

Déployer Symfony 7 en production sans SSH, sans Docker, sans Vercel — juste FTP et un fichier PHP qui se débrouille.

C'est le pattern qui fait tourner miguel.monwoo.com et info.monwoo.com depuis 12 mois chez LWS. Retour d'expérience complet, avec le système de reprise par tâche qui rend les déploiements robustes.


Le contexte : 70% des freelances FR sur hébergeur mutualisé

Quand on lit la doc Symfony officielle, on a l'impression que tout le monde déploie via Docker sur AWS, Vercel ou un VPS managé. La réalité du marché freelance français est différente : LWS, OVH, o2switch, PlanetHoster dominent — 20 à 50 € par an, pas par mois.

Mais mutualisé = pas de SSH, pas de Docker, pas de cron à la seconde près, pas de binaire exécutable, budget temps cron strict (240 s sur LWS).

Objectif : faire tourner Symfony 7 + SQLite + Webpack Encore en prod sur ce contexte. Voilà le pattern qui a stabilisé Monwoo après plusieurs itérations.

Vue d'ensemble : 1 fichier PHP fait tout le job

Un seul fichier installer.php uploadé à la racine, avec un token éphémère injecté au build. Visite de l'URL https://mon-site.fr/installer.php?token=... = l'installer prend la main, fait son travail, se self-delete à la fin.

4 modes de fonctionnement

ModeQuandAction
preview1ère visite adminAffiche checklist pré-deploy, boutons "Lancer"
launchClic adminDémarre cycle install, maintenance mode
cronCron LWS toutes les 4 minContinue le travail là où le précédent s'est arrêté
pollJS dans browserAffiche la progression en live

Pourquoi un cron, et pas tout en une fois ?

Sur LWS, une requête HTTP est tuée à 30-60 secondes par le reverse-proxy. Or un cycle complet de déploiement Symfony peut prendre 5 à 15 minutes. Solution : cron LWS exécute l'installer en CLI toutes les 4 minutes, budget 240 s par exécution. Si pas fini, arrêt propre à 200 s (marge 60 s) et reprise au prochain cron.

Le système de reprise par tâche (state file)

Un fichier state JSON track ce qui est déjà fait :

{
  "completed_phases": ["maintenance_enable", "backup", "extract", "restore"],
  "completed_commands": 1,
  "backup_dir": "/htdocs/_deploy-backups/2026-05-17-15-30"
}

Quand un nouveau cron démarre : lit le state, skip les phases OK, reprend la commande suivante, marque OK si succès.

Retour d'expérience : reprise intra-tâche (batch+resume)

Le système ci-dessus fonctionne pour les tâches qui tiennent dans 200 s. Mais quand une seule tâche dépasse (exemple : un fixture qui traite 134 000 lignes en base), le pattern doit aller plus loin : reprise intra-tâche.

Le 15 mai 2026, le fixture a dépassé le budget cron. Diagnostic : findAll() sur 134k entités + persist en boucle, sans $em->clear(). La mémoire gonfle, kill SIGTERM à 240 s.

Solution : pattern batch + clear + checkpoint resume :

$BATCH = 500;
$deadline = time() + 180;
$lastId = is_file($resumeFile) ? (int)file_get_contents($resumeFile) : 0;
while (time() < $deadline) {
    $rows = $em->createQueryBuilder()->select('c')->from(MyEntity::class, 'c')
        ->where('c.id > :id')->setParameter('id', $lastId)
        ->orderBy('c.id', 'ASC')->setMaxResults($BATCH)
        ->getQuery()->getResult();
    if (empty($rows)) { @unlink($resumeFile); return Command::SUCCESS; }
    foreach ($rows as $r) { /* process */ $lastId = $r->getId(); }
    $em->flush();
    $em->clear(); // CRITIQUE
    file_put_contents($resumeFile, (string)$lastId);
}
return Command::FAILURE; // installer retry au prochain cron

Résultat : 134 000 lignes traitées en 13 secondes au cron suivant. Les 3 ingrédients clés : WHERE c.id > :lastId (utilise PK index), $em->clear() (libère la mémoire), file_put_contents($resumeFile) (checkpoint qui survit aux kills).

Pitfalls LWS spécifiques

WAL SQLite + mix PDO/kernel

Si l'installer ouvre un PDO natif puis lance des commandes Doctrine, on peut avoir 2 connexions simultanées sur SQLite WAL = EntityManager flush bloque 60 s. Solution : $pdo = null; unset($pdo); avant le kernel Symfony.

Pas de binaire exécutable

LWS bloque l'exécution des binaires (chmod +x ne suffit pas). Solution : pré-compiler en local au build. Le ZIP embarque public/build/*, public/assets/manifest.json, public/bundles/*.

MultiViews pour clean URLs

mod_rewrite ne fonctionne pas fiablement sur LWS. Solution : Options -Indexes +MultiViews dans .htaccess — résout naturellement /cgu vers cgu.html.

Cache HTML browser stale

Après deploy, browsers servent l'ancienne version. Solution headers no-cache sur HTML/JSON :

<IfModule mod_headers.c>
  <FilesMatch "\.(html|json)$">
    Header set Cache-Control "no-cache, must-revalidate, max-age=0"
  </FilesMatch>
  <FilesMatch "_app/.*\.(js|css|woff2?)$">
    Header set Cache-Control "public, max-age=31536000, immutable"
  </FilesMatch>
</IfModule>

Avantages du pattern vs alternatives

CritèreVPS+AnsibleDocker+CIVercelInstaller cron-poll
Coût hébergement5-30€/mois20-100€/mois20-200€/mois2-5€/mois
Setup initial1-2 jours2-3 jours1h30 min
Maintenance OSOuiPartielleNonNon
Souveraineté FRVariableVariableUSAFR
Reprise crash autoManuelVariableAutoAuto (state file)

Pour aller plus loin

Code complet installer.php (~3000 lignes) + build.sh + patterns fixture batch+resume + WAL checkpoint + maintenance mode + configuration LWS étape par étape, en pack Partner Monwoo (99 €) : walkthrough vidéo screencast 45 min + démo deploy live + diagnostic crash recovery.

Accéder au pack Partner →

Cas tordus ?

Hébergeur mutualisé spécifique non couvert (OVH PERF, o2switch Cpanel, PlanetHoster N0C) ? service@monwoo.com — 30 minutes d'échange gratuit.


Les commentaires sont réservés aux abonnés. Se connecter pour voir et poster des commentaires.


Articles suggérés