A Docker-based setup for creating virtual ONVIF cameras that can be discovered and accessed as real IP cameras on your local network. This project uses macvlan networking to give each virtual camera its own IP address and MAC address, making them appear as physical devices to ONVIF clients.
- Virtual ONVIF cameras with unique IP addresses and MAC addresses
- RTSP video streaming served directly from each camera's IP address
- Full ONVIF protocol support for device discovery and control
- Configurable video quality settings (high/low quality streams)
- Support for multiple camera instances
- Combined container architecture (ONVIF + MediaMTX in single container)
Each virtual camera runs as a single container with both services:
┌─────────────────────────────────────┐
│ onvif-cam1 (10.0.0.230) │
│ ├── ONVIF Server (port 80) │
│ └── MediaMTX RTSP (port 8554) │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ onvif-cam2 (10.0.0.229) │
│ ├── ONVIF Server (port 80) │
│ └── MediaMTX RTSP (port 8554) │
└─────────────────────────────────────┘
This means:
- ONVIF discovery at
http://10.0.0.230returns RTSP URLrtsp://10.0.0.230:8554/cam1 - Both services share the same IP address per camera
- No separate host shim required for external access
- Docker and Docker Compose
- Linux host with network interface access
- Works on any architecture (ARM64, x86_64, etc.)
- Video files for streaming (MP4 format recommended)
- arp-scan for network discovery:
- Ubuntu/Debian:
sudo apt-get install arp-scan - macOS:
brew install arp-scan - RHEL/CentOS:
sudo yum install arp-scan
- Ubuntu/Debian:
git clone <this-repository>
cd onvif-devicesBuild the combined image that includes both ONVIF server and MediaMTX:
docker build -t onvif-camera:combined .This creates a single image containing:
- MediaMTX RTSP server
- Node.js ONVIF server (pulled from
ghcr.io/daniela-hase/onvif-server) - Entrypoint script that runs both services
Use the configuration generator to automatically detect your network and suggest settings:
# Run with sudo for best network scanning
sudo ./scripts/generate-config.sh
# Or specify options
sudo ./scripts/generate-config.sh -i eth0 -c 2 -w
# See all options
./scripts/generate-config.sh -hThis script will:
- Automatically detect your network subnet and gateway
- Scan for used IP addresses using arp-scan
- Suggest available IPs for cameras
- Generate unique MAC addresses
- Optionally write the configuration to
.envfile
The configuration generator creates a .env file. You can also edit it manually:
# Ethernet NIC for macvlan
PARENT_IF=eth0
# Your LAN
LAN_SUBNET=10.0.0.0/24
LAN_GATEWAY=10.0.0.1
# Virtual ONVIF devices (unique IPs + MACs)
# Each container runs ONVIF + MediaMTX, so RTSP is served from same IP
CAM1_IP=10.0.0.230
CAM1_MAC=02:11:22:A2:01:01
CAM2_IP=10.0.0.229
CAM2_MAC=02:11:22:A2:02:02
# Host directory containing video files (mounted as /media in container)
VIDEO_DIR=/home/pi/VideosImportant: Make sure the camera IPs are static or adjust your DHCP pool from your router to prevent IP conflicts.
Each camera has its own YAML configuration file. After updating .env, ensure the MAC addresses match:
onvif-cam1-macvlan.yaml:
onvif:
- mac: 02:11:22:A2:01:01 # Must match CAM1_MAC in .env
name: Cam1
uuid: 9f3d2c1e-bbcd-4f6a-9f0b-2e5a7a4e9c61
ports: { server: 80, rtsp: 8554, snapshot: 8080 }
target:
hostname: 10.0.0.230 # Must match CAM1_IP in .env
ports: { snapshot: 8080 }
highQuality: { rtsp: "/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif", width: 1920, height: 1080, framerate: 25, bitrate: 4096 }
lowQuality: { rtsp: "/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif", width: 640, height: 360, framerate: 10, bitrate: 512 }Key configuration points:
mac: Must match the correspondingCAM{N}_MACin.envports.rtsp: The port advertised in ONVIF responses (8554)target.hostname: The camera's own IP address (for RTSP URL generation)target.ports: Only includesnapshot(notrtsp) to avoid proxy conflicts
Each camera has its own MediaMTX config file (mediamtx-cam1.yml, mediamtx-cam2.yml):
rtsp: yes
rtspAddress: :8554
rtspTransports: [udp, multicast, tcp]
hls: yes
webrtc: yes
paths:
"cam/realmonitor":
source: publisher
runOnInit: ffmpeg -stream_loop -1 -re -i /media/video.mp4 -c:v copy -c:a copy -f rtsp "rtsp://127.0.0.1:8554/cam/realmonitor"
runOnInitRestart: yesConvert your video files to ONVIF-compatible format using FFmpeg:
ffmpeg -i /path/to/input/video.mp4 -vf scale=1280:720,fps=15 -pix_fmt yuv420p \
-c:v libx264 -preset slow -crf 23 -g 30 -sc_threshold 0 \
-c:a aac -ar 48000 -ac 1 -b:a 96k /path/to/output/video.mp4Place your converted video files in the directory specified by VIDEO_DIR in your .env file (defaults to /home/pi/Videos).
docker compose up -dCheck if containers are running:
docker compose psExpected output:
NAME IMAGE COMMAND SERVICE STATUS
onvif-cam1 onvif-camera:combined "/entrypoint.sh" onvif-cam1 Up
onvif-cam2 onvif-camera:combined "/entrypoint.sh" onvif-cam2 Up
View logs:
docker compose logs -fYou should see:
- MediaMTX starting and opening RTSP listener on port 8554
- ONVIF server starting on port 80
- FFmpeg publishing video to the RTSP path
Once deployed, your virtual cameras will be accessible at:
| Camera | ONVIF Discovery | RTSP Stream |
|---|---|---|
| Cam1 | http://10.0.0.230 |
rtsp://10.0.0.230:8554/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif |
| Cam2 | http://10.0.0.229 |
rtsp://10.0.0.229:8554/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif |
From any machine on your network:
# Test with ffprobe
ffprobe "rtsp://10.0.0.230:8554/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif"
# Play with ffplay
ffplay "rtsp://10.0.0.230:8554/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif"
# Play with VLC
vlc "rtsp://10.0.0.230:8554/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif"Use ONVIF-compatible software to discover devices:
- ONVIF Device Manager
- VLC Media Player
- Security camera software (Blue Iris, Shinobi, etc.)
- Custom ONVIF clients
When you query the stream URI via ONVIF, you'll receive:
rtsp://10.0.0.230:8554/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
onvif-devices/
├── scripts/
│ ├── generate-config.sh # Network configuration generator
│ ├── macvlan-setup.sh # Optional: Host shim setup (for local access)
│ └── macvlan-cleanup.sh # Optional: Host shim cleanup
├── .env # Network configuration
├── .env.example # Example configuration
├── docker-compose.yml # Docker Compose file
├── Dockerfile # Combined image (ONVIF + MediaMTX)
├── entrypoint.sh # Container entrypoint script
├── mediamtx-cam1.yml # MediaMTX config for camera 1
├── mediamtx-cam2.yml # MediaMTX config for camera 2
├── onvif-cam1-macvlan.yaml # ONVIF config for camera 1
├── onvif-cam2-macvlan.yaml # ONVIF config for camera 2
└── README.md # This file
By default, the host machine running Docker cannot access the macvlan containers directly (this is a macvlan limitation). Other machines on your network can access the cameras without any issues.
If you need to access cameras from the Docker host itself:
-
Add
HOST_SHIM_IPto your.env:HOST_SHIM_IP=10.0.0.250/24
-
Run the macvlan setup script:
./scripts/macvlan-setup.sh
-
Now you can access cameras from the host via the shim interface.
-
Containers can't be reached from other machines:
- Verify the macvlan network was created:
docker network ls | grep cam_net - Check container IPs:
docker exec onvif-cam1 ip addr show eth0 - Ensure no IP conflicts with existing devices
- Verify the macvlan network was created:
-
IP conflicts:
- Ensure camera IPs don't conflict with DHCP range
- Use static IPs or configure DHCP reservations
-
Can't access cameras from Docker host:
- This is expected with macvlan - use host shim (see "Host Access" section above)
- Or test from another machine on the network
-
ONVIF server not responding:
- Check container logs:
docker logs onvif-cam1 - Verify the combined image was built:
docker images | grep onvif-camera
- Check container logs:
-
No video streams:
- Ensure video files exist in your
VIDEO_DIR - Check logs for FFmpeg errors:
docker logs onvif-cam1 | grep -i ffmpeg
- Ensure video files exist in your
-
RTSP URL shows "undefined" port:
- Ensure
ports.rtsp: 8554is set in the ONVIF YAML config - Ensure
target.portsdoes NOT includertsp(onlysnapshot)
- Ensure
-
"invalid subinterface vlan name" error (Docker 29.x):
Docker 29.x has a bug that incorrectly parses interface names like
enp10s0as VLAN subinterfaces. The workaround is to create an interface alias with a simpler name:# Create an alias for your interface sudo ip link add link enp10s0 name eth0 type macvlan mode bridge sudo ip link set eth0 up # Update .env to use the alias PARENT_IF=eth0
Replace
enp10s0with your actual interface name. To make this persistent across reboots, add the commands to/etc/rc.localor create a systemd service. -
Network pool overlap error:
docker network rm onvif-devices_cam_net docker compose up -d
-
Container name conflicts:
docker compose down docker compose up -d
# Check container status
docker compose ps
# View logs
docker compose logs -f
# Check specific container
docker logs onvif-cam1
# Verify container IP
docker exec onvif-cam1 ip addr show eth0
# Test RTSP from another machine
ffprobe "rtsp://10.0.0.230:8554/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif"
# Restart services
docker compose restart# Stop and remove containers
docker compose down
# If using host shim, clean it up
./scripts/macvlan-cleanup.sh-
Add new camera variables to
.env:CAM3_IP=10.0.0.228 CAM3_MAC=02:11:22:A2:03:03
-
Create
onvif-cam3-macvlan.yaml(copy from cam1/cam2 and update values) -
Create
mediamtx-cam3.yml(copy from cam1/cam2) -
Add new service to
docker-compose.yml:onvif-cam3: image: onvif-camera:combined container_name: onvif-cam3 restart: unless-stopped networks: cam_net: ipv4_address: ${CAM3_IP} mac_address: ${CAM3_MAC} volumes: - ./onvif-cam3-macvlan.yaml:/onvif.yaml:ro - ./mediamtx-cam3.yml:/mediamtx.yml:ro - ${VIDEO_DIR:-/home/pi/Videos}:/media:ro
-
Restart:
docker compose up -d
