← Quay lại blog

Biến HTTP_PROXY, HTTPS_PROXY, NO_PROXY trong Linux, Docker và CI: 7 lỗi phổ biến

Tại sao biến môi trường proxy hoạt động trong một container nhưng lại hỏng trong container khác - phân tích 7 trường hợp thực tế với giải pháp và mã.

📅7 tháng 11, 2026

Các biến môi trường HTTP_PROXY, HTTPS_PROXY và NO_PROXY trông có vẻ rất đơn giản: đặt biến — toàn bộ lưu lượng truy cập đi qua proxy. Trong thực tế, chính tại điểm này các nhà phát triển mất hàng giờ để gỡ lỗi: proxy hoạt động trong terminal, nhưng không hoạt động trong container Docker; curl hoạt động qua proxy, nhưng ứng dụng Python thì không; CI runner đột nhiên chặn registry nội bộ, vì NO_PROXY được chỉ định với cú pháp sai. Hãy phân tích 7 cạm bẫy thực tế và chỉ cho bạn cách vượt qua chúng với mã cụ thể.

Cách các ứng dụng đọc các biến proxy

Quan trọng là phải hiểu: HTTP_PROXY, HTTPS_PROXY và NO_PROXY không phải là tiêu chuẩn của giao thức HTTP, mà là một quy ước mà mỗi thư viện thực hiện theo cách riêng của mình. Nhân Linux không biết gì về các biến này — chúng được đọc bởi ứng dụng hoặc thư viện cụ thể trong giai đoạn khởi tạo khách hàng mạng.

Ví dụ, curl và các tiện ích dựa trên libcurl đọc các biến mỗi khi quá trình được khởi động. Thư viện Python requests đọc chúng thông qua mô-đun urllib.request.getproxies(), quét môi trường của quá trình tại thời điểm tạo phiên. Còn fetch() tích hợp trong Node.js trước phiên bản 18 hoàn toàn không xem xét các biến này mà không có các gói bổ sung.

Từ đó, quy tắc đầu tiên: biến proxy chỉ hoạt động ở nơi mà thư viện cụ thể rõ ràng hỗ trợ. Kiểm tra qua curl không đảm bảo rằng ứng dụng của bạn trên Go, Java hoặc Node.js sẽ hoạt động giống như vậy.

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 sẽ hiển thị trong nhật ký verbose: "Trying proxy.example.com..."

Cạm bẫy 1: đăng ký biến và lỗ hổng httpoxy

Hầu hết các tiện ích console trong Linux đọc các biến ở dạng chữ thường: http_proxy, https_proxy, no_proxy. Nhiều SDK hiện đại dựa vào chữ hoa. Sự khác biệt có vẻ nhỏ, nhưng chính vì nó mà một nửa các lỗi kiểu "hoạt động cục bộ, không hoạt động trên máy chủ" xảy ra ngay trong ngày đầu thiết lập.

Một câu chuyện riêng là lỗ hổng httpoxy (CVE-2016-5385 và các liên quan). Cốt lõi là môi trường CGI (PHP, Python WSGI, một số framework Ruby) tự động biến đổi tiêu đề HTTP Proxy từ yêu cầu vào thành biến môi trường HTTP_PROXY. Kẻ tấn công gửi tiêu đề Proxy: http://attacker.com, và ứng dụng máy chủ của bạn bắt đầu gửi các yêu cầu ra ngoài qua máy chủ proxy của kẻ khác, mở đường cho các cuộc tấn công MITM và rò rỉ dữ liệu.

Giải pháp là luôn đặt các biến ở cả hai dạng chữ thường và chữ hoa cùng một lúc và rõ ràng đặt lại HTTP_PROXY ở cấp độ máy chủ web cho các yêu cầu vào:

# Đặt ở cả hai dạng chữ để tương thích
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"

# Trong nginx — chặn tiêu đề Proxy từ khách hàng
proxy_set_header Proxy "";

Cạm bẫy 2: cú pháp NO_PROXY khác nhau giữa các công cụ

NO_PROXY là danh sách ngoại lệ, nhưng định dạng ghi tên miền và mặt nạ trong đó không được chuẩn hóa. Một số công cụ hiểu .example.com như "bất kỳ tên miền phụ nào", trong khi những cái khác yêu cầu *.example.com, và một số chỉ hiểu sự trùng khớp chính xác của máy chủ mà không có wildcard.

Ví dụ, thư viện Go net/http hỗ trợ cú pháp CIDR trong NO_PROXY (10.0.0.0/8), trong khi curl cổ điển không hỗ trợ cho đến một số phiên bản nhất định. Điều này dẫn đến tình huống mà cùng một NO_PROXY loại trừ chính xác registry nội bộ cho một bước CI và không loại trừ cho một bước khác được viết bằng ngôn ngữ khác.

Công cụ Wildcard tên miền phụ Mặt nạ CIDR
curl / libcurl qua .example.com không
Go net/http qua .example.com có
Python requests qua .example.com không
npm / Node.js cần *.example.com không

