{"slug": "one-browser-per-country-running-several-vpn-locations-simultaneously", "title": "One Browser per Country: Running Several VPN Locations Simultaneously", "summary": "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.", "body_md": "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.\n\nI worked with Claude Code to mitigate this disruption based on some past ideas. Claude made the execution of the ideas immensely fast!\n\nLet 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.\n\nThe 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.\n\nThis 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.\n\nMost of this post is *written* using Claude but *verified* by a human (me).\n\nWe discuss:\n\nThe 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.\n\nThis post uses each term below with one meaning only.\n\n| Term | Definition | \n|---|---|\n| **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. | \n| **VPN provider** | A company that operates VPN servers in many countries, for example ExpressVPN, NordVPN or Mullvad. | \n| **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. | \n| **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. | \n| **Proxy** | A program that receives network traffic from an application and sends the traffic on for the application. | \n| **HTTP proxy** | A proxy type that browsers and most tools support. It carries web traffic (HTTP and HTTPS). | \n| **SOCKS5 proxy** | A more general proxy type. Some applications support only SOCKS5. | \n| **Port** | A number that identifies one service on a computer, for example `8889` . Each proxy in this setup listens on its own port. | \n| **Loopback address** | The address `127.0.0.1` . Only programs on the same computer can reach it. | \n| **Container** | A small, isolated Linux system that Docker runs. Each container has its own network interfaces, routes and firewall. | \n| **Docker Desktop** | The Mac application that runs containers. It runs them inside a hidden Linux virtual machine. | \n| **Gluetun** | An open-source program that runs in a container. It connects to a VPN provider and offers proxies to other programs. | \n| **Sandbox** | In this post: one Gluetun container that connects to one country, with one HTTP proxy port and one SOCKS5 proxy port. | \n| **Browser profile** | The folder where a browser keeps its cookies, history, logins and settings. Two profiles do not share cookies. | \n| **Browser instance** | One running copy of a browser application. Each instance in this setup has its own profile and its own proxy. | \n| **DNS** (Domain Name System) | The service that changes a name, for example `example.com` , into an IP address. | \n| **TLS** | The encryption that HTTPS uses between a browser and a website. | \n| **Kill switch** | A rule that blocks all traffic when the VPN tunnel does not work, so that no traffic leaves without encryption. | \n| **Split tunneling** | A VPN app feature that sends some applications through the VPN and other applications directly to the internet. | \n| **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. | \n| **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. | \n\nA 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.\n\nEach arrow points at the finest level that a tool can give its own route. Only separate browser instances reach the single window.\n\n**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.\n\n**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.\n\n**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.\n\nThe solution needs two separations at the same time: a separate **network path** for each country, and a separate **browser profile** for each country.\n\nEach 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.\n\nSandbox traffic goes through a local port into its own container and its own tunnel. Direct traffic never touches the containers.\n\nThe parts for the Germany sandbox:\n\n| Part | Value for Germany | Purpose | \n|---|---|---|\n| Container | `vpn-sandbox-germany` | Runs Gluetun. Holds the VPN tunnel to a German server. | \n| HTTP proxy port | `127.0.0.1:8889` | Browsers and most tools send their traffic here. | \n| SOCKS5 proxy port | `127.0.0.1:1081` | For applications that need SOCKS5. | \n| Browser profile | `~/.vpn-sandbox/profiles/germany-chrome` | Keeps the cookies and logins of the German browser instance apart from all others. | \n\nThree design choices matter:\n\n`sandboxes.conf` holds the name, provider, country, ports, time zone and default browser. To add a country, you add one line.\nA 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`.\n\nInside each container, the routing table sends all internet traffic into the tunnel, and the firewall blocks everything else. Together they make a kill switch.\n\nA **network interface** is a connection point between a system and a network. The container has two:\n\n`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.\n\nA **routing table** tells the system which interface to use for each destination address. I read these values from the running Germany container:\n\n| Destination | Interface | Purpose | \n|---|---|---|\n| `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. | \n| `128.0.0.0/1` (addresses 128.x.x.x to 255.x.x.x) | `tun0` | The second half goes into the tunnel. | \n| 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. | \n| `default` (all other addresses) | `eth0` | Never used for internet traffic. | \n\nWhy 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.\n\nThe default action of the container firewall is **drop**: it discards every packet that no rule permits. The rules permit only:\n\n| Direction | Permitted traffic | \n|---|---|\n| Out | All traffic into `tun0` . | \n| Out | UDP to the VPN server on port `1195` , on`eth0` . This is the encrypted tunnel itself. | \n| In and out | Traffic to and from the Docker network, for the proxy requests from the Mac. | \n| In and out | Replies to connections that are already open, and traffic inside the container. | \n\nWhen 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.\n\nThe 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.\n\nWhen 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.\n\nTime 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.\n\nFour points in this sequence matter:\n\n`CONNECT example.com:443` asks the proxy to open a connection. The page content stays encrypted by TLS.`example.com`.\nA request through the SOCKS5 port takes the same path. Only the first message is different: the application speaks the SOCKS5 protocol instead of `CONNECT`.\n\nBetween 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.\n\nThe 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.\n\n| Party | Can see | Cannot see | \n|---|---|---|\n| 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 | \n| The VPN provider | Your home address and the websites that you connect to | The page content | \n| The website | The VPN address, the browser fingerprint, the time zone of your Mac | Your home address | \n| The proxy on your Mac | The website names and ports | The page content | \n\nThe 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.\n\nEach failure ends in an error, never in a silent direct connection. This is the most important safety property of the design.\n\nNo arrow leads to your home connection. Each \"no\" ends in an error that you can see.\n\n**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`.\n\n**The tunnel is down.** The firewall permits no plain traffic on `eth0`, so the request waits and then fails with a timeout.\n\n**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.\n\nThe 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.\n\nAfter a one-time setup, you start a country with one command and open a browser in it with a second command.\n\n**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:\n\n```\ncp vpn-sandbox/.env.expressvpn.example vpn-sandbox/.env.expressvpn\n```\n\nGit ignores this file, so your credentials never reach the repository.\n\n**Add a country.** Add one line to `vpn-sandbox/sandboxes.conf`:\n\n```\n# name     | provider   | country | http_port | socks_port | timezone      | browser\ngermany    | expressvpn | Germany | 8889      | 1081       | Europe/Berlin | chrome\n```\n\n**Daily commands:**\n\n| Task | Command | \n|---|---|\n| Start one sandbox, or all | `./vpn-sandbox/start-vpn.sh germany` or`... all` | \n| Show the address of each sandbox | `./vpn-sandbox/check-ip.sh` | \n| Open a browser in a sandbox | `./vpn-sandbox/open-browser.sh germany` | \n| Open a different browser | `./vpn-sandbox/open-browser.sh germany firefox` | \n| Run one command in a sandbox | `./vpn-sandbox/run-in.sh germany curl https://ipinfo.io` | \n| Open a shell in a sandbox | `./vpn-sandbox/run-in.sh germany zsh` | \n| Stop one sandbox, or all | `./vpn-sandbox/stop-vpn.sh germany` or`... all` | \n\nThe output of `check-ip.sh` shows each route side by side (example values):\n\n```\nSANDBOX        PORTS        SOCKS5 PUBLIC IP (through the HTTP proxy)\ndirect         -            -      \"ip\":\"203.0.113.10\",\"city\":\"YourCity\",\"country\":\"US\",\npoland         8888/1080    ok     \"ip\":\"188.212.135.227\",\"city\":\"Warsaw\",\"country\":\"PL\",\ngermany        8889/1081    ok     ip=45.86.202.250 loc=DE\ncroatia        8892/1084    ok     \"ip\":\"85.203.20.36\",\"city\":\"Zagreb\",\"country\":\"HR\",\n```\n\n**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.\n\n**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.\n\nFive problems looked like bugs in my setup. None of them was. Each one taught something about VPNs in general.\n\nThe first country I tried was Romania. The container connected, the login succeeded, and the log said `Initialization Sequence Completed`. But every request timed out.\n\nThe 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.\n\n**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.\n\nThe 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.\n\nThe 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.\n\n**Lesson:** a VPN address is not your address. You share its reputation with strangers.\n\nA 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.\n\nIt 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.\n\n**Lesson:** the same shared-address effect, from the other side. A limit \"per IP address\" applies to everybody behind that VPN address.\n\nThe Belarus sandbox connected without errors. But three geolocation databases placed its address in three different countries:\n\n| Service | Country it showed | \n|---|---|\n| ip-api.com | Belarus | \n| ipinfo.io and Cloudflare | Netherlands | \n| ifconfig.co | United States | \n\nThe 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.\n\n**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.\n\nAfter 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.\n\n**Lesson:** read the log before you restart anything. Here, the log showed a normal shutdown, so a plain restart was the correct action.\n\nThe setup separates network paths and cookies. It does not make one computer look like several computers.\n\n| Limit | What it means | \n|---|---|\n| 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. | \n| 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. | \n| Shared addresses | Other VPN customers share your VPN address. Their behavior affects your rate limits, download limits and blocks. | \n| 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. | \n| Device limit | Each running sandbox is one connected device on your VPN account. Seven sandboxes use seven devices. | \n| 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. | \n\nFor 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.\n\nOne 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.\n\nThe 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.\n\nThe code, the setup steps and more diagrams are in the repository: [github.com/arunmk/network-sandbox](https://github.com/arunmk/network-sandbox) (MIT license). It works with any VPN provider that Gluetun supports. Reports from other providers and browsers are welcome.", "url": "https://wpnews.pro/news/one-browser-per-country-running-several-vpn-locations-simultaneously", "canonical_source": "https://dev.to/arunmk95/one-browser-per-country-running-several-vpn-locations-simultaneously-363i", "published_at": "2026-10-20 18:07:19+00:00", "updated_at": "2026-10-20 18:18:14.099367+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["Claude Code", "Docker", "Gluetun", "ExpressVPN", "Google Chrome", "macOS", "NordVPN", "Mullvad"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/one-browser-per-country-running-several-vpn-locations-simultaneously", "markdown": "https://wpnews.pro/news/one-browser-per-country-running-several-vpn-locations-simultaneously.md", "text": "https://wpnews.pro/news/one-browser-per-country-running-several-vpn-locations-simultaneously.txt", "jsonld": "https://wpnews.pro/news/one-browser-per-country-running-several-vpn-locations-simultaneously.jsonld"}}