OpenFactory Plex media lab with Plex Media Server, Tdarr transcoding farm, nginx reverse proxy and observability

Build a Plex Media Stack on OpenFactory

A four-VM Plex lab: Plex Media Server + Tdarr + nginx + observability, from one prompt

May 17, 2026

← Back to Blog

Plex is the older sibling of Jellyfin — commercial, polished, with the best client ecosystem of any media server. The 2026 r/selfhosted survey shows it slipping to Jellyfin, but it's still the fifth-most-deployed app and the right pick if you live in the Plex client apps and trust plex.tv's remote-access wiring.

This post walks through the canonical Plex shape on OpenFactory: four buildable VMs — Plex Media Server, a Tdarr transcoding node, an nginx reverse proxy, and Prometheus observability — from one prompt, shipped as bootable ISOs.

What you'll build

  • plex (10.74.0.10:32400) — the media server with/var/lib/plex/{media,Library,transcode} pre-wired.
  • tdarr (10.74.0.20:8265/8266) — transcoder farm node pointed at Plex for library refreshes.
  • nginx-proxy (10.74.0.30:80) — reverse proxy for the Plex web UI.
  • observability (10.74.0.40:9090/3000) — Prometheus scraping Plex, Tdarr, and the proxy.

Why build it on OpenFactory

  • The ISO is the spec. Library and transcode paths, env files, and scrape targets bake into bootable images.
  • Tdarr split out. Run the transcoder on a GPU host while Plex stays on the mini-PC, without rebuilding the topology.
  • Reverse proxy ready. nginx fronts the web UI for layering real TLS on top later.
  • Scenario assertions ride along. The build fails closed if Plex's identity endpoint doesn't expose its machine ID, if Tdarr isn't talking, or if the proxy can't reach Plex.

Topology

Four Debian Trixie VMs on 10.74.0.0/24. nginx fronts Plex; Tdarr hits Plex for library refreshes after transcoding; observability scrapes everyone.

The prompt

Paste this verbatim into the chat builder at console.openfactory.tech. Nothing above or below it — the builder expects the prompt body to start at the “Build a compact multi-node lab…” line.

Build a compact multi-node lab named `plex-media-stack`.

Output discipline: keep the plan small. Use one startup script per node, about 25 shell lines or less. Do not install Plex Media Server, Tdarr, FFmpeg, NVENC/QSV drivers, or the `plexmediaserver` apt repo at build time. Write deployment-time config examples and tiny Python stdlib or shell compatibility stubs only. The goal is a buildable preparation lab, not a production deployment.

## Topology

Create 4 buildable `debian-trixie` nodes, all `x86_64`, SSH enabled, DHCP/default route intact with lab aliases, firewall disabled, DNS `1.1.1.1` and `8.8.8.8`, user `ops` password `plex-ops` in `sudo`. Every recipe must set top-level `test_config` to `{ "enabled": false, "tests": [] }`.

- `plex`: role `media-server`, 4 GB RAM, 32 GB disk, alias `10.74.0.10/24`, x `230`, y `60`
- `tdarr`: role `transcoder-farm`, 4 GB RAM, 24 GB disk, alias `10.74.0.20/24`, x `110`, y `220`
- `nginx-proxy`: role `reverse-proxy`, 1 GB RAM, 8 GB disk, alias `10.74.0.30/24`, x `350`, y `220`
- `observability`: role `observability`, 2 GB RAM, 16 GB disk, alias `10.74.0.40/24`, x `230`, y `380`

Connections: `nginx-proxy` to `plex:32400`; `tdarr` to `plex:32400` as library-refresh intent; `observability` to all nodes.

## Common Recipe Requirements

All nodes: features `headless`, `ssh`; packages `openssh-server`, `python3`, `curl`, `jq`, `iproute2`, `netcat-openbsd`, `ca-certificates`. Each startup script adds the alias with `IFACE=$(ip route show default | awk '{print $5; exit}')`, `ip link set "$IFACE" up || true`, and `ip addr add <alias> dev "$IFACE" || true`. If `os.startup_scripts[].after` is present, it must be the string `"network-online.target"`, not an array.

## Node Requirements

`plex`: features `headless`, `ssh`. Write `/etc/plex/plex.env` with `PLEX_PORT=32400`, `PLEX_MEDIA=/var/lib/plex/media`, `PLEX_PREFS=/var/lib/plex/Library`. Create `/var/lib/plex/{media,Library,transcode}` mode `0750 ops:ops`. Add a Python stdlib HTTP service on `0.0.0.0:32400` exposing:
- `GET /identity` -> `200 <?xml version="1.0"?><MediaContainer machineIdentifier="plex-compat-lab" version="compat-1.0"/>` with `Content-Type: text/xml`
- `GET /:/prefs` -> `200 {"machineIdentifier":"plex-compat-lab","friendlyName":"lab-plex"}`
- `GET /metrics` -> `plex_compat_up 1`
Register `plex-compat.service`. Write `/root/plex-runbook.md` noting that real PMS needs the `plexmediaserver` apt repo, a claim token from `plex.tv`, and HW transcoding drivers.

