All apps · 0 apps
nightlight
Docker app from sauso's Repository
Overview
Readme
View on GitHubNightlight — self-hosted baby monitor
A mobile-friendly web app for watching multiple RTSP cameras, grouped by child, over your home network. Live video uses WebRTC (via MediaMTX) for sub-second latency — much lower than a typical HLS-based viewer. Use it in any browser, install it to your home screen as a PWA, or run the companion native apps for Android and iOS (nightlight-mobile) for reliable background audio and picture-in-picture (see Mobile apps below). No cloud, no subscription, no account anywhere but your own network.
⚠️ Not a safety device. Nightlight is a convenience tool, not a safety device. It is not a medical device, is not certified for safety monitoring of any kind, and must never be used as a substitute for adult supervision. Streams can drop, apps can be killed by the operating system, networks fail — never rely on this software to alert you to a child in distress.
Screenshots
See the visual walkthrough for a tour of the main screens (sign-in, the nursery dashboard, adding a camera, and settings). Those images are generated automatically by the end-to-end test suite, so they stay in sync with the actual UI.
How it works
- FFmpeg pulls each camera's RTSP stream, copies the video through untouched, and transcodes just the audio to AAC (many IP cameras send audio as G711, a codec HLS can't carry at all — WebRTC can, which is why this only matters for Compatibility/HLS mode). The result is published into MediaMTX.
- MediaMTX re-publishes that as WebRTC (WHEP) and HLS, which browsers can play natively — RTSP itself cannot be played in a browser, so this bridge is required either way.
- Backend (Node/Express + SQLite) stores children, cameras, and caregiver accounts, and manages both MediaMTX and one FFmpeg process per camera as child processes — starting them, restarting on crash, and stopping them when a camera's removed.
- Frontend (React) is a mobile-first, installable app: a live dashboard grouped by child, plus screens to manage children, cameras, and caregiver accounts. Cameras can be added by IP over ONVIF (auto-filling the stream details), and PTZ (pan/tilt) cameras get on-screen controls — see Managing cameras.
Everything above runs in a single Docker container, which needs its own routable IP on your LAN so WebRTC connects without NAT/ICE headaches (MediaMTX advertises that IP to browsers). The simple default is host networking — the container shares the host's IP — used by the quick start and the Unraid template. If host mode doesn't suit you (e.g. a port clash with another service), you can instead give the container its own dedicated LAN IP; see Networking modes below.
This is designed for local network use by default — see "Remote / internet access" below if you want it reachable from outside your home too.
Quick start
Pull and run directly from Docker Hub:
docker run -d \
--name nightlight \
--network host \
--restart unless-stopped \
--log-opt max-size=10m \
--log-opt max-file=3 \
--stop-timeout 30 \
-e PUID=99 \
-e PGID=100 \
-e TZ=UTC \
-v /path/to/your/data:/app/data \
sauso/nightlight:latest
Nightlight records video three ways — automatic clips attached to motion/sound alerts (opt-in per
camera), wake clips saved silently when your child wakes up, and on-demand recordings you
capture with the Record button. They have different retention rules, so see
docs/recording.md for all of it and for every setting under Settings →
Recording. Video defaults to <data dir>/clips; you can point it at your array instead with a
/recordings mount + CLIPS_DIR.
PUID/PGID control which user/group owns files this container creates in your data
directory - the defaults above (99/100) match Unraid's own "nobody"/"users" convention,
so they're usually already correct there. On another system, find your own with
id your_username. TZ (e.g. Australia/Melbourne) affects log timestamps only -
full list of values.
The --log-opt flags cap Docker's own log storage at 10MB × 3 files - without them,
logs default to growing unbounded, which can be a real problem on Unraid specifically
since Docker's storage there is a fixed-size image that can break the whole Docker
service if it fills up.
--stop-timeout 30 gives Nightlight enough time to stop cleanly. It needs a few seconds
on the way down — an on-demand recording is assembled from the buffer at that point, and
being killed part-way through loses it. Stopping normally takes 1-5 seconds, so 30 is
deliberately generous — and costs nothing, because the container exits as soon as it's
finished rather than waiting out the timeout. Don't rely on Docker's own default here:
recent versions don't document one, and it has been measured killing the container after
about 4 seconds, which was enough to lose the recording. If you deployed before this
was added, add --stop-timeout 30 yourself (or stop_grace_period: 30s under the
service in Compose, or the Unraid template's "Extra Parameters" field). Nothing else breaks
without it — a recording lost this way is marked failed rather than disappearing silently.
Or with Docker Compose:
cp .env.example .env
# optionally edit .env - see comments in the file; the defaults are fine to start
docker compose up -d
Then, from any phone/laptop on the same network, visit http://<server-ip>:4000. The
first time you visit, you'll be asked to create the admin account — do this first, from a
trusted device. Then add your children (Children tab), add your cameras (Cameras tab — by
IP via ONVIF, or by RTSP details; see Managing cameras), and assign
cameras to children.
Requirements
- Any always-on Linux box on the same network as your cameras (a Raspberry Pi, an Unraid
server, anything running Docker). Both
amd64andarm64are supported. - Cameras that expose an RTSP stream (almost all "dumb" IP cameras and most smart cameras
with a local RTSP option do — check the camera's manual for the RTSP path, usually
something like
/stream1). - ONVIF is optional but handy: if a camera supports it, Nightlight can fetch the stream details from just its IP, and enable pan/tilt controls on cameras that report PTZ.
Running on Unraid
An Unraid Community Applications template is included (unraid-template.xml). Until this
is submitted to the official CA feed (at which point it'll be searchable directly from the
Apps tab), install it locally by placing the file where Unraid looks for user templates:
- Open the Unraid Terminal (or SSH in), then run:
mkdir -p /boot/config/plugins/dockerMan/templates-user wget -O /boot/config/plugins/dockerMan/templates-user/my-nightlight.xml \ https://raw.githubusercontent.com/sauso/nightlight/main/unraid-template.xml - Docker tab → Add Container → Template dropdown → select nightlight. Every
field (network mode, data path, optional variables) is pre-filled from the template —
double check the Data Directory path if you want something other than the default
(
/mnt/user/appdata/nightlight), then Apply.
This is a single container — no extra plugins needed, Unraid's normal Docker UI handles it directly.
Already installed from an older copy of the template? Check that Extra Parameters
(Advanced view, on the container's edit page) contains --stop-timeout 30, and add it if
not. Unraid builds the container from the saved template, so a template downloaded before
this was added keeps the old value until you edit it — and without it a recording that was
in progress when the container restarts can be cut short. See the note under
Quick start for what it does.
Networking modes
Nightlight needs its own routable IP on your LAN so WebRTC has no NAT/ICE trouble — MediaMTX advertises that IP to browsers. There are two supported ways to provide one:
- Host networking (default, simplest). The container shares the host's IP (
--network host, or<Network>host</Network>in the Unraid template). Nothing to configure — it's what the quick start and the template use. The trade-off is that it binds its ports (4000, plus MediaMTX's) directly on the host, which clashes if something else there already uses them. - A dedicated LAN IP (ipvlan / macvlan). Put the container on a Docker
ipvlan/macvlannetwork so it gets its own address on your LAN, separate from the host. Nightlight auto-detects the container's IP and has MediaMTX advertise it, so WebRTC works on the LAN with no extra configuration (noPUBLIC_HOSTneeded for local viewing). Use this to avoid host-mode port conflicts, or just to keep the app on a tidy fixed IP of its own.- On Unraid: create a custom Docker network on your NIC (e.g.
br0, or a VLAN likebr0.10), then in the container template set the Network Type to that network and give it a fixed IP. - macvlan caveat: with plain
macvlan, the host itself usually can't reach the container (a Linux kernel limitation) — which matters if your reverse proxy (e.g. SWAG) runs on that same host. Useipvlan(or add a macvlan shim interface) so the host can reach it.
- On Unraid: create a custom Docker network on your NIC (e.g.
| Host networking | ipvlan | macvlan | |
|---|---|---|---|
| Setup | None — the default | Custom Docker network + fixed IP | Custom Docker network + fixed IP |
| Port collisions with other containers | Possible — shares the host's ports | None — has its own IP | None — has its own IP |
| Host itself can reach the container | Yes (it is the host) | Yes | No (Linux kernel limitation) — use ipvlan instead if a reverse proxy runs on the host |
| Multiple instances on one host | Awkward — only one can hold port 4000 | Easy — each gets its own IP | Easy — each gets its own IP |
Either way, the app is still served on port 4000 (change it with the PORT env var if you need to).
For remote / internet access, see "Remote / internet access" below — that part is the same in both
modes.
Running more than one instance (e.g. staging + production)
Running two copies on one host is easiest with the dedicated LAN IP mode above: give each its own IP
and they can keep the same internal ports without clashing (host networking can't — two host-mode
containers would both try to grab port 4000). Set a different PORT on an instance if you'd rather change
its HTTP port instead.
Watching both remotely in Low latency (WebRTC) takes one extra step, because each instance's WebRTC media needs its own UDP port forwarded 1:1 — the port a browser must reach has to match the port MediaMTX advertises, so a router that remaps the port breaks WebRTC:
- Give each instance a distinct WebRTC UDP port: leave one at the default
8189and set the other with theMTX_WEBRTCLOCALUDPADDRESSenv var, e.g.MTX_WEBRTCLOCALUDPADDRESS=:8190. - Forward each 1:1 on your router — external UDP
8189→ instance A's8189, external UDP8190→ instance B's8190(same number in and out). - Set
PUBLIC_HOST(your public IP or DDNS) on both. - Put each behind its own subdomain in SWAG (e.g.
nursery.example.com,nursery-dev.example.com); the HTTP and the WebRTC signalling both ride 443, so the only per-instance forwards are those two UDP media ports.
Compatibility (HLS) needs none of this — it's all HTTP over 443, so both instances work remotely with no extra port forwarding.
Managing cameras
Cameras are added and edited from the Cameras tab (admin only). A camera's address is
entered as separate fields — IP address, RTSP port (usually 554), stream path,
and the camera's username / password. Nightlight assembles the rtsp:// URL from those
itself, so the password never appears in a URL on screen.
Supported cameras — Nightlight works with ONVIF / RTSP cameras in general: video, audio, and —
on ONVIF cameras — pan/tilt and two-way audio. The fully tested and recommended option is
open-firmware Thingino cameras (inexpensive, ONVIF, pan/tilt, two-way
audio); on those, set the camera's audio codec to G711 (a-law) for reliable sound (some builds
default to AAC, which not all of them stream well — see KNOWN-ISSUES.md). Most other ONVIF cameras
work too, and Hikvision two-way audio (ISAPI) is supported. If a camera won't connect, the Add
camera screen can generate a redacted camera report to help add support for it. The report is
a JSON file that stays on your device — it holds the camera's address, username, ONVIF result and
stream codecs, with any password replaced by ***. It is meant to be attached to a GitHub issue, so
it is worth opening and reading before you post it: the redaction covers passwords, not everything a
particular camera's firmware might put in an error message. If you find something sensitive that
shouldn't be there, see SECURITY.md for how to report it privately instead.
Adding a camera
- Via ONVIF (easiest): type the camera's IP and press Fetch port & path from ONVIF. Nightlight queries the camera and fills in the port and stream path for you, and detects whether it supports pan/tilt and two-way audio. Most cameras answer this without a login; if yours needs one, fill in the username/password first. Then make sure the login is entered and Save.
- Manually: type the IP, port, path, and login yourself (see the camera's manual for its
RTSP path, e.g.
/stream1). - On Save, Nightlight briefly tests the stream first and won't store a camera it can't reach (wrong login, path, or IP) — it tells you why. If the camera just happens to be offline right then, you can choose Save anyway.
Capability badges — cameras added via ONVIF show badges in the list: PTZ and Two-way Audio, green when supported, red when not. (Manually-added cameras show neither, since their capabilities aren't probed.)
Pan/tilt (PTZ) — on cameras that report PTZ, a move button appears on the camera tile (next to the stream-quality gear). Tap it for an on-screen D-pad: each press nudges the camera a fixed amount, and holding an arrow keeps it moving; it stops on release and can't run past the limit.
Two-way audio (talk-back) — on cameras that support it, a talk button appears on the tile: hold it to speak through the camera's speaker. Works with Hikvision (over ISAPI — enter the camera's web login when adding) and with any ONVIF camera that has an audio backchannel (e.g. Thingino/Sonoff), which is set up automatically from the ONVIF probe using the camera's own stream login.
Editing — Edit shows the same fields. Leave the password blank to keep the existing one — it's never sent back to your browser. Changing the address re-tests the stream, the same as adding.
Removing — Remove asks for confirmation, then stops the stream and deletes the camera. You can always add it again.
Assigning to a child — use the "Assigned to" dropdown on each camera. (This is just for grouping the dashboard; any signed-in user can change it.)
Sleep tracking
Nightlight can estimate each child's overnight sleep from what their cameras already see and hear — no wearables, no extra hardware. It's a sleep-pattern guide, not a medical measurement, and (like everything here) never a safety device — see the warning at the top.
- Turn it on per child. Each child has a Track sleep toggle in their settings, with their own bedtime and wake time. Turning tracking off stops it entirely. A child can have more than one camera — their movement and sound are combined.
- Bedtime is a guide, not a boundary. Real bedtimes move night to night, and you shouldn't have to edit the setting each evening. The bedtime that gets reported is the one the camera actually saw — the put-down, and the child settling after it — whether that happens before the window opens or an hour into it. If your child was already asleep before the window, that sleep is counted and the night's timeline starts at the real bedtime rather than at the setting. Either way it only happens when the camera saw the child actually put into bed — a quiet room on its own is never read as a sleeping child, and neither is a put-down on its own: the bed also has to go on showing signs of being occupied afterwards. A sleeping child is never perfectly still for hours — they move a little, repeatedly — so a bed that stirs once and then nothing is an empty one, and the bedtime it seemed to start is discarded. The same applies at the other end: a morning wake is still found if it comes after the configured wake time.
- One camera does the measuring. If a child has several cameras, sleep is worked out from their main camera — the first one in the order you've arranged them, skipping any that are turned off. The others carry on streaming, alerting and recording as normal; they just don't affect the numbers. This is deliberate: if every camera were combined, the noisiest one would decide the night — a camera facing the doorway would push bedtime later and add wake-ups that never happened, with nothing saying which camera was responsible. The night's detail view names the camera it measured from. Room temperature and humidity are the exception and still come from every sensor in the room, because those are averaged rather than combined.
- How it estimates. Across the night it builds a per-minute movement + sound timeline from the child's main camera: falling still for a sustained stretch reads as falling asleep, sustained movement or noise reads as an awakening (brief stirs don't count). On a completed night, a short recorded exit-and-return can also count as an awakening even when it's too brief to show up as sustained movement on its own — as long as the bed is confirmed quiet in between and later bed movement supports the return. This only applies to trips of one to twenty minutes, with at least a minute of quiet bed time in between; it updates wake-ups, awake time, and the longest sleep stretch, but live figures and wake-clip recording still use the plain movement rule above. If you've painted a bed zone on the camera — the same area that scopes motion alerts — it also tracks movement outside the bed and lists it separately, which catches a morning wake where the child has already left the bed. Draw the zone so it comfortably contains the child including where their head ends up — a zone that cuts through a sleeping child makes their own rolling over register as movement outside the bed, and can stop a real climb-out being recognised. Only clearly-outside-the-bed movement is listed; faint changes (a shadow, the camera's night-vision adjusting) are ignored rather than reported as someone in the room.
- Getting up for the day. The morning wake is the point the bed empties and stays empty, rather than the last movement seen in it — otherwise a parent stripping the bed an hour later would be reported as the child waking. A single stray minute of movement in an otherwise still bed (an adult reaching in for a toy or a blanket) does not restart that count; several minutes together do, because that is a person at the bed rather than a passing arm. The wake is only accepted where a recorded got out of bed backs it up, so a quiet spell alone can never end the night early.
- A child who gets out and climbs straight back in has not got up. If a got into bed follows a got out of bed shortly after, the night carries on rather than ending there — and so does a second got out of bed logged within a minute of that return, because climbing back into a bed and leaving it again inside a minute is one movement being read twice, not two trips. This matters most for a very still sleeper: once they settle, their bed can look identical to an empty one for hours, so a false exit shortly after bedtime would otherwise be reported as the end of the night. A qualifying short trip can still be counted as a wake-up within that same continuing night — see "How it estimates" above. It can miss a trip that's interrupted by a data gap or a stray reading, and it cannot tell an adult's visit to the bed from the child's own trip out of it. Known limit: a genuine departure less than a minute after getting into bed is read the same way, because the two are not distinguishable from what the cameras record. Where that is the only got out of bed of the night it is still used, so the night is never left with no wake time at all; where a later one exists, the later one is reported instead.
- Tell it when it got a night wrong. The morning after, the child's page offers Was last night
right? — confirm the times or correct them, and mark any recorded got into / out of bed event as
right, wrong, or "can't tell" against the still frame it was decided from. It appears once per night
and goes for good once answered or dismissed; nothing is asked about nights with no times to
confirm. Either an admin or a caregiver can answer, deliberately — the person who was in the room at
5am is the one who knows. Type times on your own clock; they are recorded against the timezone in
Settings, the same one the sleep card displays.
- Confirming that a night was right is worth as much as correcting one — it is what makes a future change to sleep detection provable rather than arguable. Confirming and correcting are separate buttons on purpose: the times we guessed are never one stray tap from being recorded as fact.
- Point at the picture instead of typing. Each recorded event has Put down here /
Up for the day here — tap the frame that shows the real moment and the time is taken from
it, exact to the second rather than rounded from memory. Typing a time by hand instead clears
the picked frame, so only one of them is ever the answer.
- This is not the same as marking an event correct. An exit can be perfectly real and still not be the end of the night — a child who gets out at 5:45, goes back, and gets up again at 6:00 had two genuine exits and only one of them ended the night.
- Your times become the ones shown. Once you correct a night, the child's card, the history list and the sleep detail page all show your times, marked You corrected this, with the total sleep recalculated to match. This is different from Recompute this night, which re-runs the detector: correcting records what you know, recomputing re-asks the app.
- The detector's own answer is kept underneath, not overwritten. That is deliberate — it is what a future improvement gets scored against. Nothing you enter here changes how sleep is detected.
- The card confirms it. After you answer, the prompt becomes a short receipt showing what you recorded, and tapping it lets you change your mind.
- Any night can be reviewed, not just last night. The sleep detail page has Was this night right? for whichever night you are looking at — that is how you correct a night you already answered, since the card only ever offers nights you haven't. Saving from there keeps you on that night and shows the receipt in place, so working back through a run of nights doesn't send you to re-pick the date each time.
- Reviews are kept forever — unlike the sleep minute-data behind them (30 days) or the recorded events (45 days). They are a few hundred bytes a night, and their whole value is being comparable years later. A recorded event you have judged is also kept past the usual 45 days, along with its frame, so a labelled picture is never deleted on a timer.
- Noise on its own doesn't delay bedtime. A bedroom microphone hears the whole house, and bedtime is usually its loudest hour — a sibling being settled, a TV, adults talking. So when working out when your child fell asleep, a noisy minute counts as awake only if that room also moved at around the same time. The same holds for the first half hour after they fall asleep, so the tail of the household's evening isn't reported as their first wake-up. After that the rule stops: mid-night the house is quiet, so a cry with no movement counts as a wake-up as you'd expect.
- Re-working out a night (admins). A night's summary is worked out once, the morning after, and then kept as it is — so if sleep detection is improved later, an already-recorded night keeps showing the old answer while the detail view, which works the night out fresh each time you open it, shows the new one. Recompute this night on the sleep detail page reconciles them. It compares what is saved — the summary on the child's page — against what the recorded movement now says, and shows you exactly what would change (bedtime, wake time, how long they slept, how many wake-ups) before anything is saved. You can cancel. Admins only. It can never make a night worse: the minute-by-minute data behind a night is only kept for 30 days — the same span the date picker offers — so the oldest night you can browse sits right on that edge. If its data has aged out, the recompute is refused and the saved summary is left alone, rather than being replaced with "no data".
- At a glance, and live. Each child's page summarises last night — total sleep, wake-ups, longest stretch — and while a night is in progress it updates as "Tonight · so far", so an early-morning wake appears within a minute or two rather than only after the window closes. Treat those live figures as provisional: a night in progress is judged only on what has happened so far, so a bedtime can be revised later in the evening and the morning wake isn't looked for at all until the night is complete. The settled numbers are the ones on the card the following day.
- The detail view. Tap the sleep summary for the full night timeline: a to-scale bar of asleep / stirring / awake stretches on a real time axis, every wake-up listed with its time and length, a movement outside the bed list, and a date picker to step back through roughly the last month of nights. Two moment markers sit on the bar — got into bed and got out of bed — and they are deliberately the only two shown: they're the put-down and the morning departure the night's times were actually derived from. A single camera can see that something crossed the edge of the bed, but not who, so movement in between is reported as exactly that and never attributed to a person.
- Room temperature (optional). If a camera reports temperature/humidity over MQTT (set up under Settings → MQTT, e.g. via Zigbee2MQTT — the readings also show on the camera tile), the sleep detail overlays the night's room temperature beneath the timeline, aligned to the same time axis. After a handful of tracked nights, a "Sleep & room temperature" card compares wake-ups on the child's warmer vs cooler nights, so you can spot whether a warm room tends to mean more waking — a pattern, not a cause.
Adding caregivers
Once signed in as admin, go to Settings → Caregivers to create additional logins (e.g. for a partner or babysitter).
| Capability | Caregiver | Admin |
|---|---|---|
| View live cameras and media | Yes | Yes |
| Reorder / assign cameras to a child | Yes | Yes |
| Restart, reboot, or snooze camera alerts | Yes | Yes |
| Add / edit / enable / delete a camera | No | Yes |
| Add / edit a child and review sleep | Yes | Yes |
| Delete a child and its media | No | Yes |
| Change global settings (detection, notifications, providers) | No | Yes |
| Manage caregiver/admin accounts and sessions | No | Yes |
The Settings hub itself is visible to caregivers too — its admin-only pages (general, camera controls, recording, MQTT, push providers, users, logs, clip storage) are simply hidden for them rather than the whole screen being off-limits.
Changing someone’s role takes effect immediately — on their very next action, on every device they’re signed in on. Demoting an admin to caregiver does not sign them out: they keep browsing as a caregiver and simply lose the admin-only screens. Deleting an account, by contrast, ends its sessions at once and signs that person out everywhere.
Any account can also turn on two-factor authentication for its own login — see docs/mfa.md for enrolling, one-time backup codes, and how to recover if an admin loses their authenticator.
Running behind a reverse proxy (e.g. SWAG on Unraid)
A ready-to-use config is in reverse-proxy/nightlight.subdomain.conf. Copy it to
swag/config/nginx/proxy-confs/nightlight.subdomain.conf and replace UNRAID_LAN_IP
with the container's LAN IP — the host's IP in host mode, or the container's own
dedicated IP if you gave it one (see Networking modes). Either way SWAG
reaches it by that IP, not by container name.
macvlan note: if you put Nightlight on its own IP with plain
macvlan, SWAG running on the same host won't be able to reach it (the host↔macvlan-container limitation noted above) — useipvlan, or run SWAG on a different host.
Everything is proxied through a single port (4000): the app, login, all pages, and the video signaling handshake — no extra ports to open on your router for this part.
TRUST_PROXY — telling Nightlight your real client addresses
| Variable | Default | What it does |
|---|---|---|
TRUST_PROXY |
loopback |
Which upstream addresses may set X-Forwarded-For. Accepts an IP or CIDR (10.0.0.20, 172.18.0.0/16), a comma-separated list, a hop count (1), or a named range. |
Behind a proxy, every request arrives from the proxy's address unless you tell Nightlight to trust
it. The default (loopback) only trusts a proxy on 127.0.0.1 — and SWAG reaches Nightlight by its
LAN IP, so with the setup above it is not trusted and every remote visitor looks like one client.
Set it to the address your proxy connects from:
-e TRUST_PROXY=10.0.0.20 # SWAG's own LAN IP
⚠️ Only set this to an address you control. Whatever you trust here is allowed to declare a client's IP. Too broad a value — or
true, which trusts every upstream — lets anyone forgeX-Forwarded-Forand slip the login rate limit entirely, which is worse than leaving it unset. If you are not sure, leave it alone.
A value Nightlight can't make sense of is ignored, with a warning in the log, and the safe default is used instead — a typo here will never stop the app starting.
You do not have to set it. Login attempts are limited per account and per source, so even with
every remote user sharing the proxy's address, one person mistyping their password cannot lock anyone
else out. Setting TRUST_PROXY makes the per-source half meaningful as well.
Remote / internet access (watching from outside your home network)
By default this is LAN-only. There are two ways to watch remotely, and each camera tile has a toggle to switch between them ("Low latency" / "Compatibility"):
Low latency (WebRTC) — near-instant video, same as at home. This requires:
- Set up SWAG as described above (HTTPS for the app itself).
- Set
PUBLIC_HOSTto your public IP or a DDNS hostname. - Forward UDP port 8189 on your router to your server's LAN IP.
This is a hard requirement of WebRTC, not a workaround — the actual audio/video always travels over UDP between your browser and MediaMTX, no matter what. A TURN relay server doesn't change this (it only changes how the signaling connects, not the media itself), so there's no way to get the low-latency mode down to zero UDP ports. This single UDP port forward is all you need, unless you're behind CGNAT.
Compatibility (HLS) — a few seconds of delay, but pure HTTP/TCP, so it rides through the same port 443 as everything else with no extra port forwarding at all. Use this if you'd rather not forward a UDP port, or if you're ever watching from a network that blocks outbound UDP (some corporate/public Wi-Fi).
iOS background audio: On iPhone/iPad, background listening (screen off / app minimised) works in Low latency mode only — its WebRTC audio lets Nightlight fully own the lock screen (camera name, artwork, Pause/Play). Compatibility is HLS, which iOS runs its own native lock-screen session for that we can't reliably control, so Background isn't offered for a Compatibility camera on iOS (it was tried and removed for being inconsistent — see KNOWN-ISSUES.md). Compatibility still works for live viewing. Android is unaffected — its background-listening service keeps both modes alive.
Both modes work automatically once SWAG + PUBLIC_HOST are set up — Compatibility mode
needs nothing further, since it's already proxied through the app's normal port.
Installing to your home screen
The app has a web app manifest and icons for a full-screen, native-app-like home-screen experience — no browser address bar. It has no service worker, so this is an install shortcut, not an offline mode: the app still needs to reach your server over the network every time, same as opening it in a tab.
What you actually get depends on the platform and whether the site is served over HTTPS
(via the reverse-proxy setup above) or plain http:// on your LAN:
| Platform | Over HTTPS | Over plain http:// |
|---|---|---|
| Android — Chrome / Samsung Internet | Installable PWA (full-screen, own icon, install prompt) | Usually a browser-badged shortcut that still opens inside the browser, not a true standalone install — Chromium's installability criteria require HTTPS (or localhost) |
| iOS — Safari | Add to Home Screen (full-screen, own icon) | Add to Home Screen works the same over plain HTTP — iOS doesn't gate it on HTTPS the way Chromium does |
| Desktop browser | Installable as a windowed app (Chrome/Edge) | Same shortcut limitation as Android Chrome |
- Android, HTTPS: browser menu → "Add to Home screen" / "Install app" (or the automatic install banner).
- Android, plain HTTP: same menu item still adds something to your home screen, but expect a shortcut that opens in the browser rather than a standalone window.
- iOS: Share button → "Add to Home Screen" — works the same either way.
If you want the full installed experience on Android specifically, the reverse-proxy/HTTPS setup above is worth doing; otherwise the native Android app (below) gives you a true standalone app without needing HTTPS at all.
Mobile apps (Android & iOS)
Beyond the PWA, there are companion native apps in a separate repo, nightlight-mobile. They're thin native shells around this same web UI — the app loads the interface live from your own server rather than bundling it, so any server update applies to the apps automatically with no reinstall. On first launch you just enter your server's address (like the Home Assistant app); a "Change server" menu item switches later.
What the native apps add over the browser/PWA:
- Reliable background listening — keep hearing a camera with the screen off or the app minimised. Android uses a foreground service (with a wake/wifi lock and a battery- optimisation exemption); iOS uses a background audio session. Plain in-browser background audio is unreliable by comparison. On iOS this requires a camera in Low latency mode — Compatibility (HLS) can't do reliable background audio on iOS (iOS runs its own native lock-screen session for it), so Background is only offered for Low-latency cameras there; see the Low latency / Compatibility notes above and KNOWN-ISSUES.md.
- Pause/Resume from the system controls — Android's notification and iOS's Now Playing (Control Center / lock screen), plus a Stop on Android.
- Picture-in-Picture — float a camera in a small always-on-top window while you use other apps (Android; the browser has its own PiP too).
- Battery-friendly — minimising the app disconnects streams unless a camera is in Background mode, so nothing keeps pulling video in the background.
- Push notifications — a phone alert when a camera sees motion, even with the app closed (Android only for now). See below.
Push notifications (motion alerts)
Get a phone notification when a camera with motion detection sees movement — even when the app is closed. It's off by default, and the in-app Recent alerts list — on each child's page, or the combined view under Settings → Logs (admin only) — works with or without it. There are two ways to set it up (pick one), both configured under Settings → Push notifications:
Pushover (recommended — simplest, and works on iOS). A small notification service (the same one Sonarr/Radarr use). No Firebase project, no Apple Developer account.
- Install the Pushover app on your phone and note your User Key (or make a Delivery Group to alert several caregivers).
- Create a Pushover application at pushover.net/apps/build and copy its API Token.
- In Settings → Push notifications → Pushover, paste the token + your user/group key, Enable, Save (it verifies with Pushover), and Send test. Then enable Motion detection on a camera. Alerts arrive with a snapshot of what triggered them.
Firebase / FCM (Android app only). Delivers straight to the Nightlight Android app via your own
Firebase project: drop google-services.json + a firebase-service-account.json service-account key
into your data dir (e.g. /mnt/user/appdata/nightlight, keep the key chmod 600), then enable it
under Settings → Push notifications. Each phone opts in under Account → Notifications.
Full walkthrough for both, with troubleshooting: docs/notifications.md.
Getting them:
- Android — download the signed APK from the nightlight-mobile Releases page and sideload it (built and signed automatically in CI on each release).
- iOS — no App Store build yet; it's installed by sideloading (e.g. AltStore/Sideloadly with a free Apple ID). See the nightlight-mobile repo for the current status and steps.
Both apps are built entirely in GitHub Actions — see the nightlight-mobile repo for how,
and its own CHANGELOG.md for per-release notes.
Logs
Both the app and MediaMTX log to stdout, captured by Docker in the normal way:
docker logs -f nightlight
Docker's own log rotation (already configured in the docker run/Compose examples
above, and in the Unraid template's extra parameters) caps total log storage at 10MB × 3
files, so this doesn't grow unbounded - this matters particularly on Unraid, where
Docker's storage is a fixed-size image that can break the whole Docker service if it
fills up. If you deployed before this was added, add --log-opt max-size=10m --log-opt max-file=3 yourself (or the Unraid template's "Extra Parameters" field) to get the
same protection.
Timestamps use your container's local time (see the TZ variable above) rather than
UTC, so they line up with when you actually remember something happening.
Troubleshooting
- Camera shows "No signal": double check the RTSP URL works with a tool like VLC (Media → Open Network Stream) first — if VLC can't play it, the app won't either.
- Video won't connect from a phone but the pages load fine: confirm the device is on the same LAN (for Low latency mode) — see "Remote / internet access" above if it's actually a different network.
- A camera shows disconnected for a few seconds after opening the app and doesn't come back: this is usually a stale WebRTC connection on the phone itself, not the camera or server — the video connection can get "wedged" after the phone sleeps, switches networks, or hands off Wi-Fi/cellular, and won't always re-establish on its own. Pull down on the camera dashboard to reconnect — this works in the browser and in the mobile apps, and rebuilds the connection without a full restart. If a camera is actually down, every device sees it, not just one — check the Camera history panel (Settings) or the logs (below) to tell the two apart.
- Background listening stops after a while (Android app): Android's battery optimisation (Doze) will eventually freeze the app and cut its network unless it's exempt. The app asks for the exemption the first time you enable Background mode — if you declined, grant it manually under Settings → Apps → Nightlight → Battery → Unrestricted. (Background audio is a feature of the native apps, not the browser/PWA — see Mobile apps.)
- A camera says "No signal" only in Compatibility mode: usually the camera sending bad
audio timestamps, which can stall the HLS pipeline. Nightlight resamples/rewrites camera
timestamps to ride through this, but if it persists, power-cycle or update that camera's
firmware. See
KNOWN-ISSUES.md. - Checking whether MediaMTX has registered your cameras: its API is loopback-only (not
reachable directly from a browser), so check it from inside the container:
An emptydocker exec nightlight wget -qO- http://127.0.0.1:9997/v3/paths/list"items":[]with cameras added in the app means MediaMTX and the app's database have drifted apart — restarting the container re-syncs them automatically (see the startup log line "Reconciled N camera path(s)..."). - Checking logs: see the "Logs" section above —
docker logs -f nightlight.
For a catalogue of understood quirks (camera glitches, WebRTC reconnects, the watchdog's
recovery window) with what each one means and whether it needs any action, see
KNOWN-ISSUES.md.
Building from source
git clone https://github.com/sauso/nightlight.git
cd nightlight
docker build -t nightlight .
docker run -d --name nightlight --network host --stop-timeout 30 \
-e PUID=99 -e PGID=100 -v ./data:/app/data nightlight
Project layout
backend/ Express API + SQLite storage + process supervision for MediaMTX/FFmpeg
frontend/ React mobile-first UI (built into the image at build time)
mediamtx/ Default MediaMTX config (baked into the image - see src/index.js for why)
reverse-proxy/ Example SWAG config for running behind HTTPS
Dockerfile Single combined image (app + MediaMTX + FFmpeg)
unraid-template.xml Unraid Community Applications template
docker-compose.yml
License
MIT — see LICENSE.
Install Nightlight on Unraid in a few clicks.
Find Nightlight in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.
Categories
Download Statistics
Related apps
Explore more like this
Explore allDetails
sauso/nightlightRuntime arguments
- Web UI
http://[IP]:[PORT:4000]/- Network
host- Shell
sh- Privileged
- false
- Extra Params
--log-opt max-size=10m --log-opt max-file=3 --stop-timeout 30
Template configuration
Used ONLY to build the WebUI link above - with Host networking (the default), changing this number does NOT change the port the app actually listens on. To actually change the port, set the PORT variable below to the SAME number as this field.
- Target
- 4000
- Default
- 4000
- Value
- 4000
OPTIONAL - the port the app actually listens on. Leave blank for the default (4000). If you set this, also change WebUI Port above to the same number, or the WebUI link will point at the wrong port.
Stores the database and MediaMTX's config (including your added cameras)
- Target
- /app/data
- Default
- /mnt/user/appdata/nightlight
- Value
- /mnt/user/appdata/nightlight
OPTIONAL - where event-recording clips are stored. Leave unset and clips go under the Data Directory (your SSD cache). To keep large clips off the SSD, map this to a folder on your array (e.g. /mnt/user/nightlight-clips) AND set the CLIPS_DIR variable below to /recordings. Retention caps (Settings - Recording) protect the disk either way.
- Target
- /recordings
OPTIONAL - set to /recordings only if you mapped the Recordings Directory above to store clips on your array instead of under the Data Directory. Leave blank otherwise.
User ID that owns files this container creates in the Data Directory - 99 matches Unraid's own 'nobody' convention, so the default should already be correct
- Default
- 99
- Value
- 99
Group ID that owns files this container creates in the Data Directory - 100 matches Unraid's own 'users' convention, so the default should already be correct
- Default
- 100
- Value
- 100
Your local timezone, e.g. Australia/Melbourne - affects log timestamps only
- Target
- TZ
- Default
- UTC
- Value
- UTC
Optional - leave blank and a secure random secret is generated and persisted in the data directory automatically on first run
- Target
- JWT_SECRET
Your public IP or DDNS hostname - only needed if you want Low Latency (WebRTC) mode to work from outside your home network (also requires forwarding UDP port 8189 on your router). Leave blank for LAN-only use.
- Target
- PUBLIC_HOST