← Назад к блогу

Переменные HTTP_PROXY, HTTPS_PROXY, NO_PROXY в Linux, Docker и CI: 7 типичных ошибок

Почему прокси-переменные окружения работают в одном контейнере и ломаются в другом — разбираем 7 реальных кейсов с решениями и кодом.

📅7 ноября 2026 г.

Переменные окружения 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 реальных сотовых сетей и реже попадают под фильтры антифрод-систем.