Compare commits

..

2 Commits

Author SHA1 Message Date
Goyban d196fe5b2c added audiobookshelf 2026-08-20 16:10:38 +02:00
Goyban e5a1395c12 added cloudflare-tunnel 2026-08-19 15:54:37 +02:00
8 changed files with 274 additions and 3 deletions
+2
View File
@@ -36,6 +36,8 @@ handled separately — see [Exposing services](#exposing-services).
| Service | Port | Notes | | Service | Port | Notes |
|---------|------|-------| |---------|------|-------|
| [audiobookshelf](audiobookshelf/) | 13378 | Audiobooks and podcasts, with per-user progress sync |
| [cloudflare-tunnel](cloudflare-tunnel/) | — | Outbound connector. Exposes other services without opening a port |
| [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, talks to an external model backend | | [openwebui](openwebui/) | 3033 | LLM chat UI, talks to an external model backend |
| [owui-ollama](owui-ollama/) | 3034 | Open WebUI bundled with Ollama | | [owui-ollama](owui-ollama/) | 3034 | Open WebUI bundled with Ollama |
+29
View File
@@ -0,0 +1,29 @@
# Copy to .env and adjust. .env is git-ignored; this file is not.
# ─── Identity ───────────────────────────────────────────────────────
# Defaulted to 1000/1000 in compose.yaml. Unlike the LinuxServer images in
# this repo, these are NOT passed to the container as PUID/PGID -- that image
# ignores them. They feed Docker's `user:` directive, which actually changes
# the uid the process runs as. Must own CONFIG_PATH, METADATA_PATH and be able
# to read the media.
#PUID=1000
#PGID=1000
#TZ=Europe/Paris
# ─── Paths ──────────────────────────────────────────────────────────
# Audiobookshelf's database and settings. Small; defaults to ./config.
#CONFIG_PATH=./config
# Cover art, cached metadata, downloads. Grows with your library.
#METADATA_PATH=./metadata
# REQUIRED. The parent folder holding Audiobooks/ and Podcasts/.
# compose.yaml mounts ${MEDIA_PATH}/Audiobooks and ${MEDIA_PATH}/Podcasts.
MEDIA_PATH=/srv/media
# ─── Ports ──────────────────────────────────────────────────────────
# Host port only. Do NOT add PORT to the container's environment -- the image
# reads PORT to decide what it listens on internally, which would break the
# 13378:80 mapping.
#PORT=13378
+90
View File
@@ -0,0 +1,90 @@
# Audiobookshelf
Audiobook and podcast server. Tracks progress per user across devices, which is
the thing a plain file share can't do — pick up on your phone where the car
left off.
## Quick start
```bash
cp .env.example .env
$EDITOR .env # MEDIA_PATH is required
docker compose up -d
```
Open `http://<host>:13378` and create the admin account. Point the Audiobooks
library at `/audiobooks` and Podcasts at `/podcasts` — those are the paths
*inside* the container.
## Ports
| Port | Proto | Purpose |
|------|-------|---------|
| 13378 | http | Web UI and API (container listens on 80) |
## Why it looks like this
**The official image, not LinuxServer.** Audiobookshelf ships its own and it's
the one upstream actually tests. That's a different call from jellyfin, where I
stay on LinuxServer out of long habit — here there's no reason to add a layer.
**`user:` instead of `PUID`/`PGID`.** This is the part worth knowing, because
copying the pattern from the other services in this repo produces something
that *looks* right and silently isn't. The image's config:
```
User : '' <- empty, so root
Entrypoint: ['tini', '--']
Cmd : ['node', 'index.js']
```
There's no s6 layer and no init script — `tini` execs straight into Node. A
`PUID` environment variable is accepted and then read by nobody. Everything
gets written as root, and you find out when you try to move your library from
the host and can't.
Docker's own `user:` directive is the mechanism that actually works here, so
`PUID`/`PGID` in `.env` feed that instead. Same names as the rest of the repo,
different plumbing underneath.
**`/config` and `/metadata` are separate mounts** because they grow very
differently. `/config` is the SQLite database and settings — small, and the
thing to back up. `/metadata` is cover art, cached data and downloads, and
tracks the size of your library. Splitting them means a backup job can take the
first and skip the second.
**Two media mounts, not one.** Audiobookshelf wants each library rooted
separately, so `${MEDIA_PATH}` points at the parent and the compose file mounts
`Audiobooks/` and `Podcasts/` beneath it. Only `MEDIA_PATH` goes in `.env`.
## Gotchas
- **`PORT` is a container variable here.** The image reads `PORT` to decide
what it listens on internally. It's used in `compose.yaml` for the *host*
side of `13378:80` only — if you ever add it to the `environment:` block,
the container moves off port 80 and the mapping breaks with nothing in the
logs to explain it.
- **Switching to `user:` on an existing install needs a chown.** If you ran
this as root first, the files it created are root-owned and the container
now can't write them. `sudo chown -R 1000:1000 config metadata` once.
- **Back up `/config`.** It holds the database — accounts, libraries, and
everyone's listening progress. `/metadata` regenerates; `/config` doesn't.
- **Media can be mounted `:ro`** unless you turn on storing metadata alongside
the media files, which is off by default. Worth doing if you want the library
protected from the server.
## Exposing it
Publishes a plain HTTP port; nothing here is proxy-aware.
- [Reverse proxy with Caddy](../docs/reverse-proxy.md)
- [Cloudflare Tunnel](../cloudflare-tunnel/) — audio is far lighter than video,
but it's still media served through the proxy; read
[the limits](../docs/cloudflare-tunnel.md) before pointing a tunnel at it.
- [Single sign-on with Authentik](../docs/authentik-sso.md) — as with Jellyfin,
the mobile apps won't understand a forward-auth login page.
## Links
- Upstream docs: <https://www.audiobookshelf.org/docs>
- Image: <https://github.com/advplyr/audiobookshelf/pkgs/container/audiobookshelf>
+23
View File
@@ -0,0 +1,23 @@
services:
audiobookshelf:
image: ghcr.io/advplyr/audiobookshelf:latest
container_name: audiobookshelf
restart: unless-stopped
# NOT a LinuxServer image: no s6, no init script -- tini execs straight
# into node, as root. PUID/PGID would be silently ignored, so ownership is
# set with Docker's own `user:` instead. See the README.
user: "${PUID:-1000}:${PGID:-1000}"
environment:
- TZ=${TZ:-Etc/UTC}
volumes:
- ${CONFIG_PATH:-./config}:/config
- ${METADATA_PATH:-./metadata}:/metadata
- "${MEDIA_PATH:?set MEDIA_PATH in .env to the parent of your Audiobooks/Podcasts folders}/Audiobooks:/audiobooks"
- ${MEDIA_PATH}/Podcasts:/podcasts
ports:
# 13378 is the conventional host port; the container always listens on 80.
- "${PORT:-13378}:80"
+20
View File
@@ -0,0 +1,20 @@
# Copy to .env and adjust. .env is git-ignored; this file is not.
# NEVER put the real token in this file.
# ─── Required ───────────────────────────────────────────────────────
# Cloudflare Zero Trust tunnel token.
#
# Dashboard -> Zero Trust -> Networks -> Tunnels -> (your tunnel) -> Configure
# https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/
#
# The dashboard shows a pre-filled `docker run ... --token <TOKEN>` command.
# Copy only the token itself, not the whole command.
#
# Treat it like a password: anyone holding it can route traffic into your
# network. If it leaks, delete the tunnel in the dashboard and make a new one
# — the token cannot be rotated on its own.
TUNNEL_TOKEN=
# ─── Shared ─────────────────────────────────────────────────────────
# Defaulted to Etc/UTC in compose.yaml. Only affects log timestamps here.
#TZ=Europe/Paris
+82
View File
@@ -0,0 +1,82 @@
# Cloudflare Tunnel
The `cloudflared` connector. It dials **out** to Cloudflare and Cloudflare
routes public traffic back down that connection — so a service reaches the
internet with no port forwarded, no static IP and no VPS.
One tunnel serves every service; you don't need a copy of this per app.
## Quick start
```bash
cp .env.example .env
$EDITOR .env # paste TUNNEL_TOKEN
docker compose up -d
```
Get the token at **Dashboard → Zero Trust → Networks → Tunnels →** *(your
tunnel)* **→ Configure**. The page shows a pre-filled `docker run … --token
<TOKEN>` command — copy only the token part.
Docs: <https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/>
Then add routes in the same dashboard: **Public Hostname → Service**, e.g.
`gitea.example.com → http://host.docker.internal:3000`.
## Ports
**None.** Nothing listens on the host — that's the entire point. If you find
yourself adding a `ports:` block here, something has been misunderstood.
## Why it looks like this
**The token lives in `.env`, and compose refuses to start without it.** It's
written `${TUNNEL_TOKEN:?…}` rather than with a default, because a
blank token gives you a container that starts, retries, and quietly routes
nothing. The token is a genuine credential — it authorises routing traffic
into your network — so it never belongs in a committed file.
**No `PUID`/`PGID`.** Unlike most services here, this isn't a LinuxServer
image and doesn't understand those variables. It writes nothing to disk and
needs no volumes, so there's no ownership to get right. `TZ` is kept only
because it affects log timestamps.
**`extra_hosts: host.docker.internal:host-gateway`.** Routes are configured in
the dashboard, and the connector has to resolve whatever hostname you put
there. This line lets you point at services published on the Docker host
without hardcoding the LAN IP — which would otherwise break the day your
router hands out a different lease.
**`--no-autoupdate`.** The binary can update itself in place, which means the
thing routing your traffic changes without you doing anything. Pinning that
off makes updates a deliberate `docker compose pull`.
**No config file, no volume.** This is the remotely-managed style of tunnel:
routing lives in the Cloudflare dashboard, not on disk. The trade is that your
routing config isn't in this repo — the alternative (`cloudflared tunnel
create` plus a mounted `config.yml`) is version-controllable but more setup.
## Gotchas
- **Don't put video through it.** Cloudflare's terms restrict serving large
non-HTML content, and streaming a media library is the classic way to get an
account flagged. Jellyfin belongs behind your own reverse proxy.
- **~100 MB upload cap** on the free plan. Photo backup and file sync break on
this, sometimes with nothing clearer than a `413`.
- **The token can't be rotated.** If it leaks, delete the tunnel and create a
new one.
- **A dead tunnel looks like a DNS problem.** If a hostname stops resolving,
check `docker compose logs cloudflared` before suspecting Cloudflare.
## Related
- [When to use a tunnel, and when not to](../docs/cloudflare-tunnel.md) — the
longer write-up, including the comparison against a VPS reverse proxy.
- [Reverse proxy with Caddy](../docs/reverse-proxy.md)
- [Single sign-on with Authentik](../docs/authentik-sso.md) — note Cloudflare
Access can also sit in front of a tunnel; running both is usually redundant.
## Links
- Connector docs: <https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/>
- Image: <https://hub.docker.com/r/cloudflare/cloudflared>
+21
View File
@@ -0,0 +1,21 @@
services:
cloudflaretunnel:
image: cloudflare/cloudflared:latest
container_name: cloudflare-tunnel
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
# Only affects log timestamps here — nothing is written to disk.
- TZ=${TZ:-Etc/UTC}
# A real secret: it grants the ability to route traffic into your network.
# Dashboard -> Zero Trust -> Networks -> Tunnels -> (tunnel) -> Configure
- TUNNEL_TOKEN=${TUNNEL_TOKEN:?get it from Cloudflare Zero Trust > Networks > Tunnels > Configure}
# No `ports:` and no PUID/PGID on purpose — see the README.
# Lets dashboard routes reach services published on the Docker host, e.g.
# gitea.example.com -> http://host.docker.internal:3000
# Drop this if you route to containers by name on a shared network.
extra_hosts:
- host.docker.internal:host-gateway
+7 -3
View File
@@ -33,15 +33,19 @@ TODO: your preferred flow — dashboard-created tunnel vs. `cloudflared tunnel
create`. The dashboard route is easier to show on video; the CLI route is create`. The dashboard route is easier to show on video; the CLI route is
easier to keep in version control. easier to keep in version control.
The connector itself is a service in this repo — see
[`cloudflare-tunnel/`](../cloudflare-tunnel/) for the compose file, the token
setup and the gotchas. In short:
```yaml ```yaml
services: services:
cloudflared: cloudflaretunnel:
image: cloudflare/cloudflared:latest image: cloudflare/cloudflared:latest
container_name: cloudflared container_name: cloudflare-tunnel
restart: unless-stopped restart: unless-stopped
command: tunnel --no-autoupdate run command: tunnel --no-autoupdate run
environment: environment:
- TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN} - TUNNEL_TOKEN=${TUNNEL_TOKEN:?}
``` ```
The tunnel token is a **real secret** — it grants the ability to route The tunnel token is a **real secret** — it grants the ability to route