SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-13

How to Host .onion Site on VPS with Tor

Run a v3 onion service on Ubuntu with tor and nginx bound to 127.0.0.1, then stop leaks wey fit connect the .onion address to your public IP.

Wetin you dey build

Onion site na normal web server wey only dey answer through Tor network. Install tor, add two lines to /etc/tor/torrc, read the address wey tor write for you, then bind nginx to 127.0.0.1 so nothing go answer for the public IP. The install part go take ten minutes. The rest of this guide na list of things wey fit leak information, because the common way onion site dey fail na when its own configuration point directly back to the operator.

Tor start as "the onion router", and onion service na service wey you fit reach only through am. Version 3 address get 56 characters followed by .onion. Those characters na the service ed25519 public key plus checksum and version byte, encoded in base32. Version 2 addresses (16 characters) comot from the network for 2021, so anything wey you generate today na v3. The address na the key, and this get two consequences. The connection dey encrypted and authenticated end to end without certificate authority, and if you lose the key file, you don lose the address permanently.

Your server no dey accept inbound connection ever. Tor dey choose some relays as introduction points, upload signed descriptor to directory servers, and meet each visitor for rendezvous relay wey the visitor choose. Every one of those connections dey go outbound from your box. No port dey to open, and no DNS record dey to publish.

Install tor from Tor Project repository

Ubuntu dey ship tor package for universe, but e dey remain near the version wey dey current when dem freeze the release. Tor Project own repository dey track the current stable release. Na this one you want for the software wey dey decide whether your address go remain your own.

sudo apt update
sudo apt install -y apt-transport-https gnupg wget
KEYURL=https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc
wget -qO- "$KEYURL" | gpg --dearmor | sudo tee /usr/share/keyrings/deb.torproject.org-keyring.gpg >/dev/null

The repository entry dey use deb822 format, and Suites must be your Ubuntu codename. Read am from /etc/os-release instead of typing am, because wrong codename go give you repository wey resolve fine, but e no get packages for your release.

. /etc/os-release
sudo tee /etc/apt/sources.list.d/tor.sources >/dev/null <<EOF
Types: deb deb-src
URIs: https://deb.torproject.org/torproject.org/
Suites: $VERSION_CODENAME
Components: main
Signed-By: /usr/share/keyrings/deb.torproject.org-keyring.gpg
EOF
sudo apt update
sudo apt install -y tor deb.torproject.org-keyring

deb.torproject.org-keyring package dey keep the signing key current, so key rotation no go break apt update one year from now. Check say tor start and reach the network:

tor --version
sudo journalctl -u tor@default -n 20

The journal suppose end with Bootstrapped 100% (done): Done. If tor remain stuck for Bootstrapped 10%, e no get outbound path. Check your provider network firewall and your own egress rules: sudo ufw status verbose suppose show allow (outgoing) as the default.

Two names matter from here. The package dey run tor as the debian-tor user, and the running unit na tor@default.service, because tor.service for Debian and Ubuntu na wrapper around the instance. Ask for status and logs with the instance name, and you go always get the real process.

Set onion service for torrc

Add two lines to /etc/tor/torrc:

HiddenServiceDir /var/lib/tor/onion_site/
HiddenServicePort 80 127.0.0.1:8080

HiddenServiceDir na where tor dey keep the keys and address for this service. No create am yourself. Tor go create am when e start, with the owner and mode wey e need. If you create the directory as root, na the first failure for the failure list below go happen.

HiddenServicePort get two parts, and mixing dem up na the common first mistake. The first number na the port wey visitor go connect to inside the tunnel. So 80 na wetin people expect, and no reason dey to change am. The second part na the local address wey tor go forward the traffic to. A plain HiddenServicePort 80 go forward to 127.0.0.1:80. So, write the address fully and use a high port, so the onion vhost no go clash with anything wey already dey listen on port 80.

sudo systemctl restart tor@default
sudo ls -l /var/lib/tor/onion_site/

The listing suppose get hostname, hs_ed25519_public_key, hs_ed25519_secret_key, and one empty authorized_clients directory.

