Skip to content

About

Self-host Tawny: the container that lets any browser on your network be a viewer, plus the Cloudflare Worker that introduces two phones over the internet. No account, no video stored.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Note

Built with AI. Tawny, including this container, is made by its maintainer working with Claude, an AI model made by Anthropic. Most of the code, documentation and artwork here was written with Claude, under the maintainer's direction. Some people avoid AI-built software for ethical, political, professional or personal reasons, so you should know that before you install, run or contribute.

πŸ¦‰ Tawny Docker

Run your own Tawny β€” the two-way pet monitor β€” as a container on a box at home. No account, no domain, no port forwarding, no certificates. It runs over your Tailscale.

A pet monitor with no paywalls. No subscription, no locked features, no ads, nothing hosted by anyone else. You point an old phone at the pet and watch from a browser; the only thing that ever sees the stream is you.

Your pet, live on your phone Set it up from a browser: docker compose, then the /setup checklist See, hear and talk back Watch from a laptop browser, nothing to install

A browser can be the Monitor too Dim the screen, keep listening Talk back and ring a chime from a phone browser Runs on your own box: the compose file

The /setup page, laptop and phone-browser shots are captures of this repo's server in a real browser (tailnet and device names replaced with placeholders). The other three show the Android app, which uses the same web client.

Tawny Pet Monitor β€” the Android app repo
The Monitor phone runs the Tawny Android app β€” source, releases and the security model live there.

πŸ€— Supporting it

Support me on Ko-fi Tip in Bitcoin

Tawny is free: no ads, no account, no paywall, nothing locked. Chipping in unlocks nothing β€” every feature is there for everyone either way. These are the same two buttons the app shows; anything else asking for money in Tawny's name isn't us.


What this is mainly for: a bridge. The Monitor phone runs the Tawny Android app, pointed at your pet. Whoever's watching doesn't need the app at all β€” they open the https://…ts.net address this container publishes, in a plain browser, on anything signed into your tailnet: a laptop, someone else's phone, a shared family tablet. No app store, no install, no account, for them. That's the point of running this at all β€” the app download that's easy for you is a wall for a grandparent, a house-sitter, or a phone you don't control.

It doesn't stop there, though. Both ends can just as well be this same web page β€” open it on two phones, pick Monitor on one and Viewer on the other, and it works exactly the same way with nothing installed on either. Use whichever mix fits the room: app on the Monitor and a browser Viewer (the common case), or browser on both. The container serves either role and joins your tailnet so the two ends can find each other; the video and audio go straight between them and never pass through it.

The image is pulled from ghcr.io/pressf4me/tawny β€” nothing to build, and the only file you need from this repo is docker-compose.yml.


πŸ‘· Setup

Once. After this, watching is: open the app on the phone, open a URL on the Viewer, scan.

1. Auth key. At https://login.tailscale.com/admin/settings/keys β†’ generate one, Reusable, Ephemeral off.

2. HTTPS in your tailnet. At https://login.tailscale.com/admin/dns enable MagicDNS, then HTTPS Certificates. Account-wide, done once, off by default on a new tailnet. Without it tailscale serve can't publish an https://…ts.net address, and talk-back (a mic needs HTTPS) can't work. Already on if you've used tailscale serve or Funnel before.

3. Start it.

echo 'TS_AUTHKEY=tskey-auth-xxxxxxxxxxxx' > .env
docker compose up -d

Pin a version with TAWNY_TAG=2.0.3 in .env instead of tracking latest; upgrade later with docker compose pull && docker compose up -d. On start the container joins your tailnet as node tawny, detects its LAN, and publishes the app; docker logs tawny prints the URL. Advertising that LAN into the tailnet β€” what lets you watch from outside the house β€” is off until you say so: /setup asks, once, and remembers the answer.

