Alpage
Tous nos sites, en local, depuis la barre des menus.
Alpage fait tourner nos sites sur le Mac : chaque dossier devient une adresse en .test, en HTTPS, avec sa version de PHP et ses bases de données. Une app native, écrite dans Xcode, testée et livrée comme un produit.
Un site Laravel vit d'abord sur le Mac du développeur, bien avant sa mise en ligne. Pendant des années, cet environnement s'est assemblé à partir de plusieurs outils : un pour PHP, un pour les certificats, un autre pour les bases de données, chacun avec ses mises à jour et ses pannes.
En février 2026, nous avons écrit le nôtre. Alpage est une application macOS native en SwiftUI, accompagnée de la commande alpage pour le terminal. Elle fait tourner chaque jour tous nos projets, et se met à jour toute seule.
- Pour
- Développeurs PHP et Laravel
- Plateformes
- macOS
- Technologies
- Swift
- SwiftUI
- Swift Testing
- Caddy
- GitHub Actions
- Sparkle
Un dossier, une adresse, et ça tourne.

Un site par dossier
Alpage surveille les dossiers de projets. Un nouveau dossier devient aussitôt une adresse en .test, et dès que « laravel new » ou « composer create-project » a fini d'écrire, Alpage reconnaît le type de projet : Laravel, WordPress, PHP, site statique, application monopage, ou proxy vers le serveur de développement de Next.js ou Nuxt.
Chaque site répond aussi sur tous ses sous-domaines : une application multi-clients sert client.site.test en HTTPS sans rien configurer. Les certificats viennent d'une autorité locale, approuvée une seule fois par le système.


PHP, au complet
Alpage installe PHP de la 8.1 à la 8.6, cette dernière dès sa release candidate. Chaque site peut fixer sa propre version, et Node.js se gère de la même façon.
Les binaires statiques publics laissaient de côté des extensions dont Laravel a besoin, comme sodium ou intl. Nous compilons donc les nôtres, dans un dépôt dédié et par l'intégration continue : plus de quarante extensions, Redis et Imagick compris, et Xdebug pour le débogage et la couverture de code. Chaque téléchargement est vérifié par son empreinte avant d'être installé.




Les bases de données, et le partage
MySQL, MariaDB et PostgreSQL démarrent depuis la barre des menus, chacun sur son port. Dans le terminal, « alpage db » crée, exporte et importe une base quel que soit le moteur, et mysql, psql ou pg_dump visent d'office le bon port. Pour les explorer, il y a Tables.
Un site se partage sur le réseau local avec un QR code, pour l'ouvrir sur un téléphone. Pour une démo à distance, un tunnel le rend accessible depuis Internet, jusqu'à ce qu'on l'arrête.



Des services qui se relèvent
Un service qui tombe redémarre seul, à son rythme. Un pool PHP repart en quelques millisecondes ; une base de données attend plus longtemps entre deux essais, parce qu'insister sur des fichiers à moitié ouverts aggrave les choses. Un service resté stable assez longtemps retrouve ses essais, et celui qui refuse de démarrer affiche les dernières lignes de son journal au lieu de s'éteindre sans rien dire.
Le superviseur est une fonction pure : un état, un événement et une politique donnent un nouvel état et une action. Chaque règle se vérifie par un test, sans lancer un seul processus : une base de données attend plus longtemps qu'un pool PHP, le proxy a droit au plus d'essais, et aucun plafond ne passe sous le premier délai.

Et si vous tentiez de casser ce code ?
Sans droits administrateur
Depuis août 2026, rien dans Alpage ne tourne en administrateur. Le serveur web écoute sur des ports ordinaires, et une règle du pare-feu de macOS, limitée à la boucle locale, lui renvoie le trafic des ports 80 et 443.
Deux opérations seulement demandent le mot de passe, une fois : approuver le certificat local et diriger les adresses .test vers Alpage. « sudo alpage unelevate » défait tout, et les anciennes installations sont nettoyées au passage.

