ipx runs in an Arcane project of its own; docker-compose.yml is a sample (#208)
Production moved from the content project, beside audiobookshelf, immich and Plex, to a project called ipx, where the frontend and directory of the split (#207) will join it. The move changed ipx's network, and sign-in through the tunnel trusts that network's gateway: the new compose file pins the subnet to 192.168.64.0/20 so the gateway stays 192.168.64.1 if the network is remade, and config.toml's trusted_proxies names it instead of 192.168.16.1. Checked by sending a forged Access token from a Docker bridge: it reached the token check and was refused, which only a trusted peer does. The repo's compose file was a copy of production's that nobody else could run. It is now a sample: ipx alone, SQLite, folders beside the file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Add a feed no longer asks for a folder; a feed's folder is its title (#201).
|
- Add a feed no longer asks for a folder; a feed's folder is its title (#201).
|
||||||
|
- `docker-compose.yml` is a sample to start from, ipx alone on SQLite with its folders beside the file, instead of a copy of the author's server (#208).
|
||||||
|
|
||||||
## [0.12.0] - 2026-10-11
|
## [0.12.0] - 2026-10-11
|
||||||
|
|
||||||
|
|||||||
18
CLAUDE.md
18
CLAUDE.md
@@ -7,18 +7,20 @@ made here.
|
|||||||
## Where things are
|
## Where things are
|
||||||
|
|
||||||
Production is the `iPX` container on Tower (192.168.1.130), the `ipx` service of the
|
Production is the `iPX` container on Tower (192.168.1.130), the `ipx` service of the
|
||||||
Arcane project `content`: `/mnt/fast/arcane/projects/content/compose.yaml`. That file is what runs;
|
Arcane project `ipx`: `/mnt/fast/arcane/projects/ipx/compose.yaml`. That file is what runs, and
|
||||||
`docker-compose.yml` in this repo is a copy, and editing it changes nothing in production.
|
the containers of the split (#207) join it there. `docker-compose.yml` in this repo is a sample for
|
||||||
|
others, ipx alone on SQLite; editing it changes nothing in production. Until 2026-10-12 ipx was a
|
||||||
|
service of the `content` project, beside audiobookshelf, immich and Plex (#208).
|
||||||
|
|
||||||
| | Host | In the container |
|
| | Host | In the container |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Image | `192.168.1.130:5000/ipx:latest` | |
|
| Image | `192.168.1.130:5000/ipx:latest` | |
|
||||||
| Config | `/mnt/fast/appdata/ipx/config.toml`: bind address, token, trusted proxies, torrent, paths. The feeds and server settings are in the database | `/config/config.toml` |
|
| Config | `/mnt/fast/appdata/ipx/config.toml`: bind address, token, trusted proxies, torrent, paths. The feeds and server settings are in the database | `/config/config.toml` |
|
||||||
| Database | Postgres 18, database `ipodderx`, login `ipodderx`, on the `postgres` container of the Arcane project `databases` (`192.168.1.130:5433`). The URL is in `ipx.env` beside the compose file (`/mnt/fast/arcane/projects/content/ipx.env`, mode 600), passed to the container as `IPX_DATABASE_URL`. A relative `env_file`: Arcane runs compose in its own container, where `/mnt/fast/appdata` does not exist | |
|
| Database | Postgres 18, database `ipodderx`, login `ipodderx`, on the `postgres` container of the Arcane project `databases` (`192.168.1.130:5433`). The URL is in `ipx.env` beside the compose file (`/mnt/fast/arcane/projects/ipx/ipx.env`, mode 600), passed to the container as `IPX_DATABASE_URL`. A relative `env_file`: Arcane runs compose in its own container, where `/mnt/fast/appdata` does not exist | |
|
||||||
| Old database | `/mnt/user/ipx/state.db`, SQLite, used until the move to Postgres on 2026-09-18 and kept for rollback | `/data/state.db` |
|
| Old database | `/mnt/user/ipx/state.db`, SQLite, used until the move to Postgres on 2026-09-18 and kept for rollback | `/data/state.db` |
|
||||||
| Downloads | `/mnt/user/ipx/downloads` | `/downloads` |
|
| Downloads | `/mnt/user/ipx/downloads` | `/downloads` |
|
||||||
| Web UI | `192.168.1.130:8099`, also `ipodderx.sdf1.net` via a Cloudflare tunnel | `0.0.0.0:8099` |
|
| Web UI | `192.168.1.130:8099`, also `ipodderx.sdf1.net` via a Cloudflare tunnel | `0.0.0.0:8099` |
|
||||||
| Sign-in via the tunnel | Cloudflare Access app `ipodderx`, with Authentik as its identity provider; see [docs/sso.md](docs/sso.md) | trusts `Cf-Access-Authenticated-User-Email` from `192.168.16.1`, the `content_default` gateway, and only with a valid `Cf-Access-Jwt-Assertion` (`access_team`, `access_aud`) |
|
| Sign-in via the tunnel | Cloudflare Access app `ipodderx`, with Authentik as its identity provider; see [docs/sso.md](docs/sso.md) | trusts `Cf-Access-Authenticated-User-Email` from `192.168.64.1`, the `ipx_default` gateway (its subnet pinned in the compose file), and only with a valid `Cf-Access-Jwt-Assertion` (`access_team`, `access_aud`) |
|
||||||
|
|
||||||
Work to do lives in the Gitea issues at https://git.sdf1.net/rays/ipx/issues, not in a
|
Work to do lives in the Gitea issues at https://git.sdf1.net/rays/ipx/issues, not in a
|
||||||
`TODO.md`. `/src/tea` is logged in: `/src/tea issues list --login git.sdf1.net --repo rays/ipx`.
|
`TODO.md`. `/src/tea` is logged in: `/src/tea issues list --login git.sdf1.net --repo rays/ipx`.
|
||||||
@@ -47,13 +49,13 @@ Deploying a change is: build and push the image, then pull it and recreate the c
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker buildx build --tag 192.168.1.130:5000/ipx:latest . --push
|
docker buildx build --tag 192.168.1.130:5000/ipx:latest . --push
|
||||||
docker compose -f /mnt/fast/arcane/projects/content/compose.yaml pull ipx
|
docker compose -f /mnt/fast/arcane/projects/ipx/compose.yaml pull ipx
|
||||||
docker compose -f /mnt/fast/arcane/projects/content/compose.yaml up -d ipx
|
docker compose -f /mnt/fast/arcane/projects/ipx/compose.yaml up -d ipx
|
||||||
docker logs --tail 20 iPX
|
docker logs --tail 20 iPX
|
||||||
```
|
```
|
||||||
|
|
||||||
**Name the service.** A bare `up -d` recreates every container in `content`, beets and immich
|
**Name the service.** A bare `up -d` recreates every container in the project, which once the
|
||||||
included. Run `pull` before `up`, because `up` reuses whatever `latest` the host already has.
|
split lands means the frontend and directory too. Run `pull` before `up`, because `up` reuses whatever `latest` the host already has.
|
||||||
|
|
||||||
**A build that fails with `429 Too Many Requests` on a base image** is Docker Hub rate-limiting
|
**A build that fails with `429 Too Many Requests` on a base image** is Docker Hub rate-limiting
|
||||||
this host. There is no Docker Hub login here, and the build asks about `debian:trixie-slim` and
|
this host. There is no Docker Hub login here, and the build asks about `debian:trixie-slim` and
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ docker build -t ipx .
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
`docker-compose.yml` is set up for the author's own server. Point its `image` and its three volumes
|
`docker-compose.yml` is a sample: ipx alone, with its database in SQLite and its three volumes
|
||||||
(`/config`, `/data` and `/downloads`) at yours first. The UI is on port 8099. BitTorrent uses 6881
|
(`/config`, `/data` and `/downloads`) in folders beside the file. Change them to suit. The UI is on port 8099. BitTorrent uses 6881
|
||||||
over TCP and UDP. Files are written as `PUID`/`PGID`, 99:100 by default.
|
over TCP and UDP. Files are written as `PUID`/`PGID`, 99:100 by default.
|
||||||
|
|
||||||
From source:
|
From source:
|
||||||
|
|||||||
@@ -1,31 +1,26 @@
|
|||||||
|
# A sample: ipx on its own, keeping everything in SQLite. Build the image first
|
||||||
|
# (`docker build -t ipx .`), then `docker compose up -d`, and sign in at http://<host>:8099
|
||||||
|
# as admin / ipodderx. docs/configuration.md has the rest, Postgres included.
|
||||||
services:
|
services:
|
||||||
ipx:
|
ipx:
|
||||||
image: 192.168.1.130:5000/ipx:latest
|
image: ipx
|
||||||
container_name: iPX
|
container_name: ipx
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
# Who owns the files it writes. 99:100 is Unraid's nobody:users.
|
||||||
PUID: "99"
|
PUID: "99"
|
||||||
PGID: "100"
|
PGID: "100"
|
||||||
TZ: "America/Toronto"
|
TZ: "UTC"
|
||||||
IPX_LOG: "ipx=info"
|
# For Postgres instead of SQLite:
|
||||||
# One JSON object a line, which Loki (through Alloy) and the Grafana dashboard read.
|
# IPX_DATABASE_URL: "postgres://ipx:password@db-host:5432/ipx"
|
||||||
IPX_LOG_FORMAT: "json"
|
|
||||||
# Traces to Tempo, in the monitoring project; ipx sends none without it.
|
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT: "http://192.168.1.130:4318"
|
|
||||||
# What the dashboard filters on, so a daemon run by hand for testing stays out of it.
|
|
||||||
OTEL_RESOURCE_ATTRIBUTES: "deployment.environment.name=production"
|
|
||||||
# IPX_DATABASE_URL=postgres://... to use Postgres; without it, /data/state.db (SQLite).
|
|
||||||
env_file:
|
|
||||||
- ipx.env # relative: Arcane resolves it inside its own container
|
|
||||||
ports:
|
ports:
|
||||||
- "8099:8099" # web UI
|
- "8099:8099" # web UI
|
||||||
- "192.168.1.130:9187:9187" # /metrics for Prometheus, LAN only (ipx#194)
|
|
||||||
- "6881:6881/tcp" # BitTorrent peers
|
- "6881:6881/tcp" # BitTorrent peers
|
||||||
- "6881:6881/udp" # DHT
|
- "6881:6881/udp" # DHT
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/fast/appdata/ipx:/config # config.toml, and the web token
|
- ./config:/config # config.toml, written on first start
|
||||||
- /mnt/user/ipx/:/data # state.db
|
- ./data:/data # state.db
|
||||||
- /mnt/user/ipx/downloads:/downloads
|
- ./downloads:/downloads
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "ipx", "status"]
|
test: ["CMD", "ipx", "status"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|||||||
18
docs/sso.md
18
docs/sso.md
@@ -38,7 +38,7 @@ request it forwards through the tunnel, and ipx signs that person in.
|
|||||||
enabled = true
|
enabled = true
|
||||||
bind = "0.0.0.0:8099"
|
bind = "0.0.0.0:8099"
|
||||||
trusted_header = "Cf-Access-Authenticated-User-Email"
|
trusted_header = "Cf-Access-Authenticated-User-Email"
|
||||||
trusted_proxies = ["127.0.0.1", "::1", "192.168.16.1"]
|
trusted_proxies = ["127.0.0.1", "::1", "192.168.64.1"]
|
||||||
access_team = "rays-sdf1.cloudflareaccess.com"
|
access_team = "rays-sdf1.cloudflareaccess.com"
|
||||||
access_aud = "8bfe73dfbc8c548d1cb5dc11c6db6887bcaf4f5144840396f83a620a140e1c4f"
|
access_aud = "8bfe73dfbc8c548d1cb5dc11c6db6887bcaf4f5144840396f83a620a140e1c4f"
|
||||||
auto_create_users = true
|
auto_create_users = true
|
||||||
@@ -50,7 +50,7 @@ The last two turn on the token check described under [Verifying Cloudflare's tok
|
|||||||
on since 2026-09-19. Both can be read without the dashboard: a request to the site while signed
|
on since 2026-09-19. Both can be read without the dashboard: a request to the site while signed
|
||||||
out is sent to `https://<team domain>/cdn-cgi/access/login/ipodderx.sdf1.net?kid=<AUD tag>&...`.
|
out is sent to `https://<team domain>/cdn-cgi/access/login/ipodderx.sdf1.net?kid=<AUD tag>&...`.
|
||||||
|
|
||||||
Restart ipx after editing it: `docker compose -f /mnt/fast/arcane/projects/content/compose.yaml
|
Restart ipx after editing it: `docker compose -f /mnt/fast/arcane/projects/ipx/compose.yaml
|
||||||
restart ipx`.
|
restart ipx`.
|
||||||
|
|
||||||
### What was missing
|
### What was missing
|
||||||
@@ -65,22 +65,24 @@ Cloudflare and Authentik were already right. Three things on the ipx side were n
|
|||||||
`rays@sdf1.net`. With `auto_create_users` on, the first visit would have made a second, empty
|
`rays@sdf1.net`. With `auto_create_users` on, the first visit would have made a second, empty
|
||||||
account. `ipx user rename rays rays@sdf1.net` fixed that without losing anything.
|
account. `ipx user rename rays rays@sdf1.net` fixed that without losing anything.
|
||||||
|
|
||||||
### The address to trust, and why it is 192.168.16.1
|
### The address to trust, and why it is 192.168.64.1
|
||||||
|
|
||||||
`cloudflared` runs in its own container and reaches ipx through the host's published port. Docker
|
`cloudflared` runs in its own container and reaches ipx through the host's published port. Docker
|
||||||
(iptables firewall backend) masquerades traffic between its bridge networks, so the tunnel's
|
(iptables firewall backend) masquerades traffic between its bridge networks, so the tunnel's
|
||||||
requests arrive from the **gateway of ipx's own network**, `content_default`:
|
requests arrive from the **gateway of ipx's own network**, `ipx_default`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker network inspect content_default -f '{{range .IPAM.Config}}{{.Gateway}}{{end}}'
|
docker network inspect ipx_default -f '{{range .IPAM.Config}}{{.Gateway}}{{end}}'
|
||||||
```
|
```
|
||||||
|
|
||||||
That was measured, not assumed. ipx does not log where a request came from, so the addresses were
|
That was measured, not assumed. ipx does not log where a request came from, so the addresses were
|
||||||
read from the kernel's connection table inside the container while the site was open. (`/proc/net/tcp`
|
read from the kernel's connection table inside the container while the site was open. (`/proc/net/tcp`
|
||||||
lists them in hex.)
|
lists them in hex.)
|
||||||
|
|
||||||
If the `content` project's network is ever recreated, its gateway can change. Check it again, and
|
The `ipx` project's compose file pins that network's subnet to `192.168.64.0/20`, so its gateway
|
||||||
update `trusted_proxies` to match.
|
stays `192.168.64.1` when the network is recreated. It was `192.168.16.1`, `content_default`'s, while
|
||||||
|
ipx was part of the `content` project. If the subnet is ever changed, update `trusted_proxies` to
|
||||||
|
match.
|
||||||
|
|
||||||
### Names
|
### Names
|
||||||
|
|
||||||
@@ -176,7 +178,7 @@ itself, with a **Proxy Provider** and an **outpost** that adds `X-authentik-user
|
|||||||
**The header is only believed from `trusted_proxies`.** Every other source is ignored, and the
|
**The header is only believed from `trusted_proxies`.** Every other source is ignored, and the
|
||||||
request falls through to a session cookie or the shared token. That is the whole security boundary.
|
request falls through to a session cookie or the shared token. That is the whole security boundary.
|
||||||
|
|
||||||
With the tunnel reaching ipx through the host's port, `192.168.16.1` means **any container on Tower
|
With the tunnel reaching ipx through the host's port, `192.168.64.1` means **any container on Tower
|
||||||
that connects to `192.168.1.130:8099`**, not only `cloudflared`. Machines on the LAN, and Tower
|
that connects to `192.168.1.130:8099`**, not only `cloudflared`. Machines on the LAN, and Tower
|
||||||
itself, arrive under their own addresses and cannot set the header; the checks above show both
|
itself, arrive under their own addresses and cannot set the header; the checks above show both
|
||||||
sides. Never list a LAN address or range: anyone there could then send
|
sides. Never list a LAN address or range: anyone there could then send
|
||||||
|
|||||||
Reference in New Issue
Block a user