4. Approve the subnet route, and disable key expiry. Open http://<box>:8099/setup from the LAN β€” it shows live status and names the exact next step. Until the route is approved at https://login.tailscale.com/admin/machines β†’ tawny β†’ Edit route settings β†’ tick the subnet, a remote Viewer can't reach the phone. While you're in that same β‹― menu, also choose Disable key expiry β€” Tawny runs unattended, and without this Tailscale logs it out roughly every 180 days, silently breaking remote access until someone notices and pastes a fresh key in /setup or .env. If /setup says HTTPS isn't on, step 2 was skipped β€” turn it on; Tawny retries tailscale serve every few seconds while /setup is open, so no restart is needed.

5. Put your devices on the tailnet. Every viewing device and the Monitor phone need Tailscale, signed into the same tailnet, with subnet routes accepted (Linux: tailscale up --accept-routes). Nothing else on the Monitor phone changes β€” leave Tawny's Servers screen blank.

Setup survives restarts and DHCP changes: identity lives in the tawny-data volume, the route follows the LAN.


πŸ‘€ Watching

  1. Monitor phone β€” open Tawny, choose Monitor, allow the camera, let it show its pairing code. Servers screen stays blank.
  2. Viewing device β€” open the https://…ts.net URL (from /setup or docker logs tawny), not http://<box>:8099 β€” browsers only give a mic over HTTPS, so the plain address silently kills talk-back.
  3. Choose Viewer, Scan the phone's QR (or paste the copied pairing link), Join.

Video, audio, and hold-to-talk. Watch from anywhere the Viewer device has Tailscale.

Recommended: give the Monitor phone a DHCP reservation. Its address is baked into each pairing code; a lease change breaks the code and forces a rescan.


βœ… Checking it

bash probe.sh

Builds the image and exercises the real transports β€” signalling, the bridge to the phone's relay, an ICE packet exchange, the built-in relay, route approval. Two caveats: it reads the host's Tailscale state, so under the TS_AUTHKEY path it prints "not installed" for the route check (use /setup instead), and it certifies the image, not a running deployment. Stop the running container first so the relay check can bind port 3478.


🌐 Ports

Port What
8099 the app, plain HTTP. tailscale serve terminates TLS in front of it and reaches it on localhost; on your LAN it is where /setup lives.
3478, 49160-49200/udp the built-in relay β€” fallback for a network that blocks direct UDP. Nothing to forward.

No HTTPS port on the container. TLS is Tailscale's.


☠️ If something is wrong

Open http://<box>:8099/setup first β€” it diagnoses most of this and names the next step.

Symptom Cause / fix
Container never came up, /healthz dead docker logs tawny; docker ps -a if no output at all.
/setup: tailscale up FAILED / key rejected Auth key expired, already used (not Reusable), or wrong tailnet. Fresh key in .env, docker compose restart.
/setup: "leftover identity β€” needs a hand" (or a join that hangs then fails despite a good key) A Tailscale identity from an earlier run is stuck in tawny-data and the coordination server won't take it back. Tawny normally clears it automatically; if it can't, press Reset Tailscale identity on /setup, or docker exec <container> rm -rf /data/tailscale and restart. The old state is kept at /data/tailscale.broken-….
https://tawny.<tailnet>.ts.net won't open, or /setup: serve FAILED re HTTPS/MagicDNS Step 2 skipped. Enable MagicDNS then HTTPS Certificates, and keep /setup open β€” it retries and publishes within seconds, no restart.
Address opens on the box but not on your laptop/phone That device also needs Tailscale up with MagicDNS on. On the box's LAN, http://<box>:8099 works (no talk-back).
Viewer loads, "Join" hangs, no video Subnet route not approved yet (/setup confirms). If approved, check the Viewer has --accept-routes.
"Pairing code expired" Phone unreachable β€” usually its IP changed: show a fresh code. Confirm it's on the same Wi-Fi as the box, not a guest VLAN (that blocks the phone's relay).
/setup: LAN looks like a Docker bridge (172.16–172.31, br-) Auto-detect failed. Set TS_ROUTES=192.168.1.0/24 (your real LAN) in .env, docker compose up -d.
Talk-back dead, watching fine You opened http://…:8099 instead of the https://…ts.net URL.
Whole internet starts stalling/looping after setup Another device already advertises this LAN into the tailnet (NAS, Pi-hole, old Tawny box). Two subnet routers for one range is unsupported. Tawny won't advertise a conflicting route on its own β€” /setup names the other device and offers "stop advertising" / "advertise anyway".