Lời khuyên thực tiễn: liệt kê cả định dạng tên miền và định dạng wildcard trong NO_PROXY cùng một lúc để bao phủ các trình phân tích khác nhau:

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

Cạm bẫy 3: Docker build vs Docker run — các biến môi trường khác nhau

Đây có lẽ là cạm bẫy phổ biến nhất trong Docker. Các biến được đặt qua docker run -e HTTP_PROXY=... chỉ có hiệu lực trong thời gian chạy của container. Chúng KHÔNG tự động được đưa vào giai đoạn xây dựng hình ảnh. Nếu Dockerfile của bạn gọi apt-get install hoặc pip install, và việc xây dựng diễn ra qua proxy doanh nghiệp, quá trình build sẽ đơn giản bị treo do timeout.

Để xây dựng, bạn cần truyền các build-args, mà Docker từ phiên bản 1.9+ hiểu "ngay từ đầu" mà không cần khai báo trong 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 .

Nếu bạn muốn biến vẫn có sẵn bên trong container đã chạy, bạn cần rõ ràng sao chép nó qua ENV trong Dockerfile — ARG và ENV trong Docker là hai cơ chế khác nhau, và ARG sẽ biến mất ngay sau khi xây dựng lớp:

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

Một điểm khác: Docker daemon có thể tự sử dụng proxy để tải hình ảnh từ registry — điều này được cấu hình không trong Dockerfile, mà trong cấu hình của đơn vị systemd /etc/systemd/system/docker.service.d/http-proxy.conf, điều này thường bị nhầm lẫn với các biến bên trong container.

Cạm bẫy 4: docker-compose và kế thừa các biến giữa các dịch vụ

Trong docker-compose, các biến môi trường của host KHÔNG được truyền tự động vào các container, trừ khi bạn rõ ràng chỉ định chúng trong phần environment hoặc sử dụng tệp .env. Một lỗi phổ biến là đặt proxy trong terminal trước khi chạy docker-compose up và mong đợi rằng tất cả các dịch vụ sẽ nhận nó.

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

Lưu ý về NO_PROXY: bên trong mạng docker-compose, các container giao tiếp với nhau qua tên dịch vụ (db, redis), chứ không phải qua localhost. Nếu quên thêm tên dịch vụ vào các ngoại lệ, toàn bộ lưu lượng nội bộ giữa các container cũng sẽ đi qua proxy bên ngoài, điều này làm tăng độ trễ và có thể làm hỏng kết nối nếu proxy không cho phép các giao thức nội bộ như PostgreSQL hoặc Redis.

Cạm bẫy 5: CI/CD runners và rò rỉ bí mật proxy

Trong GitHub Actions, GitLab CI và Jenkins, các biến proxy thường được đặt ở cấp độ runner hoặc tổ chức hoàn toàn, và điều này tạo ra hai vấn đề. Đầu tiên, nếu URL proxy chứa tên đăng nhập và mật khẩu dưới dạng rõ ràng, nó sẽ xuất hiện trong nhật ký xây dựng bất kỳ lệnh nào in môi trường (env, printenv, đầu ra gỡ lỗi trong các kịch bản). Thứ hai, các runner tự lưu trữ đôi khi kế thừa proxy hệ thống của host, mà không được thiết kế cho lưu lượng CI job.

# .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: Kiểm tra kết nối
        run: curl -sS https://api.example.com/health

Quy tắc an toàn: chỉ lưu trữ URL proxy trong bí mật CI (GitHub Secrets, GitLab CI/CD Variables với cờ "masked"), không bao giờ in toàn bộ biến môi trường trong nhật ký gỡ lỗi, và giới hạn NO_PROXY để các yêu cầu đến chính nền tảng CI (ví dụ: github.com, gitlab.com) không đi qua proxy — nếu không, quá trình xây dựng có thể bị treo nếu proxy không khả dụng trong thời điểm tải cao.

Cạm bẫy 6: ký tự đặc biệt trong tên đăng nhập và mật khẩu proxy

Nếu mật khẩu của proxy chứa các ký tự @, :, / hoặc #, trình phân tích URL sẽ đơn giản phá vỡ chuỗi ở những vị trí sai. Lỗi cổ điển: mật khẩu P@ssw0rd trong chuỗi http://user:P@[email protected]:8080 được trình phân tích đọc như thể máy chủ là "[email protected]", chứ không phải "proxy.com".

Giải pháp là luôn mã hóa URL cho tên đăng nhập và mật khẩu trước khi thay thế vào biến:

# Python: mã hóa mật khẩu trước khi xây dựng chuỗi proxy
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: xây dựng tương tự qua 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;

Cạm bẫy 7: ứng dụng bỏ qua các biến môi trường

Không phải tất cả các HTTP client đều tôn trọng các biến proxy hệ thống. Trong Python, thư viện requests đọc chúng tự động qua requests.Session(), nhưng nếu bạn đã một lần rõ ràng truyền một từ điển proxies={} trong cuộc gọi, điều này sẽ ghi đè các biến môi trường và lưu lượng sẽ đi trực tiếp, ngay cả khi HTTP_PROXY được chỉ định trong hệ thống.

