diff --git a/README.md b/README.md index 79b487c..facd3b9 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ handled separately — see [Exposing services](#exposing-services). | [gitea](gitea/) | 3000 | Self-hosted Git with issues and PRs. Also 222 for SSH clone | | [gluetun](gluetun/) | 9880 | VPN kill switch with qBittorrent + pyload-ng inside it, plus HTTP/SOCKS proxies | | [heimdall](heimdall/) | 4080 | Dashboard / start page for the other services | +| [httpserver](httpserver/) | 8083 | Bare nginx file share. Plain HTTP on purpose — works with very old clients | | [jellyfin](jellyfin/) | 8096 | Media streaming. Also 7359/udp for auto-discovery | | [openwebui](openwebui/) | 3033 | LLM chat UI. One `OWUI_TAG` switches between CPU, CUDA and bundled-Ollama | | [overleaf](overleaf/) | 8020 | LaTeX editor. Four containers: app, Mongo (replica set), Redis, init | diff --git a/httpserver/.env.example b/httpserver/.env.example new file mode 100644 index 0000000..9187f15 --- /dev/null +++ b/httpserver/.env.example @@ -0,0 +1,20 @@ +# Copy to .env and adjust. .env is git-ignored; this file is not. + +# ─── What gets served ─────────────────────────────────────────────── +# The stack serves ${TEMPDISK}/Temp as the web root -- the "/Temp" +# suffix is in compose.yaml, so point this at the parent. +# +# Written bare as ${TEMPDISK} in compose.yaml, with no default: unset, it +# interpolates to an empty string and nginx ends up serving /Temp from the +# host root. Set it. +TEMPDISK=/mnt/tank + +# ─── Identity ─────────────────────────────────────────────────────── +# Present for consistency with the other stacks, but NOTE: this is the +# official nginx image, not LinuxServer. It has no PUID/PGID handling +# anywhere in its entrypoint -- these are read by nobody. Its workers run +# as uid 101 (`nginx`) regardless, and only need read access. +PUID=1000 +PGID=1000 +# TZ affects log timestamps only. +TZ=Europe/Paris diff --git a/httpserver/README.md b/httpserver/README.md new file mode 100644 index 0000000..eefc4a8 --- /dev/null +++ b/httpserver/README.md @@ -0,0 +1,122 @@ +# httpserver + +A directory of files over plain HTTP, and nothing else. No accounts, no +database, no JavaScript — nginx with `autoindex on` pointed at a scratch +directory. Read-only: it is for handing files out, not taking them in. + +The point is **reach**, not features. This is the share that works from a +Windows 98 machine, a games console browser, a printer's built-in fetcher, or +`wget` on a router. [copyparty](../copyparty/) and [filebrowser](../filebrowser/) +are nicer for humans on modern machines; neither will talk to a client from 1999. + +## Quick start + +```bash +cp .env.example .env +$EDITOR .env # TEMPDISK -- the parent of the Temp/ you want served +mkdir -p "$TEMPDISK/Temp" +docker compose up -d +``` + +Open `http://:8083`. + +## Ports + +| Port | Proto | Purpose | +|------|-------|---------| +| 8083 | http | Directory listing and downloads | + +No HTTPS, deliberately. See below. + +## Why it looks like this + +**Plain HTTP is the feature, not an oversight.** Windows 98 cannot do TLS 1.2, +SNI, or any cipher suite a modern server will negotiate. The moment this goes +behind an HTTPS-terminating proxy it stops working for exactly the clients it +exists for. Verified: an `HTTP/1.0` request with **no `Host:` header** — which +is what ancient clients send — returns `200 OK`, because the single server block +is the default server. + +**The generated listing is deliberately primitive.** nginx's `autoindex` emits +`` then `<h1>`, `<hr>`, `<pre>` and `<a href>`. No CSS, no +JavaScript, not even a table. That renders in IE5, Netscape 4, Lynx, and +everything since. Responses carry `Content-Length`, `Last-Modified` and +`Accept-Ranges: bytes`, so resume works — which matters more on a flaky old +machine than a modern one. + +**`include mime.types` is there despite the minimalism**, because it makes weak +clients behave *better* and costs nothing. Mounting a config over +`/etc/nginx/nginx.conf` replaces the whole file, including nginx's default +`types` block, leaving only a tiny built-in table. Measured, before and after: + +| File | Without the include | With it | +|------|--------------------|---------| +| `photo.jpg` | `image/jpeg` | `image/jpeg` | +| `page.html` | `text/html` | `text/html` | +| `photo.png` | **`text/plain`** | `image/png` | +| `archive.zip` | **`text/plain`** | `application/zip` | +| `setup.exe` | **`text/plain`** | `application/octet-stream` | +| `manual.pdf` | **`text/plain`** | `application/pdf` | +| `song.mp3` | **`text/plain`** | `audio/mpeg` | + +Only html, gif and jpeg survive without it. A modern browser usually shrugs and +downloads anyway; **IE5 on Windows 98 tries to *display* a `text/plain` +response**, so a 700 MB zip becomes a screen of garbage instead of a Save +dialog. The directive is parsed once at startup — no per-request cost, and the +file already ships in the image. + +**No upload path.** nginx can accept WebDAV `PUT`, and an earlier version of +this config did — but `autoindex` has no upload form, so a browser can never use +it. Writes would only work from a DAV client: `curl -T`, or Windows Explorer's +*Map network drive*. A real upload button needs a CGI or an app behind nginx, +which is the complexity this stack exists to avoid. Use +[copyparty](../copyparty/) or [filebrowser](../filebrowser/) when you need +people to send files *to* you. + +**The config lives in `init/`** because this repo's `.gitignore` ignores +everything inside a service directory except a short list, and `init/**` is on +it. A config in any other subdirectory is silently *not committed*, and then the +bind mount has nothing to mount — Docker creates a **directory** at that path and +nginx won't start. Same failure mode as +[overleaf's init script](../overleaf/README.md#running-it-on-openmediavault). + +## Gotchas + +- **`PUID` and `PGID` do nothing.** This is the official nginx image, not + LinuxServer; there is no PUID/PGID handling anywhere in its entrypoint. They + are accepted and read by nobody — the same trap documented in + [audiobookshelf](../audiobookshelf/README.md). `TZ` affects log timestamps at + most. It costs nothing here because the share is read-only: the workers run as + uid 101 and only ever need read access. +- **`${TEMPDISK}` has no default.** Unset, Compose interpolates an empty string + and nginx serves `/Temp` from the host root rather than failing. The repo's + `${VAR:?message}` pattern exists for exactly this. +- **Keep filenames ASCII for old clients.** `autoindex` emits filenames as raw + bytes; Windows 98 is CP1252, so accented names mangle. +- **`:latest` is unpinned.** If server weight matters, `nginx:alpine-slim` is + **8 MB** compressed against **63 MB** for `nginx:latest`, with identical + behaviour for static serving. +- **Anyone who reaches the port sees everything.** `autoindex` publishes a + browsable listing of the whole share with no authentication. Nothing is + writable, so the risk is disclosure rather than tampering — but it is the + whole directory, and that is the intended trade for a temporary share on a + trusted network. + +## Exposing it + +**Think twice.** If you put this behind a proxy for remote access, you lose the +old clients that justify it existing. + +- [Reverse proxy with Caddy](../docs/reverse-proxy.md) — fine for modern + clients; it will terminate TLS and break Windows 98. +- [Authentik forward auth](../docs/authentik-sso.md#forward-auth--putting-an-app-with-no-sso-behind-authentik) + — the way to make it safe remotely, but it needs a browser that can complete + the login, which rules out the same ancient clients. + +Running two hostnames — one bare HTTP on the LAN, one proxied for outside — is +the honest way to have both. + +## Links + +- nginx `autoindex`: <https://nginx.org/en/docs/http/ngx_http_autoindex_module.html> +- Image: <https://hub.docker.com/_/nginx> diff --git a/httpserver/compose.yaml b/httpserver/compose.yaml new file mode 100644 index 0000000..2bb61e1 --- /dev/null +++ b/httpserver/compose.yaml @@ -0,0 +1,14 @@ +services: + nginx: + image: nginx:latest + container_name: nginx-webserver + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - ./init/nginx.conf:/etc/nginx/nginx.conf:ro + - ${TEMPDISK}/Temp:/usr/share/nginx/html + ports: + - 8083:80 + restart: unless-stopped diff --git a/httpserver/init/nginx.conf b/httpserver/init/nginx.conf new file mode 100644 index 0000000..6ff2944 --- /dev/null +++ b/httpserver/init/nginx.conf @@ -0,0 +1,20 @@ +events {} +http { + # Without these every file except .html/.gif/.jpg is served as text/plain, + # from nginx's tiny built-in table. Old browsers then try to *display* a + # .zip or .exe instead of offering to save it. Parsed once at startup -- + # no per-request cost, and mime.types already ships in the image. + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server { + listen 80; + server_name localhost; + + location / { + autoindex on; + root /usr/share/nginx/html; + } + } +} +