Compare commits
3 Commits
main
..
fe76630071
| Author | SHA1 | Date | |
|---|---|---|---|
| fe76630071 | |||
| f1c75e95f2 | |||
| 712e08d038 |
@@ -36,8 +36,6 @@ handled separately — see [Exposing services](#exposing-services).
|
||||
|
||||
| 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 |
|
||||
| [openwebui](openwebui/) | 3033 | LLM chat UI, talks to an external model backend |
|
||||
| [owui-ollama](owui-ollama/) | 3034 | Open WebUI bundled with Ollama |
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# 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
|
||||
@@ -1,90 +0,0 @@
|
||||
# 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>
|
||||
@@ -1,23 +0,0 @@
|
||||
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"
|
||||
@@ -1,20 +0,0 @@
|
||||
# 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
|
||||
@@ -1,82 +0,0 @@
|
||||
# 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>
|
||||
@@ -1,21 +0,0 @@
|
||||
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
|
||||
@@ -33,19 +33,15 @@ TODO: your preferred flow — dashboard-created tunnel vs. `cloudflared tunnel
|
||||
create`. The dashboard route is easier to show on video; the CLI route is
|
||||
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
|
||||
services:
|
||||
cloudflaretunnel:
|
||||
cloudflared:
|
||||
image: cloudflare/cloudflared:latest
|
||||
container_name: cloudflare-tunnel
|
||||
container_name: cloudflared
|
||||
restart: unless-stopped
|
||||
command: tunnel --no-autoupdate run
|
||||
environment:
|
||||
- TUNNEL_TOKEN=${TUNNEL_TOKEN:?}
|
||||
- TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
|
||||
```
|
||||
|
||||
The tunnel token is a **real secret** — it grants the ability to route
|
||||
|
||||
Reference in New Issue
Block a user