Справочник настроек прокси инструкции по программам

Docker proxy: настройка в демоне, контейнере и docker-compose

Docker запускает приложения в изолированных контейнерах, у каждого из которых собственное сетевое окружение, собственная таблица маршрутов и собственный набор переменных. Настройка прокси здесь распадается на три независимые точки: сам демон, окружение запущенного контейнера и клиентская библиотека внутри приложения, и адрес посредника приходится вносить в каждую отдельно.

Я держу на рабочем сервере сборку из шести сервисов в одном docker-compose.yml: сборщик страниц на Python, очередь, база, кеш, внутренний API и маленький экспортер метрик. Наружу через посредник ходит только сборщик. Все остальные разговаривают друг с другом по внутренней сети Compose напрямую. Когда я первый раз вписал адрес посредника в общий блок переменных, наружу пошло всё сразу, включая обращения к базе по имени сервиса, и стенд встал за минуту. Ниже разобран порядок, к которому я пришёл после той переделки.

1. Что понадобится

Docker Engine с плагином Compose версии 2: проверить командой docker compose version, вывод должен начинаться с Docker Compose version v2. Строка подключения в одном из двух форматов, IP:PORT для привязанного адреса и IP:PORT:LOGIN:PASS для доступа по логину. Права sudo на хосте, если предстоит править настройки демона. Текстовый редактор и файл docker-compose.yml проекта.

Отдельно проверьте, каким способом у вас оформлена авторизация в кабинете. Привязка внешнего адреса сервера и доступ по логину настраиваются в разных местах конфигурации, и половина проблем с ответом 403 от посредника растёт именно отсюда. Список адресов выдаётся ссылкой или файлом, обе формы строки лежат рядом, так что переключиться между ними можно за минуту.

Полезно заранее выписать на бумагу имена всех сервисов проекта. Они понадобятся дважды: в списке исключений и при проверке внутреннего DNS. У меня этот список хранится комментарием в шапке docker-compose.yml, и при добавлении седьмого сервиса я правлю его первым делом. Ещё один пункт подготовки касается времени: включение пакета занимает около 5 минут, и настраивать конфигурацию раньше, чем адрес начал отвечать, смысла нет. Бесплатный тест до 2 часов покрывает весь цикл проверок с запасом, включая пересборку образа.

2. Три места, где Docker узнаёт адрес посредника

Первое, что стоит уложить в голове: настройки этих трёх точек не наследуются друг от друга. Демон читает своё окружение, контейнер получает своё, приложение внутри контейнера смотрит на то, что ему передали, и часто добавляет собственный конфиг поверх. Правка в одном месте оставляет два других нетронутыми.

Точка настройкиГде живётЧто через неё пойдётЧто перезапустить
Демон Docker/etc/systemd/system/docker.service.d/http-proxy.confdocker pull, docker push, обращения движка к реестру образовsystemctl restart docker
Клиент Docker при сборке~/.docker/config.json, секция proxiesшаги RUN в docker build, установка пакетов внутри образаничего, читается при каждом вызове
Окружение контейнераenvironment или env_file в docker-compose.ymlпроцессы, запущенные внутри работающего контейнераdocker compose up -d для сервиса
Настройки приложения.curlrc, apt.conf, параметры JVM, конфиг клиента HTTPконкретная библиотека, которая эти параметры читаетперезапуск процесса

Разница между первой и третьей строкой видна на простом опыте. Если прописать адрес посредника только демону, образ скачается через посредник, а контейнер из этого образа пойдёт наружу напрямую. Если прописать переменные только контейнеру, приложение внутри пойдёт через посредник, при этом docker pull на хосте останется прямым. Обе половины нужны редко одновременно, и я держу их разведёнными сознательно: демон тянет образы с реестра по прямому каналу, сборщик страниц ходит наружу через пул.

Четвёртая строка появляется, когда библиотека игнорирует переменные окружения. Java не читает HTTP_PROXY вообще и требует ключей -Dhttp.proxyHost и -Dhttp.proxyPort. Стандартный пакет net/http в Go переменные читает. Node.js в базовой поставке их пропускает, и адрес приходится задавать через глобальный диспетчер undici. Проверяйте документацию своего клиента до того, как начнёте искать ошибку в конфигурации Docker.

3. Демон Docker и сборка образов

Демон запускается через systemd, поэтому переменные ему передаются файлом дополнения. Каталог для таких файлов создаётся руками.

