← Back to Blog

HTTP_PROXY, HTTPS_PROXY, NO_PROXY Variables in Linux, Docker, and CI: 7 Common Mistakes

Why proxy environment variables work in one container and break in another — we analyze 7 real cases with solutions and code.

📅November 7, 2026

The environment variables HTTP_PROXY, HTTPS_PROXY, and NO_PROXY seem straightforward: set the variable, and all traffic goes through the proxy. In practice, this is where developers spend hours debugging: the proxy works in the terminal but not in the Docker container; curl goes through the proxy, but the Python application does not; the CI runner suddenly blocks the internal registry because NO_PROXY is specified with incorrect syntax. We will analyze 7 real pitfalls and show how to bypass them with specific code.

How Applications Read Proxy Variables

It is important to understand that HTTP_PROXY, HTTPS_PROXY, and NO_PROXY are not a standard of the HTTP protocol, but a convention that each library implements in its own way. The Linux kernel knows nothing about these variables; they are read by a specific application or library at the network client initialization stage.

For example, curl and utilities based on libcurl read the variables each time the process is started. The Python library requests reads them through the module urllib.request.getproxies(), which scans the process environment at the time of session creation. However, the built-in fetch() in Node.js prior to version 18 does not consider these variables at all without additional packages.

Hence the first rule: the proxy variable works only where it is explicitly supported by a specific library. Checking via curl does not guarantee that your application in Go, Java, or Node.js will behave the same way.

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 will show in the verbose log: "Trying proxy.example.com..."

Trap 1: Variable Case and httpoxy Vulnerability

Most console utilities in Linux read variables in lowercase: http_proxy, https_proxy, no_proxy. Many modern SDKs rely on uppercase. The difference seems trivial, but it is precisely because of this that half of the bugs in the "works locally, does not work on the server" style occur on the very first day of setup.

A separate story is the httpoxy vulnerability (CVE-2016-5385 and related). The essence is that the CGI environment (PHP, Python WSGI, some Ruby frameworks) automatically turns the HTTP header Proxy from the incoming request into the environment variable HTTP_PROXY. An attacker sends the header Proxy: http://attacker.com, and your server application starts sending outgoing requests through a foreign proxy server, opening the way for MITM attacks and data leaks.

The solution is to always set the variables in both cases simultaneously and explicitly nullify HTTP_PROXY at the web server level for incoming requests:

# Set in both cases for compatibility
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"

# In nginx — block the Proxy header from the client
proxy_set_header Proxy "";

Trap 2: NO_PROXY Syntax Differs Between Tools

NO_PROXY is a list of exceptions, but the format for recording domains and masks is not standardized. Some tools understand .example.com as "any subdomain," others require *.example.com, while others only understand exact host matches without any wildcards at all.

For example, the Go library net/http supports CIDR notation in NO_PROXY (10.0.0.0/8), while classic curl does not support it until certain versions. This leads to a situation where the same NO_PROXY correctly excludes the internal registry for one CI step but does not exclude it for another written in a different language.

Tool Subdomain Wildcard CIDR Mask
curl / libcurl via .example.com no
Go net/http via .example.com yes
Python requests via .example.com no
npm / Node.js needs *.example.com no

Practical advice: list both the domain and wildcard version in NO_PROXY simultaneously to cover different parsers:

export NO_PROXY="localhost,127.0.0.1,.internal.corp,*.internal.corp,10.0.0.0/8"

Trap 3: Docker Build vs Docker Run — Different Environment Variables

This is perhaps the most common pitfall in Docker. Variables set via docker run -e HTTP_PROXY=... only apply while the container is running. They do NOT automatically carry over to the image build stage. If your Dockerfile calls apt-get install or pip install inside, and the build is going through a corporate proxy, the build will simply hang on a timeout.

To build, you need to pass build-args, which Docker understands "out of the box" since version 1.9 without declaration in the 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 .

If you want the variable to remain accessible inside the running container, you need to explicitly duplicate it via ENV in the Dockerfile — ARG and ENV in Docker are fundamentally different mechanisms, and ARG disappears immediately after the layer build:

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

Another nuance: the Docker daemon itself may use a proxy to download images from the registry — this is configured not in the Dockerfile, but in the systemd unit config /etc/systemd/system/docker.service.d/http-proxy.conf, which is often confused with variables inside the container.

Trap 4: docker-compose and Variable Inheritance Between Services

In docker-compose, host environment variables are NOT automatically passed into containers unless you explicitly specify them in the environment section or use a .env file. A common mistake is to set the proxy in the terminal before running docker-compose up and expect all services to pick it 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

Note the NO_PROXY: inside the docker-compose network, containers refer to each other by service names (db, redis), not by localhost. If you forget to add service names to the exceptions, all internal traffic between containers will also go through the external proxy, which will increase latency and may break the connection if the proxy does not allow internal protocols like PostgreSQL or Redis.

