Des onglets. Une barre d’adresse. Une page où cliquer.
Ouvrez le conteneur dans votre propre navigateur et vous êtes aux commandes. Onglets, précédent, suivant, recharger, une barre d’adresse où taper, et la page diffusée en direct pendant que vous interagissez avec elle — l’équivalent d’un partage d’écran sur le portable de quelqu’un.

- onglets
- des vrais — ouvrir, changer, fermer ; un onglet ouvert par votre script apparaît ici aussitôt
- vue en direct
- le screencast de Chrome lui-même, avec votre souris et votre clavier renvoyés
- devtools
- le panneau complet, ancré dans la même page, sur l’onglet précis qui pose problème
- dedans
- Chromium depuis apt — celui sous licence BSD, aucun binaire à part auquel faire confiance
à lire
Il n’y a aucune authentification devant tout cela. Quiconque atteint le port contrôle entièrement le navigateur, y compris les comptes sur lesquels il est connecté. C’est fait pour un réseau privé ou derrière quelque chose que vous placez devant — pas pour l’internet ouvert tel quel.
Chrome sait déjà le faire.
Pour regarder un navigateur headless, on passe d’habitude soit par un service de pool de sessions enroulé autour du Chrome qu’on a déjà, soit par un serveur X complet, plus un serveur VNC, plus un client web — pour diffuser un bureau virtuel entier et regarder une seule fenêtre.
Les deux marchent. Les deux demandent plus d’infrastructure que le problème n’en mérite. Chrome sait déjà diffuser ses propres pixels et recevoir les entrées en retour : c’est le DevTools Protocol. Ceci, c’est lui, branché directement : Chromium et un petit serveur Node, dans une seule image.
comment c’est branché
- screencast
Page.startScreencastfait envoyer par Chrome une image JPEG à chaque rafraîchissement ; le serveur les transmet par WebSocket à un canvas.- entrées
Input.dispatchMouseEvent/dispatchKeyEventfont le chemin inverse. L’aller-retour est assez rapide pour qu’on oublie que ce n’est pas son propre navigateur.- onglets
Target.getTargets,activateTarget,closeTarget— aucune bibliothèque d’automatisation, juste le socket déjà ouvert.- devtools
- Chrome inscrit l’hôte qu’il attend dans son propre frontend DevTools ; un petit proxy réécrit donc l’en-tête Host au passage. La même iframe marche sur localhost comme derrière trois proxys.

Deux mains sur le même volant.
/json/* et /devtools/* sont transmis tels quels : un script Puppeteer ou Playwright s’attache donc au navigateur exact qu’affiche la barre d’onglets. Un onglet ouvert par votre script apparaît aussitôt dans la barre ; un onglet ouvert à la main n’est qu’une entrée de plus dans browser.pages().
Vous pouvez donc passer la main en cours de session sans perdre cookies ni état : laissez le script se coincer, puis finissez le parcours à la main dans le même onglet.
- browserURL
- Ne le passez pas tel quel à
puppeteer.connect(). Le/json/versionde Chrome donne son adresse WebSocket interne —ws://127.0.0.1:9222/…— qui n’a de sens que dans le conteneur. Récupérez-la vous-même et remplacez l’hôte, comme ci-contre. - close vs disconnect
browser.close()envoieBrowser.closeet tue le processus Chrome — tous les onglets, y compris ceux ouverts dans l’interface. Vous empruntez ici le navigateur de quelqu’un d’autre :.disconnect()est presque toujours ce que vous voulez.
import puppeteer from "puppeteer-core";
const CHROME_URL = "http://localhost:3000"; // wherever the container is
// Chrome reports its *internal* ws address, so swap in the host you're using
const { webSocketDebuggerUrl } = await (await fetch(`${CHROME_URL}/json/version`)).json();
const browserWSEndpoint = `${CHROME_URL.replace(/^http/, "ws")}${new URL(webSocketDebuggerUrl).pathname}`;
const browser = await puppeteer.connect({ browserWSEndpoint });
const page = await browser.newPage();
await page.goto("https://example.com");
await browser.disconnect(); // detach — leave Chrome (and the container) runningservices:
browser:
image: ghcr.io/spuntodotnet/browser-remote:latest
ports:
- "3000:3000"Quelques variables d’environnement pour plus tard : une CA personnalisée pour qu’il fasse confiance à vos certificats mkcert locaux, un autre chemin de binaire Chrome, et une option anti-fingerprinting, désactivée par défaut. Tout est dans le README.
La fenêtre qu’un agent ne vous donne pas.
Un agent qui pilote un navigateur headless est une boîte noire : il dit que le clic n’a pas marché, et vous avez une stack trace. Lancez ceci à côté et il y a une vraie page, dans un vrai onglet, où le regarder travailler — et où cliquer vous-même quand il bloque : même session, mêmes cookies, sans redémarrer.
C’est d’ailleurs ainsi que ce site est testé : chaque capture qu’on y voit a été prise par un script pilotant ce navigateur, dans un espace de travail Spunto, et c’est ce même navigateur qu’un humain ouvre pour vérifier le résultat.