sudo mkdir -p /etc/systemd/system/docker.service.d
sudo nano /etc/systemd/system/docker.service.d/http-proxy.conf

Содержимое файла:

[Service]
Environment="HTTP_PROXY=http://203.0.113.24:8000"
Environment="HTTPS_PROXY=http://203.0.113.24:8000"
Environment="NO_PROXY=localhost,127.0.0.1,::1,172.17.0.0/16,registry.internal"

Дальше перечитать юниты и перезапустить движок:

sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl show --property=Environment docker

Последняя команда печатает окружение демона в одну строку. Если в выводе пусто, файл лежит не в том каталоге или назван без расширения .conf. Такой промах у меня занял вечер: файл я создал в /etc/docker/, systemd туда не смотрит.

Обратите внимание на подсеть 172.17.0.0/16 в списке исключений. Через неё демон разговаривает с контейнерами на мосту по умолчанию, и отправлять эти обращения на внешний посредник смысла нет. Для стабильной работы движка с реестром я использую пул со сменой узла на выходе: список отдаётся живым, ротация внутри пула идёт автоматически, и подтягивание образов не спотыкается о выпавший узел.

4. Переменные окружения контейнера в docker-compose

Контейнеру переменные передаются секцией environment. Пример сервиса, который ходит наружу:

services:
  collector:
    image: python:3.12-slim
    command: python /app/run.py
    volumes:
      - ./app:/app
    environment:
      HTTP_PROXY: "http://203.0.113.24:8000"
      HTTPS_PROXY: "http://203.0.113.24:8000"
      NO_PROXY: "localhost,127.0.0.1,::1,api,db,cache,queue,.internal"
      http_proxy: "http://203.0.113.24:8000"
      https_proxy: "http://203.0.113.24:8000"
      no_proxy: "localhost,127.0.0.1,::1,api,db,cache,queue,.internal"
    depends_on:
      - db
      - cache

Дублирование трёх переменных в двух регистрах выглядит избыточно, и я сам сокращал этот блок до верхнего регистра. Через день сборщик начал уходить наружу напрямую на тех вызовах, где внутри библиотеки дёргался системный curl. Причина ниже, в разборе регистра. Пока просто примите за правило: пишите обе формы, стоимость этого нулевая.

Сервисы, которым наружу не нужно, оставляйте без переменных вовсе. База, кеш и очередь у меня их не получают, поэтому даже случайное обращение внутри их клиентов уйдёт напрямую по внутренней сети.

Общий блок переменных удобно вынести в якорь YAML, если наружу ходят два и более сервиса:

x-proxy: &proxy
  HTTP_PROXY: "http://203.0.113.24:8000"
  HTTPS_PROXY: "http://203.0.113.24:8000"
  NO_PROXY: "localhost,127.0.0.1,::1,api,db,cache,queue,.internal"
  http_proxy: "http://203.0.113.24:8000"
  https_proxy: "http://203.0.113.24:8000"
  no_proxy: "localhost,127.0.0.1,::1,api,db,cache,queue,.internal"

services:
  collector:
    image: python:3.12-slim
    environment: *proxy
  exporter:
    image: node:22-alpine
    environment: *proxy

Такой якорь избавляет от рассинхрона, когда адрес поменялся, а во втором сервисе остался старый. Трафик через пул при этом никак не считается, пакеты с безлимитным трафиком снимают вопрос об объёме выкачки, и держать десяток сервисов на одном канале можно спокойно.

5. Регистр переменных и типовые промахи

Разные клиенты читают разные формы записи, и это главный источник тихих отказов. Тихих потому, что ошибки нет: запрос просто уходит мимо посредника и возвращается успешно.

КлиентВерхний регистрНижний регистрКомментарий
curlигнорирует HTTP_PROXYчитает http_proxyHTTPS_PROXY в верхнем регистре читается
wgetигнорируетчитаетпонимает только нижний регистр
Python requestsчитаетчитаетприоритет у нижнего
Go net/httpчитаетчитаетвнутри CGI-окружения верхний игнорируется
apt внутри образане читаетне читаетнужен Acquire::http::Proxy
Javaне читаетне читаетнужны ключи -Dhttp.proxyHost

Отдельная история с curl. Верхний регистр HTTP_PROXY он пропускает намеренно: переменная с таким именем подставляется веб-сервером из заголовка Proxy входящего запроса, и чтение её в CGI-скрипте давало захват трафика. Утилита закрыла эту дыру отказом от верхнего регистра для протокола HTTP. Именно поэтому у меня в блоке шесть строк.

