← Back to Blog

Proxies in GitHub Actions and CI/CD Pipelines: Complete Guide with Code Examples

We explore how to connect a proxy to a GitHub Actions workflow so that automated tasks do not get blocked and operate from the desired region.

πŸ“…July 20, 2026
```html

GitHub Actions is a powerful automation tool: it runs tests, deploys applications, collects data, and performs dozens of other tasks. But as soon as the workflow starts accessing external resources β€” marketplaces, advertising platforms, foreign APIs β€” it immediately encounters geo-blocks and IP limits. The solution is simple: connect a proxy directly in the pipeline.

Why Use a Proxy in GitHub Actions: Real Scenarios

Many teams use GitHub Actions not only for code deployment but also for automating business tasks: monitoring competitor prices, collecting data from marketplaces, automatically checking advertising accounts, and testing websites from different regions. All these tasks share one problem β€” the GitHub Actions runner has a fixed IP from the Microsoft Azure range, and many services block or restrict it.

Here are specific situations where a proxy is indispensable:

  • Scraping Wildberries, Ozon, Avito β€” these platforms have long blacklisted IP ranges from cloud providers. A request from the GitHub Actions runner will be blocked or receive a CAPTCHA after just 2–3 attempts.
  • Geo-targeted Testing β€” marketers and QA engineers check how the website or ads appear to users from Moscow, Berlin, or New York. Without a proxy, the runner will always "see" content for one region.
  • Working with Region-Limited APIs β€” some APIs (e.g., regional versions of Google Ads, Facebook Marketing API with specific settings) return different data depending on the request's geolocation.
  • Competitor Monitoring β€” automated collection of prices, promotions, and assortments requires regular requests that are easily detected by the repeating data center IP.
  • Automating Ad Checks β€” arbitrageurs and performance marketers run automated checks on ad statuses, balances, and metrics through scripts in CI/CD.
  • Integration Tests with External Services β€” some services block requests from Azure ranges for security reasons, causing tests to fail without explanation.

In all these cases, a proxy radically solves the problem: the workflow begins to look like a request from a regular user in the desired city, not from a Microsoft cloud server.

How GitHub Actions Works with the Network

Before setting up a proxy, it's important to understand the network architecture in GitHub Actions. When you run a workflow on the standard ubuntu-latest runner, the task runs on a virtual machine within the Microsoft Azure infrastructure. Each such machine has a public IP from the Azure ranges β€” and this is the IP that external services see.

Key features of the network in GitHub Actions:

  • IP changes with each run β€” but remains within known Azure ranges, which are easily detectable.
  • No built-in proxy support β€” GitHub does not provide a native mechanism for proxying traffic.
  • Environment variables work globally β€” if you set HTTP_PROXY at the job level, all steps within that job will use the proxy.
  • Self-hosted runners β€” an alternative where you run a runner on your own server. In this case, the proxy is configured at the server level, not the workflow.

For most tasks, the optimal approach is to set up the proxy through environment variables directly in the workflow file (.github/workflows/your-workflow.yml). This is a universal method that works for most tools: curl, wget, Python requests, Node.js http, Go net/http, and others.

Which Type of Proxy to Choose for CI/CD

The choice of proxy type depends on the task. For CI/CD pipelines, three options are relevant, each with its niche:

Proxy Type For Which Tasks Speed Trust Level
Residential Proxies Scraping secure websites, geo-targeting, marketplace monitoring Average High β€” real home IPs
Mobile Proxies Testing mobile versions, working with social media, Facebook/TikTok API Average Maximum β€” carrier IPs
Datacenter Proxies Integration tests, requests to unsecured APIs, high load High Medium

Practical Rule: if your workflow scrapes Wildberries, Ozon, or other marketplaces with anti-bot protection β€” use residential proxies. If testing Facebook Ads or TikTok Ads accounts β€” use mobile proxies. For simple integration tests and requests to open APIs, datacenter proxies are sufficient: they are faster and cheaper.

πŸ’‘ Important about Protocols

For GitHub Actions, it is preferable to use HTTP/HTTPS proxies β€” they are supported by most tools without additional configuration. SOCKS5 also works, but requires explicit specification in each tool. If your provider supports both protocols β€” start with HTTP.

Setting Up Proxy via Environment Variables

The most universal way to connect a proxy in GitHub Actions is to set the standard environment variables HTTP_PROXY, HTTPS_PROXY, and NO_PROXY. Most command-line tools and programming languages automatically pick them up.

The basic structure of a workflow with a proxy looks like this:

name: Workflow with Proxy

on:
  schedule:
    - cron: '0 9 * * *'
  workflow_dispatch:

jobs:
  scrape-data:
    runs-on: ubuntu-latest

    env:
      HTTP_PROXY: http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}
      HTTPS_PROXY: http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}
      NO_PROXY: localhost,127.0.0.1,github.com

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Check current IP (for verification)
        run: curl -s https://api.ipify.org

      - name: Run main script
        run: python scripts/scraper.py

Note the NO_PROXY block β€” you need to add addresses that should not have their traffic proxied. At a minimum, this includes localhost and 127.0.0.1. It is also recommended to add github.com so that operations with the repository (checkout, push) go directly.

If the proxy does not require authentication (just IP and port), the format simplifies:

env:
  HTTP_PROXY: http://203.0.113.10:8080
  HTTPS_PROXY: http://203.0.113.10:8080
  NO_PROXY: localhost,127.0.0.1

For SOCKS5 proxies, only the scheme in the URL changes:

env:
  HTTP_PROXY: socks5://user:password@proxy-host:1080
  HTTPS_PROXY: socks5://user:password@proxy-host:1080

Proxy for curl, wget, and HTTP Requests in Shell

If the environment variables are set at the job level (as shown above), curl and wget will automatically pick them up. However, sometimes you need to explicitly pass the proxy β€” for example, for a specific step or during debugging.

Explicitly specifying the proxy in curl:

- name: Fetch data with proxy
  run: |
    curl -x http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }} \
      -s \
      -o output.json \
      https://api.example.com/data

    # Check through the proxy
    curl -x http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }} \
      -s https://api.ipify.org?format=json

For wget:

- name: Download with wget via proxy
  run: |
    wget -e "https_proxy=http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}" \
      -q \
      -O data.html \
      https://target-site.com/page

A useful step for debugging is to add an IP address check at the beginning of the workflow. If the proxy is working correctly, you will see the proxy server's IP instead of Azure:

- name: Verify proxy is active
  run: |
    echo "=== IP without proxy ==="
    curl -s --noproxy '*' https://api.ipify.org || echo "Direct request failed"
    echo ""
    echo "=== IP through proxy ==="
    curl -s https://api.ipify.org

Proxy in Python Scripts within the Workflow

Python is one of the most popular languages for scripting in CI/CD. The requests library automatically reads the HTTP_PROXY and HTTPS_PROXY environment variables if they are set. However, for more flexible control, it is better to pass the proxy explicitly.

Example of a Python script with explicit proxy passing through environment variables:

import os
import requests

# Read proxy data from environment variables
proxy_host = os.environ.get('PROXY_HOST')
proxy_port = os.environ.get('PROXY_PORT')
proxy_user = os.environ.get('PROXY_USER')
proxy_pass = os.environ.get('PROXY_PASS')

proxies = {
    'http': f'http://{proxy_user}:{proxy_pass}@{proxy_host}:{proxy_port}',
    'https': f'http://{proxy_user}:{proxy_pass}@{proxy_host}:{proxy_port}',
}

# Use the proxy in the request
response = requests.get(
    'https://www.wildberries.ru/catalog/123456/detail.aspx',
    proxies=proxies,
    timeout=30,
    headers={
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    }
)

print(f"Status: {response.status_code}")
print(f"Content length: {len(response.content)}")

In the workflow file, you need to pass the variables as separate secrets (not as a full URL) so that the script can collect them:

- name: Run Python scraper
  env:
    PROXY_HOST: ${{ secrets.PROXY_HOST }}
    PROXY_PORT: ${{ secrets.PROXY_PORT }}
    PROXY_USER: ${{ secrets.PROXY_USER }}
    PROXY_PASS: ${{ secrets.PROXY_PASS }}
  run: python scripts/scraper.py

For working with Playwright or Selenium in Python, the proxy configuration is slightly different:

# Playwright
from playwright.sync_api import sync_playwright
import os

proxy_url = f"http://{os.environ['PROXY_USER']}:{os.environ['PROXY_PASS']}@{os.environ['PROXY_HOST']}:{os.environ['PROXY_PORT']}"

with sync_playwright() as p:
    browser = p.chromium.launch(
        proxy={
            "server": proxy_url
        }
    )
    page = browser.new_page()
    page.goto("https://target-site.com")
    # ... further logic
    browser.close()

Proxy in Node.js and npm Tasks

Node.js does not automatically read the system variables HTTP_PROXY β€” you need to either use special libraries or configure the proxy explicitly. The most convenient option is the https-proxy-agent package or axios with proxy configuration.

// Using axios
const axios = require('axios');

const proxyConfig = {
  host: process.env.PROXY_HOST,
  port: parseInt(process.env.PROXY_PORT),
  auth: {
    username: process.env.PROXY_USER,
    password: process.env.PROXY_PASS
  }
};

async function fetchData(url) {
  try {
    const response = await axios.get(url, {
      proxy: proxyConfig,
      timeout: 30000,
      headers: {
        'User-Agent': 'Mozilla/5.0 (compatible; MyBot/1.0)'
      }
    });
    return response.data;
  } catch (error) {
    console.error(`Request failed: ${error.message}`);
    throw error;
  }
}

fetchData('https://api.example.com/prices')
  .then(data => console.log(JSON.stringify(data, null, 2)))
  .catch(() => process.exit(1));

For npm commands (for example, if npm tries to download packages through a corporate proxy), the configuration is simpler:

- name: Configure npm proxy
  run: |
    npm config set proxy http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}
    npm config set https-proxy http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}

- name: Install dependencies
  run: npm install

- name: Reset npm proxy (cleaning up after use)
  run: |
    npm config delete proxy
    npm config delete https-proxy

Secure Storage of Proxy Data in GitHub Secrets

Never store proxy data (host, port, username, password) directly in the workflow file in plain text. This is a serious security mistake: workflow files are stored in the repository and can be visible to all project participants or even publicly.

The right approach is GitHub Secrets. Here’s a step-by-step guide:

  1. Open the repository on GitHub
  2. Go to Settings β†’ Secrets and variables β†’ Actions
  3. Click New repository secret
  4. Create four secrets: PROXY_HOST, PROXY_PORT, PROXY_USER, PROXY_PASS
  5. In the workflow, refer to them using the syntax ${{ secrets.PROXY_HOST }}

πŸ”’ Additional Security Measures

  • Use Environment secrets instead of Repository secrets if different environments (staging/production) use different proxies
  • Restrict access to secrets through Environment protection rules β€” require manual approval for production
  • Regularly rotate proxy credentials β€” change passwords every 30–90 days
  • Do not output secret values to logs using echo β€” GitHub masks them automatically, but it's better not to take risks

If you are using rotating proxies (where the IP changes with each request or on a schedule), it is often sufficient to store only one endpoint β€” the proxy provider manages the pool of IPs itself. In this case, the secrets will only contain one host and port for the rotation gateway.

Proxy Rotation and Error Handling in the Pipeline

Even high-quality proxies can sometimes fail: the IP may get temporarily banned, the session may drop, or the server may not respond. For CI/CD pipelines that operate automatically without supervision, it is important to anticipate handling such situations.

Strategy 1: Retry with the Same Proxy

import requests
import time
import os

def fetch_with_retry(url, max_retries=3, delay=5):
    proxies = {
        'http': f"http://{os.environ['PROXY_USER']}:{os.environ['PROXY_PASS']}@{os.environ['PROXY_HOST']}:{os.environ['PROXY_PORT']}",
        'https': f"http://{os.environ['PROXY_USER']}:{os.environ['PROXY_PASS']}@{os.environ['PROXY_HOST']}:{os.environ['PROXY_PORT']}",
    }

    for attempt in range(max_retries):
        try:
            response = requests.get(url, proxies=proxies, timeout=30)
            response.raise_for_status()
            return response
        except requests.exceptions.RequestException as e:
            print(f"Attempt {attempt + 1} failed: {e}")
            if attempt < max_retries - 1:
                print(f"Retrying in {delay} seconds...")
                time.sleep(delay)
                delay *= 2  # Exponential backoff
    raise Exception(f"All {max_retries} attempts failed for {url}")

Strategy 2: List of Proxies with Switching

If you have multiple proxy servers, you can store their list in one secret (comma-separated) and switch upon error:

import os
import requests
import random

# The PROXY_LIST secret contains: "host1:port1:user1:pass1,host2:port2:user2:pass2"
proxy_list_raw = os.environ.get('PROXY_LIST', '').split(',')

def parse_proxy(proxy_str):
    parts = proxy_str.strip().split(':')
    if len(parts) == 4:
        host, port, user, password = parts
        return {
            'http': f'http://{user}:{password}@{host}:{port}',
            'https': f'http://{user}:{password}@{host}:{port}',
        }
    return None

proxies = [p for p in [parse_proxy(raw) for raw in proxy_list_raw] if p]

def fetch_with_proxy_rotation(url):
    random.shuffle(proxies)  # Random order
    for proxy in proxies:
        try:
            response = requests.get(url, proxies=proxy, timeout=20)
            if response.status_code == 200:
                return response
        except Exception as e:
            print(f"Proxy failed: {e}, trying next...")
    raise Exception("All proxies exhausted")

Strategy 3: Using a Rotating Endpoint

The simplest option is to use a proxy provider with a single rotating gateway. In this case, you connect to one address, and the provider automatically issues different IPs from the pool. No rotation logic is needed in the code β€” just one connection line is sufficient.

Real Scenarios: Scraping, Testing, Price Monitoring

Let's consider three specific scenarios that are most commonly encountered by teams using GitHub Actions with proxies.

Scenario 1: Daily Price Monitoring on Wildberries

Marketplace sellers often set up automatic collection of competitor prices. The workflow runs on a schedule (e.g., every morning at 7:00), collects data, and saves it to Google Sheets or sends it to Telegram.

name: Daily Price Monitor

on:
  schedule:
    - cron: '0 4 * * *'  # 07:00 MSK (UTC+3)

jobs:
  monitor-prices:
    runs-on: ubuntu-latest

    env:
      HTTP_PROXY: http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}
      HTTPS_PROXY: http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}
      NO_PROXY: github.com,api.github.com

    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install requests beautifulsoup4 gspread

      - name: Run price scraper
        env:
          GOOGLE_SHEETS_KEY: ${{ secrets.GOOGLE_SHEETS_KEY }}
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
        run: python scripts/price_monitor.py

      - name: Upload results artifact
        uses: actions/upload-artifact@v4
        with:
          name: price-data-${{ github.run_id }}
          path: output/prices.json

Scenario 2: Geo-targeted Website Testing

Marketers and QA teams use proxies to check how the website or ads appear to users from different cities. This is especially relevant for checking regional prices, content, and redirects.

name: Geo-targeted Site Tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test-moscow:
    runs-on: ubuntu-latest
    name: Test from Moscow
    steps:
      - uses: actions/checkout@v4
      - name: Run geo tests (RU/Moscow proxy)
        env:
          HTTP_PROXY: http://${{ secrets.PROXY_RU_USER }}:${{ secrets.PROXY_RU_PASS }}@${{ secrets.PROXY_RU_HOST }}:${{ secrets.PROXY_RU_PORT }}
          HTTPS_PROXY: http://${{ secrets.PROXY_RU_USER }}:${{ secrets.PROXY_RU_PASS }}@${{ secrets.PROXY_RU_HOST }}:${{ secrets.PROXY_RU_PORT }}
        run: |
          python tests/geo_test.py --region=RU --city=Moscow

  test-germany:
    runs-on: ubuntu-latest
    name: Test from Germany
    steps:
      - uses: actions/checkout@v4
      - name: Run geo tests (DE proxy)
        env:
          HTTP_PROXY: http://${{ secrets.PROXY_DE_USER }}:${{ secrets.PROXY_DE_PASS }}@${{ secrets.PROXY_DE_HOST }}:${{ secrets.PROXY_DE_PORT }}
          HTTPS_PROXY: http://${{ secrets.PROXY_DE_USER }}:${{ secrets.PROXY_DE_PASS }}@${{ secrets.PROXY_DE_HOST }}:${{ secrets.PROXY_DE_PORT }}
        run: |
          python tests/geo_test.py --region=DE

Scenario 3: Automatic Check of Ad Accounts

Arbitrageurs and performance marketers often use GitHub Actions for automatic checks of Facebook Ads account statuses, balances, and metrics. Requests to the Facebook Marketing API from Azure ranges may trigger additional security checks β€” the proxy helps to bypass this.

name: Ad Account Health Check

on:
  schedule:
    - cron: '*/30 6-22 * * *'  # Every 30 minutes from 6 to 22 MSK

jobs:
  check-accounts:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install requests

      - name: Check Facebook Ads accounts
        env:
          HTTP_PROXY: http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}
          HTTPS_PROXY: http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}
          FB_ACCESS_TOKEN: ${{ secrets.FB_ACCESS_TOKEN }}
          ACCOUNT_IDS: ${{ secrets.FB_ACCOUNT_IDS }}
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
        run: python scripts/check_fb_accounts.py

πŸ“‹ Checklist Before Running the Workflow with Proxy

  • βœ… Proxy data added to GitHub Secrets (not in the workflow file)
  • βœ… The NO_PROXY variable includes github.com
  • βœ… An IP check step added for debugging
  • βœ… Error handling and retry logic implemented
  • βœ… Proxy type corresponds to the task (residential for secure sites)
  • βœ… Error notifications configured (Telegram, Slack, or email)
  • βœ… Workflow tested manually via workflow_dispatch before adding a schedule

Conclusion

Setting up a proxy in GitHub Actions is not a difficult task if you know the right approach. Key takeaways from this guide:

  • Environment Variables HTTP_PROXY / HTTPS_PROXY β€” a universal method that works for most tools without changing the code.
  • GitHub Secrets β€” the only correct place to store proxy credentials.
  • Proxy Type Matters: for scraping secure marketplaces, residential IPs are needed; for advertising platforms β€” mobile proxies; for simple API requests, datacenter proxies will suffice.
  • Retry Logic is Essential for pipelines that operate unsupervised on a schedule.
  • IP Check Step at the beginning of the workflow will save hours of debugging.

If your GitHub Actions workflow interacts with marketplaces, advertising platforms, or any services with anti-bot protection, we recommend using residential proxies β€” they have real home user IPs and are significantly less likely to trigger blocks compared to the cloud addresses of GitHub servers. For tasks related to Facebook Ads, TikTok, or other social platforms, the optimal choice will be mobile proxies with carrier IPs β€” they provide the highest level of trust from the platforms.

```