Hosting a Tor-Accessible Hugo Website with Docker
Hosting a Tor-Accessible Hugo Website with Docker
In this guide, I will go over setting up a hidden service using Docker on an Ubuntu Linux host. This setup will use a simple Python HTTP server behind the Tor network to serve a Hugo static website. Docker will ensure both Tor and the web server remain operational and accessible through a .onion address.
Why use Python to host the web server and not NGINX, just wanted something easy to test. You can switch out the web server with NGINX or any other server. The goal here is to show the connection over TOR.
Since I am using Python HTTP server, please dont run this as production, its just for development and testing.
Prerequisites
- Ubuntu Linux: This tutorial assumes you have Ubuntu installed and Docker running. It was tested using Ubuntu Server 24.04.
- Docker & Docker Compose: You can follow the Docker installation guide if needed.
- Hugo: For creating the static website that will be hosted behind Tor. Not critical, you can use any static website.
Project Structure
The project structure includes Docker and Docker Compose configuration files, a folder to hold the compiled Hugo site, and directories for Tor configuration and data.
project-root/
├── Dockerfile # Builds the Docker image
├── docker-compose.yml # Docker Compose file to configure services
├── site/ # Folder to hold compiled static web site files
│ └── index.html # index.html for testing
├── tor/ # Configuration folder for Tor
│ └── torrc # Tor configuration file
└── tordata/ # Data folder for Tor hidden service (hostname and keys generated here)
Place the Dockerfile
and docker-compose.yml
at the root of project-root
, alongside the site
, tor
, and tordata
folders.
Dockerfile Overview
In this Dockerfile, we start with ubuntu:latest
as the base image and set up both Tor and a Python HTTP server to serve the site.
- Environment Setup: Configures timezone and non-interactive mode for installations.
- Install Packages: Installs Tor and Python for serving the website.
- Directory Setup: Creates necessary directories and sets permissions for logs, Tor hidden service data, and website content.
- Start Script: Creates a
start.sh
script to launch Tor and the Python server, monitor them, and restart if necessary.
Note: I have used root here, this can be replaced with a specific user, but the permissions will need to be checked
Here’s the complete Dockerfile:
FROM ubuntu:latest
ENV DEBIAN_FRONTEND=noninteractive \
TZ=Australia/Melbourne
RUN apt-get update && \
apt-get install -y tor python3 && \
mkdir -p /opt/site /var/log /var/log/tor /var/log/web /var/lib/tor/hidden_service && \
chown -R root:root /var/log/tor && \
chown root:root /var/lib/tor/hidden_service/ && \
chown -R root:root /var/log/web && \
chmod 700 /var/lib/tor/hidden_service && \
chmod 777 /var/log/tor
# Create a script to run tor and python server
RUN echo '#!/bin/bash' > /start.sh && \
echo 'start_tor() {' >> /start.sh && \
echo ' echo "Starting Tor..."' >> /start.sh && \
echo ' tor & ' >> /start.sh && \
echo ' tor_pid=$!' >> /start.sh && \
echo ' sleep 5' >> /start.sh && \
echo ' if [ -f /var/lib/tor/hidden_service/hostname ]; then' >> /start.sh && \
echo ' echo "Tor hidden service address: $(cat /var/lib/tor/hidden_service/hostname)"' >> /start.sh && \
echo ' else' >> /start.sh && \
echo ' echo "Error: Tor hostname file not found."' >> /start.sh && \
echo ' exit 1' >> /start.sh && \
echo ' fi' >> /start.sh && \
echo ' echo "Tor PID (initial): $tor_pid"' >> /start.sh && \
echo '}' >> /start.sh && \
echo '' >> /start.sh && \
echo 'start_python() {' >> /start.sh && \
echo ' echo "Starting Python server..."' >> /start.sh && \
echo ' python3 -m http.server 80 --directory /opt/site & ' >> /start.sh && \
echo ' python_pid=$!' >> /start.sh && \
echo ' echo "Python server PID: $python_pid"' >> /start.sh && \
echo '}' >> /start.sh && \
echo '' >> /start.sh && \
echo 'start_tor' >> /start.sh && \
echo 'start_python' >> /start.sh && \
echo '' >> /start.sh && \
echo 'while true; do' >> /start.sh && \
echo ' current_tor_pid=$(pgrep -f tor)' >> /start.sh && \
echo ' if [ -n "$current_tor_pid" ]; then' >> /start.sh && \
echo ' echo "Tor (PID: $current_tor_pid) is running..."' >> /start.sh && \
echo ' else' >> /start.sh && \
echo ' echo "Tor has stopped. Stopping Python server..."' >> /start.sh && \
echo ' kill $python_pid 2>/dev/null' >> /start.sh && \
echo ' wait $python_pid 2>/dev/null' >> /start.sh && \
echo ' echo "Python server has stopped. Exiting..."' >> /start.sh && \
echo ' exit 1' >> /start.sh && \
echo ' fi' >> /start.sh && \
echo ' if ! kill -0 $python_pid 2>/dev/null; then' >> /start.sh && \
echo ' echo "Python server has stopped unexpectedly."' >> /start.sh && \
echo ' exit 1' >> /start.sh && \
echo ' fi' >> /start.sh && \
echo ' sleep 15' >> /start.sh && \
echo 'done' >> /start.sh && \
chmod +x /start.sh
CMD ["/start.sh"]
Understanding the start.sh
Script
This start.sh
script is crucial for starting and monitoring both Tor and the Python server. Here’s how it works:
-
Define
start_tor
Function:- Launches the Tor service in the background and captures its process ID (
tor_pid
). - Checks for the hidden service’s hostname file. If found, it outputs the .onion address. If not, it logs an error and exits.
- Launches the Tor service in the background and captures its process ID (
-
Define
start_python
Function:- Launches a Python HTTP server on port 80 to serve files from
/opt/site
(where your Hugo files are located) and captures its process ID (python_pid
).
- Launches a Python HTTP server on port 80 to serve files from
-
Run Both Services:
- Calls both
start_tor
andstart_python
functions to start each service.
- Calls both
-
Monitoring Loop:
- Continuously checks if the Tor and Python server processes are running.
- If Tor stops, it kills the Python server and exits.
- If the Python server stops unexpectedly, the script exits.
- Pauses for 15 seconds between checks to reduce CPU load.
This script ensures that both services stay operational or terminate cleanly if one fails.
Docker Compose File
The docker-compose.yml
file manages the service configuration and volumes. Here’s what each section does:
version: '3'
services:
tor-python:
build: . # Builds the image from the Dockerfile in the current directory
container_name: my-tor-website # Assigns a custom name to the container
user: root # Runs the container as root user
environment:
- DEBIAN_FRONTEND=noninteractive # Sets the environment variable for non-interactive mode
- TZ=Australia/Melbourne # Configures the container's timezone
volumes:
- ./tor:/etc/tor # Mounts Tor configuration from the host
- ./tordata:/var/lib/tor/hidden_service # Mounts the Tor hidden service data directory
- ./site:/opt/site # Mounts the static Hugo site content directory
ports:
- "80:80" # Exposes port 80 for HTTP access
restart: unless-stopped # Automatically restarts the container unless it is manually stopped
- build: Specifies the build context as the current directory, which contains the Dockerfile.
- container_name: Names the container
my-tor-website
for easy identification. - user: Runs the container as the root user, required for certain operations.
- environment: Configures environment variables, including non-interactive installation mode and timezone.
- volumes: Maps directories on the host machine to the container’s filesystem:
./tor:/etc/tor
: Mounts the Tor configuration../tordata:/var/lib/tor/hidden_service
: Stores Tor’s generated hidden service data, including the .onion address. By using a volume here, Tor data persists across container restarts, allowing the hidden service to keep the same .onion address after a restart. Without this, Tor would generate a new address each time, which would disrupt the service’s accessibility../site:/opt/site
: Serves the static Hugo site files.
- ports: Maps port 80 on the container to port 80 on the host for HTTP access.
- restart: Ensures that the container restarts automatically unless stopped manually.
Setting Up a Hugo Website
-
Install Hugo:
sudo apt-get install hugo
-
Generate a New Site:
hugo new site <your-site-folder>
-
Build the Site: Run
hugo
in the root folder of your Hugo site to generate static files in thepublic
folder. Copy these compiled files into thesite
directory in your project structure. -
Deploy: Copy the contents of the
public
folder into thesite/
directory within the project structure to serve via the Python HTTP server.
Running and Checking the Project
-
Build the Docker Image:
docker-compose build
-
Start the Service:
docker-compose up -d
-
Finding the Tor Address:
-
After Tor starts, it generates a
.onion
address for your hidden service. Since thetordata
folder is owned by root, you’ll need elevated permissions to access thehostname
file. To retrieve the address, use:sudo cat tordata/hostname
-
This file contains the .onion address where your site is accessible. You can access it using a Tor browser.
-
-
Verify Running Containers: Use the following command to check if the container is running:
docker ps
Look for
my-tor-website
in the list, along with itsSTATUS
and uptime. -
Viewing Logs:
-
Docker Logs: Use
docker logs
to view real-time output from the container.docker logs my-tor-website
-
Monitoring Logs for Errors: If Tor or Python stops unexpectedly,
docker logs
will show corresponding messages, such as “Tor has stopped. Stopping Python server…” for troubleshooting.
-
Conclusion
This setup allows you to host a static site as a hidden service behind Tor using Docker on Ubuntu. The start.sh
script inside the Docker container monitors both Tor and the Python server, providing a reliable, self-contained, and automated way to host a Tor-accessible website. By checking the hostname
file in the tordata
folder, you can easily find the .onion address to access your site. Using a persistent volume for tordata
ensures that the same address remains available across container restarts, making your site consistently reachable.