Что ещё встречается в чужих файлах и в моих старых коммитах:

Строка без схемы. Значение 203.0.113.24:8000 часть клиентов разберёт, часть посчитает именем хоста и полезет резолвить. Пишите http:// перед адресом всегда, даже когда наружу идёт HTTPS: схема описывает канал до посредника.

Пробелы в списке исключений. Запись localhost, 127.0.0.1, db у curl работает, у ряда клиентов на Go и Python элемент с ведущим пробелом не совпадает ни с чем. Пишите список без пробелов.

Кавычки внутри значения. В YAML безопаснее взять значение в двойные кавычки целиком, иначе двоеточие в адресе может быть прочитано как разделитель ключа.

Переменные, заданные в build.args, в рантайме отсутствуют. Аргумент сборки живёт только на время шага RUN. Это правильное поведение, и попытка сэкономить строку тут выходит боком.

Директива ENV HTTP_PROXY в Dockerfile. Значение запечётся в слой образа и уедет вместе с ним куда угодно. Я такие строки вычищаю на ревью без обсуждения.

6. Файл конфигурации клиента Docker

Шаги RUN внутри docker build наследуют окружение демона не полностью, и apt-get update в образе спокойно уходит напрямую, пока вы правите юнит systemd. Для сборки настройка живёт в файле клиента Docker.

mkdir -p ~/.docker
nano ~/.docker/config.json
{
  "proxies": {
    "default": {
      "httpProxy": "http://203.0.113.24:8000",
      "httpsProxy": "http://203.0.113.24:8000",
      "noProxy": "localhost,127.0.0.1,::1,api,db,cache,.internal"
    }
  }
}

Ключи здесь пишутся в верблюжьем регистре, httpProxy и noProxy. Регистр в этом файле строгий, опечатка молча игнорируется. После правки ничего перезапускать не нужно, клиент читает файл при каждом вызове.

Клиент подставляет эти значения дважды: как аргументы сборки на шагах RUN и как переменные окружения при docker run. Второе поведение иногда удивляет, когда контейнер, запущенный руками, ходит через посредник, а тот же сервис из Compose идёт напрямую. Compose значения из config.json не подхватывает, ему нужны свои.

Для сборки через Compose аргументы задаются явно:

services:
  collector:
    build:
      context: .
      args:
        HTTP_PROXY: "http://203.0.113.24:8000"
        HTTPS_PROXY: "http://203.0.113.24:8000"
        NO_PROXY: "localhost,127.0.0.1,::1"
        http_proxy: "http://203.0.113.24:8000"
        https_proxy: "http://203.0.113.24:8000"
        no_proxy: "localhost,127.0.0.1,::1"

Объявлять эти имена через ARG в Dockerfile не требуется, они входят в набор предопределённых аргументов. Проверить попадание значения внутрь сборки помогает временная строка:

FROM python:3.12-slim
RUN env | grep -i proxy && apt-get update && apt-get install -y --no-install-recommends curl

Первая часть напечатает окружение шага прямо в лог сборки. Убедились, что переменные на месте, убрали env | grep. Для сборок, где ставится много пакетов, я использую порт с поддержкой HTTPS для сборок: туннель до реестра открывается методом CONNECT, и менеджеры пакетов через него ведут себя ровнее на длинных загрузках.

7. Соседние контейнеры и правильный NO_PROXY

Compose поднимает проекту отдельную сеть и регистрирует в её DNS имена сервисов. Обращение http://api:8080/health изнутри контейнера collector уходит на соседа за один хоп. Стоит появиться переменной HTTP_PROXY, и клиент отправит этот запрос на внешний посредник, который про имя api ничего не знает. В логе появится 502 или таймаут, и указывать он будет мимо настоящей причины.

Список исключений закрывает это полностью, если он полный. Что в него входит:

ЗаписьЧто покрываетКто понимает
localhost,127.0.0.1,::1обращения к себе внутри контейнеравсе клиенты
api,db,cache,queueимена сервисов Composeвсе клиенты
collector-api-1сгенерированные имена контейнероввсе клиенты
.internalлюбой хост в зоне, ведущая точка обязательнаcurl, Python, Go
172.18.0.0/16подсеть проекта в форме CIDRGo и curl свежих версий
host.docker.internalобращение к сервису на хостевсе клиенты

