Переменные окружения HTTP_PROXY, HTTPS_PROXY и NO_PROXY выглядят предельно просто: задал переменную — весь трафик пошёл через прокси. На практике именно на этом месте разработчики теряют часы на отладку: прокси работает в терминале, но не работает в Docker-контейнере; curl ходит через прокси, а приложение на Python — нет; CI-раннер внезапно банит внутренний registry, потому что NO_PROXY указан неправильным синтаксисом. Разберём 7 реальных ловушек и покажем, как их обойти с конкретным кодом.
Как приложения читают переменные прокси
Важно понимать: HTTP_PROXY, HTTPS_PROXY и NO_PROXY — это не стандарт протокола HTTP, а конвенция, которую каждая библиотека реализует по-своему. Ядро Linux ничего не знает об этих переменных — их читает конкретное приложение или библиотека на этапе инициализации сетевого клиента.
Например, curl и утилиты на базе libcurl читают переменные при каждом запуске процесса. Python-библиотека requests читает их через модуль urllib.request.getproxies(), который сканирует окружение процесса на момент создания сессии. А вот встроенный fetch() в Node.js до версии 18 вообще не учитывает эти переменные без дополнительных пакетов.
Отсюда первое правило: переменная прокси работает только там, где её явно поддерживает конкретная библиотека. Проверка через curl не гарантирует, что ваше приложение на Go, Java или Node.js поведёт себя так же.
export HTTP_PROXY="http://user:[email protected]:8080"
export HTTPS_PROXY="http://user:[email protected]:8080"
export NO_PROXY="localhost,127.0.0.1,.internal.corp"
curl -v https://api.example.com/ping
# curl покажет в verbose-логе: "Trying proxy.example.com..."
Ловушка 1: регистр переменных и уязвимость httpoxy
Большинство консольных утилит в Linux читают переменные в нижнем регистре: http_proxy, https_proxy, no_proxy. Многие современные SDK ориентируются на верхний регистр. Разница кажется мелочью, но именно из-за неё половина багов в стиле "работает локально, не работает на сервере" случается в первый же день настройки.
Отдельная история — уязвимость httpoxy (CVE-2016-5385 и связанные). Суть в том, что CGI-окружение (PHP, Python WSGI, некоторые Ruby-фреймворки) автоматически превращает HTTP-заголовок Proxy из входящего запроса в переменную окружения HTTP_PROXY. Злоумышленник отправляет заголовок Proxy: http://attacker.com, и ваше серверное приложение начинает слать исходящие запросы через чужой прокси-сервер, что открывает путь к MITM-атаке и утечке данных.
Решение — всегда задавать переменные в обоих регистрах сразу и явно обнулять HTTP_PROXY на уровне веб-сервера для входящих запросов:
# Задаём в обоих регистрах для совместимости
export http_proxy="http://proxy.example.com:8080"
export HTTP_PROXY="http://proxy.example.com:8080"
export https_proxy="http://proxy.example.com:8080"
export HTTPS_PROXY="http://proxy.example.com:8080"
# В nginx — блокируем заголовок Proxy от клиента
proxy_set_header Proxy "";
Ловушка 2: синтаксис NO_PROXY различается между инструментами
NO_PROXY — это список исключений, но формат записи доменов и масок в нём не стандартизирован. Одни инструменты понимают .example.com как "любой поддомен", другие требуют *.example.com, третьи понимают только точное совпадение хоста без wildcard вообще.
Например, Go-библиотека net/http поддерживает CIDR-нотацию в NO_PROXY (10.0.0.0/8), а классический curl до определённых версий — нет. Это приводит к ситуации, когда один и тот же NO_PROXY корректно исключает внутренний registry для одного CI-шага и не исключает для другого, написанного на другом языке.
| Инструмент | Wildcard поддомена | CIDR-маска |
|---|---|---|
| curl / libcurl | через .example.com | нет |
| Go net/http | через .example.com | да |
| Python requests | через .example.com | нет |
| npm / Node.js | нужен *.example.com | нет |
Практический совет: перечисляйте в NO_PROXY и доменный вариант, и wildcard-вариант одновременно, чтобы покрыть разные парсеры:
export NO_PROXY="localhost,127.0.0.1,.internal.corp,*.internal.corp,10.0.0.0/8"
Ловушка 3: Docker build vs Docker run — разные переменные окружения
Это, пожалуй, самая частая ловушка в Docker. Переменные, заданные через docker run -e HTTP_PROXY=..., действуют только во время работы контейнера. Они НЕ попадают автоматически на этап сборки образа. Если ваш Dockerfile внутри себя вызывает apt-get install или pip install, и сборка идёт за корпоративным прокси, build просто зависнет на таймауте.
Для сборки нужно передавать build-args, которые Docker с версии 1.9+ понимает "из коробки" без объявления в Dockerfile:
docker build \
--build-arg HTTP_PROXY=http://proxy.example.com:8080 \
--build-arg HTTPS_PROXY=http://proxy.example.com:8080 \
--build-arg NO_PROXY=localhost,127.0.0.1 \
-t my-app:latest .
Если вы хотите, чтобы переменная осталась доступна и внутри запущенного контейнера, нужно явно продублировать её через ENV в Dockerfile — ARG и ENV в Docker принципиально разные механизмы, и ARG исчезает сразу после сборки слоя:
ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY
ENV HTTP_PROXY=${HTTP_PROXY}
ENV HTTPS_PROXY=${HTTPS_PROXY}
ENV NO_PROXY=${NO_PROXY}
RUN apt-get update && apt-get install -y curl
Ещё один нюанс: Docker daemon сам может использовать прокси для скачивания образов из registry — это настраивается не в Dockerfile, а в конфиге systemd-юнита /etc/systemd/system/docker.service.d/http-proxy.conf, что часто путают с переменными внутри контейнера.
Ловушка 4: docker-compose и наследование переменных между сервисами
В docker-compose переменные окружения хоста НЕ прокидываются в контейнеры автоматически, если вы явно не прописали их в секции environment или не используете файл .env. Частая ошибка — задать прокси в терминале перед запуском docker-compose up и ожидать, что все сервисы его подхватят.
version: "3.9"
services:
scraper:
build:
context: .
args:
HTTP_PROXY: "${HTTP_PROXY}"
HTTPS_PROXY: "${HTTPS_PROXY}"
environment:
- HTTP_PROXY=${HTTP_PROXY}
- HTTPS_PROXY=${HTTPS_PROXY}
- NO_PROXY=localhost,127.0.0.1,db,redis
depends_on:
- db
- redis
Обратите внимание на NO_PROXY: внутри docker-compose сети контейнеры обращаются друг к другу по именам сервисов (db, redis), а не по localhost. Если забыть добавить имена сервисов в исключения, весь внутренний трафик между контейнерами тоже уйдёт через внешний прокси, что увеличит задержки и может сломать подключение, если прокси не пропускает внутренние протоколы вроде PostgreSQL или Redis.
Ловушка 5: CI/CD раннеры и утечка секретов прокси
В GitHub Actions, GitLab CI и Jenkins переменные прокси часто задаются на уровне раннера или организации целиком, и это создаёт две проблемы. Во-первых, если URL прокси содержит логин и пароль в чистом виде, он попадает в лог сборки при любой команде, которая печатает окружение (env, printenv, отладочный вывод в скриптах). Во-вторых, самохостед-раннеры иногда наследуют системный прокси хоста, который не предназначен для трафика CI-джобы.
# .github/workflows/build.yml
jobs:
build:
runs-on: ubuntu-latest
env:
HTTP_PROXY: ${{ secrets.PROXY_URL }}
HTTPS_PROXY: ${{ secrets.PROXY_URL }}
NO_PROXY: "localhost,127.0.0.1,github.com"
steps:
- uses: actions/checkout@v4
- name: Test connection
run: curl -sS https://api.example.com/health
Правило безопасности: храните URL прокси только в секретах CI (GitHub Secrets, GitLab CI/CD Variables с флагом "masked"), никогда не выводите переменные окружения целиком в логах отладки, и ограничивайте NO_PROXY так, чтобы обращения к самой платформе CI (например, github.com, gitlab.com) не шли через прокси — иначе сборка может зависнуть, если прокси недоступен в момент пиковой нагрузки.
Ловушка 6: спецсимволы в логине и пароле прокси
Если пароль от прокси содержит символы @, :, / или #, URL-парсер просто ломает строку на части в неправильных местах. Классическая ошибка: пароль P@ssw0rd в строке http://user:P@[email protected]:8080 парсер читает так, будто хост — это "[email protected]", а не "proxy.com".
Решение — всегда URL-кодировать логин и пароль перед подстановкой в переменную:
# Python: кодируем пароль перед сборкой строки прокси
import urllib.parse
user = "login"
password = "P@ss:w0rd/1"
encoded_password = urllib.parse.quote(password, safe="")
proxy_url = f"http://{user}:{encoded_password}@proxy.example.com:8080"
import os
os.environ["HTTPS_PROXY"] = proxy_url
# Node.js: аналогичная сборка через encodeURIComponent
const user = "login";
const password = "P@ss:w0rd/1";
const encoded = encodeURIComponent(password);
const proxyUrl = `http://${user}:${encoded}@proxy.example.com:8080`;
process.env.HTTPS_PROXY = proxyUrl;
Ловушка 7: приложения, которые игнорируют переменные окружения
Не все HTTP-клиенты уважают системные переменные прокси. В Python библиотека requests читает их автоматически через requests.Session(), но если вы один раз явно передали словарь proxies={} в вызов, это переопределяет переменные окружения и трафик пойдёт напрямую, даже если HTTP_PROXY задан в системе.
import requests
# Пустой словарь proxies ПЕРЕБИВАЕТ переменные окружения
response = requests.get("https://api.example.com", proxies={})
# Правильно — явно передать прокси или не передавать proxies вообще
proxies = {
"http": "http://user:[email protected]:8080",
"https": "http://user:[email protected]:8080",
}
response = requests.get("https://api.example.com", proxies=proxies)
В Node.js встроенный fetch вообще не знает о HTTP_PROXY без дополнительных библиотек. Нужно явно подключать undici с ProxyAgent или классический https-proxy-agent:
const { ProxyAgent, setGlobalDispatcher } = require("undici");
const proxyUrl = process.env.HTTPS_PROXY;
if (proxyUrl) {
setGlobalDispatcher(new ProxyAgent(proxyUrl));
}
fetch("https://api.example.com/data")
.then(res => res.json())
.then(console.log);
Java тоже требует отдельных системных свойств вместо переменных окружения: -Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy.example.com -Dhttps.proxyPort=8080. Переменные окружения HTTP_PROXY в JVM по умолчанию не читаются вообще.
Как правильно тестировать прокси-переменные
Прежде чем тратить время на отладку приложения, проверьте саму переменную и сам прокси изолированно от кода. Это экономит часы.
# 1. Проверяем, что переменная реально установлена в текущей сессии
env | grep -i proxy
# 2. Проверяем работоспособность прокси напрямую через curl
curl -x "$HTTP_PROXY" -v https://api.ipify.org
# 3. Проверяем изнутри контейнера, что переменная дошла
docker exec -it my_container env | grep -i proxy
# 4. Проверяем, что NO_PROXY реально исключает нужный хост
curl -v --noproxy "internal.corp" https://service.internal.corp/health
Если задача — массовые запросы к внешним API, парсинг маркетплейсов или тестирование гео-таргетинга рекламы из CI-пайплайна, важно выбрать правильный тип прокси под нагрузку. Для стабильных высоконагруженных сценариев в CI обычно достаточно прокси дата-центров — они быстрее и дешевле резидентных при массовом трафике сборок и тестов. А если ваш пайплайн проверяет поведение сайта с реального пользовательского IP конкретного региона, лучше подключать резидентные прокси — они снижают риск блокировки по признаку "дата-центрового" IP.
Чек-лист перед деплоем
- Переменные заданы и в верхнем, и в нижнем регистре одновременно
- Логин и пароль в URL прокси URL-кодированы через
urllib.parse.quoteилиencodeURIComponent - NO_PROXY содержит и доменный, и wildcard-формат записи для совместимости между инструментами
- В docker-compose NO_PROXY включает имена сервисов из сети Compose, а не только localhost
- Для Docker build переданы build-args отдельно от runtime env через ENV в Dockerfile
- Secrets с URL прокси в CI замаскированы и не печатаются в логах отладки
- Для Java-приложений прокси задаётся через системные свойства -D, а не только через env
- Проверена защита от httpoxy — заголовок Proxy от клиента обнуляется на уровне веб-сервера
Заключение
Переменные HTTP_PROXY, HTTPS_PROXY и NO_PROXY — простая на вид конвенция, которая на практике ломается о регистр символов, синтаксис исключений, границы между этапами Docker build и Docker run, наследование окружения в docker-compose, утечки секретов в CI-логах и, наконец, о то, что не каждая библиотека вообще читает эти переменные. Проверяйте каждую переменную изолированно через curl и docker exec, прежде чем искать баг в бизнес-логике приложения.
Если ваша инфраструктура завязана на парсинг внешних сайтов, тестирование гео-таргетинга рекламы или обход блокировок по IP из CI-пайплайна, важно не только правильно прокинуть переменные окружения, но и выбрать подходящий тип прокси под задачу. Для тестирования мобильного трафика и приложений, которые проверяют поведение с мобильных операторов, подойдут мобильные прокси — они дают IP реальных сотовых сетей и реже попадают под фильтры антифрод-систем.