😸 No Tailscale?

The app still serves on the LAN at http://<box>:8099 and two devices on that Wi-Fi can pair and watch β€” but no talk-back (needs HTTPS) and no way in from outside. Tailscale is how this is meant to run; LAN-only is a fallback.


πŸ€“ For tighter privacy [advanced]

Everything above leans on Tailscale: its coordination server, its ts.net certificate, and public STUN from Google and Cloudflare, with fallbacks that keep a session alive when something is misconfigured. If you want none of that, open /setup, scroll to For tighter privacy [advanced] and turn it on. It asks you to acknowledge the risks first, then lets you set every piece yourself:

Choices
Network Tawny's own Tailscale node Β· this machine's Tailscale Β· no Tailscale (bring your own VPN, WireGuard, port forward…) Β· a Headscale login server Β· Tailscale log upload on/off (off by default)
HTTPS tailscale serve Β· your own certificate files (Tawny serves HTTPS itself and reloads renewals) Β· your own reverse proxy Β· none
Signalling this server or an external wss:// rendezvous Β· the LAN bridge on/off Β· allowed hostnames
STUN your servers, or none (blank means none, never the public list)
TURN when needed / always (nobody learns anybody's IP) / never Β· built-in coturn with its ports, public IP and optional TLS Β· or your own TURN servers

Settings live in /data/privacy.json and, while this mode is on, override the compose file's environment. Restart and apply restarts the container in place. Nothing falls back: a wrong setting fails instead of being covered by a default, and a settings file that can't be read starts with everything networked turned off. http://<box>:8099/setup on your LAN always stays reachable to undo it. The Android app's Servers screen has a matching switch for the phone side, and it only trusts certificates from public CAs.


✌️ Two ways to self-host

This container is the bridge: the app as the Monitor, and a browser Viewer β€” any device, no install β€” over your Tailscale. It works the other way and both-ways too; either role can be a browser, either can be the app.

For phone-to-phone remote pairing without this container at all β€” both ends the Tawny app, on different networks β€” see app/rendezvous/ in this repo: a Cloudflare Worker / Deno signalling introducer (the one the stock app uses). The two paths are independent and can run together.


🫡 Building it yourself

The published image is the supported path. To build locally β€” a fork, an unreleased change, an air-gapped registry:

bash tools/tawny-sync        # refresh the web client in app/public/ from ../Tawny Android
                             #   (override the source with TAWNY_ANDROID=/path)
docker build --network=host -t ghcr.io/pressf4me/tawny:latest .
docker compose up -d         # picks up the local image (pull_policy: missing)

app/public/ is a snapshot of the web client from the Tawny app repo (the same bundle that ships inside the APK); tools/tawny-sync refreshes only that folder and writes the source commit to app/.source-commit. Everything else in app/ β€” server.js, docker/, rendezvous/, package*.json β€” lives in this repo. --network=host matters only if your Docker bridge can't reach the internet during apk add (a Tailscale/Mullvad nftables symptom).

Releases build from a vX.Y.Z tag via .github/workflows/release.yml β€” linux/amd64 + linux/arm64, with provenance.


License

MIT β€” see LICENSE. Use it, fork it, ship it; keep the copyright notice. The owlet mascot and the Tawny name are the one exception: they are the app's identity on the Play Store, so please use your own if you publish a fork.

Design

DESIGN.md β€” why it's shaped this way, what the shipped Android app can and can't be made to do, and what was measured.

About

Self-host Tawny: the container that lets any browser on your network be a viewer, plus the Cloudflare Worker that introduces two phones over the internet. No account, no video stored.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages