Conocimiento

Cómo configurar proxies residenciales en contenedores Docker

Configuración de proxy en Docker: variables de entorno vs proxy del daemon, por qué HTTP_PROXY no siempre aplica, NO_PROXY para tráfico interno, secretos, e identidad por contenedor.

Chris Collins

Chris Collins

18 de julio de 2026 · 11 min de lectura

Conectar un proxy residencial a un contenedor parece trivial hasta que no lo es. Pones HTTP_PROXY, corres la imagen, y las peticiones siguen saliendo por la IP del host. O el proxy funciona pero tus health checks empiezan a fallar porque las llamadas a servicios internos ahora se están tunelizando por una salida residencial en otro país. O, lo peor de todo, tus credenciales acaban horneadas en una capa de la imagen.

Ninguno de estos es exótico. Son las cuatro cosas que muerden de forma fiable a los equipos que meten proxies en contenedores: dónde aplica la configuración, qué debería saltársela, cómo entran los secretos, y cómo la identidad se mapea a los contenedores. Esta guía cubre cada una, con configuración que funciona, para ingenieros de DevOps y de plataforma.

La distinción que explica la mayor parte de la confusión

Docker tiene dos conceptos de proxy completamente separados, y confundirlos es la raíz de la mayoría de los tickets de “¿por qué no funciona mi proxy?”:

  • Proxy de build / del daemon — configurado en ~/.docker/config.json o vía --build-arg. Este gobierna el daemon de Docker (bajar imágenes) y el proceso de build (RUN apt-get install). No tiene nada que ver con el tráfico en runtime de tu aplicación.
  • Proxy de runtime — variables de entorno dentro del contenedor en ejecución. Esto es lo que ve el código de tu aplicación.

Si configuraste un proxy en ~/.docker/config.json y esperabas que tu scraper de Python lo usara, ese es el bug. Esos ajustes nunca llegan al entorno de runtime del contenedor.

Y una capa más: incluso en runtime, HTTP_PROXY es una convención, no una imposición. El kernel no enruta el tráfico por ahí. Cada librería elige si honrarla:

Cliente¿Honra HTTP_PROXY/HTTPS_PROXY?
curl, wget
Python requests, httpxSí (por defecto)
Node fetch / undiciNo, necesita un dispatcher/agent explícito
Go net/httpSí, vía http.ProxyFromEnvironment (el transport por defecto)
Chromium / Playwright / PuppeteerNo, necesita un flag de lanzamiento o una opción de proxy

Así que la primera pregunta de depuración nunca es “¿está puesta la variable de entorno?” sino “¿lee este cliente esa variable?”.

Proxy de runtime: la configuración básica

Pasa el proxy en tiempo de ejecución, no de build, y referencia el gateway con el targeting codificado en el nombre de usuario:

Terminal window
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:latest

Pon tanto mayúsculas como minúsculas (HTTP_PROXY y http_proxy) si no estás seguro de tus librerías; las convenciones difieren, y algunas herramientas solo leen una. Nota que HTTPS_PROXY sigue apuntando a una URL http://, eso es correcto: el esquema describe cómo hablas con el proxy, y HTTPS se tuneliza por él con CONNECT.

En Compose, mantén los valores fuera del propio archivo:

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]

Las variables se interpolan desde tu shell o desde un archivo .env que no se commitea.

NO_PROXY: el paso que la gente se salta, y luego lamenta

Este es el que causa los fallos confusos. Una vez que HTTP_PROXY está puesto, cada cliente que lo honre manda todo por el proxy, incluidas las llamadas a tu base de datos, tu caché, tus APIs internas, y los endpoints de metadatos de la nube. Consecuencias: el tráfico interno sale de tu red y vuelve (lento, y a veces roto), los health checks fallan, y quemas ancho de banda por-GB en tráfico que nunca debió salir del host.

Pon siempre NO_PROXY para cubrir:

  • localhost, 127.0.0.1, ::1
  • Nombres de servicio de Compose/Kubernetes (postgres, redis, api.default.svc.cluster.local)
  • Dominios internos y rangos privados (.internal, 10.0.0.0/8)
  • Metadatos de la nube: 169.254.169.254