Read your .onion address

sudo cat /var/lib/tor/onion_site/hostname

One line dey come back: 56 base32 characters and .onion. That string na the whole identity of the site. Nobody assign am, nobody fit transfer am, and nobody fit collect am from you as long as you hold the key file. Copy am now, because every config below need am. The rest of this guide go write am as <your-address>.onion.

Serve the site from nginx wey bind to 127.0.0.1

sudo apt install -y nginx
sudo install -d -m 755 /srv/onion

Write /etc/nginx/sites-available/onion:

server {
    listen 127.0.0.1:8080;
    server_name <your-address>.onion;

    root /srv/onion;
    index index.html;

    server_tokens off;
    etag off;
    access_log off;
    error_log /var/log/nginx/onion.error.log error;
}
echo '<h1>hello from the onion</h1>' | sudo tee /srv/onion/index.html
sudo ln -s /etc/nginx/sites-available/onion /etc/nginx/sites-enabled/onion
sudo nginx -t
sudo systemctl reload nginx

Now prove two things from the server. First, confirm say nginx dey answer for the onion name. Na this exact Host header tor go send:

curl -s -H 'Host: <your-address>.onion' http://127.0.0.1:8080/

Second, confirm say e dey answer for that address and nowhere else:

sudo ss -tlnp | grep 8080

The address column must read 127.0.0.1:8080. If e read 0.0.0.0:8080 or *:8080, your onion site dey also available for public internet. Na the first item for the leak list be this. A listen 8080; line wey no get address go bind every interface, and na the default behaviour be that.

Open the address for Tor Browser. The first load fit take some seconds while the client fetch your descriptor and build rendezvous circuit.

The Tor Project own documentation prefer unix socket instead of loopback port: HiddenServicePort 80 unix:/var/run/tor/onion_site.sock, with nginx listening on that path. Another host no fit reach socket at all, even if the box later get second interface. The cost na file permissions, because nginx dey create the socket and tor dey connect to am as debian-tor, so both users must agree on the directory. Loopback with verified ss output easier to configure correctly, and na wetin the rest of this guide assume.

With the site for loopback, the box no need any inbound rule for am. Keep 22 open for yourself and deny the rest (the ufw defaults wey worth setting for VPS). Remember say firewall no fit undo service wey bind 0.0.0.0. E only filters packets wey reach the firewall. Containers make this matter more serious, because publishing a Docker port dey write iptables rules ahead of ufw, so -p 8080:80 puts your onion backend for the public IP while ufw still dey report say the port dey denied. Publish container ports as -p 127.0.0.1:8080:80.

Leaks wey fit de-anonymise an onion site

Tor dey hide where the server dey. But Tor no dey hide wetin the server talk. Every item below na something wey your own stack dey publish.

Na the same site dey answer for your public IP

Na this one dey catch people. Scanners dey index the HTTP response of every routable address continuously, and those results dey public and searchable. If you serve the same page for your public IP and your onion address, joining both na one query: same title, same favicon hash, same ETag, same header order. The listen 127.0.0.1:8080; line above na the fix. Verify am from another machine, no be from the server:

curl -sv --max-time 5 http://<your-public-ip>:8080/

Connection refused or timeout na the correct result. Any HTML mean say the site dey public. If the box still dey run a clearnet site, give that vhost its own root and keep an explicit default_server block on the public listener, so unmatched Host header no fit fall through to the onion vhost.

Version banners

curl -sI http://127.0.0.1:8080/ | grep -i '^server'

Default nginx dey answer Server: nginx/1.24.0. That version string, together with the exact order of the other headers, na fingerprint wey fit match your onion with your clearnet host. server_tokens off; reduce am to Server: nginx. E no remove the header, and nginx no get built-in directive wey fit do that, so headers-more module na the usual answer if you want am gone. PHP dey add X-Powered-By until you set expose_php = Off. etag off; belong for the same list, because nginx dey build the ETag from file modification time and size, so the same files copied to two servers go give out the same ETag for both.

Absolute URLs wey dey point to your clearnet domain

