No description
Find a file
2026-02-03 16:52:30 -08:00
.dockerignore hello everybody! 2026-02-01 18:41:23 -08:00
.env.example env example 2026-02-03 16:52:30 -08:00
app.py fix nomenclature, set up bootstrap script 2026-02-03 16:30:29 -08:00
docker-compose.yml fix nomenclature, set up bootstrap script 2026-02-03 16:30:29 -08:00
Dockerfile hello everybody! 2026-02-01 18:41:23 -08:00
nginx.conf hello everybody! 2026-02-01 18:41:23 -08:00
README.md update readme 2026-02-03 16:40:55 -08:00
requirements.txt hello everybody! 2026-02-01 18:41:23 -08:00
supervisord.conf hello everybody! 2026-02-01 18:41:23 -08:00

Stone Soup Cluster: Web Node

The Stone Soup Cluster is a decentralized web hosting and data storage cluster operated by Secret Server Club. Web Nodes are redundant webservers scattered around Los Angeles (and hopefully, eventually, the world) which share a virtual private network created using Headscale. Web nodes serve websites through a VPS gateway on Digital Ocean (named Dusty) who routes traffic to the internet.

Dusty also host a simple site creator and deployment tool which can clone websites from a Git repo and add them to the registry of sites served on the network, automatically cloning the code to every webserver in the network.

This project is in a very early experimental stage, but we are working on a bootstrap script distributed through sneakernet, which can add servers to the network with one line of code.

More soon!

Love, SSC

Manual Setup

Prerequisites

  • Docker and Docker Compose installed
  • Access to Headscale/Tailscale network
  • Headscale IP of your dusty orchestration server

Steps

  1. Clone this repository:
git clone https://git.secretserver.club/Secret-Server-Club/web-node.git
cd web-node
  1. Create .env file from example:
cp .env.example .env
  1. Edit .env with your configuration:
DUSTY_URL=http://100.64.0.2:5000          # Your dusty server's Headscale IP
HEADSCALE_IP=                              # This server's Headscale IP (auto-detected if empty)
SERVER_NAME=web-node                       # Your server's name
SERVER_LOCATION=None of your business      # Server location
  1. Create data directory:
mkdir -p data
  1. Build and start the container:
docker-compose build
docker-compose up -d
  1. Check logs to verify registration:
docker-compose logs -f

Look for: ✅ Auto-registered with dusty! Headscale IP: 100.64.x.x

How It Works

  1. Dusty calls /api/deploy with domain, git_repo, and git_branch
  2. Web Node clones or updates the git repository to /app/data/sites/{domain}
  3. Web Node generates an nginx config for the domain in /app/data/nginx/sites-available/{domain}
  4. Web Node enables the nginx site (creates symlink in /app/data/nginx/sites-enabled/)
  5. Web Node reloads nginx
  6. The site is now accessible via nginx on port 80

Architecture

  • Docker Container: Runs both nginx and Flask API via supervisord
  • Data Volume: ./data directory is mounted to /app/data in container
    • Sites: /app/data/sites/{domain}
    • Nginx configs: /app/data/nginx/sites-available/ and sites-enabled/
    • Homepage: /app/data/homepage/index.html
  • Network: Uses external tailscale0 network for Headscale connectivity
  • Ports:
    • 80: nginx (HTTP)
    • 5000: Flask API

API Endpoints

POST /api/deploy

Deploy a site from a git repository.

Request:

{
  "domain": "example.com",
  "git_repo": "https://github.com/user/repo",
  "git_branch": "main"
}

Response:

{
  "status": "deployed",
  "domain": "example.com",
  "site_path": "/app/data/sites/example.com",
  "deployed_at": "2024-01-01T00:00:00"
}

GET /api/info

Get server information (for dusty to discover).

Response:

{
  "name": "web-node",
  "location": "None of your business",
  "headscale_ip": "100.x.x.x",
  "sites_served": 5,
  "status": "online"
}

GET /api/health

Health check endpoint.

Response:

{
  "status": "healthy",
  "nginx": "active",
  "sites_served": 5,
  "timestamp": "2024-01-01T00:00:00"
}

GET /api/sites

List all deployed sites.

Response:

[
  {
    "domain": "example.com",
    "path": "/app/data/sites/example.com",
    "exists": true
  }
]

GET /api/homepage

Get server homepage HTML.

PUT /api/homepage

Update server homepage HTML.

Request:

{
  "html": "<html>...</html>"
}

Nginx Configuration

  • Base nginx config: /etc/nginx/nginx.conf (copied into container)
  • Site configs: /app/data/nginx/sites-available/{domain}
  • Enabled sites: /app/data/nginx/sites-enabled/{domain} (symlinks)
  • Nginx includes all configs from sites-enabled/* automatically

Each deployed site gets its own nginx config that serves from /app/data/sites/{domain} and listens on port 80 for the domain name.

Auto-Registration

Web Node automatically registers itself with dusty on startup. It:

  1. Detects its Headscale IP (from HEADSCALE_IP env var, or auto-detects)
  2. Sends registration request to DUSTY_URL/api/servers/register
  3. Includes server name, location, and Headscale IP

If auto-registration fails, check:

  • DUSTY_URL is correct and dusty is reachable
  • HEADSCALE_IP is set correctly (or can be auto-detected)
  • Container logs: docker-compose logs

Notes

  • Sites are served directly from the git repository directory
  • If a site already exists, it will be updated via git pull
  • Nginx configs are auto-generated and managed by the API
  • No sudo access needed - everything runs in Docker container
  • Data persists in ./data directory on host
  • Multiple sites share one nginx instance (more efficient than per-site containers)