Tutorial

How to Set Up a Proxy on Linux

Proxy on Linux: the export line, the permanent files, why sudo apt ignores it, the apt, git, pip and curl settings, and the proof. Ubuntu 24.04, measured.

HProxy Team··Updated September 2, 2026·17 min read
HProxy.Tutorial

Free proxies won't hold up here.

Shared datacenter IPs get flagged and dropped fast. When it has to hold, gaming, streaming, accounts, you need mobile and residential IPs that read as a real device, from $0.44/GB, pay as you go.

See plans & pricing

On Linux the proxy is three environment variables, set in the shell you are working in. With a proxy at host and port, with or without a login, the line is:

export http_proxy="http://user:pass@host:port" https_proxy="http://user:pass@host:port" no_proxy="localhost,127.0.0.1,::1"

Both variables start with http://, even the one for HTTPS, because they name the proxy and the proxy speaks plain HTTP. After that line, curl, wget, git, pip, Python and apt (run as your own user) send their traffic through the proxy, and sudo does not, which is the part of this page most people arrive for.

Our own terminal on Ubuntu 24.04: the export line with a public proxy, env showing the three variables, curl reporting the proxy address as its origin, the curl -v lines that prove the CONNECT tunnel, sudo env showing nothing, and sudo -E env showing the variables again.
Captured on our own machine on 2 September 2026, WSL2 Ubuntu 24.04.3 with curl 8.5.0, through a public entry from our free proxy list. sudo drops the variables; sudo -E keeps them.

Everything on this page was run on 2 September 2026 on Ubuntu 24.04.3 with curl 8.5.0, wget 1.21.4, git 2.43.0, pip 24.0, Python 3.12.3, apt 2.8.3, sudo 1.9.15p5 and bash 5.2.21, through public entries from our free proxy list, with a dead port and an unroutable address for the failure cases. Where a step was not run, the desktop setting above all, the page says so.

What you need before you start

  • A proxy address in the form host:port, plus a username and password if the proxy needs a login. If your provider gave you the four values in another order, the proxy format guide sorts them out.
  • A terminal. Everything here works on a server without a desktop, inside WSL, and on a desktop.
  • For a first test, any HTTP entry from our free proxy list will do. Expect it to die within hours; the verify section shows how to tell.
toolversion measuredreads the proxy from
curl8.5.0http_proxy (lower case only), https_proxy or HTTPS_PROXY, no_proxy or NO_PROXY, ~/.curlrc, -x
wget1.21.4http_proxy, https_proxy, no_proxy (lower case; the upper-case forms were ignored), ~/.wgetrc, -e
git2.43.0the same variables through libcurl, and http.proxy in its config, which wins
pip24.0http_proxy, https_proxy, HTTPS_PROXY, pip.conf, which wins, and --proxy
Python3.12.3urllib.request.getproxies(), both cases
apt2.8.3http_proxy (the upper-case form was ignored), and Acquire::http::Proxy in apt.conf, which wins
sudo1.9.15p5none of them, unless -E is given or the variables sit in /etc/environment

How do I set the proxy for the current shell?

Step 1: export the three variables

export http_proxy="http://203.0.113.42:3128"
export https_proxy="http://203.0.113.42:3128"
export no_proxy="localhost,127.0.0.1,::1"

With a login, the URL form is http://user:pass@203.0.113.42:3128. A password with @, : or / in it has to be percent-encoded first (%40, %3A, %2F), because those characters have a meaning inside a URL.

Step 2: check that the shell has them

env | grep _proxy

Three lines come back. If nothing comes back, the export did not happen in this shell.

Step 3: test with curl

curl -s https://httpbin.org/ip

The answer carries the address the target saw. With the proxy in place it is the proxy's address; without it, your own. curl also tells you which variable it used and how it reached the site, and those lines are the proof to keep:

curl -sv -o /dev/null https://httpbin.org/ip 2>&1 | grep -E 'Uses proxy|CONNECT|established'
* Uses proxy env variable https_proxy == 'http://103.237.102.191:11111'
* CONNECT tunnel: HTTP/1.1 negotiated
> CONNECT httpbin.org:443 HTTP/1.1
< HTTP/1.1 200 Connection established
* CONNECT tunnel established, response 200

An https:// site goes through the proxy as a tunnel. The proxy opens a connection to the site, answers 200 Connection established, and from then on forwards bytes it cannot read, which RFC 9110 describes as "blind forwarding of data, in both directions". The TLS handshake happens inside that tunnel, with the site, not with the proxy.

Which tool reads which variable

The spelling matters more than the guides admit. We set each variable alone and watched who obeyed.

variable set alonecurlwgetgitpipPythonapt
http_proxy (lower case)yesyesyesyesyesyes
HTTP_PROXY (upper case), http:// targetnononot provennot testedyesno
https_proxy (lower case)yesyesyesyesyesnot relevant
HTTPS_PROXY (upper case)yesnoyesyesyesnot relevant
no_proxy (lower case)yesyesyesyesyesnot tested
NO_PROXY (upper case)yesnonot testednot testedyesnot tested

The curl manual states the rule that explains the first two rows: "The environment variables can be specified in lower case or upper case. The lower case version has precedence. http_proxy is an exception as it is only available in lower case." wget's manual names only the lower-case forms, and in our run it ignored HTTPS_PROXY and NO_PROXY outright. Python's getproxies() "scans the environment for variables named _proxy, in a case insensitive approach", so it is the one tool that does not care. Set the lower-case three and you are done; the upper-case HTTPS_PROXY is a harmless extra.

How do I make it permanent for my own user?

Add the export line to the end of ~/.bashrc and open a new terminal. bash reads that file when "an interactive shell that is not a login shell is started", per its manual. On Ubuntu, ~/.profile sources ~/.bashrc as well, so a login shell gets the line too. We proved both, and one thing more: a script started with bash file.sh does not read ~/.bashrc, so a cron job or a deployment script never sees a proxy set there. Give such scripts the variables in their own first lines, or use one of the system-wide places below.

zsh users put the same line into ~/.zshrc.

How do I make it permanent for every user?

Put three lines into /etc/environment:

http_proxy="http://203.0.113.42:3128"
https_proxy="http://203.0.113.42:3128"
no_proxy="localhost,127.0.0.1,::1"

No export, no $VAR, nothing after the value. The file "must consist of simple NAME=VALUE pairs on separate lines", says the pam_env.conf manual, and it is the PAM module pam_env that reads it, at login, when a session starts. That decides who sees it:

how the shell or command startedgot the variables from /etc/environment?
the terminal that was already openno
bash -l in that terminal (a login shell, but no PAM session)no
su - root -c (a PAM session)yes
sudo env (a PAM session with readenv=1)yes
a new WSL session started from Windowsno

The last row surprises people on WSL: Windows starts the distribution without a PAM login, so /etc/environment did nothing for a fresh wsl session in our test. On WSL, use ~/.bashrc, or a file in /etc/profile.d/.

One more trap is the advice to run source /etc/environment to apply the file in the current shell. That sets the variables inside the shell without exporting them; env did not list them and curl went direct in our run. If you must do it by hand, set -a; source /etc/environment; set +a exports everything the file sets. Log out and in again, or open a new session, and none of this is needed.

Why does sudo apt update ignore my proxy?

Because sudo starts the command with a clean environment. The sudoers manual: "By default, the env_reset flag is enabled. This causes commands to be executed with a new, minimal environment." The stock /etc/sudoers on Ubuntu carries Defaults env_reset, and our measurement matched: sudo env | grep _proxy printed nothing, sudo curl went direct, and sudo apt-get download failed against a dead proxy exactly as if no proxy had been set. Three fixes, in order of how long they last.

Fix 1: sudo -E, for this one command

sudo -E apt update

-E, --preserve-env "indicates to the security policy that the user wishes to preserve their existing environment variables", per the sudo manual. In our run sudo -E env showed the variables and sudo -E apt-get download went through the proxy. It has to be typed every time.