static func rules(httpFrom: Int, httpTo: Int, httpsFrom: Int, httpsTo: Int) -> String {
"""
rdr pass on lo0 inet proto tcp from any to any port = \(httpFrom) -> 127.0.0.1 port \(httpTo)
rdr pass on lo0 inet proto tcp from any to any port = \(httpsFrom) -> 127.0.0.1 port \(httpsTo)
"""
}Le terminal, et le docteur
« alpage php », « alpage composer » et « alpage laravel » lancent la version de PHP du dossier courant. « alpage secure --all » passe tous les sites en HTTPS en un seul rechargement, et le plugin Vite de Laravel y retrouve ses certificats comme avec Valet.
« alpage doctor » passe toute la machine en revue : chaque problème vient avec la commande exacte qui le règle, et « alpage doctor fix » applique seul les réparations qui ne demandent aucun mot de passe. Un contrôle qui ne peut pas conclure ne dit rien : un avertissement sur lequel on ne peut pas agir ne sert à rien.


Écrite dans Xcode
Alpage est une app SwiftUI. Une fenêtre et un menu dans la barre des menus partagent les mêmes gestionnaires d'état, en Swift moderne : @Observable, async/await, sans Combine.
La ligne de commande est un second produit, compilé avec Swift Package Manager. Elle reprend les modèles et les générateurs de configuration de l'app, si bien qu'un site sécurisé dans le terminal apparaît sécurisé dans la fenêtre.


Plus de six cents tests
Alpage touche au DNS, au pare-feu, aux certificats et aux bases de données de la machine. Ces décisions sont isolées dans des fonctions pures, que plus de six cents tests Swift Testing exercent un à un : le diagnostic du docteur, les règles du pare-feu, les manifestes d'empreintes, la reprise des services.
Chaque push lance la compilation de la ligne de commande, celle de l'app, puis toute la suite. C'est elle qui a révélé qu'un appel bloquant pouvait épuiser les threads de Swift Concurrency : la suite se figeait, et le docteur, qui lance ses contrôles en parallèle, courait le même risque chez l'utilisateur.



@Test("The delay doubles with each attempt")
func delayDoubles() {
let first = step(.starting(attempt: 1), .startTimedOut)
let second = step(.starting(attempt: 2), .startTimedOut)
#expect(first.action == .waitThenRetry(delay: .seconds(1)))
#expect(second.action == .waitThenRetry(delay: .seconds(2)))
}
@Test("A mismatching digest throws and deletes the artifact")
func mismatchDeletesArtifact() throws {
try withTempFile(contents: Data("abc".utf8)) { url in
#expect(throws: ChecksumError.self) {
try ChecksumVerifier.verify(fileAt: url, expected: .sha256(emptySha256))
}
#expect(!FileManager.default.fileExists(atPath: url.path))
}
}Une app maison, livrée comme un produit.
- Versions de PHP
- 8.1–8.6
- Versions de PHP
- Extensions compilées
- 40+
- Extensions compilées
- Tests automatisés
- 600+
- Tests automatisés
- Versions publiées depuis juillet
- 12
- Versions publiées depuis juillet
Du tag à la mise à jour
Une version se publie en poussant un tag. L'intégration continue construit l'app en Release, la signe avec le certificat Developer ID, la fait notariser par Apple et agrafe le ticket. L'archive est ensuite signée pour Sparkle, publiée, et le flux des mises à jour se régénère en gardant les versions précédentes.
Le code reste privé, les binaires vivent dans un dépôt public. Les Mac où Alpage est installée vérifient chaque jour s'il y a du nouveau : de la v1.1 à la v1.6, douze versions sont arrivées ainsi entre juillet et septembre 2026, sans réinstallation.

- name: Notarize and staple
run: |
ditto -c -k --keepParent "$RUNNER_TEMP/export/Alpage.app" "$RUNNER_TEMP/Alpage-notarize.zip"
xcrun notarytool submit "$RUNNER_TEMP/Alpage-notarize.zip" \
--key "$RUNNER_TEMP/AuthKey.p8" \
--key-id "$APPLE_API_KEY_ID" \
--issuer "$APPLE_API_ISSUER_ID" \
--wait
xcrun stapler staple "$RUNNER_TEMP/export/Alpage.app"
- name: Sign update and generate appcast
run: |
"$RUNNER_TEMP/sparkle/bin/generate_appcast" \
--ed-key-file "$RUNNER_TEMP/ed_private_key" \
--download-url-prefix "https://github.com/${RELEASES_REPO}/releases/download/${GITHUB_REF_NAME}/" \
releases/
