Hosting a Tor-Accessible Hugo Website with Docker

Page content

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

  1. Ubuntu Linux: This tutorial assumes you have Ubuntu installed and Docker running. It was tested using Ubuntu Server 24.04.
  2. Docker & Docker Compose: You can follow the Docker installation guide if needed.
  3. 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.

  1. Environment Setup: Configures timezone and non-interactive mode for installations.
  2. Install Packages: Installs Tor and Python for serving the website.
  3. Directory Setup: Creates necessary directories and sets permissions for logs, Tor hidden service data, and website content.
  4. 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:

  1. 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.
  2. 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).
  3. Run Both Services:

    • Calls both start_tor and start_python functions to start each service.
  4. 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

  1. Install Hugo:

    sudo apt-get install hugo
    
  2. Generate a New Site:

    hugo new site <your-site-folder>
    
  3. Build the Site: Run hugo in the root folder of your Hugo site to generate static files in the public folder. Copy these compiled files into the site directory in your project structure.

  4. Deploy: Copy the contents of the public folder into the site/ directory within the project structure to serve via the Python HTTP server.


Running and Checking the Project

  1. Build the Docker Image:

    docker-compose build
    
  2. Start the Service:

    docker-compose up -d
    
  3. Finding the Tor Address:

    • After Tor starts, it generates a .onion address for your hidden service. Since the tordata folder is owned by root, you’ll need elevated permissions to access the hostname 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.

  4. 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 its STATUS and uptime.

  5. 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.