Fix 2: /etc/environment, for every sudo command

/etc/pam.d/sudo on Ubuntu contains session required pam_env.so readenv=1, so sudo's own session reads /etc/environment. With the three lines in that file, sudo env listed them and sudo apt-get download fetched the package through the proxy without -E. This is the fix for a machine that always sits behind one proxy.

Fix 3: an apt.conf.d file, for apt alone

Create /etc/apt/apt.conf.d/95proxies:

Acquire::http::Proxy "http://203.0.113.42:3128";
Acquire::https::Proxy "http://203.0.113.42:3128";

The semicolons are part of the syntax. Without the one at the end of a line, apt-config dump and every apt command stop with E: Syntax error /etc/apt/apt.conf.d/95proxies:2: Extra junk at end of file, which we reproduced. Confirm the file is read:

apt-config dump | grep -i proxy

The apt-transport-http manual lays out the options: "The environment variable http_proxy is supported for system wide configuration. Proxies specific to APT can be configured via the option Acquire::http::Proxy. Proxies which should be used only for certain hosts can be specified via Acquire::http::Proxy::host", and "the special value DIRECT" means no proxy for that host. It also accepts socks5h:// proxies. Two facts from the measurement decide how to use the file. The file wins over the environment: with the file pointing at a live proxy and http_proxy pointing at a dead one, apt fetched the package through the file's proxy. And a host can be exempted with DIRECT:

Acquire::http::Proxy "http://203.0.113.42:3128";
Acquire::http::Proxy::archive.ubuntu.com "DIRECT";

With the global proxy dead and that line in place, apt-get download hello still fetched from the archive directly.

How do I set the proxy for one tool only?

Each of these tools has its own setting, and where a tool has one, that setting beats the environment.

curl

For one command, -x:

curl -x http://203.0.113.42:3128 -s https://httpbin.org/ip

For every command, a line in ~/.curlrc:

proxy = "http://203.0.113.42:3128"

curl read it and went through the proxy; wget, run right after, did not, because wget does not read ~/.curlrc. To skip the proxy for one command while the variables are set, curl --noproxy '*' went direct in our run.

wget

For one command, -e executes a wgetrc command on the spot:

wget -e use_proxy=yes -e https_proxy=http://203.0.113.42:3128 -qO- https://httpbin.org/ip

For every command, ~/.wgetrc:

use_proxy = on
http_proxy = http://203.0.113.42:3128
https_proxy = http://203.0.113.42:3128
no_proxy = localhost,127.0.0.1

wget's manual gives use_proxy a second job: "When set to off, don't use proxy even when proxy-related environment variables are set." We confirmed it: with use_proxy = off in the file and https_proxy exported, wget went direct.

git

git config --global http.proxy http://203.0.113.42:3128

The git-config manual describes http.proxy as an override of "the HTTP proxy, normally configured using the http_proxy, https_proxy, and all_proxy environment variables". Override is the right word. With http.proxy pointing at a dead proxy and https_proxy exported to a live one, git ls-remote failed on the dead one. And an empty value switches the proxy off for git alone: with http.proxy "" set and https_proxy pointing at the dead proxy, git connected to github.com directly. The errors git prints on a broken proxy are in git proxy settings and errors.

pip

pip's user guide lists three ways: --proxy on the command line, proxy in a configuration file, and "the standard environment-variables http_proxy, https_proxy and no_proxy". The file wins here too. With pip config set global.proxy pointing at a dead proxy and https_proxy exported to a live one, pip download failed with Could not find a version that satisfies the requirement, which is pip's way of saying it never reached the index. For one command:

pip download --proxy http://203.0.113.42:3128 --no-deps six

That error text is worth remembering. pip does not say "proxy"; it says no version was found. The full list of what it prints and why is in pip proxy errors.

Python

urllib.request.getproxies() returned both entries from the lower-case variables, and proxy_bypass("httpbin.org") returned True with no_proxy=httpbin.org set, so a script that uses urllib or requests follows the same variables as curl, in either case.

