Compare commits
3 Commits
04e84fde7a
...
f1f184c36e
| Author | SHA1 | Date | |
|---|---|---|---|
| f1f184c36e | |||
| 3c2f03d130 | |||
| 18e468c88f |
+11
@@ -7,6 +7,10 @@
|
|||||||
# ─── …except the files that define a stack ──────────────────────────
|
# ─── …except the files that define a stack ──────────────────────────
|
||||||
!*/compose.yaml
|
!*/compose.yaml
|
||||||
!*/docker-compose.yaml
|
!*/docker-compose.yaml
|
||||||
|
# A service's alternative compose file, published as documentation. Narrow on
|
||||||
|
# purpose: `!*/compose.*.yaml` would also un-ignore compose.override.yaml,
|
||||||
|
# which is exactly where people keep local secrets.
|
||||||
|
!*/compose.personal.yaml
|
||||||
!*/.env.example
|
!*/.env.example
|
||||||
!*/README.md
|
!*/README.md
|
||||||
|
|
||||||
@@ -26,3 +30,10 @@
|
|||||||
*.crt
|
*.crt
|
||||||
secrets/
|
secrets/
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
|
# VPN configs carry private keys. Named explicitly because the `!*/init/**`
|
||||||
|
# rule above would otherwise happily commit one dropped in a service's
|
||||||
|
# init/ directory -- which is exactly where this repo's own conventions
|
||||||
|
# tell you to put runtime config fragments.
|
||||||
|
wg*.conf
|
||||||
|
*.ovpn
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ handled separately — see [Exposing services](#exposing-services).
|
|||||||
| [copyparty](copyparty/) | 3923 | File server built for big/resumable transfers, WebDAV, media index |
|
| [copyparty](copyparty/) | 3923 | File server built for big/resumable transfers, WebDAV, media index |
|
||||||
| [filebrowser](filebrowser/) | 3672 | Web file manager for one directory tree (FileBrowser Quantum fork) |
|
| [filebrowser](filebrowser/) | 3672 | Web file manager for one directory tree (FileBrowser Quantum fork) |
|
||||||
| [firefly](firefly/) | 8564 | Personal finance / double-entry ledger. Also 8566 for the data importer |
|
| [firefly](firefly/) | 8564 | Personal finance / double-entry ledger. Also 8566 for the data importer |
|
||||||
|
| [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 |
|
||||||
| [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,38 @@
|
|||||||
|
# Copy to .env and adjust. .env is git-ignored; this file is not.
|
||||||
|
#
|
||||||
|
# Only TEMPDISK is required -- everything else has a default in
|
||||||
|
# compose.yaml. (compose.personal.yaml has no defaults at all: it uses
|
||||||
|
# bare ${PUID} etc., which Compose turns into empty strings rather than
|
||||||
|
# failing, so with that file set all four.)
|
||||||
|
|
||||||
|
# ─── Identity ───────────────────────────────────────────────────────
|
||||||
|
# Owner of the downloads directory and the two config directories.
|
||||||
|
# id -u ; id -g
|
||||||
|
#PUID=1000
|
||||||
|
#PGID=1000
|
||||||
|
#TZ=Europe/Paris
|
||||||
|
|
||||||
|
# ─── Where downloads land ───────────────────────────────────────────
|
||||||
|
# The parent of downloads/torrents and downloads/pyload. Point this at
|
||||||
|
# the disk with room on it -- this is the one path that cannot have a
|
||||||
|
# sensible default.
|
||||||
|
TEMPDISK=/mnt/tank
|
||||||
|
|
||||||
|
# ─── Proxy authentication ───────────────────────────────────────────
|
||||||
|
# Both proxies are UNAUTHENTICATED unless these are set -- anyone who can
|
||||||
|
# reach ports 9980/9998 can route traffic through your VPN account. Set
|
||||||
|
# both (gluetun needs user and password together) and authentication turns
|
||||||
|
# on for the HTTP and SOCKS5 proxies at once.
|
||||||
|
#PROXY_USER=
|
||||||
|
#PROXY_PASSWORD=
|
||||||
|
|
||||||
|
# ─── Ports ──────────────────────────────────────────────────────────
|
||||||
|
#QBT_PORT=9880
|
||||||
|
#PYLOAD_PORT=8880
|
||||||
|
#SOCKS_PORT=9980
|
||||||
|
#HTTPPROXY_PORT=9998
|
||||||
|
|
||||||
|
# ─── NOT in this file ───────────────────────────────────────────────
|
||||||
|
# Your WireGuard credentials do NOT belong here. They live in wg0.conf
|
||||||
|
# next to compose.yaml, which is git-ignored (see the README). Do not
|
||||||
|
# move that file into init/ and do not paste its keys into .env.
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
# gluetun + qBittorrent + pyload-ng
|
||||||
|
|
||||||
|
A VPN container with two download clients living inside its network stack. The
|
||||||
|
point is the **kill switch**: qBittorrent and pyload-ng have no network of their
|
||||||
|
own, so if the tunnel drops they lose connectivity entirely rather than falling
|
||||||
|
back to your real address.
|
||||||
|
|
||||||
|
Also exposes gluetun's HTTP and SOCKS5 proxies, so other machines and browser
|
||||||
|
extensions (SwitchyOmega and friends) can route through the same tunnel without
|
||||||
|
running a VPN client themselves.
|
||||||
|
|
||||||
|
Configured for **TorGuard WireGuard** via gluetun's `custom` provider.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
$EDITOR .env # TEMPDISK is the one you must set
|
||||||
|
|
||||||
|
# Get a WireGuard config from TorGuard's config generator, save it as
|
||||||
|
# wg0.conf NEXT TO THIS FILE. It is git-ignored. Do not put it in init/.
|
||||||
|
cp ~/Downloads/torguard-wg.conf ./wg0.conf
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
docker compose logs -f gluetun # watch for the public IP line
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the tunnel is actually carrying traffic before you seed anything:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec gluetun wget -qO- https://ipinfo.io/ip # should be TorGuard's
|
||||||
|
curl -x http://localhost:9998 https://ipinfo.io/ip # same, via the HTTP proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ports
|
||||||
|
|
||||||
|
Every port is published on the **gluetun** service, because the other two
|
||||||
|
containers have no network namespace of their own.
|
||||||
|
|
||||||
|
| Host | Container | Purpose |
|
||||||
|
|------|-----------|---------|
|
||||||
|
| 9880 | 9880 | qBittorrent web UI |
|
||||||
|
| 8880 | 8000 | pyload-ng web UI |
|
||||||
|
| 9980 | 1080 | SOCKS5 proxy |
|
||||||
|
| 9998 | 8888 | HTTP proxy |
|
||||||
|
|
||||||
|
In `compose.personal.yaml` two of these differ: pyload is published `8880:8880`
|
||||||
|
against a container that listens on 8000, and the SOCKS5 server isn't running at
|
||||||
|
all. See the gotchas.
|
||||||
|
|
||||||
|
## Two compose files
|
||||||
|
|
||||||
|
**[`compose.yaml`](compose.yaml)** is the maintained one and what
|
||||||
|
`docker compose up` uses.
|
||||||
|
|
||||||
|
**[`compose.personal.yaml`](compose.personal.yaml)** is the older variant still
|
||||||
|
running on my own box. It works and its HTTP proxy is in daily use, so it's kept
|
||||||
|
as-is rather than tidied — but it has the quirks listed under gotchas below.
|
||||||
|
Compose won't load it unless you name it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f compose.personal.yaml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
What the maintained file changes:
|
||||||
|
|
||||||
|
| Change | Why |
|
||||||
|
|--------|-----|
|
||||||
|
| `SOCKS5_ENABLED` / `SOCKS5_LISTENING_ADDRESS` | `SOCKSPROXY*` are not gluetun variables; the SOCKS5 server never starts |
|
||||||
|
| `HTTPPROXY_LISTENING_ADDRESS` | `HTTPPROXY_PORT` works but logs a deprecation warning |
|
||||||
|
| `8880:8000` | pyload-ng listens on 8000; its port isn't settable by env var |
|
||||||
|
| `HTTPPROXY_USER` / `SOCKS5_USER` + passwords | opt-in proxy authentication, off unless set in `.env` |
|
||||||
|
| `qmcgaw/gluetun:v3.41.3` | pinned; this container holds the tunnel |
|
||||||
|
| `${PUID:-1000}`, `${TEMPDISK:?…}` | defaults where sensible, loud failure where a value can't be guessed |
|
||||||
|
| `condition: service_healthy` | gluetun ships a healthcheck; wait for the tunnel instead of racing it |
|
||||||
|
| `lscr.io/linuxserver/qbittorrent` | matches pyload-ng's registry; same image |
|
||||||
|
|
||||||
|
Same design, same ports, same volumes.
|
||||||
|
|
||||||
|
## Other VPN providers
|
||||||
|
|
||||||
|
Nothing here is TorGuard-specific except `wg0.conf`. gluetun natively supports
|
||||||
|
around twenty providers, and for most of them you delete the file mount and set
|
||||||
|
two or three variables instead:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- VPN_SERVICE_PROVIDER=mullvad
|
||||||
|
- VPN_TYPE=wireguard
|
||||||
|
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
|
||||||
|
- WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
|
||||||
|
- SERVER_CITIES=Amsterdam
|
||||||
|
```
|
||||||
|
|
||||||
|
**Native WireGuard** — the easy path: AirVPN, FastestVPN, IVPN, Mullvad,
|
||||||
|
NordVPN, ProtonVPN, Surfshark, Windscribe.
|
||||||
|
|
||||||
|
**Native OpenVPN** — every supported provider, usually just `OPENVPN_USER` and
|
||||||
|
`OPENVPN_PASSWORD`: the list above plus Cyberghost, ExpressVPN, Giganews,
|
||||||
|
HideMyAss, IPVanish, Privado, Private Internet Access, PrivateVPN, PureVPN,
|
||||||
|
SlickVPN, TorGuard, VPNSecure, VPN Unlimited, VyprVPN.
|
||||||
|
|
||||||
|
**WireGuard via the custom provider** — Cyberghost, Private Internet Access,
|
||||||
|
PrivateVPN, PureVPN, TorGuard, VPN Unlimited and VyprVPN have no native
|
||||||
|
WireGuard support in gluetun, so they take the `wg0.conf` route this stack uses.
|
||||||
|
|
||||||
|
That last line is why this stack looks the way it does: **TorGuard *is* a
|
||||||
|
built-in provider (`VPN_SERVICE_PROVIDER=torguard`), but only for OpenVPN.**
|
||||||
|
Wanting TorGuard *and* WireGuard is precisely the case gluetun's own
|
||||||
|
documentation sends to the custom provider. It isn't a workaround — it's the
|
||||||
|
documented path.
|
||||||
|
|
||||||
|
Switching provider changes nothing else: the kill switch, the shared network
|
||||||
|
namespace and both proxies work the same way regardless of who terminates the
|
||||||
|
tunnel. Check your provider's page in
|
||||||
|
[the gluetun wiki](https://github.com/qdm12/gluetun-wiki) for its exact
|
||||||
|
variables.
|
||||||
|
|
||||||
|
## Why it looks like this
|
||||||
|
|
||||||
|
**`network_mode: "service:gluetun"` is the whole design.** qBittorrent and
|
||||||
|
pyload-ng don't get their own network stack — they share gluetun's. That's what
|
||||||
|
makes the kill switch absolute: there is no route out except the tunnel, so a
|
||||||
|
misconfiguration fails closed rather than leaking. The cost is that those
|
||||||
|
containers **cannot declare their own `ports:`**; Docker refuses it. Every port
|
||||||
|
they need is published on gluetun instead, which is why the list above looks
|
||||||
|
like it belongs to the wrong service.
|
||||||
|
|
||||||
|
**gluetun's firewall is on by default.** Confirmed in its startup summary
|
||||||
|
(`Firewall settings: Enabled: yes`) — you don't have to configure a kill switch,
|
||||||
|
you have to avoid breaking the one that's already there.
|
||||||
|
|
||||||
|
**`VPN_SERVICE_PROVIDER=custom` plus a mounted `wg0.conf`.** TorGuard isn't one
|
||||||
|
of gluetun's built-in providers, so rather than translating their config into
|
||||||
|
`WIREGUARD_PRIVATE_KEY` / `VPN_ENDPOINT_IP` environment variables, the whole
|
||||||
|
config file is mounted at `/gluetun/wireguard/wg0.conf` and gluetun parses it.
|
||||||
|
Verified working: gluetun reads the endpoint, interface address and keys
|
||||||
|
straight out of that file. It also keeps the private key out of `docker inspect`
|
||||||
|
and out of your shell history, which the environment-variable approach does not.
|
||||||
|
|
||||||
|
**`:latest` on all three images**, which is upstream's habit rather than a
|
||||||
|
decision. Worth pinning gluetun in particular — it's the one holding the tunnel.
|
||||||
|
|
||||||
|
## Security: the proxies are unauthenticated by default
|
||||||
|
|
||||||
|
Both proxies ship with no credentials, so **anyone who can reach ports 9998 or
|
||||||
|
9980 can route traffic through your VPN account**. On a trusted LAN that may be
|
||||||
|
exactly what you want — it's what makes them useful from a browser extension on
|
||||||
|
another machine. It is not something to expose beyond that, and not something to
|
||||||
|
forget you did.
|
||||||
|
|
||||||
|
`compose.yaml` has the variables wired already; set both in `.env` and
|
||||||
|
authentication switches on for both proxies at once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PROXY_USER=someone
|
||||||
|
PROXY_PASSWORD=a-long-random-string
|
||||||
|
```
|
||||||
|
|
||||||
|
Leave them unset and behaviour is unchanged. gluetun needs the user *and*
|
||||||
|
password together — one alone does nothing.
|
||||||
|
|
||||||
|
Never port-forward these from your router. An open proxy on the public internet
|
||||||
|
is found within hours, and everything that follows is attributed to your VPN
|
||||||
|
account.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **`SOCKSPROXY` is not a gluetun variable.** Neither is `SOCKSPROXY_PORT`.
|
||||||
|
gluetun ignores both silently — no warning — so the SOCKS5 server never
|
||||||
|
starts and its published port maps to nothing. Its own settings summary is
|
||||||
|
the proof: `SOCKS5 proxy server settings: Enabled: no`. The real names are
|
||||||
|
`SOCKS5_ENABLED=on` and `SOCKS5_LISTENING_ADDRESS=:1080`. Fixed in
|
||||||
|
`compose.yaml`; still present in `compose.personal.yaml`, whose HTTP proxy
|
||||||
|
works fine and whose SOCKS port does not.
|
||||||
|
- **`HTTPPROXY_PORT` is deprecated but does work.** gluetun logs `WARN You are
|
||||||
|
using the old environment variable HTTPPROXY_PORT, please consider changing
|
||||||
|
it to HTTPPROXY_LISTENING_ADDRESS`, then brings the proxy up on 8888 anyway.
|
||||||
|
- **pyload-ng listens on 8000.** The LinuxServer image's web UI port is 8000
|
||||||
|
and is *not* settable by environment variable — only inside pyload's own
|
||||||
|
config in `./pyloadconfig`. So a `8880:8880` mapping reaches nothing unless
|
||||||
|
you already changed it there; `compose.yaml` publishes `8880:8000` instead.
|
||||||
|
(Click'n'Load on 9666 isn't published at all.)
|
||||||
|
- **`wg0.conf` must exist before the first `up`.** It's a bind mount of a
|
||||||
|
*file*; if the path is missing, Docker creates a **directory** there, gluetun
|
||||||
|
finds no config, and the error doesn't mention the mount. Same failure mode as
|
||||||
|
[overleaf's init script](../overleaf/README.md#running-it-on-openmediavault).
|
||||||
|
- **Never put `wg0.conf` in `init/`.** This repo's `.gitignore` un-ignores
|
||||||
|
`*/init/**` so that runtime config fragments get committed — which for a
|
||||||
|
WireGuard config means publishing your private key. `wg*.conf` and `*.ovpn`
|
||||||
|
are now explicitly ignored everywhere as a guard, but the habit is the real
|
||||||
|
protection.
|
||||||
|
- **Restarting gluetun cuts the other two off.** They share its namespace, so
|
||||||
|
when it goes away their network does too, and `depends_on` doesn't bring them
|
||||||
|
back. Restart the whole stack, not one service.
|
||||||
|
- **`TORRENTING_PORT=6881` is not published**, so incoming peer connections
|
||||||
|
don't arrive and you're limited to outgoing ones. Fixing that properly needs
|
||||||
|
port forwarding from TorGuard, then publishing the matching port on gluetun.
|
||||||
|
- **In `compose.personal.yaml` the variables have no defaults.** `${PUID}`,
|
||||||
|
`${PGID}`, `${TZ}` and `${TEMPDISK}` are bare, so Compose interpolates empty
|
||||||
|
strings and starts the containers anyway. `compose.yaml` uses
|
||||||
|
`${PUID:-1000}` and `${TEMPDISK:?…}`, which aborts with a readable message
|
||||||
|
instead.
|
||||||
|
- **Check for leaks after any change**, not just at setup. `docker compose exec
|
||||||
|
gluetun wget -qO- https://ipinfo.io/ip` is the ten-second version.
|
||||||
|
|
||||||
|
## Exposing it
|
||||||
|
|
||||||
|
**Don't.** This stack has no business being reachable from the internet: two
|
||||||
|
admin UIs with weak-by-default auth and two unauthenticated open proxies.
|
||||||
|
|
||||||
|
If you need the UIs remotely, reach them over your own VPN or a
|
||||||
|
[Cloudflare Tunnel](../cloudflare-tunnel/) with
|
||||||
|
[Authentik forward auth](../docs/authentik-sso.md#forward-auth--putting-an-app-with-no-sso-behind-authentik)
|
||||||
|
in front — and leave the proxy ports off it entirely.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- gluetun (now at the passteque org): <https://github.com/passteque/gluetun>
|
||||||
|
- gluetun wiki: <https://github.com/qdm12/gluetun-wiki>
|
||||||
|
- TorGuard WireGuard + gluetun guide:
|
||||||
|
<https://www.torguard.net/support/articles/TorGuard-Software-Features/torguard-wireguard-gluetun-docker-setup.php>
|
||||||
|
- qBittorrent image: <https://docs.linuxserver.io/images/docker-qbittorrent/>
|
||||||
|
- pyload-ng image: <https://docs.linuxserver.io/images/docker-pyload-ng/>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
services:
|
||||||
|
gluetun:
|
||||||
|
image: qmcgaw/gluetun
|
||||||
|
container_name: gluetun
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
devices:
|
||||||
|
- /dev/net/tun:/dev/net/tun
|
||||||
|
environment:
|
||||||
|
- VPN_SERVICE_PROVIDER=custom
|
||||||
|
- VPN_TYPE=wireguard
|
||||||
|
|
||||||
|
# Enable SOCKS5 proxy
|
||||||
|
- SOCKSPROXY=on
|
||||||
|
- SOCKSPROXY_PORT=1080
|
||||||
|
|
||||||
|
# Optional HTTP proxy
|
||||||
|
- HTTPPROXY=on
|
||||||
|
- HTTPPROXY_PORT=8888
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "9880:9880/tcp" # Qbittorrent Web UI
|
||||||
|
- "8880:8880/tcp" # PyLoad Web-UI
|
||||||
|
- "9980:1080/tcp" # Socks Proxy
|
||||||
|
- "9998:8888/tcp" # HTTP Proxy
|
||||||
|
volumes:
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
- ./wg0.conf:/gluetun/wireguard/wg0.conf
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
qbittorrent:
|
||||||
|
image: linuxserver/qbittorrent:latest
|
||||||
|
container_name: qbittorrent
|
||||||
|
environment:
|
||||||
|
- PUID=${PUID}
|
||||||
|
- PGID=${PGID}
|
||||||
|
- TZ=${TZ}
|
||||||
|
- WEBUI_PORT=9880
|
||||||
|
- TORRENTING_PORT=6881
|
||||||
|
volumes:
|
||||||
|
- ./config:/config
|
||||||
|
- '${TEMPDISK}/downloads/torrents:/downloads'
|
||||||
|
network_mode: "service:gluetun"
|
||||||
|
depends_on:
|
||||||
|
- gluetun
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
pyload-ng:
|
||||||
|
image: lscr.io/linuxserver/pyload-ng:latest
|
||||||
|
container_name: pyload-ng
|
||||||
|
environment:
|
||||||
|
- PUID=${PUID}
|
||||||
|
- PGID=${PGID}
|
||||||
|
- TZ=${TZ}
|
||||||
|
volumes:
|
||||||
|
- ./pyloadconfig:/config
|
||||||
|
- ${TEMPDISK}/downloads/pyload:/downloads
|
||||||
|
network_mode: "service:gluetun"
|
||||||
|
depends_on:
|
||||||
|
- gluetun
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# The maintained version of this stack -- this is what `docker compose up`
|
||||||
|
# uses. See compose.personal.yaml for the older variant still running on
|
||||||
|
# my own box, and the README for what differs between them.
|
||||||
|
|
||||||
|
services:
|
||||||
|
gluetun:
|
||||||
|
# Pinned. This container holds the tunnel; an unattended pull breaking it
|
||||||
|
# means the download clients lose all connectivity (by design).
|
||||||
|
image: qmcgaw/gluetun:v3.41.3
|
||||||
|
container_name: gluetun
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
devices:
|
||||||
|
- /dev/net/tun:/dev/net/tun
|
||||||
|
|
||||||
|
environment:
|
||||||
|
- VPN_SERVICE_PROVIDER=custom
|
||||||
|
- VPN_TYPE=wireguard
|
||||||
|
|
||||||
|
# SOCKS5 proxy. The names matter: SOCKSPROXY / SOCKSPROXY_PORT are NOT
|
||||||
|
# gluetun variables -- it ignores them silently and the proxy never
|
||||||
|
# starts. Verified against gluetun's own settings summary.
|
||||||
|
- SOCKS5_ENABLED=on
|
||||||
|
- SOCKS5_LISTENING_ADDRESS=:1080
|
||||||
|
|
||||||
|
# HTTP proxy. HTTPPROXY_PORT still works but logs a deprecation warning;
|
||||||
|
# LISTENING_ADDRESS is the current name.
|
||||||
|
- HTTPPROXY=on
|
||||||
|
- HTTPPROXY_LISTENING_ADDRESS=:8888
|
||||||
|
|
||||||
|
# Proxy authentication. Empty means NO authentication -- anyone who can
|
||||||
|
# reach the published ports can use the VPN. Set both vars in .env to
|
||||||
|
# turn it on; leaving them unset keeps the current behaviour.
|
||||||
|
- HTTPPROXY_USER=${PROXY_USER:-}
|
||||||
|
- HTTPPROXY_PASSWORD=${PROXY_PASSWORD:-}
|
||||||
|
- SOCKS5_USER=${PROXY_USER:-}
|
||||||
|
- SOCKS5_PASSWORD=${PROXY_PASSWORD:-}
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "${QBT_PORT:-9880}:9880/tcp" # qBittorrent Web UI
|
||||||
|
# pyload-ng listens on 8000 inside the container and the port is not
|
||||||
|
# settable by environment variable -- only in pyload's own config.
|
||||||
|
- "${PYLOAD_PORT:-8880}:8000/tcp" # pyLoad Web UI
|
||||||
|
- "${SOCKS_PORT:-9980}:1080/tcp" # SOCKS5 proxy
|
||||||
|
- "${HTTPPROXY_PORT:-9998}:8888/tcp" # HTTP proxy
|
||||||
|
# Incoming BitTorrent peers. Needs a forwarded port from TorGuard, and
|
||||||
|
# the number here must match TORRENTING_PORT below.
|
||||||
|
#- "6881:6881/tcp"
|
||||||
|
#- "6881:6881/udp"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
# Bind mount of a FILE. It must exist before the first `up` or Docker
|
||||||
|
# creates a directory here and gluetun finds no config. Git-ignored.
|
||||||
|
- ./wg0.conf:/gluetun/wireguard/wg0.conf:ro
|
||||||
|
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
qbittorrent:
|
||||||
|
image: lscr.io/linuxserver/qbittorrent:latest
|
||||||
|
container_name: qbittorrent
|
||||||
|
environment:
|
||||||
|
- PUID=${PUID:-1000}
|
||||||
|
- PGID=${PGID:-1000}
|
||||||
|
- TZ=${TZ:-Etc/UTC}
|
||||||
|
- WEBUI_PORT=9880
|
||||||
|
- TORRENTING_PORT=6881
|
||||||
|
volumes:
|
||||||
|
- ${CONFIG_PATH:-./config}:/config
|
||||||
|
- "${TEMPDISK:?set TEMPDISK in .env to the disk your downloads live on}/downloads/torrents:/downloads"
|
||||||
|
# No network of its own: everything goes through the tunnel, and there is
|
||||||
|
# no route out if the tunnel drops. This is also why it cannot declare
|
||||||
|
# its own `ports:` -- they live on gluetun above.
|
||||||
|
network_mode: "service:gluetun"
|
||||||
|
depends_on:
|
||||||
|
gluetun:
|
||||||
|
# gluetun ships a healthcheck; wait for the tunnel rather than racing it.
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
pyload-ng:
|
||||||
|
image: lscr.io/linuxserver/pyload-ng:latest
|
||||||
|
container_name: pyload-ng
|
||||||
|
environment:
|
||||||
|
- PUID=${PUID:-1000}
|
||||||
|
- PGID=${PGID:-1000}
|
||||||
|
- TZ=${TZ:-Etc/UTC}
|
||||||
|
volumes:
|
||||||
|
- ${PYLOAD_CONFIG_PATH:-./pyloadconfig}:/config
|
||||||
|
- ${TEMPDISK}/downloads/pyload:/downloads
|
||||||
|
network_mode: "service:gluetun"
|
||||||
|
depends_on:
|
||||||
|
gluetun:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Copy to .env and adjust. .env is git-ignored; this file is not.
|
||||||
|
#
|
||||||
|
# compose.yaml currently writes these as bare ${PUID} / ${PGID} / ${TZ},
|
||||||
|
# with no defaults, so a clone without a .env starts the container with
|
||||||
|
# empty values instead of failing. Set all three -- or add `:-` defaults
|
||||||
|
# to compose.yaml, which is what the rest of this repo does.
|
||||||
|
|
||||||
|
# ─── Identity ───────────────────────────────────────────────────────
|
||||||
|
# Must own the data directory, or Heimdall can't write its database.
|
||||||
|
# id -u ; id -g
|
||||||
|
PUID=1000
|
||||||
|
PGID=1000
|
||||||
|
TZ=Europe/Paris
|
||||||
|
|
||||||
|
# ─── Paths ──────────────────────────────────────────────────────────
|
||||||
|
# Mounted at /config. Holds the SQLite database, uploaded icons, the
|
||||||
|
# nginx config -- and the API keys of every "enhanced" app you add.
|
||||||
|
# Back it up; never commit it.
|
||||||
|
#DATA_PATH=./data
|
||||||
|
|
||||||
|
# ─── Ports ──────────────────────────────────────────────────────────
|
||||||
|
#PORT=4080
|
||||||
|
#HTTPS_PORT=4043
|
||||||
|
|
||||||
|
# ─── Reaching your own LAN ──────────────────────────────────────────
|
||||||
|
# NOT set in compose.yaml, and the one you will most likely need.
|
||||||
|
# Heimdall blocks enhanced-app lookups to private/reserved IPs by
|
||||||
|
# default -- which is every service in your house. See the README.
|
||||||
|
#ALLOW_INTERNAL_REQUESTS=true
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Heimdall
|
||||||
|
|
||||||
|
A start page for everything else in this repo. Tiles that link to your services,
|
||||||
|
and for some of them an "enhanced" tile that polls the app's API and shows
|
||||||
|
something useful on the face of it — disk free, active downloads, unread count.
|
||||||
|
|
||||||
|
Actively maintained, despite its age: v2.8.3 landed in September 2026.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
$EDITOR .env # PUID/PGID/TZ have no defaults in compose.yaml
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://<host>:4080`. There is no login and no setup wizard — you get an
|
||||||
|
empty dashboard, and everything is configured by clicking around. Read the
|
||||||
|
security note below before you put anything on it.
|
||||||
|
|
||||||
|
## Ports
|
||||||
|
|
||||||
|
| Host | Container | Purpose |
|
||||||
|
|------|-----------|---------|
|
||||||
|
| 4080 | 80 | Web UI |
|
||||||
|
| 4043 | 443 | Heimdall's own HTTPS listener — see gotchas |
|
||||||
|
|
||||||
|
## Why it looks like this
|
||||||
|
|
||||||
|
**The LinuxServer image**, same reasoning as [jellyfin](../jellyfin/README.md):
|
||||||
|
`PUID`/`PGID`/`TZ`, `/config`, s6, one rebuild cadence. For a service this small
|
||||||
|
the win is that it behaves like everything else on the box.
|
||||||
|
|
||||||
|
**`./data` mounted at `/config`.** The names disagree because the repo's
|
||||||
|
convention is `data/` on the host and the image's convention is `/config` in the
|
||||||
|
container. Whatever it's called, it is the entire service: the SQLite database,
|
||||||
|
uploaded icons, the nginx config, and the API keys of every enhanced tile.
|
||||||
|
|
||||||
|
**Nothing else.** No database container, no cache, no environment variables
|
||||||
|
beyond the LinuxServer three. This is one of the genuinely simple stacks — which
|
||||||
|
is why the interesting content below is about what Heimdall *doesn't* tell you.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **Heimdall blocks requests to your own LAN by default.** This is the one that
|
||||||
|
wastes an afternoon. Enhanced tiles — the ones that show live data — make a
|
||||||
|
server-side request to the app's API, and the image ships
|
||||||
|
`ALLOW_INTERNAL_REQUESTS=false`, which refuses lookups to private or reserved
|
||||||
|
IP addresses. Every service in your house is on a private address, so every
|
||||||
|
enhanced tile fails, and the tile just sits there looking broken. Add
|
||||||
|
`ALLOW_INTERNAL_REQUESTS=true` to the environment block. It is not in
|
||||||
|
`compose.yaml` today.
|
||||||
|
- **There is no authentication out of the box.** Anyone who reaches port 4080
|
||||||
|
gets a map of your entire infrastructure, with working links. Heimdall can
|
||||||
|
password-protect itself, or you can put basic auth in front via the image's
|
||||||
|
nginx:
|
||||||
|
```bash
|
||||||
|
docker exec -it heimdall htpasswd -c /config/nginx/.htpasswd <username>
|
||||||
|
```
|
||||||
|
then uncomment the basic auth lines in `/config/nginx/site-confs/default.conf`
|
||||||
|
and restart. Better still, put [Authentik forward
|
||||||
|
auth](../docs/authentik-sso.md#forward-auth--putting-an-app-with-no-sso-behind-authentik)
|
||||||
|
in front — a dashboard is the textbook case for it, since it has no accounts
|
||||||
|
worth integrating and every client is a browser.
|
||||||
|
- **Enhanced tiles store API keys in the data directory.** That's a Sonarr key,
|
||||||
|
a Nextcloud token, whatever you connected. `data/` is git-ignored by this
|
||||||
|
repo's deny-by-default rule — verified — but it means the directory is a
|
||||||
|
credential store, not just cosmetics. Back it up accordingly, and don't paste
|
||||||
|
it into a gist when asking for help.
|
||||||
|
- **Port 4043 is Heimdall's own TLS**, with a self-signed certificate. In this
|
||||||
|
repo TLS terminates at the proxy, so the mapping is redundant — drop the line
|
||||||
|
unless you're reaching it directly and are willing to click through the
|
||||||
|
warning.
|
||||||
|
- **`${PUID}`, `${PGID}` and `${TZ}` are bare.** No `:-1000` defaults, so a
|
||||||
|
clone with no `.env` gets empty strings and starts anyway — confirmed:
|
||||||
|
Compose emits `The "PUID" variable is not set. Defaulting to a blank string.`
|
||||||
|
and carries on. The rest of this repo writes `${PUID:-1000}`.
|
||||||
|
- **`:latest`.** Heimdall ships real releases (`2.8.3`, `v2.8.3-ls363`); pin one
|
||||||
|
if you'd rather not be surprised.
|
||||||
|
- **Back up `data/`.** Rebuilding a dashboard by hand is an hour you won't enjoy,
|
||||||
|
and the icons you uploaded aren't anywhere else.
|
||||||
|
|
||||||
|
## Exposing it
|
||||||
|
|
||||||
|
- [Reverse proxy with Caddy](../docs/reverse-proxy.md) — the normal route; use
|
||||||
|
port 4080 and ignore 4043.
|
||||||
|
- [Single sign-on with Authentik](../docs/authentik-sso.md) — **strongly
|
||||||
|
recommended if this leaves the LAN.** Forward auth is the right fit: no
|
||||||
|
accounts to map, browser-only clients, and it stops the dashboard being a
|
||||||
|
directory of your network for anyone who finds it.
|
||||||
|
- [Cloudflare Tunnel](../cloudflare-tunnel/) — fine, it's a small HTML page.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Upstream: <https://github.com/linuxserver/Heimdall>
|
||||||
|
- Image: <https://docs.linuxserver.io/images/docker-heimdall/>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
heimdall:
|
||||||
|
image: lscr.io/linuxserver/heimdall:latest
|
||||||
|
container_name: heimdall
|
||||||
|
environment:
|
||||||
|
- PUID=${PUID}
|
||||||
|
- PGID=${PGID}
|
||||||
|
- TZ=${TZ}
|
||||||
|
volumes:
|
||||||
|
- ./data:/config
|
||||||
|
ports:
|
||||||
|
- 4080:80
|
||||||
|
- 4043:443
|
||||||
|
restart: unless-stopped
|
||||||
Reference in New Issue
Block a user