Overview
Joojoo is a lightweight static file server built with PHP 8. It focuses on serving static content with minimal overhead, inspired by the web server found in TP-Link modems.
One day I noticed my modem had a tiny built-in web server — it served the admin panel over plain HTTP with no frameworks, no dependencies, nothing. I got curious about how something so simple could actually work, and that curiosity turned into this project. I started building it to understand the system, and it naturally grew from there.
Development Roadmap
graph LR
A["✓ Core"] --> B["✓ Keep-Alive"] --> C["✓ Gzip"] --> D["✓ Cache-Control + ETag"] --> E["→ Range"] --> F["→ SSL"]
style A fill:#90EE90
style B fill:#90EE90
style C fill:#90EE90
style D fill:#90EE90
style E fill:#FFE4B5
style F fill:#FFE4B5
How It Works
Architecture
Show architecture diagram
graph TD
A["Master Process"] --> B["Create Server Socket"]
B --> C["Prefork Workers"]
C --> W1["Worker 1"]
C --> W2["Worker 2"]
C --> WN["Worker N"]
W1 --> L["Accept and Handle Client Connections"]
W2 --> L
WN --> L
L --> H["Resolve Content-Type"]
H --> C1["Cache-Control\n(always enabled)"]
C1 --> E1["Generate ETag"]
E1 --> R1["If-None-Match check\n(304 or 200)"]
R1 --> K["Keep-Alive Reuse\n(timeout/max requests)"]
Request Flow
Show request flow diagram
sequenceDiagram
participant Client
participant Worker
participant FileSystem
Client->>Worker: HTTP GET /index.html (+/- Accept-Encoding, If-None-Match)
Worker->>Worker: Parse path + headers
Worker->>FileSystem: Check if file exists
FileSystem-->>Worker: File found
Worker->>Worker: Resolve Content-Type
Worker->>Worker: Build ETag from mtime+size
alt ETag matches If-None-Match
Worker-->>Client: HTTP/1.1 304 Not Modified + ETag
else ETag does not match
Worker->>Worker: Set Cache-Control
alt Client accepts gzip
Worker->>FileSystem: Check if /index.html.gz exists
alt Precompressed file exists
FileSystem-->>Worker: .gz file found
else No precompressed file
FileSystem-->>Worker: .gz not found
Worker->>Worker: gzip original file on the fly
end
Worker->>Worker: Set Content-Encoding: gzip
Worker->>Worker: Set Vary: Accept-Encoding
else Client does not accept gzip
Worker->>Worker: Send original uncompressed file
end
Worker->>Worker: Set ETag and Content-Length
Worker-->>Client: HTTP/1.1 200 OK + file
end
Worker->>Worker: Keep-alive: reuse connection
Quick Start
Local Usage
php server.php --web-dir /path/to/site
Configuration
CLI Arguments:
-h, --help— Show CLI usage help and exit--web-dir PATH— Set root directory for serving files--workers-count N— Set worker process count (must be >= 1)
Environment Variables:
BASE_WEB_DIR— Environment variable for web root (used if--web-diris not provided)WORKERS_COUNT— Environment variable for worker count (used if--workers-countis not provided)
Precedence: CLI arguments override environment variables.
Docker
# Build
docker build -t joojoo .
# Run
docker run --name joojoo --init --rm \
-v /path/to/your/website:/html \
-p 80:8000 \
joojoo