How do I keep local addresses off the proxy?

no_proxy is the list of hosts that never go through the proxy. Three forms were tested:

entrycurlwgetPythonmeaning
httpbin.orgbypassedbypassedbypassedthe host and its subdomains
.orgbypassedbypassednot testedevery host ending in .org
*bypassednot testednot testedeverything; curl's manual: "If set to an asterisk '*' only, it matches all hosts"

Why localhost,127.0.0.1,::1 belongs in the list: we started a tiny server on 127.0.0.1:8000 and requested it with http_proxy set and no no_proxy. The request went to the proxy, and the proxy answered with its own 503 error page, because it could not reach our machine's localhost. With no_proxy=localhost,127.0.0.1,::1 the server answered hello from the local server. Every developer who has wondered why a local API stopped answering after a proxy was set has met this. curl's manual adds that the list "can also be include numerical IP addresses" and, since curl 7.86, CIDR ranges such as 192.168.0.0/16.

What about the desktop setting?

GNOME keeps its own proxy setting, separate from the variables. GNOME's help puts it at Settings, Network, then "Select Network proxy from the list on the left", and describes the Manual mode as: "For each proxied protocol, define the address of a proxy and port for the protocols. The protocols are HTTP, HTTPS, FTP and SOCKS." The same setting from a terminal, using the keys of the org.gnome.system.proxy schema, whose mode "supported values are none, manual, auto":

gsettings set org.gnome.system.proxy mode 'manual'
gsettings set org.gnome.system.proxy.http host '203.0.113.42'
gsettings set org.gnome.system.proxy.http port 3128
gsettings set org.gnome.system.proxy.https host '203.0.113.42'
gsettings set org.gnome.system.proxy.https port 3128

gsettings set org.gnome.system.proxy mode 'none' switches it off. This setting reaches GNOME applications and browsers that follow it; it does not set the variables in a terminal, and a terminal that was open before the change keeps its old environment. We did not run the desktop steps: the machine behind this page runs Ubuntu inside WSL, which has no desktop session, so the GNOME part is documented from GNOME's help and the installed schema and nothing more. KDE Plasma has an equivalent panel in its System Settings; we did not measure it.

What about Docker, snap and services?

Programs that start outside your login session never read your shell files, so each has its own place for the proxy.

  • Docker daemon. Docker's documentation: "Create a file named /etc/systemd/system/docker.service.d/http-proxy.conf that adds the HTTP_PROXY environment variable" under [Service], then systemctl daemon-reload and restart Docker. Builds and containers read a different setting, ~/.docker/config.json with a proxies block, per the CLI documentation.
  • snap. sudo snap set system proxy.http="http://203.0.113.42:3128" and the same for proxy.https. snapd's own source writes those values into /etc/environment, so setting them there by hand and through snap ends in the same file.
  • A systemd service. An Environment= line in the unit, which per systemd.exec "sets environment variables for executed processes", with the whole assignment in quotes: Environment="http_proxy=http://203.0.113.42:3128".

None of the three was exercised on our WSL machine (no daemon, no snapd, no service behind a proxy), so this section rests on the vendors' documentation and not on a measurement.

How do I prove the proxy really carries my traffic?

Three checks, in the order they catch things.

  1. env | grep _proxy in the shell that will run the program. No lines means no proxy for that shell.
  2. curl -s https://httpbin.org/ip and compare with the same command after curl --noproxy '*'. Different addresses mean the proxy carried the request; the same address means it went direct.
  3. curl -sv -o /dev/null https://httpbin.org/ip 2>&1 | grep -E 'Uses proxy|CONNECT|established' for the proof lines shown at the top of the page. wget shows the same with -d: CONNECT httpbin.org:443 HTTP/1.1 and proxy responded with: [HTTP/1.1 200 Connection established.

