Deploy the complete WatchParty service with Docker, Traefik, Firebase, and Headscale
Run the full WatchParty service: synced rooms, chat, playlists, Firebase login, PostgreSQL persistence, Redis coordination, Traefik routing, and optional Headscale-only VBrowser.
WatchParty is a private watch-together service. It gives users shared rooms where playback state, playlist changes, chat, room permissions, screen sharing, and optional virtual browser sessions stay synchronized.
This guide documents the complete WatchParty service, not just the VBrowser feature. VBrowser matters because it is the most infrastructure-sensitive part, but it is only one component of the stack.
The complete homelab pattern is:
- WatchParty runs as a Node/React application behind Traefik.
- PostgreSQL persists rooms, owners, vanity links, room metadata, subscribers, and VBrowser assignments.
- Redis supports fast runtime coordination and cache-style state.
- Firebase handles native application login instead of a generic reverse-proxy login wall.
- Rooms support synced media playback, playlists, chat, camera/video chat, screen sharing, subtitles, and room locking.
- VBrowser containers are created on demand for sites that need a real shared browser.
- VBrowser WebRTC can stay private over Headscale/Tailscale instead of public router port forwarding.
- Old or broken VBrowser containers are removed automatically.
- Container names are readable enough for operators to debug quickly.
All domains, IP addresses, OAuth IDs, API keys, and passwords in this post are placeholders. Replace them in your own environment, but do not publish real infrastructure details or secrets.
What this service does
Route or access pattern:
1
2
3
watch.example.com # WatchParty web app
watch.example.com/vb-0/ # VBrowser web route through Traefik
<tailnet-ip>:59000 # private WebRTC UDP/TCP mux candidate
Main components:
1
WatchParty web UI, Node.js server, Socket.IO realtime rooms, PostgreSQL, Redis, Firebase Auth, Docker socket access for optional VBrowser, Neko/Chromium VBrowser container, Traefik, Headscale/Tailscale.
Network and port model:
1
2
3
4
5
6
Public HTTPS traffic:
Browser -> Traefik :443 -> WatchParty container :8080
Browser -> Traefik :443 -> VBrowser container :8080 under /vb-0
Private WebRTC media path:
Client on tailnet -> <server-tailnet-ip>:59000 -> VBrowser container mux port
The normal WatchParty app is a standard HTTPS web service. The special design choice is for VBrowser: its WebRTC mux port does not need to be forwarded publicly on the router. Users who need VBrowser join the private tailnet first.
Architecture
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Public HTTPS
User browser
↓
https://watch.example.com
↓
Traefik
↓
WatchParty app
├── React web UI
├── Node.js / Socket.IO room server
├── PostgreSQL persistent room/account data
├── Redis runtime coordination
├── Firebase Auth verification
└── Docker API launches optional disposable VBrowser
Private WebRTC
User device on Headscale/Tailscale
↓
<server-tailnet-ip>:59000 UDP/TCP
↓
Neko / Chromium VBrowser container
This split keeps the full WatchParty web service easy to reach while keeping the optional browser streaming path private.
Core WatchParty features
The base WatchParty service should work even when VBrowser is disabled. Document and verify these features first:
| Feature | What it does | Infrastructure dependency |
|---|---|---|
| Rooms | Creates isolated spaces where users watch together | Node server, Socket.IO, PostgreSQL for persistence |
| Synced playback | Play, pause, seek, timestamps, playback rate, loop state | Socket.IO realtime room events |
| Playlists | Add, move, remove, and advance media entries | Room state and optional database persistence |
| Chat | Text chat, reactions, and moderation actions | Socket.IO room events |
| User identity | Names, avatars, authenticated UID, room ownership | Firebase Auth and room state |
| Room control | Locking, owner-only settings, kicking, disabled chat | Firebase UID + owner/lock checks |
| Video chat | Peer signaling for camera/microphone calls | HTTPS + WebRTC signaling |
| Screen sharing | Share a tab, window, or screen into the room | HTTPS + browser permissions + WebRTC signaling |
| Stream-your-own-file | Let users share local/video files through the browser | Browser APIs and room signaling |
| Internet media | Play direct HTTP video files and HLS streams | Client/server media handling |
| YouTube search/playback | Optional search integration | YouTube Data API key if search is enabled |
| Magnet/WebTorrent | Optional torrent-style media sharing | WebTorrent/browser support |
| VBrowser | Shared browser for websites that resist normal embedding | Docker + Neko + WebRTC networking |
That order matters operationally. If rooms, login, and synced playback are broken, VBrowser cleanup will not save the service.
Room and permission model
WatchParty is room-centered rather than classic admin-dashboard-centered.
Important room concepts:
1
2
3
4
5
6
7
8
9
roomId unique room identifier
vanity optional human-readable room path/name
owner Firebase UID that owns the room
password optional room password
lock optional UID allowed to control playback
roster currently connected users
playlist queued media entries
chat room chat history/state
mediaPath currently hosted or active media path
A clean permission model is:
- anonymous or logged-in users can join public rooms, depending on room settings;
- authenticated Firebase users can own persistent rooms;
- the room owner can change room-level settings;
- a locked room can restrict playback controls to one UID;
- subscriber flags can unlock optional subscriber-only behavior;
- VBrowser control should follow the same room lock/owner rules as other room control actions.
This avoids inventing a global admin/user model when the application is mostly based on rooms, owners, and authenticated UIDs.
Data model and persistence
PostgreSQL is what makes the service survive restarts cleanly.
Useful persistent tables include:
1
2
3
4
5
room room metadata, owner, vanity name, media state, room settings
subscriber subscription/customer status mapped to Firebase UID
link_account linked external accounts
active_user recent activity tracking
vbrowser optional VM/container assignment state
Redis is runtime infrastructure. Treat it as fast coordination/cache state, not the only copy of important long-term room data.
Back up PostgreSQL seriously. Back up Redis only if you configure it to hold state you cannot recreate.
Media and realtime sync flow
A normal WatchParty session looks like this:
1
2
3
4
5
6
7
8
9
10
11
User opens room
↓
React app connects to the Node/Socket.IO namespace for that room
↓
Server sends room state: host media, playlist, roster, chat, lock state
↓
A controller plays, pauses, seeks, changes playlist, or updates subtitles
↓
Socket.IO broadcasts the change to every connected client
↓
Clients adjust playback to stay synchronized
For regular video URLs or HLS streams, the media path is the main state. For WebRTC-based features like video chat, screen sharing, or VBrowser, the room server also handles signaling so peers can negotiate media connections.
Optional integrations
Enable only what you use:
- Firebase Auth for accounts, room ownership, and identity verification.
- YouTube Data API if you want YouTube search inside the app.
- Stripe/subscriptions if you use subscriber-only features.
- Discord bot/linking if you expose Discord integration.
- VBrowser/Docker if you want shared browsing.
- Headscale/Tailscale if you want private WebRTC paths without public port forwarding.
Keeping optional integrations explicit makes the service easier to secure and debug.
Folder layout
Use one service directory:
1
2
3
4
5
6
7
8
/home/ubuntu/watchparty/
├── app/ # cloned WatchParty source repository
├── docker-compose.yml
├── .env # private
├── secrets/
│ └── firebase-adminsdk.json
├── postgres/ # or a named Docker volume
└── redis/ # or a named Docker volume
Keep the Firebase Admin SDK file and .env private:
1
chmod 600 .env secrets/firebase-adminsdk.json
Never commit those files to Git.
Get the WatchParty source repository
The Docker build expects the WatchParty application source to exist at ./app.
Clone the upstream WatchParty repository into the service directory:
1
2
3
4
mkdir -p /home/ubuntu/watchparty
cd /home/ubuntu/watchparty
git clone https://github.com/howardchung/watchparty.git app
If you prefer SSH and already have deploy keys configured:
1
git clone [email protected]:howardchung/watchparty.git app
After cloning, inspect the app structure:
1
2
ls -la app
ls -la app/server app/src app/sql
The important files for this deployment are:
1
2
3
4
5
6
7
app/Dockerfile
app/package.json
app/server/server.ts
app/server/config.ts
app/server/vm/docker.ts
app/sql/schema.sql
app/sql/migrations.sql
If you maintain local patches, commit them in your own fork or keep a small deployment patch script. Do not edit files manually on the server without recording what changed, or upgrades will become painful.
Recommended fork workflow:
1
2
3
4
5
6
cd /home/ubuntu/watchparty/app
git remote -v
# origin should point to your fork if you want to keep custom production patches.
git checkout -b homelab-production
Update workflow later:
1
2
3
4
5
cd /home/ubuntu/watchparty/app
git fetch --all
git status
# Review upstream changes, reapply your local patches, then rebuild.
Environment files
Treat environment configuration as real files, not loose notes. I like to show it in two parts:
- a required production
.envthat the Compose stack actually loads; - optional
.envadditions you only add when enabling extra integrations.
Create the real file here:
1
2
nano /home/ubuntu/watchparty/.env
chmod 600 /home/ubuntu/watchparty/.env
Required baseline:
# Public app route
WATCHPARTY_HOST=watch.example.com
NODE_ENV=production
PORT=8080
# PostgreSQL container bootstrap
POSTGRES_USER=watchparty
POSTGRES_PASSWORD=<generate-a-long-database-password>
POSTGRES_DB=watchparty
# App database/runtime connections
DATABASE_URL=postgres://watchparty:<same-database-password>@postgres:5432/watchparty
REDIS_URL=redis://redis:6379
# Firebase web login config
VITE_FIREBASE_API_KEY=<firebase-web-api-key>
VITE_FIREBASE_AUTH_DOMAIN=<project>.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=<firebase-project-id>
VITE_FIREBASE_APP_ID=<firebase-web-app-id>
VITE_FIREBASE_SIGNIN_METHODS=github,google,email
# Firebase server verification config
FIREBASE_PROJECT_ID=<firebase-project-id>
FIREBASE_CLIENT_EMAIL=<firebase-admin-client-email>
FIREBASE_PRIVATE_KEY_FILE=/run/secrets/firebase-adminsdk.json
Optional additions, only when you use these features:
# YouTube search inside WatchParty
YOUTUBE_API_KEY=<youtube-data-api-key-if-used>
# Paid/subscriber features, if enabled
STRIPE_SECRET_KEY=<stripe-secret-if-used>
# Discord integration, if enabled
DISCORD_BOT_TOKEN=<discord-token-if-used>
DISCORD_ADMIN_BOT_TOKEN=<discord-admin-token-if-used>
# Optional VBrowser / Neko
VBROWSER_HOSTNAME=watch.example.com
VBROWSER_TAILNET_IP=<server-tailnet-ip>
VBROWSER_MUX_PORT=59000
VM_MANAGER_CONFIG=<json-or-empty-depending-on-your-deployment>
VBROWSER_SESSION_SECONDS=10800
Why split it this way:
- the required block is enough to boot the main WatchParty service;
- optional integrations stay visibly optional;
- secrets are easier to audit;
- readers can copy the required file without accidentally enabling Stripe, Discord, or VBrowser.
Notes:
- Firebase web config is not the same as the Admin SDK secret.
- OAuth provider client secrets stay in Firebase or the provider console, not in public docs.
- YouTube, Stripe, Discord, and VBrowser settings are optional. Do not add secrets for integrations you do not use.
- The tailnet IP should be private to your Headscale/Tailscale network.
- Keep this file out of Git and readable only by the deployment user.
Compose pattern
A simplified Compose pattern looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
services:
watchparty:
build: ./app
container_name: watchparty
restart: unless-stopped
env_file:
- .env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./secrets/firebase-adminsdk.json:/run/secrets/firebase-adminsdk.json:ro
networks:
- proxy
- internal
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.watchparty.entrypoints=https"
- "traefik.http.routers.watchparty.rule=Host(`watch.example.com`)"
- "traefik.http.routers.watchparty.tls=true"
- "traefik.http.routers.watchparty.tls.certresolver=cloudflare"
- "traefik.http.services.watchparty.loadbalancer.server.port=8080"
postgres:
image: postgres:16-alpine
restart: unless-stopped
env_file:
- .env
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U watchparty"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
networks:
- internal
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
networks:
proxy:
external: true
internal:
internal: true
For a production-style deployment, add a few hardening details:
- initialize PostgreSQL from
sql/schema.sqlon first boot; - keep Redis persistent if you care about runtime continuity during restarts;
- bind any debug host port to
127.0.0.1only; - attach the app to both
internalandproxy, but keep databases only oninternal; - use healthchecks so Compose starts the app only after dependencies are ready.
A more complete pattern:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
env_file: .env
volumes:
- postgres_data:/var/lib/postgresql/data
- ./app/sql/schema.sql:/docker-entrypoint-initdb.d/001-schema.sql:ro
networks:
- internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 30s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- internal
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 5s
retries: 5
watchparty:
build:
context: ./app
image: local/watchparty:latest
container_name: watchparty
restart: unless-stopped
env_file: .env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- ./secrets/firebase-adminsdk.json:/run/secrets/firebase-adminsdk.json:ro
ports:
- "127.0.0.1:8088:8080" # optional local-only debug access
networks:
- internal
- proxy
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.watchparty.entrypoints=http"
- "traefik.http.routers.watchparty.rule=Host(`watch.example.com`)"
- "traefik.http.middlewares.watchparty-https-redirect.redirectscheme.scheme=https"
- "traefik.http.routers.watchparty.middlewares=watchparty-https-redirect"
- "traefik.http.routers.watchparty-secure.entrypoints=https"
- "traefik.http.routers.watchparty-secure.rule=Host(`watch.example.com`)"
- "traefik.http.routers.watchparty-secure.tls=true"
- "traefik.http.routers.watchparty-secure.tls.certresolver=cloudflare"
- "traefik.http.routers.watchparty-secure.service=watchparty"
- "traefik.http.services.watchparty.loadbalancer.server.port=8080"
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8080/ || exit 1"]
interval: 60s
timeout: 10s
retries: 5
start_period: 90s
volumes:
postgres_data:
redis_data:
networks:
internal:
proxy:
external: true
If VBrowser runs Docker containers on the same host, the app needs a controlled way to start them. Some deployments mount the Docker socket; others SSH back to the Docker host. Either way, treat that permission as privileged operator access, not a normal web-app permission.
Database initialization and migrations
On a fresh install, PostgreSQL can load the initial schema automatically when the data volume is empty:
1
2
volumes:
- ./app/sql/schema.sql:/docker-entrypoint-initdb.d/001-schema.sql:ro
That only runs on first database creation. If the database volume already exists, apply schema changes intentionally:
1
docker compose exec -T postgres psql -U watchparty -d watchparty < app/sql/migrations.sql
Before applying migrations:
1
docker compose exec postgres pg_dump -U watchparty watchparty > backups/watchparty-before-migration.sql
For a small homelab, this simple workflow is usually enough:
- back up PostgreSQL;
- rebuild the WatchParty image;
- apply any SQL migrations;
- restart the app;
- verify login, room creation, and playback sync.
Firebase Auth setup
In Firebase Authentication:
- Add your public WatchParty domain as an authorized domain.
- Enable the sign-in methods you want, for example:
- Email/password
- GitHub
- For GitHub OAuth, configure the callback URL shown by Firebase:
1
https://<project>.firebaseapp.com/__/auth/handler
In the frontend, redirect-based OAuth is usually more reliable than popup-based OAuth when the app runs behind reverse proxies, strict browsers, or mobile webviews.
Use Firebase custom claims only if the app actually needs global roles. Many room-based apps can avoid classic admin/user roles and instead rely on:
- authenticated Firebase UID;
- room owner permissions;
- per-room membership;
- optional subscription flags.
Optional VBrowser launch model
The VBrowser container should be disposable. A good launch flow is:
- Remove stale containers with the VBrowser label.
- Generate a fresh browser password.
- Generate a Docker-safe container name.
- Start Neko/Chromium with one mux port.
- Add Traefik labels for the
/vb-0route. - Return the container ID to WatchParty so it can terminate the session later.
A safe name format is:
1
vbrowser-YYYYMMDD-HHMMSS-randomid
Example:
1
vbrowser-20260513-121500-a1b2c3d4
Avoid / and : in Docker names. They are useful in human timestamps, but invalid for container names.
Optional Headscale-only VBrowser WebRTC
Neko can use a single mux port for WebRTC instead of a wide range of dynamic ports.
Example environment for the VBrowser container:
NEKO_WEBRTC_UDPMUX=59000
NEKO_WEBRTC_TCPMUX=59000
NEKO_WEBRTC_NAT1TO1=<server-tailnet-ip>
The NAT candidate should be the server’s private tailnet IP, not the public internet address, when the goal is Headscale-only access.
That gives a clean rule:
1
Only devices connected to the tailnet can complete the VBrowser media path.
This is especially useful when you do not want to open UDP/TCP WebRTC ports on the home router.
Optional Traefik route for VBrowser
The VBrowser web UI can still route through public HTTPS under a path prefix:
1
2
3
4
5
6
7
8
9
10
11
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.vbrowser-0.entrypoints=https"
- "traefik.http.routers.vbrowser-0.priority=100"
- "traefik.http.routers.vbrowser-0.rule=Host(`watch.example.com`) && PathPrefix(`/vb-0`)"
- "traefik.http.routers.vbrowser-0.middlewares=vbrowser-0-stripprefix"
- "traefik.http.middlewares.vbrowser-0-stripprefix.stripprefix.prefixes=/vb-0"
- "traefik.http.routers.vbrowser-0.tls=true"
- "traefik.http.routers.vbrowser-0.tls.certresolver=cloudflare"
- "traefik.http.services.vbrowser-0.loadbalancer.server.port=8080"
The HTTPS route loads the VBrowser interface. The WebRTC media connection still uses the private mux candidate.
VBrowser cleanup policy
VBrowser containers should not become permanent pets. They should be cattle:
- remove stale VBrowser-labeled containers before launching a new one;
- stop the container when the room closes;
- stop the container when the room has been empty and idle for a short grace period;
- keep a hard maximum session duration;
- use
--rmon the VBrowserdocker runcommand when possible.
Useful operator checks:
1
docker ps -a --filter label=vbrowserUS
Remove stale VBrowser containers safely by label:
1
docker ps -aq --filter label=vbrowserUS | xargs -r docker rm -f
Be careful with broad Docker cleanup commands. Removing by a dedicated label is much safer than deleting every stopped container on the host.
Deployment steps
Create the service directory and clone the app:
1
2
3
4
mkdir -p /home/ubuntu/watchparty/secrets
cd /home/ubuntu/watchparty
git clone https://github.com/howardchung/watchparty.git app
If you already cloned it, update or inspect it instead:
1
2
3
4
cd /home/ubuntu/watchparty/app
git status
git pull --ff-only
cd ..
Create private config:
1
2
nano .env
chmod 600 .env
Add the Firebase Admin SDK JSON:
1
2
nano secrets/firebase-adminsdk.json
chmod 600 secrets/firebase-adminsdk.json
Build and start from the service root:
1
2
3
4
cd /home/ubuntu/watchparty
docker compose build watchparty
docker compose up -d
This works because the Compose file uses build.context: ./app, so Docker builds the cloned WatchParty repository.
Watch logs during the first boot:
1
docker compose logs -f watchparty
Operations runbook
Useful daily commands:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Service status
docker compose ps
# App logs
docker compose logs --tail=200 watchparty
# Database logs
docker compose logs --tail=100 postgres
# Redis health
docker compose exec redis redis-cli ping
# Public HTTP check
curl -I https://watch.example.com
Inspect persistent rooms:
1
docker compose exec postgres psql -U watchparty -d watchparty -c 'select "roomId", vanity, owner, "lastUpdateTime" from room order by "lastUpdateTime" desc limit 10;'
Inspect active VBrowser assignments:
1
docker compose exec postgres psql -U watchparty -d watchparty -c 'select id, pool, vmid, state, "roomId", "assignTime" from vbrowser order by id desc limit 10;'
Clean only VBrowser containers, never the whole Docker host:
1
docker ps -aq --filter label=vbrowserUS | xargs -r docker rm -f
Upgrade workflow:
1
2
3
4
5
6
7
8
mkdir -p backups
docker compose exec postgres pg_dump -U watchparty watchparty > backups/watchparty-$(date -u +%Y%m%d-%H%M%S).sql
docker compose build watchparty
docker compose up -d
docker compose logs --tail=100 watchparty
Rollback is just as important as upgrade:
1
2
3
4
# Example rollback idea: use the previous image tag or restore from backup.
# Exact commands depend on how you tag builds in your own deployment.
docker compose down
docker compose up -d
For real production, tag images instead of relying only on latest.
Verification checklist
Verify the app container is healthy:
1
docker compose ps
Verify the public route:
1
curl -I https://watch.example.com
Verify Firebase login and core room behavior in a real browser:
1
2
3
4
5
6
7
8
Open https://watch.example.com
Sign in with an enabled provider
Create a room
Open the room in a second browser/device
Test chat
Test playlist add/remove
Test play, pause, seek, and timestamp sync
Test room owner settings or lock behavior if enabled
Verify persistence after restart:
1
docker compose restart watchparty
Then reopen a persistent room and confirm metadata, owner settings, and playlist behavior are still correct.
If you use VBrowser, verify the server has a tailnet IP:
1
tailscale ip -4
Verify a VBrowser session creates a readable container name:
1
2
3
docker ps --filter label=vbrowserUS --format 'table {{.Names}} {{.Status}} {{.Ports}}'
Expected name pattern:
1
vbrowser-20260513-121500-a1b2c3d4
Verify cleanup after stopping or leaving the room:
1
docker ps -a --filter label=vbrowserUS
The list should become empty after the app’s cleanup grace period.
Backup checklist
Back up:
1
2
3
4
5
6
/home/ubuntu/watchparty/docker-compose.yml
/home/ubuntu/watchparty/.env # private backup only
/home/ubuntu/watchparty/secrets/firebase-adminsdk.json # private backup only
/home/ubuntu/watchparty/app/ # if locally patched
PostgreSQL data or dumps
Optional uploaded/media directories, if configured
Create database dumps in addition to volume backups:
1
docker compose exec postgres pg_dump -U watchparty watchparty > watchparty.sql
Do not publish database dumps. They can contain user accounts, room history, tokens, and application state.
Monitoring and maintenance
Minimum checks worth automating:
| Check | Why it matters | Example |
|---|---|---|
| HTTPS route | Confirms Traefik, DNS, and app are reachable | curl -I https://watch.example.com |
| App healthcheck | Confirms Node server responds | docker compose ps watchparty |
| PostgreSQL backup age | Prevents silent data-loss risk | check newest file in backups/ |
| Redis health | Catches runtime coordination issues | redis-cli ping |
| VBrowser leftovers | Keeps the host clean | docker ps -a --filter label=vbrowserUS |
| Disk usage | Video/browser workloads can grow logs fast | df -h and Docker log limits |
Suggested maintenance rhythm:
- daily: check containers are healthy;
- weekly: verify a database dump can be created;
- weekly: remove stale VBrowser containers by label if any exist;
- monthly: test restore in a temporary database;
- after every upgrade: create a room, test chat, playlist, playback sync, login, and VBrowser if enabled.
Common problems
Login works but room ownership does not
Check that the frontend sends a valid Firebase ID token and the server verifies it with the Firebase Admin SDK. Also confirm the Firebase project ID in the frontend config matches the Admin SDK project.
Docker build fails because ./app does not exist
The WatchParty source repository was not cloned into the expected path. From the service root, run:
1
2
cd /home/ubuntu/watchparty
git clone https://github.com/howardchung/watchparty.git app
Then rebuild:
1
docker compose build watchparty
Rooms disappear after restart
Room persistence is missing or the database schema was not initialized. Check DATABASE_URL, PostgreSQL connectivity, and migrations/schema setup.
Playback is not synchronized
Check Socket.IO connectivity in the browser devtools network tab. If the socket disconnects or falls back badly through the proxy, inspect Traefik websocket headers and the WatchParty server logs.
Chat, roster, or playlist updates lag
Check Redis and server load first. Then inspect whether the app container has enough CPU/memory and whether reverse-proxy websocket timeouts are too aggressive.
YouTube search does not work
The basic service can still play direct media without YouTube search. If search is required, verify YOUTUBE_API_KEY, API enablement, quotas, and restrictions in Google Cloud.
Screen sharing or camera does not work
These browser APIs require HTTPS and user permission. Verify the page is loaded over HTTPS and that browser/site permissions are not blocked.
VBrowser page loads but the stream does not connect
Check that the client is connected to the Headscale/Tailscale network and can reach the server tailnet IP:
1
ping <server-tailnet-ip>
Then check that the mux port is reachable from a tailnet client. If it is not, inspect host firewall rules and container port publishing.
Browser container starts with an ugly random Docker name
Give the VBrowser launch command an explicit --name value:
1
--name="vbrowser-$(date -u +%Y%m%d-%H%M%S)-$RANDOM_ID"
Do not use / or : in the name.
Stale VBrowser containers block new sessions
Make the launch path remove old containers with the VBrowser label before starting a new one:
1
docker ps -aq --filter label=vbrowserUS | xargs -r docker rm -f
Also confirm that normal room shutdown calls the app’s VM release or terminate function.
OAuth login fails after redirect
Check:
- Firebase authorized domains;
- OAuth provider callback URL;
- frontend Firebase config;
- browser console redirect errors;
- whether the app uses redirect login when popups are blocked.
Traefik returns 404 under /vb-0
Check:
- the router rule includes
PathPrefix("/vb-0"); - the strip-prefix middleware is attached;
- the VBrowser container is on the
proxyDocker network; - the router priority is higher than the main WatchParty route if needed.
Security notes
- Protect the main WatchParty route with the app’s native auth model, not a reverse-proxy wall that breaks login callbacks.
- Keep Firebase Admin SDK credentials private and mode
600. - Do not expose PostgreSQL, Redis, or internal admin APIs directly to the internet.
- Do not publish the Docker socket through a web API.
- Enable optional integrations only when you actually use them.
- Prefer Headscale-only VBrowser WebRTC when you do not want public media ports.
- Use labels for targeted VBrowser cleanup instead of broad host-wide cleanup.
- Keep public documentation generic: use
watch.example.com,<server-tailnet-ip>, and<secret>placeholders. - If you expose any WebRTC ports publicly, document the firewall rule and why it is necessary.
Service documentation map
This post connects to the rest of the homelab stack:
- Traefik reverse proxy for HTTPS routing and path-prefix rules.
- Headscale control server for the private tailnet used by VBrowser WebRTC.
- Private Pi-hole DNS over Headscale for private DNS patterns on tailnet clients.
- Authentik SSO if you want extra protection around admin-only tools.
- Cloudflare Companion if DNS records are created from Traefik labels.
- Portainer Docker management for inspecting containers and labels.
- Jekyll documentation site for publishing guides like this one.
- Pterodactyl Panel and Wings for private game hosting with public Panel access and Headscale-only Wings/game ports.