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:
2026-10-12 03:13:00 +00:00
parent fca704f7ff
commit c2cea0fc5b
5 changed files with 35 additions and 35 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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