cd /news/developer-tools/one-browser-per-country-running-seve… · home › topics › developer-tools › article
[ARTICLE · art-153966] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=↑ positive

One Browser per Country: Running Several VPN Locations Simultaneously

A developer built a Docker-based setup that runs multiple VPN connections simultaneously on a single Mac, letting each browser window or application route through a different country via its own local proxy port. The system uses Gluetun containers as per-country sandboxes, each exposing HTTP and SOCKS5 proxy ports, and currently runs seven countries in parallel while the host machine stays on its home connection. The setup was tested on macOS with Google Chrome and ExpressVPN and works with any provider Gluetun supports.

read17 min views1 publishedOct 20, 2026

There is always a case where I would like to use a VPN in order to browse some sites from other countries: it could be because I am comparing prices, looking at local products and other reasons. However, I do have numerous other tabs currently open and with work in progress. These would get disrupted if a computer network-wide VPN is used which switches the country for all processes on the computer. I do not want this disruption. Most (all?) common VPN apps provide the network-wide functionality.

I worked with Claude Code to mitigate this disruption based on some past ideas. Claude made the execution of the ideas immensely fast!

Let us take an abstract scenario: I want three browser windows on one Mac: one Poland, one in Germany and one in the Netherlands, all open simultaneously. My normal existing browser(s) had to stay on my home connection. A normal VPN app cannot do this, so what do we do? A naive answer is to build a smalI 'router' with Docker that can be set up with multiple VPNs.

The core aspect is to build a set of sandboxes. Each sandbox is a VPN connection to one country. Each sandbox has its own local proxy port. A browser, a command-line tool or another application uses a sandbox when you point it at that port. The setup now runs seven countries in parallel on one laptop.

This post explains the design with diagrams, from the full picture down to a single network request. It also describes the problems that I found on the way, because those problems teach more than the parts that worked at once.

Most of this post is written using Claude but verified by a human (me).

We discuss:

The code is on GitHub (link at the end). The setup is tested on macOS with Google Chrome and ExpressVPN. It can use any VPN provider that the open-source program Gluetun supports.

This post uses each term below with one meaning only.

Term Definition
VPN (virtual private network) An encrypted connection from your computer to a server of a VPN provider. Websites see the address of that server, not your home address.
VPN provider A company that operates VPN servers in many countries, for example ExpressVPN, NordVPN or Mullvad.
IP address A number that identifies a device on a network, for example 203.0.113.10 . A website sees the IP address that a request comes from.
Tunnel The encrypted path between your computer and the VPN server. Traffic goes into the tunnel at one end and comes out decrypted at the other end.
Proxy A program that receives network traffic from an application and sends the traffic on for the application.
HTTP proxy A proxy type that browsers and most tools support. It carries web traffic (HTTP and HTTPS).
SOCKS5 proxy A more general proxy type. Some applications support only SOCKS5.
Port A number that identifies one service on a computer, for example 8889 . Each proxy in this setup listens on its own port.
Loopback address The address 127.0.0.1 . Only programs on the same computer can reach it.
Container A small, isolated Linux system that Docker runs. Each container has its own network interfaces, routes and firewall.
Docker Desktop The Mac application that runs containers. It runs them inside a hidden Linux virtual machine.
Gluetun An open-source program that runs in a container. It connects to a VPN provider and offers proxies to other programs.
Sandbox In this post: one Gluetun container that connects to one country, with one HTTP proxy port and one SOCKS5 proxy port.
Browser profile The folder where a browser keeps its cookies, history, logins and settings. Two profiles do not share cookies.
Browser instance One running copy of a browser application. Each instance in this setup has its own profile and its own proxy.
DNS (Domain Name System) The service that changes a name, for example example.com , into an IP address.
TLS The encryption that HTTPS uses between a browser and a website.
Kill switch A rule that blocks all traffic when the VPN tunnel does not work, so that no traffic leaves without encryption.
Split tunneling A VPN app feature that sends some applications through the VPN and other applications directly to the internet.
Browser fingerprint A set of facts that a website reads with JavaScript, for example the screen size, the fonts and the graphics hardware. Together, these facts can identify one computer.
Geolocation database A list that a website uses to find the country of an IP address. Different companies make different lists, and the lists do not always agree.