Ведущая точка работает как маска поддоменов. Запись internal без точки часть клиентов трактует как совпадение с концом имени, часть требует точного равенства, поэтому обе формы я пишу подряд: internal,.internal. Форма CIDR понимается далеко не всеми, и опираться только на неё не стоит. Имена сервисов дешевле и надёжнее.

Проверить имена сети проекта можно так:

docker compose ps --format '{{.Service}} {{.Name}}'
docker network inspect $(docker compose ps -q collector | head -1) --format '{{json .}}' 2>/dev/null | head -c 200
docker compose exec collector getent hosts api db cache

Третья команда печатает адреса соседей из внутреннего DNS. Если она отвечает пустотой, сервис живёт в другой сети, и посредник тут ни при чём.

8. Привязка адреса при работе из контейнера

Контейнер получает адрес из внутренней подсети вида 172.18.0.5. Наружу этот адрес никогда не выходит: пакет проходит через NAT на хосте и появляется у посредника уже с внешним адресом хост-машины. Значит, в кабинете привязывать нужно именно внешний адрес сервера. Внутренний адрес контейнера в поле привязки уйдёт впустую, и посредник ответит 403 на каждом запросе.

Посмотреть, какой адрес видит внешняя сторона, можно с самого хоста:

curl -s https://api.ipify.org
ip -4 addr show scope global | grep inet

Первая строка печатает внешний адрес, вторая перечисляет локальные интерфейсы. В кабинет вносится первое значение. В пакет входят две привязки, менять их можно свободно, так что рабочий сервер и запасной живут рядом. Помните про делёж потоков: при двух привязанных адресах лимит потоков делится между ними пополам, и на обычном пакете каждый получает по 500 из 1000.

Вариант с логином освобождает от привязки вовсе и удобен, когда внешний адрес хоста плавает. Строка выдаётся в формате IP:PORT:LOGIN:PASS и разворачивается в URL посредника:

cat > proxy.env <<'EOF'
HTTP_PROXY=http://u4821:s7Kd2Rm9@203.0.113.24:8000
HTTPS_PROXY=http://u4821:s7Kd2Rm9@203.0.113.24:8000
NO_PROXY=localhost,127.0.0.1,::1,api,db,cache,queue,internal,.internal
http_proxy=http://u4821:s7Kd2Rm9@203.0.113.24:8000
https_proxy=http://u4821:s7Kd2Rm9@203.0.113.24:8000
no_proxy=localhost,127.0.0.1,::1,api,db,cache,queue,internal,.internal
EOF
chmod 600 proxy.env

Подключается файл одной строкой:

services:
  collector:
    image: python:3.12-slim
    env_file:
      - ./proxy.env

Файл proxy.env держите вне репозитория, добавив его в .gitignore. Специальные символы в пароле кодируются процентами: @ превращается в %40, двоеточие в %3A. Иначе разбор URL сломается на первом же символе.

Есть и третий путь, когда контейнеру нужен ровно тот же сетевой стек, что у хоста. Режим network_mode: host убирает NAT целиком, контейнер садится на интерфейсы машины, и привязанный адрес работает без оговорок. Изоляция сети при этом снимается, поэтому режим я оставляю для одиночных сборщиков. Для стенда из шести сервисов мне ближе приватный доступ к пулу адресов поверх обычной сети Compose: пул закрыт от посторонних, а внутренняя сеть проекта остаётся изолированной.

Пул в районе 12 000 адресов и автоматическая ротация внутри него означают, что конкретный узел на выходе меняется сам. Приложению это безразлично, пока строка подключения одна. Для длинных прогонов удобно, что безлимитный трафик на всех пакетах снимает арифметику с объёма, и масштаб выкачки упирается только в потоки.

9. Проверка после настройки

Сначала смотрим, что переменные вообще доехали до процесса внутри контейнера:

docker compose exec collector env | grep -i proxy
docker compose exec collector sh -c 'echo "${http_proxy}"'

Дальше пробный запрос наружу с расшифровкой того, куда он пошёл:

docker compose exec collector \
  curl -sS -o /dev/null -w 'code=%{http_code} peer=%{remote_ip}:%{remote_port} t=%{time_total}\n' \
  https://example.net/

Поле peer показывает адрес, с которым установлено соединение. Если там стоит адрес посредника, канал собран верно. Если там адрес целевого сайта, запрос ушёл напрямую, и дело в регистре переменных.

Сверка внешнего адреса делается парой команд, одна из контейнера, вторая с хоста:

