Before you start
Have these ready:
- Docker, with Compose.
- A running Jellyfin. The container and your devices must both be able to reach it.
Only want solo playback? Skip to Get the apps. You can add the server later.
Docker Compose
Save compose.yaml in a new folder
Make a folder for the server, such as
screenfin, and save this in it ascompose.yaml. It runs the published image,ghcr.io/screenfin/screenfin-server. Every setting is a line in.env, anddocker compose up -dapplies a change.compose.yamlname: screenfin # One container — the relay and the web client. services: screenfin-server: image: ghcr.io/screenfin/screenfin-server:${SCREENFIN_VERSION:-latest} restart: unless-stopped ports: # PORT may carry a bind address: 127.0.0.1:8484 is loopback, for a proxy on # this host. Unset, it is 8484 on every interface, which televisions on a # ws:// LAN address need. - '${PORT:-8484}:8484' environment: JELLYFIN_URL: ${JELLYFIN_URL:?set JELLYFIN_URL in .env} ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-https://${APP_DOMAIN:?set APP_DOMAIN in .env}} ADVERTISED_URLS: ${ADVERTISED_URLS:-wss://${APP_DOMAIN:?set APP_DOMAIN in .env}/v1/ws} # Behind a proxy this must name it, or every client shares one address. TRUST_PROXY: ${TRUST_PROXY:-false} MAX_ROOM_PARTICIPANTS: ${MAX_ROOM_PARTICIPANTS:-16} volumes: # The identity key and the live rooms — back this up. - relay-data:/data # The process writes /data and nothing else, needs no capabilities, and a # runaway is a container restart rather than the host's memory. read_only: true tmpfs: - /tmp cap_drop: - ALL security_opt: - no-new-privileges:true mem_limit: ${MEM_LIMIT:-512m} volumes: relay-data:Save .env beside it
.envJELLYFIN_URL=http://192.168.1.10:8096 APP_DOMAIN=screenfin.example.com PORT=127.0.0.1:8484JELLYFIN_URLis your Jellyfin as the container reaches it — a LAN address or another container’s name, neverlocalhost, which is the container itself.APP_DOMAINis the hostname people will open Screenfin at; the browser and socket addresses derive from it. Both are required.PORTkeeps the server on this machine only until you choose how people reach it. Everything else has a default, listed under Settings.Start it
docker compose pull docker compose up -d docker compose logs -f screenfin-serverThe server keeps its identity key and live rooms on a volume named
relay-data— the one thing worth backing up.
Reaching it
Choose how people reach the server.
On your home network
Open the port to your network and use the machine’s own address. Add these lines to .env, replacing its PORT, with 192.168.1.20 as the machine running Docker:
PORT=8484
ALLOWED_ORIGINS=http://192.168.1.20:8484
ADVERTISED_URLS=ws://192.168.1.20:8484/v1/wsWith a hostname and HTTPS
Put any reverse proxy in front and forward everything on the hostname to the container, WebSockets included:
https://screenfin.example.com/* → 127.0.0.1:8484Then name the proxy in TRUST_PROXY, so each visitor counts as themselves rather than all of them as the proxy. For a proxy on the same machine, that is the Docker network’s gateway:
docker network inspect screenfin_default -f '{{(index .IPAM.Config 0).Gateway}}'
# then in .env:
TRUST_PROXY=172.18.0.1Check the route answers:
curl http://127.0.0.1:8484/healthz # on the host
curl https://screenfin.example.com/healthz # through the proxyPort 8484 speaks plain HTTP, so keep it off the internet. Using Traefik? The next section has a ready setup; for other proxies, see the repository’s reverse-proxy notes.
Traefik (optional)
Only if Traefik already runs on this machine. Replace your compose.yaml with this one: the lines marked # Traefik, and the labels under them, are the only change.
name: screenfin
# One container — the relay and the web client.
services:
screenfin-server:
image: ghcr.io/screenfin/screenfin-server:${SCREENFIN_VERSION:-latest}
restart: unless-stopped
ports:
# PORT may carry a bind address: 127.0.0.1:8484 is loopback, for a proxy on
# this host. Unset, it is 8484 on every interface, which televisions on a
# ws:// LAN address need.
- '${PORT:-8484}:8484'
environment:
JELLYFIN_URL: ${JELLYFIN_URL:?set JELLYFIN_URL in .env}
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-https://${APP_DOMAIN:?set APP_DOMAIN in .env}}
ADVERTISED_URLS: ${ADVERTISED_URLS:-wss://${APP_DOMAIN:?set APP_DOMAIN in .env}/v1/ws}
# Behind a proxy this must name it, or every client shares one address.
TRUST_PROXY: ${TRUST_PROXY:-false}
MAX_ROOM_PARTICIPANTS: ${MAX_ROOM_PARTICIPANTS:-16}
volumes:
# The identity key and the live rooms — back this up.
- relay-data:/data
# The process writes /data and nothing else, needs no capabilities, and a
# runaway is a container restart rather than the host's memory.
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
mem_limit: ${MEM_LIMIT:-512m}
networks: [proxy] # Traefik
labels: # Traefik
traefik.enable: 'true'
traefik.http.routers.screenfin.rule: Host(`screenfin.example.com`)
traefik.http.routers.screenfin.entrypoints: websecure
traefik.http.routers.screenfin.tls: 'true'
traefik.http.routers.screenfin.tls.certresolver: letsencrypt
traefik.http.services.screenfin.loadbalancer.server.port: '8484'
volumes:
relay-data:
networks: # Traefik
proxy:
external: true
Change proxy to the name of Traefik’s Docker network, websecure and letsencrypt to your entrypoint and certificate resolver, and the host to your APP_DOMAIN.
Traefik reaches the container over that network, so TRUST_PROXY is the network’s subnet:
docker network inspect proxy -f '{{(index .IPAM.Config 0).Subnet}}'
# then in .env:
TRUST_PROXY=172.22.0.0/16WebSockets pass through Traefik with no extra setup.
Pair your devices
Every time it starts, the server prints a branding mark. Add that line to Jellyfin so every device — phone, tablet, television, browser — can find the server and verify it.
Take the line from the log
docker compose logs --no-log-prefix screenfin-server | grep '^/\* screenfin' | tail -1Paste it into Jellyfin
As a Jellyfin administrator, open Dashboard → General → Branding → Custom CSS, paste the line at the end, and save.
Open the web client
Go to
https://screenfin.example.com, or the home-network address. On first visit it asks which Jellyfin to use.
The mark is the whole of the pairing: paste a fresh line whenever an address changes, and know that removing it un-pairs every client on its next launch.
Televisions on your network can connect directly as well: add the LAN address after the first one in ADVERTISED_URLS, for example wss://screenfin.example.com/v1/ws,ws://192.168.1.20:8484/v1/ws, and set PORT=8484. Skip this if your proxy runs on this same machine and trusts the gateway: with the port open, anyone on your network could pass as the proxy.
Get the apps
Install Screenfin from the App Store (iPhone, iPad, Apple TV) or Google Play (Android TV, phones, tablets). Sign in with your Jellyfin address and your Jellyfin credentials — there is no Screenfin account. On a television, Quick Connect lets you approve the sign-in from your phone instead of typing a password with a remote.
If you paired the server, party controls appear after sign-in. There is nothing else to configure in the app.
Recommended plugins
Screenfin works with a plain Jellyfin install. Two Jellyfin plugins turn on features in the apps; install them on your Jellyfin server, not on the Screenfin server, and every app picks them up with nothing to set in Screenfin.
- Intro Skipper
- defaultrecommended
- Skip Intro, and intro and outro markers in the player. Adds its own plugin repository.
- Open Subtitles
- defaultfor subtitle search
- Find and download subtitles while watching, in the iOS, Apple TV and Android apps. From Jellyfin’s own catalog.
Intro Skipper
In Jellyfin, open
Dashboard → Plugins → Repositories, addhttps://intro-skipper.org/manifest.json, then install Intro Skipper from the catalog and restart Jellyfin. It analyses your episodes in the background; Skip Intro appears on each one once it has been analysed.Open Subtitles
Install Open Subtitles from
Dashboard → Plugins → Catalog, restart Jellyfin, and sign in to a free opensubtitles.com account in the plugin’s settings. Subtitle search in the apps is shown to Jellyfin accounts allowed to manage subtitles.
Tested with Intro Skipper 12.0.4 and Open Subtitles 25.0 on Jellyfin 12.0. Both plugins are made and supported by their own authors, not by Screenfin.
Update
docker compose pull
docker compose up -dThe relay-data volume is unchanged by an update, so the identity key and the pairing survive it.
Settings
Beyond the two required lines, every setting has a default. More on each one is in the repository’s README.
- PORT
- default8484
- The host port, with the address it listens on.
- TRUST_PROXY
- defaultfalse
- Your reverse proxy’s address or subnet, as the container sees it. Never a number or true.
- ALLOWED_ORIGINS
- defaulthttps://APP_DOMAIN
- The browser addresses allowed to connect. Set it for plain http or a LAN address.
- ADVERTISED_URLS
- defaultwss://APP_DOMAIN/v1/ws
- The socket addresses the branding mark names, first choice first.
- MAX_ROOM_PARTICIPANTS
- default16
- The largest party the server accepts, 1–100.
- MEM_LIMIT
- default512m
- The container’s memory ceiling. Past it the container restarts, not the host.
- SCREENFIN_VERSION
- defaultlatest
- The image tag. Pin a release such as 1.0.0, or let latest follow every release.
If something is off
- The page loads but parties never connect
- The proxy is not forwarding WebSocket upgrades on
/v1/ws, or is closing idle connections. The browser’s network tab shows the proxy’s answer. - Sign-in fails in Screenfin but works in Jellyfin
- The app signs in to Jellyfin directly from the device, so a LAN address only works on that LAN.
AUTH_UNAVAILABLEin the server’s logs means the container cannot reachJELLYFIN_URL. - Everyone is refused new connections at once
- A proxy is in front and
TRUST_PROXYdoes not name it. Set it as in Reaching it. - The socket is refused with 4006
- The address in the browser does not match
ALLOWED_ORIGINSexactly — scheme included, no trailing slash. - The health check fails through the proxy
- If the check in Reaching it answers on the host but not through the proxy, the proxy route is the problem.
docker compose logs screenfin-serverhas the rest.
Still stuck? Open an issue on screenfin/community with your SCREENFIN_VERSION and which client, or write to [email protected].