Making My Blog Available on Tor
Setting up a Tor hidden service for my blog using Docker, and configuring the Onion-Location header to let Tor Browser users know an onion version exists.
I wanted to make this blog available as a Tor hidden service. Not because I expect many visitors via Tor, but because it felt like a small contribution to a more private web. If someone wants to read my posts without revealing their IP address, they should be able to.
The setup has two parts: a Docker container running Tor and Nginx on my home server, and an HTTP header on the regular site that advertises the onion address.
The Docker setup
My blog is hosted on Netlify, so the hidden service works as a proxy. Tor users connect to the onion address, and the container fetches the content from the public site on their behalf.
Here’s the docker-compose.yml:
services:
tor-dmcc-sites:
build: .
container_name: tor-dmcc-sites
restart: unless-stopped
volumes:
- ./hidden_service_blog:/var/lib/tor/hidden_service_blog
- ./hidden_service_website:/var/lib/tor/hidden_service_website
The volumes persist the hidden service keys. Once Tor generates your .onion address, you want to keep those keys. Otherwise you’ll get a new address every time the container restarts.
The Dockerfile:
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y tor nginx curl && \
apt-get clean && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /var/lib/tor/hidden_service_blog && \
mkdir -p /var/lib/tor/hidden_service_website && \
chown -R debian-tor:debian-tor /var/lib/tor && \
chmod 700 /var/lib/tor/hidden_service_blog && \
chmod 700 /var/lib/tor/hidden_service_website
COPY torrc /etc/tor/torrc
COPY nginx.conf /etc/nginx/nginx.conf
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
CMD ["/entrypoint.sh"]
The torrc configuration defines the hidden services:
# Disable SOCKS (we only need hidden services)
SocksPort 0
# Log to stdout for docker logs
Log notice stdout
# Hidden service for blog.dmcc.io
HiddenServiceDir /var/lib/tor/hidden_service_blog/
HiddenServicePort 80 127.0.0.1:8081
# Hidden service for dmcc.io (main website)
HiddenServiceDir /var/lib/tor/hidden_service_website/
HiddenServicePort 80 127.0.0.1:8082
Each hidden service points to a local port where Nginx is listening.
The nginx.conf proxies requests to the public sites:
user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
events {
worker_connections 768;
}
http {
sendfile on;
tcp_nopush on;
include /etc/nginx/mime.types;
default_type application/octet-stream;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
server {
listen 8081;
location / {
proxy_pass https://blog.dmcc.io;
proxy_set_header Host blog.dmcc.io;
proxy_ssl_server_name on;
}
}
server {
listen 8082;
location / {
proxy_pass https://dmcc.io;
proxy_set_header Host dmcc.io;
proxy_ssl_server_name on;
}
}
}
Finally, the entrypoint.sh starts both services:
#!/bin/bash
set -e
chown -R debian-tor:debian-tor /var/lib/tor/hidden_service_blog
chown -R debian-tor:debian-tor /var/lib/tor/hidden_service_website
chmod 700 /var/lib/tor/hidden_service_blog
chmod 700 /var/lib/tor/hidden_service_website
nginx
exec su -s /bin/sh debian-tor -c "tor -f /etc/tor/torrc"
After running docker compose up -d, the onion address is generated in the hidden service directory:
cat hidden_service_blog/hostname
This gives you something like yxtxzre2jg3zhzlnqxrifaqbkuyd3e3sdgbjqf3tisrtypsbyoclrzqd.onion.
The Onion-Location header
Once the hidden service is running, the next step is telling Tor Browser users it exists. The Onion-Location HTTP header does exactly this. When Tor Browser sees it, a .onion available button appears in the address bar.
Since my blog is hosted on Netlify, I added a static/_headers file:
/*
Onion-Location: http://yxtxzre2jg3zhzlnqxrifaqbkuyd3e3sdgbjqf3tisrtypsbyoclrzqd.onion
For other setups:
Nginx:
add_header Onion-Location http://your-onion-address.onion$request_uri;
Apache:
Header set Onion-Location "http://your-onion-address.onion%{REQUEST_URI}s"
Caddy:
header Onion-Location "http://your-onion-address.onion{uri}"
HTML meta tag (if you can’t set headers):
<meta http-equiv="onion-location" content="http://your-onion-address.onion" />
Testing it
Open Tor Browser, visit any page of my blog, and look for the purple .onion available button in the address bar. Click it to switch to the hidden service.
You can also verify the header is set:
curl -I https://blog.dmcc.io | grep -i onion-location
Final thoughts
The whole setup took maybe half an hour. The Docker container runs quietly on my home server, and visitors using Tor Browser get a prompt that an onion version exists. It’s a small thing, but it means anyone who wants to read this blog privately can do so without trusting their connection to a third party.