A VPN app, split tunneling and a proxy extension all fail for the same reason: they choose one route for a whole application or a whole browser profile, never for one window.

Each arrow points at the finest level that a tool can give its own route. Only separate browser instances reach the single window.

A VPN app changes the routes of the whole computer. When the app connects to Germany, every application uses Germany. The app has one connection to one country at a time.

Split tunneling chooses by application. You can tell the app "Chrome uses the VPN, Mail does not". But two Chrome windows are the same application, Google Chrome.app. Split tunneling cannot put one window in Poland and another window in Germany.

A browser extension can change the proxy of the browser with the Chrome chrome.proxy interface. Chrome applies that proxy to all tabs and windows of the profile. An extension can switch the profile from Poland to Germany, or send some websites to Germany, but it cannot give each tab its own country. The cookies also stay the same when the proxy changes, so a website can link your visits from the different countries.

The solution needs two separations at the same time: a separate network path for each country, and a separate browser profile for each country.

Each country gets three parts of its own: a Gluetun container with its own VPN tunnel, a proxy port on the Mac, and a browser profile. Your normal applications keep the direct route.

Sandbox traffic goes through a local port into its own container and its own tunnel. Direct traffic never touches the containers.

The parts for the Germany sandbox:

Part Value for Germany Purpose
Container vpn-sandbox-germany Runs Gluetun. Holds the VPN tunnel to a German server.
HTTP proxy port 127.0.0.1:8889 Browsers and most tools send their traffic here.
SOCKS5 proxy port 127.0.0.1:1081 For applications that need SOCKS5.
Browser profile ~/.vpn-sandbox/profiles/germany-chrome Keeps the cookies and logins of the German browser instance apart from all others.

Three design choices matter:

sandboxes.conf holds the name, provider, country, ports, time zone and default browser. To add a country, you add one line. A Chrome instance uses a sandbox through two start options. --user-data-dir gives the instance its own profile folder. Chrome starts a separate instance for a separate profile folder, so the second option applies to that instance only: --proxy-server=http://127.0.0.1:8889.

Inside each container, the routing table sends all internet traffic into the tunnel, and the firewall blocks everything else. Together they make a kill switch.

A network interface is a connection point between a system and a network. The container has two:

eth0 connects the container to its Docker network. Requests from the Mac come in on tun0 is the tunnel. Traffic that goes into A request enters on eth0, passes the proxy and the routing table into the tunnel, and leaves eth0 again as encrypted UDP. The values come from the running Germany container.

A routing table tells the system which interface to use for each destination address. I read these values from the running Germany container:

Destination Interface Purpose
0.0.0.0/1 (addresses 0.x.x.x to 127.x.x.x) tun0 The first half of all internet addresses goes into the tunnel.
128.0.0.0/1 (addresses 128.x.x.x to 255.x.x.x) tun0 The second half goes into the tunnel.
The VPN server address only eth0 The encrypted packets reach the server outside the tunnel. Without this route, the packets would loop back into the tunnel.
default (all other addresses) eth0 Never used for internet traffic.

Why two halves and not one default route? A more specific route always wins over a less specific one. The two /1 routes together cover all addresses, and each is more specific than default. Thus, all internet traffic goes into the tunnel, and the default route stays only for the VPN server.

The default action of the container firewall is drop: it discards every packet that no rule permits. The rules permit only:

Direction Permitted traffic
Out All traffic into tun0 .
Out UDP to the VPN server on port 1195 , oneth0 . This is the encrypted tunnel itself.
In and out Traffic to and from the Docker network, for the proxy requests from the Mac.
In and out Replies to connections that are already open, and traffic inside the container.

When the tunnel fails, no rule permits plain internet traffic on eth0. The traffic cannot leave the container without encryption. Gluetun also starts the firewall before the VPN connection, so the kill switch works during the start too.

The proxies ask the DNS server inside the container, at 127.0.0.1:53. That DNS server sends its questions with DNS over TLS (encrypted DNS) through the tunnel. Your internet provider never sees the names of the websites. For SOCKS5, the address form socks5h:// matters: the h tells the application to let the proxy do the name lookup.

