Route your VPS traffic through Tor
Install the tor daemon on a VPS, then send one process or one container through its local SOCKS proxy. Know what that hides and what it does not.
What routing your VPS traffic through Tor actually does
Routing your VPS traffic through Tor means running the tor daemon on the server. It opens a SOCKS (socket secure) proxy on 127.0.0.1:9050, and each program you point at that proxy leaves through three Tor relays instead of connecting directly. The destination sees the IP address of the last relay in the chain, the exit, and never your server's own address.
That is the whole mechanism. Everything below is about scope: which programs you point at that proxy, and why "all of them" is the wrong answer on a box you also administer over SSH.
Tor is ordinary network infrastructure and running the client is legal in most of the world. This guide covers the outbound side only, your server acting as a Tor client. Making a service on your server reachable through Tor is the opposite direction and a different setup.
What this protects, and what it does not
It protects two things. The destination does not learn your server's IP address, because the connection reaches it from an exit relay. And your hosting provider, or anyone watching the link into your VPS, sees an encrypted connection to a Tor guard relay rather than the name of the host you are talking to.
It does not protect any of these, and each one has quietly undone somebody's assumption of privacy:
- Anything the application itself sends. A login, a session cookie, an API key or an account name identifies you exactly as well over Tor as without it.
- Names resolved outside the proxy. If a program looks the hostname up itself and then hands the resulting IP address to the SOCKS proxy, the DNS (domain name system) query already left your server in clear text.
- Timing and volume. Someone who can watch your server's link and the destination at the same time can line up when one sends and the other receives.
- The existence of the server. If the same VPS also serves a clearnet website, its address is public by definition.
That last point deserves a plain statement: a server that runs a clearnet service and also sends traffic through Tor is trivially correlated. Anyone can see the public address, and lining up when the box is busy with when the Tor traffic happened is not hard work. If your goal is that nobody learns which machine is behind a service, outbound Tor is the wrong layer. That job belongs to an onion service, either publishing the site itself as an onion service or reaching the box for admin over an onion address. If your goal is the narrower one, that a particular program's requests should not carry your server's IP address, the rest of this page is that. For how the two tools differ in what they hide and from whom, Tor and a VPN answer different questions.
Install the tor daemon: Ubuntu's package or the Tor Project's repository
Pick one deliberately and write down which, because the two give you different versions.
Ubuntu 24.04 carries tor 0.4.8.10 in the universe component (as of September 2026). That version is frozen for the life of the release, and fixes arrive as backports on Ubuntu's schedule. It is one command, and for most uses it is enough.
sudo apt update
sudo apt install -y torThe Tor Project publishes its own Debian and Ubuntu repository, which follows the current stable series and carries upstream releases within days. Use it when you want the newer client. These are the commands from the Tor Project's own instructions:
sudo apt install -y apt-transport-https gnupg wget
wget -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc \
| gpg --dearmor | sudo tee /usr/share/keyrings/deb.torproject.org-keyring.gpg >/dev/nullWrite /etc/apt/sources.list.d/tor.sources, replacing the suite with your own codename from lsb_release -cs. noble is Ubuntu 24.04 and bookworm is Debian 12. Only current codenames are published, so a guessed name gives a 404 on the next apt update.
Types: deb
URIs: https://deb.torproject.org/torproject.org/
Suites: noble
Components: main
Signed-By: /usr/share/keyrings/deb.torproject.org-keyring.gpgsudo apt update
sudo apt install -y tor deb.torproject.org-keyringThe deb.torproject.org-keyring package is not decoration. It ships the signing key as a package, so when the key is rotated your apt update keeps working instead of failing on a signature you then have to repair by hand.
Now pin it, because both repositories carry a package called tor and apt otherwise chooses on version number alone. One Ubuntu security backport that sorts higher and your server moves back to the distribution build without telling you. Write /etc/apt/preferences.d/tor:
Package: tor tor-geoipdb deb.torproject.org-keyring
Pin: origin "deb.torproject.org"
Pin-Priority: 990Then check that the pin took:
apt-cache policy torThe candidate line should name the deb.torproject.org version, and the repository line under it should show priority 990. If every entry still reads 500, the preferences file is not matching anything, which is almost always a typo in the package name list.
Is the daemon healthy? Read the right unit
sudo systemctl status tor@default
sudo journalctl -u tor@default -n 20 --no-pagerUse tor@default, not tor. On Debian and Ubuntu, tor.service is a wrapper that starts instance units, and the daemon itself runs as tor@default.service. That is why journalctl -u tor shows an almost empty log and people decide the install failed. The instance unit holds the log.
A healthy start ends on this line:
Bootstrapped 100% (done): DoneIf it stalls lower and repeats a warning shaped like this one, outbound traffic is being blocked:
Problem bootstrapping. Stuck at 5%: Connecting to directory server. (Network is unreachable; NOROUTE; count 3; recommendation warn; host 4A0CCD2D... at 198.51.100.7:443)NOROUTE means the connection attempt never left the machine. Check your own output firewall chain first, then your provider's separate network firewall, which is a different control on most panels. A Tor client needs outbound TCP, mostly to ports 443 and 9001. It needs no inbound port at all, which is the structural difference between a client and a relay.
Confirm the listener:
ss -lntp | grep 9050You want 127.0.0.1:9050. The packaged config binds SOCKS to localhost and sets no ORPort, so this daemon carries your traffic only.
Shape 1: send one process through the SOCKS proxy
Prefer the program's own proxy setting over any wrapper. It is explicit, and you can still read it in the config six months later.
curl https://check.torproject.org/api/ip
curl --socks5-hostname 127.0.0.1:9050 https://check.torproject.org/api/ipThe first prints {"IsTor":false,"IP":"..."} holding your server's public address. The second prints {"IsTor":true,"IP":"..."} holding an exit relay's address. Run both. One command that succeeds proves nothing about which path it took.
--socks5-hostname matters more than it looks. --socks5 resolves the hostname on your server and passes the proxy an IP address. --socks5-hostname passes the name and lets Tor resolve it inside the network. Both fetch the page, so the output never tells you which happened, and the first one leaked a DNS query for the destination from your server's own address. Watch it: run sudo tcpdump -ni any port 53 in a second session while you try each version. In URL form the same distinction is socks5h://, which resolves at the proxy, against socks5://, which does not. Write socks5h everywhere.
git config --global http.proxy socks5h://127.0.0.1:9050
ALL_PROXY=socks5h://127.0.0.1:9050 curl -s https://check.torproject.org/api/ipFor an application with a config file, set it there instead of in the environment. SearXNG's outgoing.proxies block takes the httpx form, so a self-hosted SearXNG instance can send its upstream engine requests through the local daemon:
outgoing:
proxies:
all://:
- socks5h://127.0.0.1:9050When a program has no proxy setting at all, torsocks is the fallback:
torsocks curl https://check.torproject.org/api/iptorsocks works through LD_PRELOAD, replacing libc functions such as connect() and getaddrinfo() at load time. That mechanism sets its limits, and its README states them: a program that does not use libc, or that issues raw system calls, is untouched, so its traffic goes out directly. Go binaries are the common case, because the Go runtime makes syscalls itself. Statically linked binaries are the other. Torsocks also rejects anything that is not TCP and will stop the program rather than let traffic leave outside Tor, so torsocks ping cannot work: ICMP is not TCP, and Tor carries no UDP.
The habit that follows: verify every program you route, against check.torproject.org or against the destination's own logs. Do not assume the setting took.
Shape 2: send one container through Tor
A container has its own network namespace, so it is worth being exact about what Tor can do here. The pattern people copy comes from VPN sidecars, and Tor is not one.
When you route a container's traffic through a VPN with gluetun, the sidecar owns a tunnel interface and the app container's default route points into it. Every packet leaves through the tunnel whether the app knows about it or not. Tor gives you no interface. It gives you a SOCKS proxy. Putting an app into a Tor container's namespace with network_mode: "service:tor" moves that proxy onto the app's own 127.0.0.1 and changes nothing else. The app keeps a working default route, so an app that ignores its proxy setting still reaches the internet directly.
The shape that works is a Tor container on a private compose network plus an app that speaks SOCKS. Build the image from the distribution package, so no third-party image sits in the path.
tor/Dockerfile:
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends tor \
&& rm -rf /var/lib/apt/lists/*
COPY torrc /etc/tor/torrc
USER debian-tor
CMD ["tor", "-f", "/etc/tor/torrc"]tor/torrc:
SocksPort 0.0.0.0:9050
SocksPolicy accept 172.28.0.0/16
SocksPolicy reject *
DataDirectory /var/lib/tor
Log notice stdoutcompose.yaml:
services:
tor:
build: ./tor
restart: unless-stopped
networks:
- tornet
app:
image: your-app:latest
restart: unless-stopped
environment:
ALL_PROXY: socks5h://tor:9050
depends_on:
- tor
networks:
- tornet
networks:
tornet:
ipam:
config:
- subnet: 172.28.0.0/16Four details in there carry weight:
- No
ports:entry for 9050, ever. SOCKS is unauthenticated by design, and the Tor manual warns against binding it anywhere but localhost for that reason. Published on a VPS's public address it is an open proxy, and scanners find open proxies in hours. SocksPolicyis the second lock. Tor accepts SOCKS connections from the compose subnet and rejects the rest, which is why the subnet is pinned in theipamblock instead of letting Docker pick one.depends_onwaits for the container to start, not for Tor to build its first circuit. Requests in the first seconds fail. Watchdocker compose logs -f torforBootstrapped 100% (done): Donebefore deciding something is broken.- The app has to honour
ALL_PROXYor an equivalent setting. Read its documentation. If it honours nothing, this shape does not apply, andtorsocksinside the app image is the fallback.
Verify from the network itself, not from the host:
docker network ls
docker run --rm --network myproject_tornet curlimages/curl:latest \
-s --socks5-hostname tor:9050 https://check.torproject.org/api/ipAn IsTor value of true means the path works end to end. curl: (7) means the name tor did not resolve or the port was closed, and the usual cause is that the throwaway container joined a different network from the compose project's. Running Docker on a VPS covers the network basics if that part is new.
One note on state: this container does not persist /var/lib/tor, so each recreate fetches a fresh directory consensus and picks new entry guards. Mount a volume there if you recreate the stack often, because keeping the same guard over time is the design intent rather than an accident.
Shape 3: the whole host, and why that is the wrong answer here
Tor can do this. TransPort accepts transparently redirected TCP connections, DNSPort answers DNS queries over the network, and firewall rules in the output path push everything into them. Both options default to 0, which means off.
TransPort 127.0.0.1:9040
DNSPort 127.0.0.1:5353Turning them on is the easy half. The redirect rules are the other half, and they are where this goes wrong on a general-purpose server. The daemon's own traffic has to be excluded from the redirect, or it loops: tor's connections to relays get redirected back into tor, and the client never bootstraps. The exclusion is by user id, skuid != debian-tor in an nftables output chain, or -m owner ! --uid-owner debian-tor with iptables. Miss it and the box has no working network at all.
Then the costs, which are the real argument:
- It hides nothing about the server. Inbound SSH still lands on the public IP address, and any service you host still advertises that address. Only outbound connections change, so you pay a large latency price for a property you did not gain.
- UDP does not travel through Tor. NTP (network time protocol) is UDP, so clock sync stops, and a drifting clock later breaks TLS (transport layer security) validation and makes log timestamps useless. UDP monitoring agents fail the same way.
- Package updates and health checks now leave through exit relays. Some are slow and some are blocked by the destination, so routine maintenance becomes unreliable.
- Every later failure gains a second candidate cause, and you will spend the time ruling it out.
Whole-host Tor makes sense on a machine with no other job and no clearnet service, which is the design Tails and Whonix implement properly on a workstation. On a rented VPS that you also administer over SSH, scope Tor to the process that needs it and leave the host on the clearnet.
Relays, exits, and networks that block Tor
Nothing in this guide makes your VPS part of Tor's routing. The client sets no ORPort, so it carries your traffic and no one else's. A relay is a deliberate configuration change with bandwidth and uptime commitments attached, and running a Tor relay on a VPS covers what that involves. An exit relay is a much heavier commitment than a middle relay, because the exit's address is the one every destination sees, so the complaints arrive at its operator. An exit belongs on a host whose provider agreed to it in advance and whose operator has signed up to answer that mail. It does not belong on a general-purpose VPS that also runs your own services.
If the problem is that the network in front of you blocks Tor itself rather than the destination, the client's bridge support exists for exactly that case and the Tor Project documents how to request one. What to run when your VPN is blocked covers the wider version of the same problem.
Keeping it working
Config changes need a check and a reload. Verify the file first, so a typo does not take the daemon down on restart:
sudo -u debian-tor tor --verify-config -f /etc/tor/torrc
sudo systemctl reload tor@default--verify-config prints Configuration was valid and exits without starting anything. Reload sends SIGHUP, so tor re-reads the config and keeps running rather than dropping every stream.
By default the streams from one SOCKS port can share circuits, so two applications on the same port can look to a destination like one client. SocksPort 127.0.0.1:9050 IsolateDestAddr gives a separate circuit per destination address. A second SOCKS port on a different number is the simple way to keep one application's circuits to itself.
FAQ
Does routing my VPS traffic through Tor hide the server?
No. It changes which address your outbound connections show, and nothing else. Inbound SSH still arrives on your server's public IP address, and any website or API you host still advertises it. A box that does both is easy to correlate, because the clearnet service already proves the address exists. To keep the machine behind a service unknown, publish that service as an onion service instead of sending outbound traffic through Tor.
Should I route the whole host through Tor?
On a server you administer over SSH, no. It needs TransPort plus redirect rules that exempt tor's own user id, and a mistake there leaves the box with no network. It also breaks every UDP protocol, starting with NTP, so the clock drifts and TLS validation starts failing for reasons that look unrelated. Most importantly it does not hide the server, since inbound traffic still uses the public address. Point the one program that needs Tor at 127.0.0.1:9050.
My app connects through the proxy. How do I know it is not leaking DNS?
Check whether it resolves names itself before handing the connection to the proxy. socks5:// and curl's --socks5 resolve locally, so a DNS query for the destination leaves your server in clear text while the connection itself goes through Tor. socks5h:// and --socks5-hostname send the name to the proxy and Tor resolves it. To confirm which your app does, run sudo tcpdump -ni any port 53 on the server and trigger one request. Queries for the destination name mean the app is resolving locally.
Does installing tor turn my VPS into a relay or an exit?
No. The packaged client sets no ORPort, so it opens a local SOCKS proxy, makes outbound connections and accepts nothing from the network. Becoming a relay takes a deliberate config change. Becoming an exit takes more than that: an exit relay's address is what destinations see, so its operator receives the abuse mail, and it needs a provider that agreed to host one. Keep that role off a server that also runs your services.