docker compose exec collector curl -s https://api.ipify.org; echo
curl -s https://api.ipify.org; echo

Значения должны отличаться. Совпадение означает, что контейнер идёт мимо посредника. Повторите проверку несколько раз подряд: адрес из контейнера будет меняться, потому что ротация внутри пула работает без вашего участия.

Третья проверка касается соседей. Обращение к внутреннему сервису обязано идти напрямую:

docker compose exec collector \
  curl -sS -v -o /dev/null http://api:8080/health 2>&1 | grep -E 'Connected to|Uses proxy'

В выводе должна быть строка Connected to api. Строка про использование переменной окружения означает, что имя api в списке исключений отсутствует. Тот же тест прогоните для базы и кеша.

Полезно посмотреть и на подробный лог одного внешнего запроса целиком:

docker compose exec collector curl -v -x "${HTTP_PROXY}" https://example.net/ 2>&1 | head -30

Явный ключ -x перебивает окружение и показывает, работает ли сама строка подключения. Когда с ключом запрос проходит, а без ключа падает, ошибка сидит в переменных. Когда падает в обоих случаях, проверяйте привязку адреса и пароль. Я такой прогон делаю после каждой правки docker-compose.yml, привычка экономит мне часы на разборе. Для регулярных выгрузок с чужих сайтов я отдельно проверяю поведение на серии из 200 запросов подряд, потому что адреса под регулярный парсинг должны держать темп на всей серии. Одиночный curl покажет только то, что канал собран.

10. Разбор ошибок

СообщениеПричинаЧто делать
curl: (5) Could not resolve proxy: httpсхема написана дважды, значение вида http://http://203.0.113.24:8000оставить одну схему в начале строки
curl: (7) Failed to connect to 203.0.113.24 port 8000исходящий порт закрыт на хосте или на сетевом экране провайдераоткрыть исходящий TCP на порт посредника
curl: (56) Received HTTP code 403 from proxy after CONNECTвнешний адрес хоста не привязан либо привязан адрес контейнеравнести внешний адрес хоста в кабинете
curl: (56) ... 407 Proxy Authentication Requiredлогин с паролем не доехали до клиентапроверить env_file и процент-кодирование пароля
502 Bad Gateway при обращении к http://api:8080имя соседнего сервиса ушло на внешний посредникдобавить имя сервиса в NO_PROXY в обеих формах регистра
dial tcp: lookup api: no such hostсервис поднят в другой сети проектасвести сервисы в одну сеть Compose, проверить docker compose ps
Get "https://registry-1.docker.io/v2/": proxyconnect tcpдемон не знает адрес посредникасоздать drop-in и выполнить daemon-reload с рестартом
E: Failed to fetch ... Connection timed out на шаге RUN apt-getаргументы сборки не переданызаполнить build.args либо секцию proxies в ~/.docker/config.json
x509: certificate signed by unknown authorityвнутри образа отсутствуют корневые сертификатыпоставить пакет ca-certificates на шаге сборки
Client sent an HTTP request to an HTTPS serverв HTTPS_PROXY записана схема https:// для обычного посредникапоставить http:// перед адресом в обеих переменных
переменные видны в env, запросы идут напрямуюприложение читает только нижний регистрпродублировать все три переменные строчными буквами
docker compose up не подхватывает правкустарый контейнер жив со старым окружениемпересоздать сервис командой docker compose up -d --force-recreate collector

Отдельно про последнюю строку. Переменные окружения фиксируются в момент создания контейнера, и docker compose restart их не обновляет. Я на этом терял время дважды, пока не перешёл на up -d --force-recreate для любой правки блока environment.

Ещё один частый сюжет: сборка проходит, контейнер стартует, а через сутки запросы начинают отваливаться пачками. Смотрите на потоки, конфигурацию тут можно отложить. Пакеты по потокам не складываются, и при двух привязках лимит делится пополам, так что параллельный сборщик на 800 потоков при второй активной привязке упрётся в границу и начнёт получать отказы на установке соединения. Считайте параллелизм по фактической половине лимита.

Соседние страницы справочника закрывают смежные сценарии на серверной стороне: запуск заданий с прокси в PowerShell для машин под Windows, обмен данными в 1С через посредник и выгрузки в Google Таблицы через Apps Script. Если Docker крутится на рабочей станции под Windows и хочется задать адрес один раз для всей машины, начните с системных сетевых настроек Windows 11.