When the German browser instance opens https://example.com, the container looks up the name, opens the connection through the tunnel, and then steps aside: the browser and the website talk with TLS end to end.

Time runs from top to bottom. Dashed arrows are replies. The two blue arrows carry the page itself, encrypted by TLS through the proxy and the VPN server.

Four points in this sequence matter:

CONNECT example.com:443 asks the proxy to open a connection. The page content stays encrypted by TLS.example.com. A request through the SOCKS5 port takes the same path. Only the first message is different: the application speaks the SOCKS5 protocol instead of CONNECT.

Between the Mac and the VPN server, the traffic has two layers of encryption: TLS from the browser, inside the VPN tunnel. After the VPN server, only TLS remains.

The grey band is TLS, which covers the whole path. The blue band is the VPN tunnel, which covers only the part through your home network and your internet provider.

Party Can see Cannot see
Your router and internet provider That you connect to a VPN server, how much data, and when The websites, the website names, the page content
The VPN provider Your home address and the websites that you connect to The page content
The website The VPN address, the browser fingerprint, the time zone of your Mac Your home address
The proxy on your Mac The website names and ports The page content

The second row is the price of every VPN: you move your trust from your internet provider to the VPN provider. A plain http:// page has no TLS layer, so the VPN provider can read it.

Each failure ends in an error, never in a silent direct connection. This is the most important safety property of the design.

No arrow leads to your home connection. Each "no" ends in an error that you can see.

The container does not run. Nothing listens on the proxy port. Chrome shows ERR_PROXY_CONNECTION_FAILED. Chrome has no fallback for a proxy that you set with --proxy-server.

The tunnel is down. The firewall permits no plain traffic on eth0, so the request waits and then fails with a timeout.

The VPN server accepts the login but forwards nothing. This happened to me with one server (see "Problems" below). Gluetun runs a health check, a regular test of the connection, and restarts the tunnel when the test fails.

The scripts add a fourth safeguard. The scripts that open a browser or run a command test the proxy first. If the test fails, they do not start the application at all.

After a one-time setup, you start a country with one command and open a browser in it with a second command.

One-time setup. Install Docker Desktop. Get the manual-configuration credentials of your VPN provider (for ExpressVPN: the OpenVPN username and password from the "Manual Configuration" page, not your account login). Copy the template credentials file and fill it in:

cp vpn-sandbox/.env.expressvpn.example vpn-sandbox/.env.expressvpn

Git ignores this file, so your credentials never reach the repository.

Add a country. Add one line to vpn-sandbox/sandboxes.conf:

germany    | expressvpn | Germany | 8889      | 1081       | Europe/Berlin | chrome

Daily commands:

Task Command
Start one sandbox, or all ./vpn-sandbox/start-vpn.sh germany or... all
Show the address of each sandbox ./vpn-sandbox/check-ip.sh
Open a browser in a sandbox ./vpn-sandbox/open-browser.sh germany
Open a different browser ./vpn-sandbox/open-browser.sh germany firefox
Run one command in a sandbox ./vpn-sandbox/run-in.sh germany curl https://ipinfo.io
Open a shell in a sandbox ./vpn-sandbox/run-in.sh germany zsh
Stop one sandbox, or all ./vpn-sandbox/stop-vpn.sh germany or... all

The output of check-ip.sh shows each route side by side (example values):

SANDBOX        PORTS        SOCKS5 PUBLIC IP (through the HTTP proxy)
direct         -            -      "ip":"203.0.113.10","city":"YourCity","country":"US",
poland         8888/1080    ok     "ip":"188.212.135.227","city":"Warsaw","country":"PL",
germany        8889/1081    ok     ip=45.86.202.250 loc=DE
croatia        8892/1084    ok     "ip":"85.203.20.36","city":"Zagreb","country":"HR",

Other applications. run-in.sh sets the standard proxy variables HTTP_PROXY, HTTPS_PROXY and ALL_PROXY and then runs your command. Most command-line tools read these variables: curl, git, pip, npm, yt-dlp. Desktop applications with a proxy setting can use the ports directly.

