Added simple http server
This commit is contained in:
@@ -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 |
|
| [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 |
|
| [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 |
|
| [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 |
|
| [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 |
|
| [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 |
|
| [overleaf](overleaf/) | 8020 | LaTeX editor. Four containers: app, Mongo (replica set), Redis, init |
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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://<host>: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
|
||||||
|
`<html><head><title>` 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>
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user