This project runs a virtual monitor (Xvfb), launches Chromium in kiosk mode on that virtual display, captures video + webpage audio, and streams it to YouTube Live via RTMPS.
docker-compose.yml— service definition and environment variablesDockerfile— container build instructionsstart.sh— starts Xvfb + PulseAudio + Chromium + FFmpeglogin.mjs— optional Puppeteer automation for username/password login
- Docker Engine
- Docker Compose plugin (
docker compose) - A YouTube Live stream key
If Docker is not installed, follow the official instructions for your OS:
- Docker Engine: https://docs.docker.com/engine/install/
- Docker Compose plugin: https://docs.docker.com/compose/install/
Use the official Docker APT repository to install both Docker Engine and the Compose plugin.
- Supported 64-bit Ubuntu versions: Jammy 22.04 (LTS), Noble 24.04 (LTS), Plucky 25.04, Questing 25.10
- Supported architectures:
x86_64 (amd64),armhf,arm64,s390x,ppc64le - Firewall note: Docker bypasses
ufw/firewalldfor published ports and is only compatible withiptables-nftoriptables-legacy. Use the DOCKER-USER chain for custom rules.
sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc | cut -f1)This may report that none are installed, which is fine. Existing Docker data in /var/lib/docker/ is not removed.
- Add Docker’s official GPG key:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc- Add the Docker APT repository:
sudo tee /etc/apt/sources.list.d/docker.sources <<'EOF'
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update- Install Docker packages:
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginOptional: install a specific version instead of latest:
apt list --all-versions docker-ce
VERSION_STRING=5:29.1.3-1~ubuntu.24.04~noble
sudo apt install docker-ce=$VERSION_STRING docker-ce-cli=$VERSION_STRING containerd.io docker-buildx-plugin docker-compose-pluginsudo systemctl status dockerIf not running:
sudo systemctl start dockerTest Docker Engine:
sudo docker run hello-worldTest Docker Compose:
docker compose versionsudo groupadd docker
sudo usermod -aG docker $USERLog out/in (or run newgrp docker) to apply group changes, then test:
docker run hello-world-
Clone or unzip this folder on the target machine.
-
Edit
docker-compose.ymland set:WEB_URLto the webpage you want to streamYT_STREAM_KEYto your YouTube Live stream key
-
Build and start the container:
docker compose up -d --build
-
Watch logs:
docker logs -f web2yt
-
Stop the stream:
docker compose down
You can adjust streaming quality or display settings via environment variables:
| Variable | Description | Default |
|---|---|---|
WEB_URL |
Webpage to render and stream | https://your-webpage.example.com |
YT_RTMPS_URL |
YouTube ingest URL | rtmps://a.rtmps.youtube.com/live2 |
YT_STREAM_KEY |
YouTube stream key | xxxx-xxxx-xxxx-xxxx |
WIDTH |
Capture width in pixels | 1920 |
HEIGHT |
Capture height in pixels | 1080 |
FPS |
Frames per second | 30 |
CHROME_BIN |
Chromium executable inside container | google-chrome |
VIDEO_BITRATE |
FFmpeg video bitrate | 6500k |
VIDEO_MAXRATE |
FFmpeg max bitrate | 7500k |
VIDEO_BUFSIZE |
FFmpeg buffer size | 13000k |
AUDIO_BITRATE |
FFmpeg audio bitrate | 160k |
LOGIN_USER |
Optional username for Puppeteer login | unset |
LOGIN_PASS |
Optional password for Puppeteer login | unset |
LOGIN_USER_SELECTOR |
CSS selector for username input | input[placeholder="User ID"], input[autocomplete="username"] |
LOGIN_PASS_SELECTOR |
CSS selector for password input | input[placeholder="Password"], input[autocomplete="current-password"] |
LOGIN_SUBMIT_SELECTOR |
CSS selector for submit button | button[type="submit"] |
LOGIN_COOKIE_ACCEPT_SELECTOR |
Optional CSS selector for cookie-consent accept button | unset |
LOGIN_SUCCESS_SELECTOR |
Optional selector that indicates login success | unset |
CHROME_DEBUG_PORT |
Chromium remote debugging port for Puppeteer | 9222 |
LOGIN_TIMEOUT_MS |
Login timeout in milliseconds | 30000 |
LOGIN_DEBUG |
Enable verbose login automation logs (1/true) |
0 |
If your page requires credentials, set LOGIN_USER and LOGIN_PASS in docker-compose.yml.
When both are set, startup runs Puppeteer against the already-open Chromium session to:
- wait for the login form
- type username/password
- accept a cookie banner when detected (or via an explicit selector)
- press Enter
You can override selectors if your login form changes:
LOGIN_USER_SELECTORLOGIN_PASS_SELECTORLOGIN_SUBMIT_SELECTORLOGIN_COOKIE_ACCEPT_SELECTOR(optional explicit cookie-accept button)LOGIN_SUCCESS_SELECTOR(optional explicit post-login signal)
Security: avoid committing production credentials into source control.
A lightweight local harness is included to validate login automation behavior without starting the full YouTube streaming stack.
It exercises five fixture scenarios:
- successful login in main DOM
- delayed render of fields
- iframe login form
- wrong selector failure (and verifies debug artifacts are written)
- cookie banner required
Run it from this repository:
LOGIN_SCRIPT=./login.mjs scripts/test-login.shBy default, the script expects a Chrome-compatible binary named google-chrome and the production path /app/login.mjs used inside the container image.
For local runs, set LOGIN_SCRIPT=./login.mjs as shown above.
Useful overrides:
CHROME_BIN=google-chrome-stable LOGIN_TIMEOUT_MS=12000 LOGIN_DEBUG=1 LOGIN_SCRIPT=./login.mjs scripts/test-login.shOn expected failure scenarios, artifacts must exist at /tmp/login-debug/failure.png and /tmp/login-debug/failure.html.
Default settings are tuned for mixed motion UI + voice:
VIDEO_BITRATE=6500k,VIDEO_MAXRATE=7500k,VIDEO_BUFSIZE=13000kAUDIO_BITRATE=160k
If CPU is high, try lowering to ~4500k and/or using -preset superfast (in start.sh).
If you have an NVIDIA GPU and want NVENC, replace the FFmpeg encode line in start.sh.
- Ensure
shm_size: "1g"is present (it is by default). - Try lowering resolution or FPS temporarily to debug.
If you see repeated lines like Failed to connect to socket /run/dbus/system_bus_socket, the container is missing a running DBus system bus.
Recent images start a lightweight system bus automatically before launching Chromium to reduce this log noise. If you still see it, rebuild and recreate the container so the latest image/script is running:
docker compose build --no-cache
docker compose up -d --force-recreate- Ensure the webpage is actually playing audio (not muted).
- Inside the container:
pactl list short sinks pactl list short sources | grep virtSink.monitor
If WEB_URL points to a hostname that only resolves on your host machine (for example http://boondock-test:4000), Chromium inside the container may not resolve or reach it.
Check from inside the running container:
docker exec -it web2yt getent hosts boondock-test
docker exec -it web2yt curl -I http://boondock-test:4000If those fail, use one of these options:
- Use
http://host.docker.internal:4000and map host gateway indocker-compose.yml:extra_hosts: - "host.docker.internal:host-gateway"
- Or publish your local service on an address reachable from the container network.
This means the container is invoking Ubuntu's snap wrapper (chromium-browser) instead of a real Chromium binary.
Use the bundled google-chrome-stable package in the image. You can also set an explicit binary path/name with:
environment:
CHROME_BIN: "google-chrome"The startup script now fails fast if Chromium exits immediately, so you will see an explicit error in logs instead of streaming a blank screen.
This indicates Puppeteer could not reach Chromium's DevTools endpoint in time.
Checks:
- Confirm Chromium is still running in the container (
docker exec -it web2yt pgrep -a chrome). - Ensure no conflicting
DBUS_SESSION_BUS_ADDRESSis injected from the environment. Invalid values can produce repeated DBus parse errors and unstable Chromium startup. - Increase
LOGIN_TIMEOUT_MSfor slower pages/hosts.
Recent startup logic now waits for http://127.0.0.1:${CHROME_DEBUG_PORT}/json/version before attempting login, and logs a warning if the endpoint never becomes reachable.
- Confirm Xvfb is running and
DISPLAY=:99:xdpyinfo -display :99 | head
Your YouTube stream key is sensitive. Keep docker-compose.yml private.