A rel="canonical" tag, an Open Graph og:url, an RSS feed, a sitemap, a password reset email, or hardcoded logo URL. Any one of dem dey name the clearnet site inside a page wey the onion dey serve. Use root-relative paths like /static/logo.svg, and make the application read its base URL from the request host instead of constant value. Redirects na the same bug for another place: return 301 https://example.com$request_uri; for a catch-all block go send onion visitor to your real domain, and Location header go hand dem the answer directly.

TLS certificate wey you share with the clearnet site

Onion address dey authenticate itself because the address na the public key, so http:// over an onion connection already dey encrypted end to end, and Tor Browser dey treat am as secure context. If you install your existing certificate for the onion vhost, e go publish the link between both, because every publicly trusted certificate dey recorded for Certificate Transparency logs, and those logs dey public, permanent, and searchable by name. Keep Let's Encrypt certificates for the clearnet vhost and leave the onion vhost on plain HTTP.

Third-party fonts and analytics

Font from a CDN (content delivery network), or analytics script. Visitor browser dey fetch each one directly, so the third party go know say person load your page and usually which page, while Tor Browser stricter security levels dey block the request anyway and leave broken layout. Self-host every asset wey the page need.

Host header mismatch

If server_name no match Host header wey tor send, nginx go fall back to the default server for that listen address. For box wey get one vhost, this no dey visible, because the only server block na also the default. If you add clearnet vhost later, onion requests fit start landing on am, together with its canonical tags and redirects. Run the curl -H 'Host: ...' check again after every nginx change, and grep the result for your real domain:

curl -s -H 'Host: <your-address>.onion' http://127.0.0.1:8080/ | grep -o 'https\?://[^"]*' | sort -u

To know which process dey own which socket na most of this work (how ports and listening sockets dey work for Linux).

Wetin remain for the logs

Every request dey arrive from 127.0.0.1, so nginx no get visitor address to record and access_log off; no cost you anything. The application above am na different matter, because order, email address, or metadata from uploaded file na your responsibility to handle. Your own habits still count: administering the box through an unhardened login dey outside anything Tor dey protect, so treat SSH hardening for the same VPS as part of this build.

Back up private key, because na e be the address

/var/lib/tor/onion_site/hs_ed25519_secret_key na the service. Registrar no dey, and recovery no dey. If you lose am, the address don disappear. Copy am, and anybody wey hold the copy fit serve their own content for your address. You no get any way to revoke anything.

sudo systemctl stop tor@default
sudo tar -C /var/lib/tor -czf onion-keys.tgz onion_site
sudo chmod 600 onion-keys.tgz
sudo systemctl start tor@default

Encrypt that archive (gpg -c onion-keys.tgz) and move am comot from the server. To restore am for new VPS, na the archive plus the ownership wey tor expect:

sudo systemctl stop tor@default
sudo tar -C /var/lib/tor -xzf onion-keys.tgz
sudo chown -R debian-tor:debian-tor /var/lib/tor/onion_site
sudo chmod 700 /var/lib/tor/onion_site
sudo systemctl start tor@default
sudo cat /var/lib/tor/onion_site/hostname

The same address go return for the new hardware, one or two minutes after tor republishes the descriptor. Na so the whole migration be: no DNS change and no certificate reissue.

Onion-Location, when the site still dey available for clearnet

If the onion na convenience instead of secret, advertise am from the clearnet vhost:

add_header Onion-Location http://<your-address>.onion$request_uri;

Tor Browser go then show a .onion available button for the address bar and offer the switch. E go only honour the header when the clearnet page dey served over HTTPS and the value na valid onion URL.

One nginx rule fit cause problem here. add_header directives dey inherited by a location block only when that block no declare any of its own, so location wey get its own add_header go silently drop Onion-Location. Repeat am there, or keep all response headers for one place. Publishing this header deliberately join the two sites, and that correct for a mirror but wrong for anything wey suppose remain unlinked.

Vanity addresses

