Compare commits

...

4 Commits

Author SHA1 Message Date
Goyban 32b997d2c3 added MySpeed 2026-09-25 15:45:26 +02:00
Goyban 595c20a0a1 added Immich 2026-09-23 15:12:08 +02:00
Goyban 9355beaee2 added LibreSpeed 2026-09-23 15:11:31 +02:00
Goyban 8ff5428bb1 Added simple http server 2026-09-16 10:34:30 +02:00
14 changed files with 736 additions and 0 deletions
+4
View File
@@ -45,7 +45,11 @@ 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 |
| [immich](immich/) | 2283 | Photo and video library with local ML search. Four containers, Postgres + VectorChord |
| [librespeed](librespeed/) | 3011 | Self-hosted speed test. Measures the path to your box, not your ISP |
| [jellyfin](jellyfin/) | 8096 | Media streaming. Also 7359/udp for auto-discovery | | [jellyfin](jellyfin/) | 8096 | Media streaming. Also 7359/udp for auto-discovery |
| [myspeed](myspeed/) | 5216 | Scheduled speed tests with history graphs. Records what librespeed measures on demand |
| [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 |
+20
View File
@@ -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
+122
View File
@@ -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>
+14
View File
@@ -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
+20
View File
@@ -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;
}
}
}
+40
View File
@@ -0,0 +1,40 @@
# Copy to .env and adjust. .env is git-ignored; this file is not.
#
# Immich is the one stack here where .env is NOT optional: compose.yaml
# has `env_file: .env` on two services, so Compose refuses to start
# without the file existing. It is also the application's own config, not
# just a few variables Compose interpolates.
#
# This mirrors upstream's example.env for the pinned release. When you
# update compose.yaml, re-fetch this too:
# curl -fLO https://github.com/immich-app/immich/releases/latest/download/example.env
# ─── Storage ────────────────────────────────────────────────────────
# Your photos and videos. Mounted at /data inside the container -- do not
# change the container side, only this. This is the directory to back up.
UPLOAD_LOCATION=./library
# The Postgres data directory. Network shares (NFS/SMB) are NOT supported
# here and will corrupt the database; it must be local storage.
DB_DATA_LOCATION=./postgres
# ─── Version ────────────────────────────────────────────────────────
# Pin the MAJOR. Immich ships breaking changes between majors and the
# compose file changes with them, so leaving this unset (which resolves
# to `release`) means an unattended `docker compose pull` can jump a
# major and leave your compose file behind. See the README.
IMMICH_VERSION=v3
# ─── Database ───────────────────────────────────────────────────────
# Change this. Letters and digits only -- no special characters or
# spaces, they break the connection string.
# openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 32
DB_PASSWORD=
# ─── Optional ───────────────────────────────────────────────────────
# Uncomment and set to a tz identifier if the server clock isn't right.
#TZ=Europe/Paris
# ─── Leave these alone ──────────────────────────────────────────────
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
+121
View File
@@ -0,0 +1,121 @@
# Immich
Self-hosted photo and video library. Phone backup that runs on your own disk,
with the parts that make Google Photos worth using — face and object search,
places, memories, shared albums — done locally by a machine-learning container.
The heaviest stack in this repo: four containers, a Postgres with vector
extensions, and a model cache. Budget a few GB of RAM and expect the first
import to work the CPU hard.
## Quick start
```bash
cp .env.example .env
$EDITOR .env # UPLOAD_LOCATION, DB_DATA_LOCATION, DB_PASSWORD
docker compose up -d
```
Open `http://<host>:2283` and register. **The first account created is the
admin** — there is no separate bootstrap step, so do this before the port is
reachable by anyone else.
## Ports
| Port | Proto | Purpose |
|------|-------|---------|
| 2283 | http | Web UI, mobile app API, everything |
Postgres and Valkey are reachable only on the stack's internal network.
## Why it looks like this
**This file is upstream's, verbatim, and should stay that way.** Immich ships a
`docker-compose.yml` with each release, pins two of its images *by digest*, and
changes the file between versions — the mount path, the Postgres extension
build, added `shm_size`. Local edits turn every update into a three-way merge.
The repo convention of `${VAR:-default}` is deliberately **not** applied here for
the same reason: keeping it byte-identical to upstream means you can re-fetch and
`diff` to see exactly what a release changed.
```bash
curl -fLo /tmp/immich-new.yml \
https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
diff -u compose.yaml /tmp/immich-new.yml
```
Pinned at **v3.2.2** (released 2026-09-15).
**Four containers.** `immich-server` is the API and web UI. `immich-machine-learning`
runs the CLIP and face-detection models — it is separate because it is the part
you might want on a GPU, and because its model cache is a named volume rather
than something you back up. `database` is Postgres with **VectorChord**, which
is what makes similarity search possible; a stock Postgres image will not work.
`redis` is Valkey, the Redis fork, used as the job queue.
**`env_file: .env` is a hard dependency.** Unlike every other stack here, where
`.env` is optional because the compose file carries defaults, Compose will refuse
to start this one if the file does not exist.
**Images are digest-pinned** for Valkey and Postgres
(`@sha256:…`), so those two are byte-identical to what upstream tested against
this release. The Immich images themselves float on `${IMMICH_VERSION:-release}`
— which is why pinning `IMMICH_VERSION` in `.env` matters.
## Gotchas
- **Pin `IMMICH_VERSION` to a major.** Unset, it resolves to `release`, so a
routine `docker compose pull` can carry you across a major version while your
compose file stays behind. Upstream's own `example.env` now sets
`IMMICH_VERSION=v3` for exactly this reason.
- **The container media path changed, and it is easy to miss.** Older compose
files mount `${UPLOAD_LOCATION}:/usr/src/app/upload`; since **v1.140.0** it is
`${UPLOAD_LOCATION}:/data`, because `IMMICH_MEDIA_LOCATION` now defaults to
`/data`. Run a new image against an old compose file and the server looks for
your library at an unmounted path — it appears empty, and anything written
goes into the container and is lost when it is recreated. If you are adopting
this file over an older one, that mount line is the one to check first.
- **`DB_DATA_LOCATION` must be local storage.** Not NFS, not SMB, not a
network share of any kind. Postgres on a network filesystem corrupts, and
upstream says so in the example env.
- **Upgrading Postgres extensions is a real migration.** The image here is
`14-vectorchord0.4.3-pgvectors0.2.0`; moving from an older VectorChord build
makes Immich alter the database on first boot, which "can take seconds to
minutes". Back up `DB_DATA_LOCATION` before any version jump, and read
[the upgrade notes](https://docs.immich.app/install/upgrading/).
- **`shm_size: 128mb` on the database is load-bearing**, not decoration.
Postgres uses shared memory for larger queries and Docker's 64 MB default is
not enough here. Upstream's upgrade guide lists adding it as a required step.
- **`DB_STORAGE_TYPE: 'HDD'` is commented out** in the database environment.
Uncomment it if the Postgres data directory sits on a spinning disk — it tunes
the planner's assumptions about random reads. Leave it alone on SSD or NVMe.
- **Back up `UPLOAD_LOCATION` and `DB_DATA_LOCATION` together.** The files are
the photos; the database holds every album, face cluster, share and edit.
Either alone is most of a disaster.
- **The first import is expensive.** Machine learning runs over the whole
library once. On a low-power box expect it to saturate the CPU for hours —
the same caveat as [copyparty](../copyparty/README.md) on an N100, and worth
scheduling rather than discovering.
## Exposing it
The mobile apps talk to the same port as the browser, so whatever you put in
front has to work for both.
- [Reverse proxy with Caddy](../docs/reverse-proxy.md) — the normal route.
Raise the upload body limit or large videos fail on the way in.
- [Cloudflare Tunnel](../cloudflare-tunnel/) — **the ~100 MB body cap is exactly
the wrong limit for this.** A phone's 4K video will exceed it. Read
[the limits](../docs/cloudflare-tunnel.md) first.
- [Single sign-on with Authentik](../docs/authentik-sso.md) — Immich speaks OIDC
natively, so use that rather than forward auth, which would break every mobile
client. Keep a local admin account until the flow is proven.
## Links
- Install guide: <https://docs.immich.app/install/docker-compose>
- Release compose file: <https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml>
- Environment variables: <https://docs.immich.app/install/environment-variables>
- Upgrading: <https://docs.immich.app/install/upgrading/>
- Hardware-accelerated ML: <https://docs.immich.app/features/ml-hardware-acceleration>
- Compose builder: <https://immich.app/docker-compose-builder>
+76
View File
@@ -0,0 +1,76 @@
#
# WARNING: To install Immich, follow our guide: https://docs.immich.app/install/docker-compose
#
# Make sure to use the docker-compose.yml of the current release:
#
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
#
# The compose file on main may not be compatible with the latest release.
name: immich
services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
volumes:
# Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file
- ${UPLOAD_LOCATION}:/data
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports:
- '2283:2283'
depends_on:
- redis
- database
restart: always
healthcheck:
disable: false
immich-machine-learning:
container_name: immich_machine_learning
# For hardware acceleration, add one of -[armnn, cuda, rocm, openvino, rknn] to the image tag.
# Example tag: ${IMMICH_VERSION:-release}-cuda
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
# extends: # uncomment this section for hardware acceleration - see https://docs.immich.app/features/ml-hardware-acceleration
# file: hwaccel.ml.yml
# service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable
volumes:
- model-cache:/cache
env_file:
- .env
restart: always
healthcheck:
disable: false
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:70739f85ad2ee01a726a965584a0f94895f01b0c60b3cc8b0aeef11eaa6888cf
healthcheck:
test: redis-cli ping | grep -q PONG || exit 1
restart: always
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums'
# Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs
# DB_STORAGE_TYPE: 'HDD'
volumes:
# Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
shm_size: 128mb
restart: always
healthcheck:
disable: false
volumes:
model-cache:
+50
View File
@@ -0,0 +1,50 @@
# Copy to .env and adjust. .env is git-ignored; this file is not.
#
# NOTE: compose.yaml currently sets these values literally in its
# `environment:` block, so nothing here is read yet. Switch a line to
# ${VAR} in compose.yaml to start using it -- worth doing for the two
# marked SECRET below, which should never sit in a committed file.
# ─── Basics ─────────────────────────────────────────────────────────
#TITLE=LibreSpeed
#TAGLINE=
# Port on the host; the container always listens on WEBPORT (8080).
#PORT=3011
# ─── Telemetry: off by default ──────────────────────────────────────
# Turn this on and results are stored -- which needs a database AND a
# volume, neither of which compose.yaml has today. See the README.
#TELEMETRY=false
# SECRET. Protects the stats page at /results/stats.php. Required once
# telemetry is on, or anyone can read every result.
#PASSWORD=
# Shown in the privacy policy as the contact for deletion requests.
# Required for a production install with telemetry. NOTE: the old EMAIL
# variable is deprecated -- this is the current name.
#GDPR_EMAIL=you@example.com
# Telemetry logs client IPs unless you turn this on.
#REDACT_IP_ADDRESSES=false
#ENABLE_ID_OBFUSCATION=false
# 2-byte hex, e.g. 0x1234abcd. Random if unset -- which means IDs change
# every restart.
#OBFUSCATION_SALT=
# ─── Database (only when TELEMETRY=true) ────────────────────────────
# sqlite needs /database mounted or results vanish on recreate.
#DB_TYPE=sqlite
#DB_NAME=
#DB_HOSTNAME=
#DB_PORT=
#DB_USERNAME=
#DB_PASSWORD=
# ─── ISP name and distance ──────────────────────────────────────────
# SECRET. From ipinfo.io. Without it there is no ISP name, and DISTANCE
# does nothing.
#IPINFO_APIKEY=
#DISABLE_IPINFO=false
# km, mi, or empty to hide it. Needs IPINFO_APIKEY.
#DISTANCE=km
+93
View File
@@ -0,0 +1,93 @@
# LibreSpeed
A speed test you host yourself. One container, no Flash, no Java, no account —
open the page and it measures the link between the browser and *your* server.
That last part is the whole point and the whole catch: this does not tell you
how fast your internet is. It tells you how fast the path to this box is, which
is the more useful number when you are debugging your own LAN, your VPN, or a
neighbour's Wi-Fi — and a meaningless one if you point it through a CDN.
## Quick start
```bash
docker compose up -d
```
Open `http://<host>:3011`. There is nothing to configure and no `.env` needed
for the default read-only setup — see [`.env.example`](.env.example) for what
becomes relevant once you turn telemetry on.
## Ports
| Host | Container | Purpose |
|------|-----------|---------|
| 3011 | 8080 | Web UI and the test endpoints |
The container port follows `WEBPORT`, which defaults to 8080.
## Why it looks like this
**`MODE: standalone`** means one container is both the page and the test
endpoint. The alternatives matter only at scale: `backend` is a bare test point,
`frontend` serves a UI that talks to a list of remote backends, and `dual` is
both. For a single box, standalone is the whole story.
**No volumes, because telemetry is off.** With `TELEMETRY` unset the container
stores nothing and is entirely disposable — `docker compose down` costs you
nothing. The moment you enable it, that stops being true; see the gotchas.
**Most of the environment block is commented out on purpose.** It is upstream's
menu of options kept close to hand, which is more useful in a file you revisit
once a year than a link to documentation.
## Gotchas
- **Turning on telemetry needs a volume that isn't there.** `TELEMETRY: "true"`
stores results in sqlite by default, inside the container, at `/database`.
With no volume mounted the results survive restarts but vanish the moment the
container is recreated — which is to say, on the next `docker compose pull`.
Add `- ./data:/database` before enabling it.
- **Telemetry also wants `PASSWORD`**, which protects the stats page at
`/results/stats.php`. Without it, anyone who finds the URL can read every
result you've recorded, including client IPs.
- **`EMAIL` is deprecated — the current name is `GDPR_EMAIL`.** It is the
address shown in the privacy policy for deletion requests, and upstream
considers it required for a production install with telemetry on. The
commented block in `compose.yaml` still uses the old name.
- **Results log client IP addresses by default.** `REDACT_IP_ADDRESSES: "true"`
turns that off. Worth thinking about before you hand the URL to guests.
- **`DISTANCE` does nothing without `IPINFO_APIKEY`**, and that key is a
credential — it belongs in `.env` and a `${IPINFO_APIKEY}` reference, never in
the committed compose file.
- **A slow result may be your server, not your network.** The test works by
generating and swallowing garbage data, so on a low-power host the CPU can cap
the number before the link does. If you see a suspiciously round ceiling,
check `docker stats` while a test runs — the same trap as
[copyparty on an N100](../copyparty/README.md).
- **`:latest` is unpinned.** Note the Docker tags drop the `v` that the git tags
carry: the release is `v6.3.0`, the image tag is **`6.3.0`** (also `6.3`, `6`,
and `-alpine` variants of each). Pinning `v6.3.0` fails with a manifest error
that looks like a network problem.
- **`restart: always` rather than `unless-stopped`**, so a deliberate stop does
not survive a reboot. The rest of this repo uses the latter.
## Exposing it
- [Reverse proxy with Caddy](../docs/reverse-proxy.md) — works, but understand
what you are then measuring: the path to the proxy, shaped by whatever the
proxy does. For a LAN test, reach it directly by IP and skip the proxy.
- [Cloudflare Tunnel](../cloudflare-tunnel/) — **no.** This is a tool whose job
is to move as many gigabytes as it can for as long as you let it. Putting it
behind a tunnel produces a number that measures Cloudflare, while pushing
bulk transfer through a service not meant to carry it.
- [Single sign-on with Authentik](../docs/authentik-sso.md) — forward auth fits
fine; it is a browser-only app with no accounts of its own. Sensible if this
faces the internet, though a public speed test is also a free bandwidth sink
for whoever finds it.
## Links
- Upstream: <https://github.com/librespeed/speedtest>
- Docker options: <https://github.com/librespeed/speedtest/blob/master/doc_docker.md>
- Image: <https://github.com/librespeed/speedtest/pkgs/container/speedtest>
+19
View File
@@ -0,0 +1,19 @@
services:
speedtest:
container_name: speedtest
image: ghcr.io/librespeed/speedtest:latest
restart: unless-stopped
environment:
MODE: standalone
TITLE: "Gooyban's Speed Meter"
#TELEMETRY: "false"
#ENABLE_ID_OBFUSCATION: "false"
#REDACT_IP_ADDRESSES: "false"
#PASSWORD:
#GDPR_EMAIL::
#DISABLE_IPINFO: "false"
#IPINFO_APIKEY: "your api key"
#DISTANCE: "km"
#WEBPORT: 8080
ports:
- "3011:8080" # webport mapping (host:container)
+30
View File
@@ -0,0 +1,30 @@
# Copy to .env and adjust. .env is git-ignored; this file is not.
# Everything here has a default in compose.yaml, so a fresh clone runs
# with no .env at all.
# ─── Version ────────────────────────────────────────────────────────
# Defaults to `latest`, which is worth knowing about: upstream's last
# tagged release is v1.0.9 from May 2024, and `latest` is that same image.
# It will move if and when a new release is tagged. Active development
# ships separately as `development`, rebuilt Aug 2026.
#
# latest follows releases; today that is the May 2024 build
# 1.0.9 the same image, named explicitly
# development current build
#
#MYSPEED_TAG=latest
# ─── Paths ──────────────────────────────────────────────────────────
# The SQLite history database and every setting -- schedule, retention,
# provider, alerts. Months of measurements live here and nothing in it
# regenerates. This is the whole backup.
#DATA_PATH=./data
# ─── Ports ──────────────────────────────────────────────────────────
#PORT=5216
# ─── Timezone ───────────────────────────────────────────────────────
# There is no TZ variable to set: MySpeed takes no environment
# configuration at all, and the image documents none. Since tests run on
# cron expressions, container time decides when they fire -- UTC unless
# you uncomment the two clock mounts in compose.yaml.
+103
View File
@@ -0,0 +1,103 @@
# MySpeed
Speed test *history*. It runs a test on a cron schedule, keeps the results for as
long as you tell it to, and draws the graph — so you can answer "is it slow right
now, or has it been slow every evening for a fortnight?"
Pairs with [librespeed](../librespeed/) rather than competing with it: that one
is an on-demand test you open when you want a number, this one is the unattended
recorder. MySpeed can even use LibreSpeed as its measurement backend — it offers
**Ookla, LibreSpeed and Cloudflare** as test providers, so the instance next door
can be the thing it measures against.
## Quick start
```bash
cp .env.example .env # optional -- every value has a default
docker compose up -d
```
Open `http://<host>:5216`. Everything — schedule, retention, provider, alerts —
is configured in the web UI, not in this file.
## Ports
| Port | Proto | Purpose |
|------|-------|---------|
| 5216 | http | Web UI and API |
## Why it looks like this
**A bind mount instead of upstream's named volume.** The official install is
`docker run -v myspeed:/myspeed/data`, a Docker-managed volume. Here it is
`${DATA_PATH:-./data}`, so the history database sits next to the compose file
where you can see it and a backup job can find it without knowing Docker's
layout. Same reasoning as [jellyfin](../jellyfin/README.md)'s `/config`.
**MySpeed itself takes no environment configuration** — no `PUID`, no `TZ`, no
options. State and settings both live in the SQLite database in that one
directory, which makes the stack trivially portable and the backup story simple:
copy the directory. The variables in `.env.example` are therefore Compose-level
only: the image tag, the host port, and where that directory lives.
## Gotchas
- **`latest` is a build from May 2024.** The project is alive — the repository
was pushed to in September 2026 — but the last tagged release is `v1.0.9` and
Docker Hub's `latest` still points at that same image:
| Tag | Pushed | Size | Digest |
|-----|--------|------|--------|
| `development` | 2026-08-02 | 102 MB | `sha256:043c2f00…` |
| `latest` | 2024-05-21 | 59 MB | `sha256:3a3e774b…` |
| `1.0.9` | 2024-05-21 | 59 MB | `sha256:3a3e774b…` |
So `latest` here does not mean recent — it means "the newest *release*", and
there hasn't been one since May 2024. It will start moving again if upstream
tags one. Until then, "update to the newest version" means
`MYSPEED_TAG=development`, which is a choice to make knowingly rather than a
`docker compose pull` away.
- **Scheduled tests run on container time.** The whole feature is cron
expressions, and the image documents no `TZ` variable — so "run at 03:00" is
03:00 **UTC** unless you tell the container otherwise. The two clock mounts are
in `compose.yaml`, commented out; uncomment them to follow the host instead.
They are off by default because turning them on shifts every existing schedule
by your UTC offset. Same class of trap as the cron container in
[firefly](../firefly/README.md).
- **A speed test measures the path from *this box*.** If MySpeed runs on the NAS,
the graph is the NAS's connectivity — which is usually what you want, but it is
not your laptop's Wi-Fi. And a low-power host can cap the result on CPU before
the link saturates; see [librespeed](../librespeed/README.md) for the same
caveat.
- **Back up the data directory.** Months of measurements and every setting are in
there, and nothing regenerates.
## What else it does
Worth knowing about before you go looking for another tool:
- **Health checks with alerting** — email, Signal, WhatsApp or Telegram when a
test fails or the line goes down.
- **Prometheus and Grafana export**, if you would rather graph this beside
everything else.
- **Multiple servers on one instance**, so a single dashboard can watch more than
one site.
- **Configurable retention**, from a few days to forever.
## Exposing it
- [Reverse proxy with Caddy](../docs/reverse-proxy.md) — fine; the UI is just a
dashboard, and the tests run server-side regardless of how you reach it.
- [Cloudflare Tunnel](../cloudflare-tunnel/) — fine for the dashboard, unlike
[librespeed](../librespeed/README.md): the measurements do not travel through
the tunnel, only the page does.
- [Single sign-on with Authentik](../docs/authentik-sso.md) — forward auth suits
it: browser-only, no accounts of its own. MySpeed has no authentication
built in, so anyone who reaches the port can change the schedule and read the
history.
## Links
- Upstream: <https://github.com/gnmyt/MySpeed>
- Website and docs: <https://myspeed.dev> · <https://docs.myspeed.dev/setup/linux>
- Image: <https://hub.docker.com/r/germannewsmaker/myspeed>
+24
View File
@@ -0,0 +1,24 @@
name: myspeed
services:
myspeed:
# Note what `latest` means here: upstream's last tagged release is
# v1.0.9 from May 2024, and `latest` still points at that same image.
# Active development ships as `development` (rebuilt Aug 2026), so
# set MYSPEED_TAG=development in .env if you want the current build.
image: germannewsmaker/myspeed:${MYSPEED_TAG:-latest}
container_name: myspeed
restart: unless-stopped
ports:
- "${PORT:-5216}:5216"
volumes:
# History database and every setting MySpeed has. This directory
# is the entire backup -- nothing in it regenerates.
- ${DATA_PATH:-./data}:/myspeed/data
# MySpeed schedules tests with cron expressions and reads no TZ
# variable, so container time decides when they run -- UTC by
# default. Uncomment to follow the host clock instead.
#- /etc/localtime:/etc/localtime:ro
#- /etc/timezone:/etc/timezone:ro