import requests

# Từ điển proxies trống GHI ĐÈ các biến môi trường
response = requests.get("https://api.example.com", proxies={})
# Đúng — rõ ràng truyền proxy hoặc không truyền proxies
proxies = {
    "http": "http://user:[email protected]:8080",
    "https": "http://user:[email protected]:8080",
}
response = requests.get("https://api.example.com", proxies=proxies)

Trong Node.js, fetch tích hợp hoàn toàn không biết về HTTP_PROXY mà không có các thư viện bổ sung. Bạn cần rõ ràng kết nối undici với ProxyAgent hoặc https-proxy-agent cổ điển:

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 cũng yêu cầu các thuộc tính hệ thống riêng thay vì các biến môi trường: -Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy.example.com -Dhttps.proxyPort=8080. Các biến môi trường HTTP_PROXY trong JVM mặc định không được đọc.

Cách kiểm tra đúng các biến proxy

Trước khi lãng phí thời gian gỡ lỗi ứng dụng, hãy kiểm tra chính biến và proxy một cách độc lập khỏi mã. Điều này tiết kiệm hàng giờ.

# 1. Kiểm tra xem biến thực sự đã được thiết lập trong phiên hiện tại
env | grep -i proxy

# 2. Kiểm tra tính khả thi của proxy trực tiếp qua curl
curl -x "$HTTP_PROXY" -v https://api.ipify.org

# 3. Kiểm tra từ bên trong container, xem biến đã đến
docker exec -it my_container env | grep -i proxy

# 4. Kiểm tra xem NO_PROXY thực sự loại trừ máy chủ cần thiết
curl -v --noproxy "internal.corp" https://service.internal.corp/health

Nếu nhiệm vụ là gửi nhiều yêu cầu đến các API bên ngoài, phân tích các thị trường hoặc kiểm tra geo-targeting quảng cáo từ pipeline CI, quan trọng là chọn loại proxy phù hợp với tải. Đối với các kịch bản có tải cao ổn định trong CI, thường chỉ cần proxy trung tâm dữ liệu — chúng nhanh hơn và rẻ hơn so với proxy cư trú khi có lưu lượng lớn trong quá trình xây dựng và kiểm tra. Nếu pipeline của bạn kiểm tra hành vi của trang web từ IP của người dùng thực tế trong một khu vực cụ thể, tốt hơn là kết nối proxy cư trú — chúng giảm thiểu rủi ro bị chặn do IP "trung tâm dữ liệu".

Danh sách kiểm tra trước khi triển khai

  • Các biến được đặt ở cả dạng chữ hoa và chữ thường cùng một lúc
  • Tên đăng nhập và mật khẩu trong URL proxy được mã hóa URL qua urllib.parse.quote hoặc encodeURIComponent
  • NO_PROXY chứa cả định dạng tên miền và định dạng wildcard để tương thích giữa các công cụ
  • Trong docker-compose, NO_PROXY bao gồm các tên dịch vụ từ mạng Compose, không chỉ localhost
  • Đối với Docker build, các build-args được truyền riêng biệt từ môi trường runtime qua ENV trong Dockerfile
  • Bí mật với URL proxy trong CI được ẩn và không được in trong nhật ký gỡ lỗi
  • Đối với các ứng dụng Java, proxy được đặt qua các thuộc tính hệ thống -D, không chỉ qua env
  • Đã kiểm tra bảo vệ chống lại httpoxy — tiêu đề Proxy từ khách hàng được đặt lại ở cấp độ máy chủ web

Kết luận

Các biến HTTP_PROXY, HTTPS_PROXY và NO_PROXY — một quy ước có vẻ đơn giản, nhưng trong thực tế lại bị phá vỡ bởi cách viết hoa ký tự, cú pháp ngoại lệ, ranh giới giữa các giai đoạn Docker build và Docker run, kế thừa môi trường trong docker-compose, rò rỉ bí mật trong nhật ký CI và cuối cùng, không phải thư viện nào cũng đọc các biến này. Hãy kiểm tra từng biến một cách độc lập qua curl và docker exec trước khi tìm kiếm lỗi trong logic kinh doanh của ứng dụng.

Nếu cơ sở hạ tầng của bạn phụ thuộc vào việc phân tích các trang web bên ngoài, kiểm tra geo-targeting quảng cáo hoặc vượt qua các chặn IP từ pipeline CI, quan trọng không chỉ là truyền đúng các biến môi trường mà còn chọn loại proxy phù hợp cho nhiệm vụ. Đối với việc kiểm tra lưu lượng di động và các ứng dụng kiểm tra hành vi từ các nhà mạng di động, các proxy di động sẽ phù hợp — chúng cung cấp IP từ các mạng di động thực tế và ít bị lọc bởi các hệ thống chống gian lận.