mkp224o dey generate key pairs until one produce address wey start with the prefix wey you request. Na brute force search be this, so nothing dey to configure apart from the prefix and how long you ready to wait.

sudo apt install -y git gcc libc6-dev libsodium-dev make autoconf
git clone https://github.com/cathugger/mkp224o
cd mkp224o
./autogen.sh
./configure --enable-amd64-51-30k
make
./mkp224o -d onionkeys blog

Every successful result dey enter onionkeys/<address>.onion/ wey hold hostname and hs_ed25519_secret_key. Install am by stopping tor, copying that directory over your HiddenServiceDir, then applying the same chown and chmod 700 wey restore above use.

Prefix length na the full cost. The address na base32, so every extra character wey you request dey multiply the expected number of keys by 32. Short prefix go finish for laptop. Long one no go finish for anything wey you own. Vanity prefix also dey teach readers to recognise the first few characters instead of the complete address, and na that habit phishing copies of onion sites dey use.

Failure modes, with the strings you will see

No hostname file after the restart. Tor no start, or e reject the directory. sudo journalctl -u tor@default -n 50 name am:

/var/lib/tor/onion_site/ is not owned by this user (debian-tor, 108) but by root (0). Perhaps you are running Tor as the wrong user?

Na so directory wey person create by hand dey look. Correct the ownership and mode, or delete the directory make tor build am.

Tor Browser shows Onionsite Not Found (0xF0). The client no fit fetch descriptor, so as far as the network concern, nothing dey published for that address. Confirm say tor dey run and don bootstrap, compare the address wey you type with sudo cat /var/lib/tor/onion_site/hostname character by character, then check the clock. Tor need accurate time to publish and validate descriptors, and timedatectl suppose report System clock synchronized: yes.

The address resolves but the page never loads. Tor complete the rendezvous, then e fail for the last hop, from tor go nginx. That hop dey local, so tor log no go show anything. Run curl -sI http://127.0.0.1:8080/ for the server. Connection refused mean say nginx dey down or e dey listen on address different from the one wey HiddenServicePort point to.

The page loads and every link goes to your real domain. Na absolute URLs for the templates cause am. Run the grep -o 'https\?://[^"]*' check above and fix wetin e print before you share the address anywhere.

It works, then stops after a reboot. Reboot the box once on purpose before you depend on the site, then run sudo systemctl status tor@default and sudo systemctl status nginx. Service wey person start by hand go look exactly like enabled service until the machine restart.

FAQ

I need open port for Tor onion service for my firewall?

No. The tor daemon dey make only outbound connections go directory servers, introduction points, and each rendezvous relay. So you no need inbound rule, and the web server itself dey listen on 127.0.0.1. Keep ufw for default deny for incoming traffic, with SSH allowed. This same property mean say onion service fit work from machine wey dey behind NAT (network address translation), even if e no get public IP.

Why I no fit reach my .onion address for Tor Browser?

Start your check from the server outward. sudo journalctl -u tor@default -n 50 suppose show Bootstrapped 100% (done): Done. Then curl -sI http://127.0.0.1:8080/ for the server suppose return status line. After that, compare the address wey you type with the hostname file, because one wrong character mean say na different service. Onionsite Not Found (0xF0) mean say e no find descriptor for the address. Most times, this happen because tor no dey run or the system clock wrong.

I fit move my onion site go new server and keep the same address?

Yes. The address come from hs_ed25519_secret_key. Copy the complete HiddenServiceDir go the new box, set ownership to debian-tor and mode 700, then start tor. The address go work again after the descriptor publish again. You no need update any DNS record. If you lose that file, you no fit recover the address. Back it up encrypted and keep the backup outside the server from the day you create am.

Onion site need HTTPS certificate?

No. The 56-character address na the service public key. So the connection already get end-to-end encryption and authentication, and Tor Browser treats http:// for a .onion name as secure context. Reusing your clearnet certificate for the onion vhost worse pass doing nothing, because Certificate Transparency logs dey public and dey permanently record the names wey share the certificate. The only reason to buy certificate for a .onion name na brand assurance from CA wey issues such certificates. That link dey public by design.