Then the check that matters for a public entry: does it still work a minute later? Of 40 fresh entries pulled from our list at 14:07 UTC on the day of the measurement, 21 carried an HTTPS tunnel, 15 carried plain HTTP and 12 did both, and the entries we used for the runs were dead within hours. Our proxy checker runs the full battery, exit address, anonymity and speed, in one paste, and the free proxy list shows a last-checked time for every entry. When the work has to keep running, our residential pool gives you one address that stays alive, and nothing on this page changes.

What does each failure look like?

Every line below is from our runs on 2 September 2026.

what you didcurl 8.5.0wget 1.21.4git 2.43.0pip 24.0apt 2.8.3
proxy on a closed port(7) Failed to connect to 127.0.0.1 port 9 after 0 ms: Couldn't connect to serverConnecting to 127.0.0.1:9... failed: Connection refused., exit 4fatal: unable to access '...': Failed to connect to 127.0.0.1 port 9 after 0 ms: Couldn't connect to server, exit 128Could not find a version that satisfies the requirement, exit 1Could not connect to 127.0.0.1:9 (127.0.0.1). - connect (111: Connection refused), exit 100
proxy that never answers(28) Connection timed out after 8000 millisecondsfailed: Connection timed out. Giving up., exit 4
https:// written in https_proxy for a plain proxy(35) OpenSSL/3.0.13: error:0A00010B:SSL routines::wrong version numberworked, one run, unexplained by the manual
a proxy that carries http:// but refuses the tunnelhttp 200, https (56) CONNECT tunnel failed, response 403Proxy tunneling failed: Proxy ErrorUnable to establish SSL connection., exit 4
socks5h:// written for an HTTP proxy(28) Connection timed out after 20000 milliseconds

Two of those deserve a sentence. The https:// scheme mistake is the most common one in the guides, and curl's error names TLS, not the proxy, so people chase certificates. And the tunnel refusal, 403 to CONNECT, comes from a live proxy that carries plain HTTP and nothing else; our probe found 15 of 40 fresh entries like that. The wider curl catalogue is in how to fix curl proxy errors, and Firefox's version of a refused proxy in the proxy server is refusing connections.

How do I turn it off again?

Each place is independent, so undo each one you used.

unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY

Then remove the lines from ~/.bashrc or /etc/environment, delete /etc/apt/apt.conf.d/95proxies, and clear the tool settings:

git config --global --unset http.proxy
pip config unset global.proxy
gsettings set org.gnome.system.proxy mode 'none'

env | grep _proxy, apt-config dump | grep -i proxy, git config --global --list and pip config list should all come back empty. A forgotten setting keeps sending traffic to a proxy that is gone, and the symptom is a machine that seems to have lost the internet while the network is fine.

Where to go from here

How to use proxies with curl goes deeper into -x, authentication and SOCKS. How to use proxychains forces a program through a SOCKS proxy when it ignores the variables. The proxy format guide sorts host, port, username and password into the URL form. When a proxy asks for a login and the tool cannot give one, how to fix 407 Proxy Authentication Required is the next page.

Sources

  • curl 8.5.0 manual page, sections ENVIRONMENT, PROXY PROTOCOL PREFIXES and EXIT CODES (installed on Ubuntu 24.04, read 2 September 2026).
  • GNU Wget 1.21.4 manual page and the Wgetrc Commands section of the info manual (installed, read 2 September 2026).
  • apt-transport-http(1), apt 2.8.3, Proxy Configuration, and apt.conf(5) (installed, read 2 September 2026).
  • sudoers(5) and sudo(8), sudo 1.9.15p5, env_reset and --preserve-env (installed, read 2 September 2026); the stock /etc/sudoers and /etc/pam.d/sudo of Ubuntu 24.04.
  • pam_env.conf(5), libpam-modules 1.5.3, on /etc/environment (installed, read 2 September 2026).
  • bash(1) 5.2.21, INVOCATION (installed, read 2 September 2026).
  • git-config(1), git 2.43.0, http.proxy (installed, read 2 September 2026).
  • systemd.exec(5), systemd 255, Environment= (installed, read 2 September 2026).
  • GNOME Help, Connect to a proxy server (help.gnome.org, read 2 September 2026), and the org.gnome.system.proxy GSettings schema as installed.
  • pip documentation, User Guide, Using a Proxy Server (pip.pypa.io, read 2 September 2026).
  • Python documentation, urllib.request, getproxies (docs.python.org, read 2 September 2026).
  • Docker documentation, Configure the daemon to use a proxy and Configure the Docker client to use a proxy (docs.docker.com, read 2 September 2026).
  • snapd source, overlord/configstate/configcore/proxy.go (github.com/canonical/snapd, read 2 September 2026).
  • RFC 9110, HTTP Semantics, section 9.3.6 CONNECT (IETF, June 2022).
  • Our own measurements on 2 September 2026: measure1.sh, measure1b.sh and measure2.sh on WSL2 Ubuntu 24.04.3 against public entries from hproxy.com/free-proxy-list, a closed port and an unroutable address; a probe of 40 fresh entries; the terminal capture above.