Dos advertencias que vale la pena conocer: la coincidencia de NO_PROXY no está implementada de forma consistente entre librerías (el soporte de CIDR en particular es irregular, y algunas hacen match por sufijo mientras otras necesitan un punto inicial), así que verifica en lugar de asumir. Y en Kubernetes, .svc.cluster.local y tus CIDRs de pods/servicios también pertenecen a NO_PROXY.

Secretos: no hornees credenciales en la imagen

Las credenciales de proxy son secretos. Las reglas:

Nunca las pongas en un Dockerfile vía ENV o ARG, ambos persisten en las capas de la imagen y docker history las imprimirá encantado. Cualquiera que pueda bajar la imagen tiene tus credenciales.

Inyéctalas en runtime en su lugar. Para Compose, un archivo de entorno mantenido fuera de git; para orquestación, un almacén de secretos de verdad:

# Kubernetes: credenciales desde un Secret, no desde el manifiesto
env:
- name: SHIFTER_USER
valueFrom:
secretKeyRef: { name: proxy-creds, key: username }
- name: SHIFTER_PASS
valueFrom:
secretKeyRef: { name: proxy-creds, key: password }

Luego construye la URL del proxy dentro de la app a partir de esas dos variables, para que la cadena completa de credenciales nunca aparezca en un manifiesto, una línea de log, o docker inspect. Si necesitas el proxy durante el build (instalar paquetes), usa secretos de BuildKit (--mount=type=secret) en lugar de ARG, para que nada aterrice en una capa.

Además: limpia las URLs de proxy de los logs. Un volcado de fallo que imprima la configuración efectiva filtrará user:pass@host directo a tu agregador de logs.

Mapear identidad a contenedores

Aquí es donde la arquitectura de contenedores se encuentra con la arquitectura de proxy. Como el targeting vive en el nombre de usuario, cada contenedor puede llevar su propia identidad solo con recibir una variable de entorno distinta, sin endpoints separados, sin listas de IPs.

Dos patrones cubren la mayoría de las necesidades:

Un contenedor, una geo. Corre workers por mercado variando el flag de país:

Terminal window
docker run -d -e HTTP_PROXY="http://${U}-country-us:${P}@p.shifter.io:443" scraper:latest
docker run -d -e HTTP_PROXY="http://${U}-country-de:${P}@p.shifter.io:443" scraper:latest

Un contenedor, una sesión sticky. Dale a cada réplica un sid distinto para que mantenga su propia IP de salida, útil cuando un contenedor es dueño de un flujo de varios pasos:

services:
worker:
image: scraper:latest
environment:
# {{.Task.Slot}} da a cada réplica de Swarm un id de sesión único y estable
HTTP_PROXY: "http://${U}-country-us-sid-w{{.Task.Slot}}-ttl-600:${P}@p.shifter.io:443"
deploy:
replicas: 4

Una precaución: una variable de entorno a nivel de contenedor es una identidad estática durante la vida de ese contenedor. Si tu carga de trabajo necesita rotación por petición o por unidad de trabajo, no lo finjas reiniciando contenedores, pon el proxy en el código de la aplicación donde puedas variar el sid por job (el patrón del post de balanceo de carga). Las variables de entorno son la herramienta correcta para identidad gruesa por contenedor; el código es la herramienta correcta para rotación fina.

Clientes que ignoran el entorno

Dos casos comunes que encontrarás dentro de contenedores:

El fetch/undici de Node no lee las variables de entorno de proxy. Cablealo explícitamente:

import { ProxyAgent, setGlobalDispatcher } from "undici";
setGlobalDispatcher(new ProxyAgent(process.env.HTTP_PROXY));

Los navegadores headless también las ignoran, Chromium necesita su proxy pasado en el lanzamiento, y las credenciales manejadas por el mecanismo propio del framework (Playwright cubre las particularidades, incluido por qué las credenciales en línea fallan en Chromium). El requests/httpx de Python sí honra las variables de entorno, aunque pasar los proxies explícitamente es más claro (guía de Python).

También vale la pena notar: SOCKS5 vía variables de entorno no es fiable entre clientes en contenedores. Para la mayoría de las cargas de trabajo en contenedor, el proxy HTTP es el default correcto (tradeoffs de SOCKS5).

Verifícalo desde dentro del contenedor

Nunca asumas, comprueba la IP de salida desde dentro del contenedor en ejecución:

Terminal window
docker exec -it my-scraper sh -c \
'curl -s http://ip-api.com/json | head -c 200'
# Espera una IP residencial en el país objetivo, no la IP de tu host.

