Pharos runs on your box, against your Plex or Hearth (opens Hearth in a new tab), your downloader, your indexers and your disks. This is the whole setup, wired the way ours is: from an empty machine to friends asking for shows.
Pharos sits in the middle. Your people reach the web app; the backend talks to everything else and files onto one drive. Choose any box to jump to its reel; on a wide screen, point at one to light its wires.
A machine that stays onLinux, a NAS, or Windows or macOS with Docker Desktop.
Storage: a drive, a DAS or a NASAn internal drive, a USB or Thunderbolt DAS, or a NAS share the box mounts. Downloads and libraries on one volume, so filing is a move, not a copy.
Plex, or Hearth, or bothPharos files into them and reads them back. It is not a player itself.
A free TMDB keyTitles, posters and air dates.
A VPN accountAny provider the bundled VPN supports, or one with its own SOCKS5 server. For qBittorrent and the indexers.
A usenet provider and indexerOnly if you want NZBs in your results.Optional
Twelve reels · in order
Thread the projector
One reel per connector. Each says what it does, the exact settings to change, the lines it fills in your .env, how ours is wired, and what has gone wrong on a real install.
01
Reel 01
The box and the folders
Pharos is three small containers (the web app, the backend and the front door that puts both on one address) next to the services you already run. Any machine that stays on and runs Docker will do: a Linux server, a NAS, or Windows or macOS with Docker Desktop.
Linux
Windows
macOS
Synology
Unraid
TrueNAS SCALE
QNAP
1
Install Docker with Compose v2, then get the Pharos source.
git clone https://github.com/CarbonNode/Plexseek.git pharos
cd pharos
2
Start with a media folder, or mount separate local disks, DAS arrays and NAS shares. Pharos, your downloader and Plex must see the same files. Admin → Storage controls destinations, drive reserves, copy/hardlink/move rules and filing windows.
media/
├── Downloads finished downloads land here
├── Incomplete in progress
├── Movies
├── Series
├── Cartoons
├── Anime
└── Anime Movies
3
Put that folder on any storage the box can mount: an internal drive, a DAS (a USB or Thunderbolt enclosure), or a NAS share. Point MEDIA_ROOT at it.
# a drive or a DAS: its folder
MEDIA_ROOT=/mnt/das/media
# on Windows
MEDIA_ROOT=D:/Media
# a NAS share, mounted first (NFS or SMB)
MEDIA_ROOT=/mnt/nas/media
4
Or run the whole stack on the NAS itself. Any NAS that runs Docker Compose will do: Synology Container Manager, Unraid, TrueNAS SCALE or QNAP Container Station. MEDIA_ROOT is then a local folder on the NAS.
5
Copy the settings file. The cue sheet further down this page fills it in for you.
cp selfhost/.env.example selfhost/.env
How ours is wired
One always-on machine holds everything: the containers, the downloader, Plex, and a DAS with the whole library on it. Keeping it on one box means every service sees the same folders.
For multiple drives, use selfhost/compose.storage.example.yml as your mount template, then register each backend path in Admin → Storage. Missing drives wait; existing libraries are never migrated automatically.
Watch
Auto mode keeps seeding files: it hardlinks on the same filesystem and copies across drives. Choose strict hardlink, copy or verified move separately for each media type.
Watch
A DAS that drops off, even for a second, leaves running containers holding a dead mount (“No such device”). Once it is back, restart the containers that use it.
Watch
On a NAS share, PUID and PGID must be a user the share lets write, or downloads land and then cannot be filed.
Watch
Docker Desktop on Windows does not see mapped network drive letters. Use a local drive or a DAS there, or run the stack on the NAS.
Watch
Folder names come from Admin → Storage when its rules are enabled; otherwise MOVIES_DIR_NAME and the other startup settings apply. Changing a destination does not migrate existing files.
Watch
Every piece of backend state lives in selfhost/state. That is the one folder to back up.
Watch
Only MEDIA_ROOT has to be in .env. Every connection on the reels below can be entered in the app instead (Admin → Settings → Connections), with a Test button for each, and a value saved there wins over .env.
02
Reel 02
qBittorrent, the downloader
qBittorrent downloads what you pick, and its RSS downloader catches tracked shows the day they air. Use the one bundled in the stack, or point Pharos at yours.
1
Bundled: start the stack with --profile qbittorrent (or qbittorrent-vpn to run it inside the VPN, reel 04). Its Web UI is on port 8080, and the first start prints a temporary admin password in its log.
Your own: set QB_URL to it. If it sees the media folder under a different path than /media, map the two with QB_PATH_MAP.
3
Before you track a single show, add your trackers’ RSS feeds in qBittorrent. A tracked show’s rule takes the feeds that exist when it is created.
4
Anime is separate: Pharos adds its own Anime/Subs and Anime/Dubs feeds to qBittorrent from ANIME_SUBS_FEED and ANIME_DUBS_FEED. Subs is the default; leave the dubs feed empty to offer subs only. Admin → Settings → Anime changes both later.
WhereSetWhy
Options → Web UIA user name and password (copy them into QB_USERNAME / QB_PASSWORD)
Options → RSSEnable fetching RSS feeds, and Enable auto downloading of RSS torrentsPharos writes the rules; qBittorrent runs them. Neither switch is on by default.
Options → Downloads → Excluded file namesOn, with the list belowA program file riding along in a torrent is never written to disk.
Options → Advanced → Network interfaceYour VPN’s interfaceIf the VPN drops, downloads stop instead of carrying on from your home address.
In the appAdmin → Settings → Connections → qBittorrent: the address, user name, password and path map, each with a Test button.
Watch
Do not make categories by hand. Pharos files with tv, movie, anime, cartoon and anime_movie and qBittorrent creates them on the first add.
Watch
Pharos also reads every torrent’s file list and throws out a release that carries a program file, so the excluded list is a second wall, not the only one.
03
Reel 03
Prowlarr, the indexers
One search in Pharos fans out to every indexer you add to Prowlarr. Adding a site is a Prowlarr setting, never code.
1
Bundled: start the stack with --profile prowlarr (port 9696). Your own: set PROWLARR_URL.
2
Copy the API key from Prowlarr → Settings → General into PROWLARR_API_KEY.
3
Add the indexers you have access to. Set each one’s privacy correctly: Pharos reads it, and only people you grant private sources can search a private one.
4
A site behind a Cloudflare check needs FlareSolverr. Run it on the same Docker network, add it in Prowlarr → Settings → Indexers as an indexer proxy tagged cloudflare, then give that tag to the indexers that need it.
Each indexer → CategoriesLeave them alonePharos asks for the categories it wants on every search.
Settings → General → Proxyhttp://vpn:8888 with the bundled VPN, or your VPN’s SOCKS5 server (reel 04)Indexer traffic should leave from the VPN, not your home address.
How ours is wired
A handful of public indexers plus one private tracker, all in Prowlarr, with FlareSolverr in front of the two sites that sit behind a Cloudflare check.
In the appAdmin → Settings → Connections → Prowlarr: the address, API key, indexers to skip and whether usenet results are included.
Watch
FlareSolverr must use the same exit as Prowlarr. A Cloudflare pass is tied to the address that solved it, so a FlareSolverr on your home line hands Prowlarr a pass that fails.
Watch
Behind a proxy, FlareSolverr needs version 3.4.2 or later and its PROXY_URL setting. HTTP_PROXY is ignored.
04
Reel 04
The VPN
Two things should never leave from your home address: qBittorrent’s peers and the searches that reach your indexers. Pharos works with any VPN that gives it a proxy, and every five minutes it checks that the proxy really changes its address.
1
Bundled VPN: start the stack with --profile vpn and fill in your provider’s settings. They use gluetun’s own names, and its wiki lists what each provider needs. Then point Pharos at it.
Or use your provider’s own SOCKS5 server, or any proxy on a machine that is already tunnelled.
VPN_PROXY=socks5h://USER:PASSWORD@HOST:1080
3
VPN_PROXY does not cover qBittorrent’s peer traffic, and that is most of what a torrent client sends. Bundled qBittorrent: start --profile vpn --profile qbittorrent-vpn instead of --profile qbittorrent and set QB_URL=http://vpn:8080. Its Web UI moves to QBIT_VPN_PORT (8081), and it has no route out at all while the VPN is down. Your own qBittorrent: bind it to the VPN interface (reel 02).
4
Prowlarr has its own proxy: Settings → General → Proxy, pointed at http://vpn:8888 (or your SOCKS5 server), with localhost,127.0.0.1,host.docker.internal on the bypass list.
5
Open Admin → Settings → Safety. It shows whether the proxy address differs from the direct one. If they match, Pharos refuses private-tracker traffic until it is fixed.
How ours is wired
qBittorrent runs on the host, bound to the VPN interface. The containers that need the VPN go through the VPN’s proxy instead of trusting split tunnelling.
In the appAdmin → Settings → Connections → VPN: the proxy, and “This whole server is on the VPN”. The bundled VPN’s provider settings stay in .env, because its container reads them when it starts.
Watch
On Docker Desktop (Windows and macOS), split tunnelling cannot see container traffic. Containers leave from your home address even while the VPN app says connected.
Watch
Set VPN_ALLOW_SAME_IP=1 only when this whole machine already goes through the VPN, so the direct and proxied addresses are both the VPN’s.
Watch
Use socks5h:// rather than socks5:// so names are looked up inside the tunnel too.
Watch
Behind a VPN, qBittorrent gets no incoming peers unless your provider forwards a port. Downloads still work; they find fewer peers.
Check · Ask from inside the container, never from your own shell
docker exec <container> curl -s https://ifconfig.io
# must print the VPN's address, not your home one
05
Reel 05
Plex, the screen
Pharos reads your Plex library to badge what you already own and to find subtitles, and files new downloads straight into the folders Plex watches.
1
Point PLEX_URL at your server. On the same machine as the containers that is host.docker.internal:32400.
2
Get a token: sign in at app.plex.tv, open any title, choose Get Info → View XML, and copy the X-Plex-Token value from the address bar.
3
Make one Plex library per folder, named Movies, TV Shows (or Series), Cartoons, Anime and Anime Movies. The library finder sorts by these names.
How ours is wired
Plex runs on the same box with one library per folder, so a finished download is on the shelf as soon as it is filed.
In the appAdmin → Settings → Connections → Plex: the address, token and path map, and the Test button lists the libraries it found.
Watch
Plex must see the same folders under MEDIA_ROOT. A library pointed at a copy is a library Pharos cannot file into.
Watch
English subtitles come from Plex’s own subtitle search and are written beside the file, so every viewer gets them. If Plex sees the media under another path than /media (say /data), set PLEX_PATH_MAP=/data=/media; otherwise the subtitle stays a Plex selection for one account.
06
Reel 06
TMDB, the posters
Titles, posters, air dates and the Hearth matching all come from The Movie Database. A key is free.
1
Make a free account at themoviedb.org, then Settings → API → request a v3 key.
2
Put it in TMDB_API_KEY. The stack passes it to the backend and builds it into the web app.
How ours is wired
One key used for nothing else, because the web app carries it to the browser.
Lines in your .env
TMDB_API_KEY=
In the appAdmin → Settings → Connections → TMDB. Keep it in .env as well: the web app’s own poster lookups use the copy built into it.
Watch
The key is built into the web app and visible in the browser. Use a key you made for Pharos.
Watch
Changing it means rebuilding the web container: up -d --build web.
07
Reel 07
Filing, in Admin
File completed downloads into the libraries you choose. Storage rules control the drives, transfer methods and timing; the unzipper checks archives before opening them.
1
Nothing to do on a new install: the organizer starts on, watching /media/Downloads and filing into the library folders your .env names (/media/Movies, /media/Series and so on). Admin → Settings → Media Organizer shows them and can change them.
2
For more control, open Storage & drives. Register your mounted paths, choose download inboxes and destinations for each media type, set reserves and filing windows, preview, then enable storage rules. These rules take over filing from the older organizer.
3
Choose hardlink-or-copy to keep seeding, copy for independent files, hardlink-only to wait for a compatible destination, or move to remove verified source media after its torrent stops. Test with one completed title and check Preview & activity.
How ours is wired
Downloads → Movies, Series, Cartoons, Anime and Anime Movies, with the junk purged and archives inspected before they open.
In the appAdmin → Settings → Storage & drives: mounted disks and shares, inboxes, destinations, fallback drives, reserves, transfer modes, schedules, pause and preview. Archive folders remain in Unzipper.
Watch
Once storage rules are enabled, edit filing there. Turning them off restores the older organizer. Neither changing a destination nor registering a drive migrates an existing library.
Watch
Filing windows control when a new transfer starts; a copy already running finishes safely. Download-client schedules and incomplete folders remain in the client.
Watch
An archive that needs a password, holds a program file, or has no video in it is left where it is and logged as [GATE]. That is deliberate: read the reason before you unpack one by hand.
08
Reel 08 · optional
Hearth, the second library
Hearth is a second home for movies and shows beside Plex. Pharos lists both on one shelf, badges what each one holds, and plays from either.
In Hearth, open the Videos library tools → API access… and create a key named Pharos. It starts with hk_ and is shown once.
2
On the Hearth server, list Pharos’s web address in HEARTH_API_CORS_ORIGINS and set HEARTH_PUBLIC_URL, so a browser may play what Pharos lists.
3
In Pharos, set HEARTH_URL to Hearth’s LAN address (not its public one) and HEARTH_API_KEY to the key.
4
The other direction is HearthSeek: in Pharos open Hearth in the side menu, create a connection token (hs_…), and paste it into Hearth → Torrents → HearthSeek → Connect Pharos.
How ours is wired
Plex and Hearth side by side: one search, one shelf, “In Plex”, “In Hearth” or “In library” on every poster.
In the appAdmin → Settings → Connections → NZBGet: the address, user name and password.
Watch
Change NZBGet’s settings in its web UI or in nzbget.conf. Writing config through its saveconfig API once erased the server list, the main folder and the login.
Watch
Usenet results have no seeders, and Pharos never applies the seeder floor to them.
10
Reel 10 · optional
Outside access
Pharos answers on one address, like a Plex server on its one port: the web app and the backend sit behind a small front door (Caddy) on port 3000. At home that is already done. For friends, give it a name and open one port, or use a tunnel.
1
At home: open http://<your box>:3000. Nothing to set.
2
Forward a port, like Plex. Get a name that points at your home address: your own domain, or a free one at duckdns.org. With duckdns, start the stack with --profile duckdns so the name follows your address when it changes.
On your router, forward TCP port 443 to this box’s port 8443 (PHAROS_HTTPS_PORT), then start the stack again. Caddy fetches the HTTPS certificate and renews it by itself. Send your friends https://yourname.duckdns.org.
4
Or a tunnel, when your provider gives you no public address (CGNAT) or you would rather open no port: point Cloudflare Tunnel or Tailscale Funnel at http://<your box>:3000, leave PHAROS_SITE empty, and set PLEXSEEK_PUBLIC_API_URL and PHAROS_WEB_URL to the tunnel’s address.
How ours is wired
A tunnel publishes Pharos, so nothing on the box has an open port. Most homes are better served by the port forward: no account anywhere, and HTTPS on its own.
In the appAdmin → Settings → Connections → Site: the web address people open and the public hostnames. The name, the ports and the certificate stay in .env, because the front door reads them when it starts.
Watch
Never forward port 3000 itself. It is plain HTTP, and every password would cross the internet unencrypted.
Watch
Only 443 has to be open: Caddy proves the name over that same port. Port 80 is not needed.
Watch
If the whole box goes out through a VPN, the name points at the VPN and a port forward cannot reach you. Use a tunnel then.
Watch
Many routers keep a dynamic name updated themselves (DDNS). Use that instead of --profile duckdns if yours does.
Check · From a machine outside your network
curl -s https://yourname.duckdns.org/api/health
# an answer means the port, the name and the certificate all work
11
Reel 11
Opening night
Start the stack, make the first admin, and invite your people. Everyone signs in with their own account; nobody shares yours.
1
Start everything. Leave out a --profile for any service you run yourself.
Open http://<your box>:3000, the front door. With no admin set in .env, it sends you to /setup, which asks for a one-time token from the backend log or selfhost/state/data/setup_token.txt. The token stops working once the first admin exists.
Invite people from Admin → Users. The same page decides who may use private indexers: “Private sources by default” (admins only, everyone or nobody) and a switch on each person. PRIVATE_SOURCES_FOR only sets the starting default.
4
A request from someone who is not an admin waits on Admin → Requests until you approve it, unless you set them to auto-approve. REQUEST_APPROVALS=off turns the gate off.
5
Optional: let people reset a forgotten password by email. Fill in the SMTP_ lines (any mail provider’s SMTP server; for Gmail use an app password) or Admin → Connections → Email, and set PHAROS_WEB_URL so the link points at your address. Without email, you get a notice and make the reset link on Admin → Users yourself.
6
Optional: connect an AI assistant. In claude.ai or the Claude app, add a custom connector with your Pharos address followed by /mcp, and sign in with a Pharos account. It acts as that person, so approvals still apply, and each friend can connect their own. Claude Code and other clients take a token from Integrations → AI assistants instead.
https://<your Pharos address>/mcp
How ours is wired
Family and friends each have an account. A friend’s request waits for an admin to approve it; trusted people are switched to auto-approve.
After reinstalling or restoring qBittorrent, check that the excluded file names list is still on. A reinstall loses it without a word.
How ours is wired
Updates after a pull, one backup of the state folder, and a VPN check after anything on the network changes.
The cue sheet
Write your .env
Pick the booth closest to yours, switch what you have, and fill in your own values. The file and the start command build as you type. It all stays in this tab. Only MEDIA_ROOT is required: the connections can wait for Admin → Settings → Connections once Pharos is running.
When the film breaks
Nine fixes
The things that go wrong on a first night, and the reel that fixes each one.
Downloads finish but nothing reaches the library+
Open Admin → Settings → Media Organizer: check it is on and that Monitored Folder and each library folder read /media/… and exist on disk under MEDIA_ROOT.
qBittorrent’s RSS switches are off, or the feeds were added after the show was tracked. Turn on both RSS options, then open the show’s rule in qBittorrent’s RSS downloader and tick the feeds it should read.
Check PROWLARR_API_KEY, that Prowlarr has at least one enabled indexer, and that a Cloudflare-protected site has the cloudflare tag and a working FlareSolverr.
Check from outside your network: the router must forward TCP 443 to the box’s port 8443, PHAROS_SITE must be the name you share, and that name must point at your current home address. Behind CGNAT no port forward can work; use a tunnel.
Pharos found the same address through VPN_PROXY as without it. Admin → Settings → Safety shows both; fix the proxy, or set VPN_ALLOW_SAME_IP=1 if the whole machine is behind the VPN.
claude.ai reaches your server from the internet, so it needs the HTTPS address friends use (see Outside access), followed by /mcp. On a LAN-only box use Claude Code or Claude Desktop with a token from Integrations → AI assistants.