`tdarr`: features `headless`, `ssh`. Write `/etc/tdarr/tdarr.env` with `SERVER_PORT=8265`, `WEB_PORT=8266`, `NODE_NAME=tdarr-node-1`, `PLEX_HOST=http://10.74.0.10:32400`. Add a Python stdlib service exposing two ports: `0.0.0.0:8265` for `GET /api/v2/status` -> `200 {"status":"ok","node":"tdarr-node-1"}`, and `0.0.0.0:8266` for `GET /` -> `200 {"ui":"compat"}` and `GET /metrics` with `tdarr_compat_up 1`. Register `tdarr-compat.service`.

`nginx-proxy`: features `headless`, `ssh`, `nginx`; packages `nginx`. Write `/etc/nginx/sites-enabled/plex.conf` proxying `:80` to `http://10.74.0.10:32400` with appropriate `proxy_set_header` directives. Restart nginx.

`observability`: features `headless`, `ssh`, `prometheus`, `monitoring`; packages `python3`. Write `/etc/prometheus/prometheus.yml` scraping `10.74.0.10:32400`, `10.74.0.20:8266`, `10.74.0.30:80`. Add tiny Python listeners for `:9090/-/healthy` and `:3000/api/health`.

## Scenario

Emit exactly one group scenario named `plex-media-stack-validation`. Put `custom_tests[].assertions[]` inside the scenario entry; leave `scenarios[].tests` empty. Every assertion needs `on_vm`. Use only `port_listening`, `command_output`, and `http_responds`; do not emit `vm_boots`, `network_reachable`, or `service_running`.

- `Stack ports listen`: `port_listening` for `plex:32400`, `tdarr:8265`, `tdarr:8266`, `nginx-proxy:80`, `observability:9090`, `observability:3000`.
- `Plex identity`: on `plex`, `curl -fsS http://localhost:32400/identity | grep -q 'machineIdentifier="plex-compat-lab"' && echo plex-id`.
- `Tdarr status`: on `tdarr`, `curl -fsS http://localhost:8265/api/v2/status | jq -e '.status == "ok"' >/dev/null && echo tdarr-ok`.
- `Proxy reaches plex`: on `nginx-proxy`, `nc -z -w 5 10.74.0.10 32400 && echo upstream-reachable`.
- `Observability targets`: on `observability`, grep all three scrape targets in `/etc/prometheus/prometheus.yml` and echo `targets`; add `http_responds` for `http://localhost:9090/-/healthy` and `http://localhost:3000/api/health`.

Preserve warnings that real Plex Media Server, the Plex claim-token flow with plex.tv, HW-accelerated transcoding driver selection, Tdarr GPU/CPU node configuration, library scan scheduling, remote-access port mapping, real TLS on the proxy, and `10.74.0.0/24` aliasing are deployment-time concerns.

Running it

  1. Open the chat builder at console.openfactory.tech and paste the prompt into a new conversation.
  2. Review the streamed build plan. You'll see the topology, per-node recipes, and the scenario assertions that will run after boot. Edit the prompt and re-run if anything is off.
  3. Click Build group. OpenFactory fans the plan out to per-node ISO builds. When every ISO reaches built, boot the group on the runner network from the same UI.
  4. Exercise the stack. The scenario assertions run automatically against the live VMs. From the host you can also hit the service ports directly to confirm end-to-end behavior.

Driving OpenFactory from an AI agent instead of the browser? The same flow is exposed through the OpenFactory MCP server — submit the prompt programmatically, get the build-plan preview back, and call create_build / start_vm on the resulting recipes. Single-image builds go straight through the openfactory CLI.

What's still your responsibility

The prompt produces a buildable preparation lab — the right topology, the right ports listening, deployment-time config templates dropped in the right places, and tiny compatibility services that prove the wiring works. A few things still sit outside the recipe and need operator attention before this carries real load:

  • Real Plex Media Server. Install from the plexmediaserver apt repo and claim the server with a token from plex.tv.
  • HW transcoding drivers. VAAPI / NVENC / QSV plus a working /dev/dri/renderD128. Plex pass and HW transcoding are why this stack stays low-power.
  • Tdarr node config. Worker count, priority, GPU assignment all happen in the Tdarr UI after the real binary ships.
  • Remote access. Either trust plex.tv's NAT-traversal or front the proxy with real TLS and a public DNS name.
  • Backups. /var/lib/plex/Library holds watch history, library metadata, and user accounts. Worth snapshotting.

Where to go next

If Plex is your front-end, you probably want the *arr suite feeding it. The *arr media automation post wires Sonarr + Radarr + Prowlarr + Bazarr into a parallel four-VM stack. Want Jellyfin's open-source equivalent? See the Jellyfin media stack post. And the Enterprise & GxP page covers fleet rollouts.

Ready to ship this in production?

OpenFactory's free flow is for browsing. Persistent VMs, SSH access, snapshots, your own ISO, and fleet deployment live on a paid plan.