Brancher un proxy résidentiel dans un conteneur semble trivial jusqu’à ce que ça ne le soit plus. Vous posez HTTP_PROXY, lancez l’image, et les requêtes sortent toujours par l’IP de l’hôte. Ou le proxy marche mais vos health checks commencent à échouer parce que les appels aux services internes sont désormais tunnelisés par une sortie résidentielle dans un autre pays. Ou, pire de tout, vos identifiants finissent cuits dans une couche d’image.
Aucun de ces cas n’est exotique. Ce sont les quatre choses qui mordent systématiquement les équipes qui mettent des proxys dans des conteneurs : où la config s’applique, ce qui devrait la contourner, comment les secrets entrent, et comment l’identité se mappe aux conteneurs. Ce guide couvre chacune, avec de la config qui marche, pour les ingénieurs DevOps et plateforme.
La distinction qui explique l’essentiel de la confusion
Docker a deux concepts de proxy complètement séparés, et les confondre est la racine de la plupart des tickets « pourquoi mon proxy ne marche pas » :
- Proxy de build / du daemon — configuré dans
~/.docker/config.jsonou via--build-arg. Il gouverne le daemon Docker (tirer des images) et le processus de build (RUN apt-get install). Il n’a rien à voir avec le trafic d’exécution de votre application. - Proxy d’exécution — variables d’environnement dans le conteneur qui tourne. C’est ce que voit le code de votre application.
Si vous avez configuré un proxy dans ~/.docker/config.json en attendant que votre scraper Python l’utilise, c’est le bug. Ces réglages n’atteignent jamais l’environnement d’exécution du conteneur.
Et une couche de plus : même à l’exécution, HTTP_PROXY est une convention, pas une contrainte. Le noyau n’y route pas le trafic. Chaque bibliothèque choisit de l’honorer ou non :
| Client | Honore HTTP_PROXY/HTTPS_PROXY ? |
|---|---|
curl, wget | Oui |
Python requests, httpx | Oui (par défaut) |
Node fetch / undici | Non, nécessite un dispatcher/agent explicite |
Go net/http | Oui, via http.ProxyFromEnvironment (le transport par défaut) |
| Chromium / Playwright / Puppeteer | Non, nécessite un flag de lancement ou une option proxy |
La première question de débogage n’est donc jamais « la variable d’environnement est-elle posée ? » mais « ce client la lit-il ? ».
Proxy d’exécution : la configuration de base
Passez le proxy à l’exécution, pas au build, et référencez le gateway avec le ciblage encodé dans le nom d’utilisateur :
docker run --rm \ -e HTTP_PROXY="http://${SHIFTER_USER}-country-us:${SHIFTER_PASS}@p.shifter.io:443" \ -e HTTPS_PROXY="http://${SHIFTER_USER}-country-us:${SHIFTER_PASS}@p.shifter.io:443" \ -e NO_PROXY="localhost,127.0.0.1,postgres,redis,.internal,169.254.169.254" \ my-scraper:latestPosez les versions majuscules et minuscules (HTTP_PROXY et http_proxy) si vous n’êtes pas sûr de vos bibliothèques ; les conventions diffèrent, et certains outils ne lisent que l’une. Notez que HTTPS_PROXY pointe toujours vers une URL http://, c’est correct : le schéma décrit comment vous parlez au proxy, et HTTPS y est tunnelisé via CONNECT.
Dans Compose, gardez les valeurs hors du fichier lui-même :
services: scraper: image: my-scraper:latest environment: HTTP_PROXY: "http://${SHIFTER_USER}-country-us:${SHIFTER_PASS}@p.shifter.io:443" HTTPS_PROXY: "http://${SHIFTER_USER}-country-us:${SHIFTER_PASS}@p.shifter.io:443" NO_PROXY: "localhost,127.0.0.1,postgres,redis,.internal" depends_on: [postgres, redis]Les variables s’interpolent depuis votre shell ou un fichier .env qui n’est pas commité.
NO_PROXY : l’étape que les gens sautent, et regrettent
C’est celle qui cause les pannes déroutantes. Une fois HTTP_PROXY posé, chaque client qui l’honore envoie tout par le proxy, y compris les appels à votre base de données, votre cache, vos APIs internes, et les endpoints de métadonnées cloud. Conséquences : le trafic interne quitte votre réseau et revient (lent, et parfois cassé), les health checks échouent, et vous brûlez de la bande passante au Go sur du trafic qui n’aurait jamais dû quitter l’hôte.
Posez toujours NO_PROXY pour couvrir :
localhost,127.0.0.1,::1- Les noms de service Compose/Kubernetes (
postgres,redis,api.default.svc.cluster.local) - Les domaines internes et plages privées (
.internal,10.0.0.0/8) - Les métadonnées cloud :
169.254.169.254
Deux réserves à connaître : la correspondance de NO_PROXY n’est pas implémentée de façon cohérente entre bibliothèques (le support CIDR en particulier est inégal, et certaines matchent des suffixes quand d’autres exigent un point initial), vérifiez donc plutôt que de supposer. Et dans Kubernetes, .svc.cluster.local et vos CIDR de pods/services appartiennent aussi à NO_PROXY.
Secrets : ne cuisez pas les identifiants dans l’image
Les identifiants de proxy sont des secrets. Les règles :
Ne les mettez jamais dans un Dockerfile via ENV ou ARG, les deux persistent dans les couches d’image et docker history les imprimera volontiers. Quiconque peut tirer l’image a vos identifiants.
Injectez plutôt à l’exécution. Pour Compose, un fichier d’environnement gardé hors de git ; pour l’orchestration, un vrai coffre à secrets :
# Kubernetes : identifiants depuis un Secret, pas depuis le manifesteenv: - name: SHIFTER_USER valueFrom: secretKeyRef: { name: proxy-creds, key: username } - name: SHIFTER_PASS valueFrom: secretKeyRef: { name: proxy-creds, key: password }Construisez ensuite l’URL du proxy dans l’app à partir de ces deux variables, pour que la chaîne d’identifiants complète n’apparaisse jamais dans un manifeste, une ligne de log, ou docker inspect. Si vous avez besoin du proxy pendant le build (installer des paquets), utilisez les secrets BuildKit (--mount=type=secret) plutôt qu’ARG, pour que rien n’atterrisse dans une couche.
Aussi : nettoyez les URL de proxy dans les logs. Un dump de crash qui imprime la config effective fera fuiter user:pass@host droit dans votre agrégateur de logs.
Mapper l’identité aux conteneurs
C’est ici que l’architecture de conteneurs rencontre l’architecture proxy. Parce que le ciblage vit dans le nom d’utilisateur, chaque conteneur peut porter sa propre identité rien qu’en recevant une variable d’environnement différente, pas de points de terminaison séparés, pas de listes d’IP.
Deux motifs couvrent la plupart des besoins :
Un conteneur, une géo. Lancez des workers par marché en variant le flag pays :
docker run -d -e HTTP_PROXY="http://${U}-country-us:${P}@p.shifter.io:443" scraper:latestdocker run -d -e HTTP_PROXY="http://${U}-country-de:${P}@p.shifter.io:443" scraper:latestUn conteneur, une session sticky. Donnez à chaque réplique un sid distinct pour qu’elle garde sa propre IP de sortie, utile quand un conteneur possède un flux à plusieurs étapes :
services: worker: image: scraper:latest environment: # {{.Task.Slot}} donne à chaque réplique Swarm un id de session unique et stable HTTP_PROXY: "http://${U}-country-us-sid-w{{.Task.Slot}}-ttl-600:${P}@p.shifter.io:443" deploy: replicas: 4Une mise en garde : une variable d’environnement au niveau conteneur est une identité statique pour la durée de vie de ce conteneur. Si votre charge a besoin de rotation par requête ou par unité de travail, ne la simulez pas en redémarrant des conteneurs, posez le proxy dans le code applicatif où vous pouvez varier sid par job (le motif du post sur la répartition de charge). Les variables d’environnement sont le bon outil pour une identité grossière par conteneur ; le code est le bon outil pour une rotation fine.
Les clients qui ignorent l’environnement
Deux cas courants que vous rencontrerez dans des conteneurs :
Le fetch/undici de Node ne lit pas les variables d’environnement de proxy. Câblez-le explicitement :
import { ProxyAgent, setGlobalDispatcher } from "undici";setGlobalDispatcher(new ProxyAgent(process.env.HTTP_PROXY));Les navigateurs headless les ignorent aussi, Chromium a besoin de son proxy passé au lancement, et des identifiants gérés par le mécanisme propre du framework (Playwright couvre les spécificités, dont pourquoi les identifiants en ligne échouent dans Chromium). Les requests/httpx de Python honorent bien les variables d’environnement, même si passer les proxys explicitement est plus clair (guide Python).
À noter également : SOCKS5 via variables d’environnement n’est pas fiable selon les clients dans les conteneurs. Pour la plupart des charges conteneurisées, le proxy HTTP est le bon défaut (compromis SOCKS5).
Vérifiez-le depuis l’intérieur du conteneur
Ne supposez jamais, vérifiez l’IP de sortie depuis le conteneur en cours d’exécution :
docker exec -it my-scraper sh -c \ 'curl -s http://ip-api.com/json | head -c 200'# Attendez une IP résidentielle dans le pays ciblé, pas l'IP de votre hôte.Si elle renvoie l’IP de votre hôte, le client n’honore pas les variables d’environnement (voir le tableau plus haut). Ajoutez ceci comme assertion de démarrage en staging pour qu’un déploiement mal configuré échoue bruyamment au lieu de scraper silencieusement depuis votre IP de datacenter, et vérifiez que NO_PROXY fonctionne en confirmant qu’un appel à un service interne ne traverse pas le proxy. Des méthodes de mesure plus larges sont dans comment tester la vitesse, le taux de réussite et la précision de localisation d’un proxy.
Pièges de conteneurs qui coûtent un après-midi
localhostdésigne le conteneur. Un proxy sur l’hôte n’est pas joignable à127.0.0.1depuis l’intérieur ; utilisezhost.docker.internal(Docker Desktop) ou l’adresse réseau de l’hôte.- Les proxys de build et d’exécution sont différents. Poser l’un ne pose pas l’autre.
- Les changements d’env exigent un recreate. Modifier l’environment dans Compose exige
up --force-recreate, pas un restart. - Le lieu de la résolution DNS varie. Certains clients résolvent localement, d’autres laissent le proxy résoudre. Si des résultats géo-sensibles semblent faux, c’est un suspect.
- Les images peuvent manquer de certificats CA. Les bases slim/alpine ont parfois besoin de
ca-certificatesinstallé pour que HTTPS à travers le proxy valide. - La facturation au Go est par conteneur. Dix répliques tirant des pages complètes multiplient votre bande passante par dix, réduire les coûts de bande passante s’applique par réplique.
FAQ
Pourquoi mon conteneur n’utilise-t-il pas le proxy alors que HTTP_PROXY est posé ?
Parce que HTTP_PROXY est une convention, pas du routage. La bibliothèque doit l’honorer. Le fetch de Node et les navigateurs headless ne le font pas ; curl, requests de Python, et le transport par défaut de Go si. Vérifiez d’abord votre client, puis confirmez l’IP de sortie depuis le conteneur.
Devrais-je poser le proxy dans le Dockerfile ?
Non. Les valeurs ENV/ARG persistent dans les couches d’image et apparaissent dans docker history, faisant fuiter les identifiants vers quiconque peut tirer l’image. Injectez à l’exécution via des variables d’environnement issues d’un coffre à secrets, et utilisez les secrets BuildKit si vous avez besoin d’un proxy pendant le build.
Comment empêcher le trafic interne de passer par le proxy ?
Posez NO_PROXY avec localhost, vos noms de service, domaines internes, plages privées, et 169.254.169.254. Sinon le trafic de base de données et de health check est tunnelisé dehors par une sortie résidentielle, ce qui est lent, fragile, et facturable.
Chaque conteneur peut-il avoir une IP ou un pays différents ? Oui. Le ciblage est encodé dans le nom d’utilisateur du proxy, donc une variable d’environnement différente par conteneur donne à chacun son propre pays ou sa session sticky, sans points de terminaison supplémentaires. Pour une rotation par requête, posez le proxy dans le code applicatif.
Le proxy fonctionne-t-il pour docker build ?
Seulement si vous configurez séparément le proxy de build/daemon (~/.docker/config.json ou build args). Les variables d’environnement d’exécution du conteneur n’affectent pas les builds, et les identifiants de build ne devraient pas être passés en ARG.
En résumé
La plupart des problèmes de proxy sous Docker se ramènent à quatre choses. Sachez où la config s’applique (build vs exécution, et quels clients honorent seulement les variables d’environnement), posez NO_PROXY pour que le trafic interne reste interne, gardez les identifiants hors des couches d’image et injectez-les à l’exécution, et décidez délibérément comment l’identité se mappe aux conteneurs (variables d’environnement pour une identité grossière par conteneur, code applicatif pour la rotation par requête). Puis vérifiez l’IP de sortie depuis l’intérieur du conteneur plutôt que de faire confiance à la config.
Réussissez cela et la collecte conteneurisée devient ennuyeuse dans le bon sens. Le gateway résidentiel aide ici parce que le ciblage voyage dans le nom d’utilisateur : un point de terminaison, et la géo ou la session de n’importe quel conteneur n’est qu’une variable d’environnement différente. La qualité du pool détermine toujours à quelle fréquence vous réessayez tout court (réputation d’IP), et la page tarifs propose les forfaits au Go, en gardant à l’esprit que la bande passante scale avec votre nombre de répliques.