Verified against LightNVR 0.37.x.
This guide provides comprehensive information about deploying LightNVR using Docker.
On Windows? Everything here applies, but the host setup has its own pitfalls. Start with Running LightNVR on Windows with Podman + WSL2.
On Home Assistant OS or Supervised? Use the add-on instead — it handles networking, storage, and backups for you. See LightNVR on Home Assistant.
- Quick Start
- Container Architecture
- Volume Management
- Network Configuration
- Environment Variables
- First Run Experience
- WebRTC Configuration
- Troubleshooting
- Advanced Configuration
No clone or build required — the compose file pulls the published image. Save the repository's docker-compose.yml into an empty directory (or create it from the example below):
services:
lightnvr:
image: ghcr.io/opensensor/lightnvr:latest
container_name: lightnvr
restart: unless-stopped
ports:
- "8080:8080" # Web UI
- "8554:8554" # RTSP
- "8555:8555" # WebRTC TCP
- "8555:8555/udp" # WebRTC UDP
- "1984:1984" # go2rtc API
volumes:
# DO NOT mount /var/lib/lightnvr directly - it will overwrite web assets!
- ./config:/etc/lightnvr
- ./data:/var/lib/lightnvr/data
environment:
- TZ=UTC # Change to your timezone (e.g., America/New_York)
# ONVIF discovery network for containers, e.g. 192.168.1.0/24
# - LIGHTNVR_ONVIF_NETWORK=192.168.1.0/24Then start it:
# Pull the image and start the container
docker compose up -d
# View logs
docker compose logs -f
# Access the web UI
# http://localhost:8080
# Default credentials: admin / admin - change immediately, see First Run ExperienceTo build the image from source instead, clone the repository (with git submodule update --init --recursive) and switch the image: line in docker-compose.yml to the commented-out build: block — see Building from Source.
docker run -d \
--name lightnvr \
--restart unless-stopped \
-p 8080:8080 \
-p 8554:8554 \
-p 8555:8555 \
-p 8555:8555/udp \
-p 1984:1984 \
-v ./config:/etc/lightnvr \
-v ./data:/var/lib/lightnvr/data \
-e TZ=America/New_York \
ghcr.io/opensensor/lightnvr:latestThe LightNVR Docker container is built using a multi-stage build process:
- Builder Stage - Compiles LightNVR and go2rtc from source
- Runtime Stage - Minimal Debian-based image with only runtime dependencies
- Automatic Initialization - Creates default configs on first run
- Persistent Configuration - Config files survive container restarts
- Protected Web Assets - Web UI files stored in template location
- Health Checks - Built-in health monitoring
- Multi-Architecture - Supports amd64, arm64, and armv7
The container uses two primary volume mounts:
/etc/lightnvr/ # Configuration files
├── lightnvr.ini # Main configuration
└── go2rtc/
└── go2rtc.yaml # go2rtc configuration
/var/lib/lightnvr/data/ # Persistent data
├── database/
│ └── lightnvr.db # SQLite database
├── recordings/
│ ├── hls/ # HLS recordings
│ └── mp4/ # MP4 recordings
└── models/ # Object detection models
/var/lib/lightnvr directly!
Mounting the entire /var/lib/lightnvr directory will overwrite the web assets and break the web UI. Always mount only the subdirectories you need:
✅ Correct:
volumes:
- ./config:/etc/lightnvr
- ./data:/var/lib/lightnvr/data❌ Incorrect:
volumes:
- ./config:/etc/lightnvr
- ./data:/var/lib/lightnvr # This will break the web UI!When using NFS volumes (common with NAS devices like Synology), there are some important considerations:
NFS volumes may have different permission models than local filesystems. If you encounter "Operation not permitted" errors:
-
Check NFS mount options: Ensure your NFS share is mounted with appropriate permissions
volumes: config: driver_opts: type: "nfs" o: "nfsvers=4,addr=192.168.1.100,rw,nolock" # Add nolock if needed device: ":/volume1/docker/lightnvr/config"
-
UID/GID mapping: The container runs as root by default. Ensure your NFS export allows root access or map UIDs appropriately:
- On Synology NAS: Enable "Map all users to admin" or "Squash" options in NFS permissions
- Or use
all_squash,anonuid=1000,anongid=1000in NFS export options
-
Directory pre-creation: For better reliability with NFS, pre-create the directory structure on your NAS:
# On your NAS or NFS server mkdir -p /volume1/docker/lightnvr/config/go2rtc mkdir -p /volume1/docker/lightnvr/data/recordings/mp4 mkdir -p /volume1/docker/lightnvr/data/database mkdir -p /volume1/docker/lightnvr/data/models chmod -R 755 /volume1/docker/lightnvr
If recordings are not being created:
- Check write permissions: The container logs will show write permission test results on startup
- Verify NFS mount: Ensure the NFS volume is actually mounted inside the container:
docker exec lightnvr-latest ls -la /var/lib/lightnvr/data/recordings/mp4 - Check available space: Ensure your NAS has sufficient free space
- Review logs: Check for "Failed to open input" or "Operation not permitted" errors:
docker logs lightnvr-latest | grep -i error
Here's a complete example for Synology NAS with Traefik:
volumes:
config:
driver_opts:
type: "nfs"
o: "nfsvers=4,addr=${IpAddressNFS},rw,nolock"
device: ":/${NFSVolumePath}/${SystemId}/lightnvr-config"
data:
driver_opts:
type: "nfs"
o: "nfsvers=4,addr=${IpAddressNFS},rw,nolock"
device: ":/${NFSVolumePath}/${SystemId}/lightnvr-data"Note: The nolock option can help with some NFS permission issues but may reduce file locking safety. Use with caution in production environments.
Web assets are baked into the image at /var/lib/lightnvr/www during the build. The
entrypoint verifies index.html is present and refuses to start if it is not; there is no
copy-on-first-run step. This means:
- The web UI works immediately after container start
- Pulling a new image updates the web UI
- Never mount a volume over
/var/lib/lightnvr— it would hide the assets and the container will fail to start. Mount/var/lib/lightnvr/datainstead.
| Port | Protocol | Service | Description |
|---|---|---|---|
| 8080 | TCP | Web UI | Main web interface |
| 8554 | TCP | RTSP | RTSP streaming server |
| 8555 | TCP/UDP | WebRTC | WebRTC streaming |
| 1984 | TCP | go2rtc API | go2rtc REST API |
services:
lightnvr:
ports:
- "8080:8080"
- "8554:8554"
- "8555:8555"
- "8555:8555/udp"
- "1984:1984"services:
lightnvr:
network_mode: hostNote: Host mode provides better performance but exposes all ports directly on the host.
| Variable | Default | Description |
|---|---|---|
TZ |
UTC |
Container timezone |
GO2RTC_CONFIG_PERSIST |
true |
Persist go2rtc config across restarts |
LIGHTNVR_ONVIF_NETWORK |
(none) | Override ONVIF discovery network (e.g., 192.168.1.0/24) |
LIGHTNVR_MIGRATIONS_DIR |
(none) | Override where database migrations are read from. Falls back to ./db/migrations, then /usr/share/lightnvr/migrations, then a path relative to the binary. |
LIGHTNVR_AUTO_INITandLIGHTNVR_WEB_ROOTare set by the image and bydocker-compose.yml, but nothing reads them — neither the entrypoint nor LightNVR itself. Setting them has no effect. They are left in place because removing them from existing compose files would be a breaking-looking change for no gain; treat them as inert. Config initialization always runs when/etc/lightnvr/lightnvr.iniis absent, and the web root is fixed at/var/lib/lightnvr/www.
environment:
- TZ=America/New_York
- GO2RTC_CONFIG_PERSIST=true
- LIGHTNVR_AUTO_INIT=true
- LIGHTNVR_ONVIF_NETWORK=192.168.1.0/24When running in a container, ONVIF auto-detection skips Docker bridge interfaces by default. To enable ONVIF camera discovery in containerized deployments, set the LIGHTNVR_ONVIF_NETWORK environment variable to specify which network to scan:
services:
lightnvr:
environment:
# Specify the network where your cameras are located
- LIGHTNVR_ONVIF_NETWORK=192.168.1.0/24Network Priority:
- Explicit network parameter (API calls)
LIGHTNVR_ONVIF_NETWORKenvironment variablediscovery_networkin config file ([onvif]section)- Auto-detection (skips Docker interfaces)
Finding Your Network:
# On the Docker host, find your camera network
ip addr show
# Example: If your host IP is 192.168.1.100 with netmask 255.255.255.0
# Use: LIGHTNVR_ONVIF_NETWORK=192.168.1.0/24On first container start, the entrypoint script automatically:
-
Creates Directory Structure
/etc/lightnvr/ /var/lib/lightnvr/data/database/ /var/lib/lightnvr/data/recordings/ /var/lib/lightnvr/data/models/ -
Verifies Web Assets
- Checks that
/var/lib/lightnvr/www/index.htmlexists — the assets are baked into the image at build time and are not copied at startup - Startup fails with an error if they are missing, which normally means
/var/lib/lightnvrwas mounted over. Mount/var/lib/lightnvr/data, never/var/lib/lightnvritself.
- Checks that
-
Creates Default Configuration
lightnvr.iniwith sensible defaultsgo2rtc.yamlwith WebRTC/STUN configuration
-
Seeds Detection Models
- Copies
/usr/share/lightnvr/models/into/var/lib/lightnvr/data/models/, only if the destination is empty
- Copies
-
Initializes Database
- Creates SQLite database on first access
- Sets up default admin user
- Username:
admin - Password:
admin
0.0.0.0 inside the container, so once you publish port 8080
these credentials are the only thing in front of the first-login flow. LightNVR requires
the default password to be replaced immediately after login and blocks the rest of the UI
and password-authenticated APIs until that succeeds. You then sign in again with the new
password; MFA, when enabled, follows on that sign-in.
To avoid the default ever being valid, set password in the [web] section of
config/lightnvr.ini before the first start — the admin account is then created with
that password instead. The setting is only read when the account is created; afterwards
users are managed from the Users page.
Existing accounts are not flagged during an upgrade. API-key authentication and demo mode remain available while a freshly bootstrapped administrator is awaiting a password change.
The container includes pre-configured WebRTC support with STUN servers for NAT traversal.
webrtc:
listen: :8555
ice_servers:
- urls: [stun:stun.l.google.com:19302]
candidates:
- "*:8555"
- stun:stun.l.google.com:19302Edit ./config/go2rtc/go2rtc.yaml:
webrtc:
listen: :8555
ice_servers:
- urls: [stun:stun.l.google.com:19302]
- urls: [turn:your-turn-server.com:3478]
username: your-username
credential: your-password
candidates:
- "YOUR_PUBLIC_IP:8555"Restart the container to apply changes:
docker compose restartIf running behind NAT/firewall, forward these ports:
- 8555/TCP - WebRTC signaling
- 8555/UDP - WebRTC media
Symptom: Accessing http://localhost:8080 shows nothing or 404 error
Causes:
- Mounted
/var/lib/lightnvrdirectly (overwrote web assets) - Web assets not copied during initialization
Solution:
# Stop container
docker compose down
# Remove incorrect volume mount
# Edit docker-compose.yml to use /var/lib/lightnvr/data instead
# Remove web directory to force re-initialization
rm -rf ./data/web
# Start container
docker compose up -dSymptom: All streams and settings disappear after container restart
Cause: Data volume not mounted correctly
Solution:
# Verify volume mounts
docker inspect lightnvr | grep -A 10 Mounts
# Should show:
# /etc/lightnvr
# /var/lib/lightnvr/dataSymptom: WebRTC streams fail to connect (ICE connection failures)
Causes:
- ICE candidates not resolving to the correct host IP address
- UDP port 8555 not forwarded
- Firewall blocking WebRTC
- STUN server not reachable
Solution:
The most common fix is setting your host machine's local IP as external_ip in ./config/lightnvr.ini:
[go2rtc]
external_ip = 192.168.1.100 ; Replace with your machine's local IPTo find your local IP:
# Linux
hostname -I | awk '{print $1}'
# macOS
ipconfig getifaddr en0 # or en1 for Wi-FiThen restart the container:
docker compose restartIf that doesn't resolve it, check go2rtc connectivity:
# Check if go2rtc is running
docker exec lightnvr ps aux | grep go2rtc
# Check go2rtc logs
docker exec lightnvr cat /var/log/lightnvr/go2rtc.log
# Test STUN connectivity
docker exec lightnvr nc -vzu stun.l.google.com 19302Symptom: Changes to go2rtc.yaml are lost on restart
Cause: GO2RTC_CONFIG_PERSIST set to false
Solution:
environment:
- GO2RTC_CONFIG_PERSIST=trueMount a custom config file:
docker run -d \
--name lightnvr \
-v /path/to/custom/lightnvr.ini:/etc/lightnvr/lightnvr.ini \
-v ./data:/var/lib/lightnvr/data \
ghcr.io/opensensor/lightnvr:latestservices:
lightnvr-1:
image: ghcr.io/opensensor/lightnvr:latest
ports:
- "8080:8080"
- "8554:8554"
volumes:
- ./config-1:/etc/lightnvr
- ./data-1:/var/lib/lightnvr/data
lightnvr-2:
image: ghcr.io/opensensor/lightnvr:latest
ports:
- "8081:8080"
- "8555:8554"
volumes:
- ./config-2:/etc/lightnvr
- ./data-2:/var/lib/lightnvr/dataservices:
lightnvr:
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '1'
memory: 512Mservices:
lightnvr:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"If you're upgrading from an older version that mounted /var/lib/lightnvr directly, change
the mount to /var/lib/lightnvr/data and move your existing content down one level:
docker compose down
# Your old volume held database/, recordings/ and models/ at its root, alongside
# the web assets. Only the first three should persist.
mkdir -p ./data
mv ./old-volume/database ./old-volume/recordings ./old-volume/models ./data/ 2>/dev/null
# Then point the volume at the data subdirectory:
# - ./data:/var/lib/lightnvr/data
docker compose up -dMounting /var/lib/lightnvr itself hides the web assets baked into the image, and the
container will refuse to start with a "Web assets not found" error.
# Clone repository
git clone https://github.com/opensensor/lightNVR.git
cd lightNVR
# Initialize submodules (required for go2rtc)
git submodule update --init --recursive
# Build image
docker build --pull -t lightnvr:local .
# Run locally built image
docker run -d \
--name lightnvr \
-p 8080:8080 \
-v ./config:/etc/lightnvr \
-v ./data:/var/lib/lightnvr/data \
lightnvr:localFor issues and questions:
- GitHub Issues: https://github.com/opensensor/lightNVR/issues
- Documentation: https://github.com/opensensor/lightNVR/tree/main/docs