Frequently asked questions

What is the command to set a proxy on Linux?
export http_proxy="http://host:port" https_proxy="http://host:port", with user:pass@ in front of the host when the proxy needs a login, and no_proxy="localhost,127.0.0.1,::1" next to them. That covers curl, wget, git, pip, apt run without sudo, and Python. It lasts until the terminal closes; we measured every one of those programs on Ubuntu 24.04.
Why does sudo apt update ignore my proxy?
Because sudo starts the command with a fresh environment: env_reset is on in the stock sudoers file, and in our test sudo env showed no proxy variable at all. Three fixes work: sudo -E, which keeps your environment; the three lines in /etc/environment, which sudo reads through PAM; or an apt.conf.d file, which apt obeys over every variable.
How do I set a proxy permanently on Linux?
For one user, add the export line to the end of ~/.bashrc; a new interactive terminal reads it, and root's ~/.profile pulls it into login shells too. For every user, put http_proxy, https_proxy and no_proxy into /etc/environment as plain NAME=value lines. That file is read by PAM at login, by su - and by sudo, not by the shell you already have open.
Do I write https:// in https_proxy?
No. Both variables name the proxy, and the proxy speaks plain HTTP, so both start with http://. With https:// in https_proxy, curl 8.5 failed with SSL routines wrong version number in our test, because it tried to open TLS to a proxy that expects a plain request.
Upper case or lower case, HTTP_PROXY or http_proxy?
Lower case, always, and add the upper-case HTTPS_PROXY if you like. curl reads http_proxy in lower case only. wget ignored an upper-case HTTPS_PROXY and an upper-case NO_PROXY in our test. Python reads both. apt ignored an upper-case HTTP_PROXY. Lower case is the one spelling every tool agreed on.
How do I stop localhost going through the proxy?
Set no_proxy="localhost,127.0.0.1,::1". Without it, a request to a server on your own machine went to the proxy and came back as a 503 error page from the proxy in our test; with it, the local server answered. A name in the list matches the host and its subdomains; .org with a leading dot matches every .org host; a single * switches the proxy off for everything in curl.
My password has an @ in it and the proxy stops working. Why?
In the URL form, @ separates the login from the host, so a password containing one splits the URL in the wrong place. Percent-encode it: @ becomes %40, a colon becomes %3A and a slash becomes %2F. The password is right and the URL is wrong, which is why it is hard to spot.
How do I turn the proxy off again?
unset http_proxy https_proxy no_proxy in the shell, then remove the lines from ~/.bashrc or /etc/environment, delete the apt.conf.d file, run git config --global --unset http.proxy and pip config unset global.proxy, and set the desktop mode back to none. Each place is independent, and a forgotten one keeps sending traffic to a proxy that is gone.

Proxies that don't die mid-job

Residential, ISP, datacenter and mobile, verified by the same engine that runs tens of millions of checks. They read as a real device and hold up under load. Pay as you go, and your balance never expires. $0.44/GB is the 2,000 GB+ rate; a single gigabyte is $0.50/GB, with no minimum order.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup

HProxy.

Honest guides and comparisons on proxies, scraping and staying unblocked, from the team that runs the network.

RSS feed