Caution: a tool that ignores the proxy variables connects directly, without the VPN, and shows no error. Test each new tool once against a site that shows your address.

Five problems looked like bugs in my setup. None of them was. Each one taught something about VPNs in general.

The first country I tried was Romania. The container connected, the login succeeded, and the log said Initialization Sequence Completed. But every request timed out.

The interface counters showed the cause. The tunnel interface tun0 had sent 19,400 bytes and received 0 bytes. The server accepted the tunnel but sent nothing back. The same container with a Netherlands or Germany server worked at once.

Lesson: a successful VPN login does not prove a working VPN. Test real traffic. The start script was updated to wait until a request through the proxy succeeds.

The check script asked ipinfo.io for the location of each sandbox. For Germany, the service answered HTTP 429: Rate limit hit. The VPN worked: other sites loaded through the same proxy.

The cause: thousands of VPN customers share each VPN address, and they all use the same free services. The free request quota of that address was already gone. The script was updated to fall back to the Cloudflare trace page (/cdn-cgi/trace), which shows the address and the country code.

Lesson: a VPN address is not your address. You share its reputation with strangers.

A file-download site refused a free download in the Poland browser with "Your download has not finished yet". At the same time, my normal browser had a download running on the same site. It looked as if the site had linked the two browsers.

It had not. I checked every connection of the Poland browser instance: all internet traffic went to the proxy port, and the proxy added no header with my home address. The two browsers had two different public addresses. The site limits free downloads by IP address, and another customer of my VPN provider, on the same Poland address, most likely had a download running.

Lesson: the same shared-address effect, from the other side. A limit "per IP address" applies to everybody behind that VPN address.

The Belarus sandbox connected without errors. But three geolocation databases placed its address in three different countries:

Service Country it showed
ip-api.com Belarus
ipinfo.io and Cloudflare Netherlands
ifconfig.co United States

The address belongs to a hosting company with data centers in Western Europe and the US. The provider most likely runs "Belarus" as a virtual location: the server stands in another country, and the address is only registered as Belarusian.

Lesson: "the country of an IP address" is an opinion of a database, not a fact. Check the location with the databases that matter for your target websites.

After a restart of Docker Desktop, all seven containers stood still with exit code 1. The containers have the restart policy unless-stopped, but Docker had also installed an update during the restart and did not start them again. The exit code itself was harmless: the HTTP proxy only took more than 400 milliseconds to shut down.

Lesson: read the log before you restart anything. Here, the log showed a normal shutdown, so a plain restart was the correct action.

The setup separates network paths and cookies. It does not make one computer look like several computers.

Limit What it means
Browser fingerprint All browser instances run on the same hardware, with the same screen, fonts and graphics chip. A website can see the same fingerprint from two countries at the same time. A different browser in each sandbox (Chrome in one, Firefox in another) makes the fingerprints only partly different.
Time zone Every instance reports the time zone of the Mac. A Polish address with a California time zone is a strong sign of a VPN.
Shared addresses Other VPN customers share your VPN address. Their behavior affects your rate limits, download limits and blocks.
TCP only The proxies carry TCP traffic: web pages, downloads and most tools. They do not carry UDP traffic, which some video calls and games use.
Device limit Each running sandbox is one connected device on your VPN account. Seven sandboxes use seven devices.
Terms of use Many websites forbid VPNs or several connections to get around their limits. Read the terms of a website before you use this method on it.

For stronger separation, run a full virtual machine for each country. A virtual machine has its own hardware fingerprint, and every application inside it uses the VPN, UDP included. The cost is more memory and more setup.

One container, one port and one profile for each country give you several VPN locations side by side on one Mac. The routes send all container traffic into the tunnel. The firewall makes a kill switch. The separate profiles keep the cookies apart. And nothing ever falls back to your home connection.

The problems taught more than the design. A VPN login is not a working VPN. A VPN address is shared with strangers. The country of an address is the opinion of a database.

The code, the setup steps and more diagrams are in the repository: github.com/arunmk/network-sandbox (MIT license). It works with any VPN provider that Gluetun supports. Reports from other providers and browsers are welcome.

── more in #developer-tools 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/one-browser-per-coun…] indexed:0 read:17min 2026-10-20 · —