Si devuelve la IP de tu host, el cliente no está honrando las variables de entorno (ve la tabla de arriba). Añade esto como una aserción de arranque en staging para que un deploy mal configurado falle ruidosamente en lugar de scrapear en silencio desde tu IP de datacenter, y verifica que NO_PROXY funciona confirmando que una llamada a un servicio interno no atraviesa el proxy. Métodos de medición más amplios están en cómo probar la velocidad, tasa de éxito, y precisión de ubicación de un proxy.

Trampas de contenedores que gastan una tarde

  • localhost significa el contenedor. Un proxy en el host no es alcanzable en 127.0.0.1 desde dentro; usa host.docker.internal (Docker Desktop) o la dirección de red del host.
  • Los proxies de build y de runtime son distintos. Poner uno no pone el otro.
  • Los cambios de entorno necesitan recrear. Editar el environment en Compose requiere up --force-recreate, no un restart.
  • La ubicación de la resolución DNS varía. Algunos clientes resuelven localmente, otros dejan que resuelva el proxy. Si los resultados sensibles a geo salen mal, ese es un sospechoso.
  • Las imágenes pueden carecer de certificados CA. Las bases slim/alpine a veces necesitan ca-certificates instalado para que HTTPS a través del proxy valide.
  • La facturación por GB es por contenedor. Diez réplicas trayendo páginas completas multiplican tu ancho de banda por diez, recortar los costes de ancho de banda aplica por réplica.

Preguntas frecuentes

¿Por qué mi contenedor no usa el proxy aunque HTTP_PROXY esté puesto? Porque HTTP_PROXY es una convención, no enrutamiento. La librería tiene que honrarla. El fetch de Node y los navegadores headless no lo hacen; curl, requests de Python, y el transport por defecto de Go sí. Comprueba tu cliente primero, luego confirma la IP de salida desde dentro del contenedor.

¿Debería poner el proxy en el Dockerfile? No. Los valores de ENV/ARG persisten en las capas de la imagen y aparecen en docker history, filtrando credenciales a cualquiera que pueda bajar la imagen. Inyéctalos en runtime vía variables de entorno desde un almacén de secretos, y usa secretos de BuildKit si necesitas un proxy durante el build.

¿Cómo evito que el tráfico interno vaya por el proxy? Pon NO_PROXY con localhost, tus nombres de servicio, dominios internos, rangos privados, y 169.254.169.254. Si no, el tráfico de base de datos y de health checks se tuneliza fuera por una salida residencial, lo cual es lento, frágil, y facturable.

¿Puede cada contenedor tener una IP o país distintos? Sí. El targeting está codificado en el nombre de usuario del proxy, así que una variable de entorno distinta por contenedor le da a cada uno su propio país o sesión sticky, sin endpoints extra. Para rotación por petición, pon el proxy en el código de la aplicación.

¿Funciona el proxy para docker build? Solo si configuras el proxy de build/daemon por separado (~/.docker/config.json o build args). Las variables de entorno de runtime del contenedor no afectan a los builds, y las credenciales de build no deberían pasarse como ARG.

En resumen

La mayoría de los problemas de proxy en Docker se reducen a cuatro cosas. Sabe dónde aplica la configuración (build vs runtime, y qué clientes honran siquiera las variables de entorno), pon NO_PROXY para que el tráfico interno se quede interno, mantén las credenciales fuera de las capas de la imagen e inyéctalas en runtime, y decide deliberadamente cómo la identidad se mapea a los contenedores (variables de entorno para identidad gruesa por contenedor, código de aplicación para rotación por petición). Luego verifica la IP de salida desde dentro del contenedor en lugar de confiar en la configuración.

Acierta esas y la recolección en contenedores es aburrida en el mejor sentido. El gateway residencial ayuda aquí porque el targeting viaja en el nombre de usuario: un endpoint, y la geo o sesión de cualquier contenedor es solo una variable de entorno distinta. La calidad del pool sigue determinando con qué frecuencia estás reintentando siquiera (reputación de IP), y la página de precios tiene los planes por GB, vale la pena recordar que el ancho de banda escala con tu número de réplicas.

¿Listo para empezar?

Prueba los proxies residenciales de Shifter, más de 205M IPs, más de 195 países, desde 0,75 $/GB.

Comenzar