Trap 5: CI/CD Runners and Proxy Secrets Leakage

In GitHub Actions, GitLab CI, and Jenkins, proxy variables are often set at the runner or organization level, creating two problems. First, if the proxy URL contains a username and password in plain text, it appears in the build log for any command that prints the environment (env, printenv, debug output in scripts). Second, self-hosted runners sometimes inherit the system proxy of the host, which is not intended for CI job traffic.

# .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

Security rule: store the proxy URL only in CI secrets (GitHub Secrets, GitLab CI/CD Variables with the "masked" flag), never output environment variables entirely in debug logs, and limit NO_PROXY so that requests to the CI platform itself (e.g., github.com, gitlab.com) do not go through the proxy — otherwise, the build may hang if the proxy is unavailable during peak load.

Trap 6: Special Characters in Proxy Credentials

If the proxy password contains characters like @, :, /, or #, the URL parser simply breaks the string into parts in the wrong places. A classic mistake: the password P@ssw0rd in the string http://user:P@[email protected]:8080 is read by the parser as if the host is "[email protected]," not "proxy.com."

The solution is to always URL-encode the username and password before substituting them into the variable:

# Python: encode the password before building the proxy string
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: similar construction using 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;

Trap 7: Applications Ignoring Environment Variables

Not all HTTP clients respect system proxy variables. In Python, the requests library reads them automatically through requests.Session(), but if you once explicitly passed a dictionary proxies={} in the call, it overrides the environment variables, and traffic will go directly even if HTTP_PROXY is set in the system.

import requests

# An empty proxies dictionary OVERRIDES environment variables
response = requests.get("https://api.example.com", proxies={})
# Correct — explicitly pass the proxy or do not pass proxies at all
proxies = {
    "http": "http://user:[email protected]:8080",
    "https": "http://user:[email protected]:8080",
}
response = requests.get("https://api.example.com", proxies=proxies)

In Node.js, the built-in fetch does not know about HTTP_PROXY at all without additional libraries. You need to explicitly include undici with ProxyAgent or the classic 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 also requires separate system properties instead of environment variables: -Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy.example.com -Dhttps.proxyPort=8080. The HTTP_PROXY environment variables are not read by the JVM by default.

How to Properly Test Proxy Variables

Before spending time debugging the application, check the variable and the proxy itself in isolation from the code. This saves hours.

# 1. Check that the variable is actually set in the current session
env | grep -i proxy

# 2. Check the proxy's functionality directly via curl
curl -x "$HTTP_PROXY" -v https://api.ipify.org

# 3. Check from inside the container that the variable has reached
docker exec -it my_container env | grep -i proxy

# 4. Check that NO_PROXY actually excludes the desired host
curl -v --noproxy "internal.corp" https://service.internal.corp/health

If the task is mass requests to external APIs, scraping marketplaces, or testing geo-targeting of ads from a CI pipeline, it is important to choose the right type of proxy for the load. For stable high-load scenarios in CI, data center proxies are usually sufficient — they are faster and cheaper than residential ones for mass traffic of builds and tests. If your pipeline checks the behavior of a site from a real user IP of a specific region, it is better to connect residential proxies — they reduce the risk of blocking by the "data center" IP.

Pre-deployment Checklist

  • Variables are set in both uppercase and lowercase simultaneously
  • Username and password in the proxy URL are URL-encoded using urllib.parse.quote or encodeURIComponent
  • NO_PROXY contains both domain and wildcard format for compatibility between tools
  • In docker-compose, NO_PROXY includes service names from the Compose network, not just localhost
  • For Docker build, build-args are passed separately from runtime env via ENV in the Dockerfile
  • Secrets with proxy URL in CI are masked and not printed in debug logs
  • For Java applications, the proxy is set via system properties -D, not just through env
  • Protection against httpoxy is verified — the Proxy header from the client is nulled at the web server level

Conclusion

The variables HTTP_PROXY, HTTPS_PROXY, and NO_PROXY are a seemingly simple convention that in practice breaks on character case, exception syntax, boundaries between Docker build and Docker run stages, environment inheritance in docker-compose, secrets leakage in CI logs, and finally, that not every library reads these variables at all. Check each variable in isolation via curl and docker exec before searching for bugs in the application's business logic.

If your infrastructure is tied to scraping external sites, testing geo-targeting of ads, or bypassing IP blocks from a CI pipeline, it is important not only to correctly pass environment variables but also to choose the appropriate type of proxy for the task. For testing mobile traffic and applications that check behavior from mobile operators, mobile proxies are suitable — they provide IPs from real cellular networks and are less likely to be caught by anti-fraud filters.