A self-hosted web interface for Reolink IP cameras. Browse recordings by date, watch clips and images in-browser, view live streams via WebRTC, and track storage usage — all running locally with no cloud dependency.
Main Gallery
Stats Page
- Recordings browser — files grouped by time of day (Morning / Afternoon / Evening / Night), paginated with configurable page size
- Video thumbnails — generated client-side, lazy-loaded as cards scroll into view
- Modal viewer — play videos and view images in-browser with keyboard navigation (← / →) and a download button
- Bookmarks — star any file to save it; filter to bookmarked items only
- Today summary — quick stats on today's recordings at the top of the page
- Live view — WebRTC streams from both cameras with auto-reconnect, fullscreen, and live clock overlay
- Stats tab — 30-day bar chart of recordings per day with per-day drill-down, per-camera storage breakdown
- Date browser — sidebar tree navigation by year / month / day
| Component | Role |
|---|---|
| vsftpd | FTP server — receives recordings from cameras |
| go2rtc | RTSP → WebRTC bridge for live streaming |
| nginx | Serves frontend, proxies go2rtc WebSocket |
| Docker Compose | Orchestrates nginx + go2rtc |
| Vanilla JS/HTML | Single-file frontend, no build step |
- Docker and Docker Compose
- Reolink cameras with FTP upload support (tested on RLC-520A)
- Linux host with nftables or iptables
- AMD GPU with VAAPI support recommended for H.264 transcoding (software fallback works too)
nvr-site/
├── docker-compose.yml # nginx + go2rtc containers
├── nginx.conf # nginx config with go2rtc WebSocket proxy
├── go2rtc.yaml # go2rtc streams config (gitignored — see example)
├── go2rtc.yaml.example # example config with placeholders
├── .gitignore
└── gallery/
└── index.html # entire frontend in one file
Install and configure vsftpd to receive camera uploads. The cameras write to subdirectories per camera:
/your/nvr/path/
├── Front/
│ └── YYYY/MM/DD/
└── Rear/
└── YYYY/MM/DD/
Key vsftpd settings:
anonymous_enable=YES
no_anon_password=YES
anon_root=/your/nvr/path
anon_upload_enable=YES
anon_mkdir_write_enable=YES
anon_other_write_enable=YES
write_enable=YES
local_enable=NO
ssl_enable=YES
allow_anon_ssl=YES
force_anon_logins_ssl=NO
force_anon_data_ssl=NO
require_ssl_reuse=NO
pasv_enable=YES
pasv_min_port=40000
pasv_max_port=40100
pasv_address=YOUR_SERVER_IP
seccomp_sandbox=NO
isolate_network=NONote: The
anon_rootdirectory must be owned by root with755permissions. The camera subdirectories (Front/,Rear/) should be owned by theftpuser with777permissions.
Copy the example config and fill in your values:
cp go2rtc.yaml.example go2rtc.yaml
nano go2rtc.yamlReplace YOUR_CAMERA_PASSWORD, FRONT_CAMERA_IP, and REAR_CAMERA_IP with your actual values.
Edit nginx.conf and update the proxy_pass addresses to match your server's LAN IP.
In gallery/index.html, update the camera names and IPs in the live view section to match your setup.
docker compose up -dThe gallery will be available at http://YOUR_SERVER_IP:8889.
21 FTP control
40000-40100 FTP passive data
1984 go2rtc API
8555 go2rtc WebRTC (TCP + UDP)
8889 Gallery web UI
In each camera's settings:
| Field | Value |
|---|---|
| FTP Server | Your server's LAN IP |
| Port | 21 |
| Anonymous | Enabled |
| Transport Mode | Auto |
| Remote Directory | Front or Rear (no slashes) |
| FTPs Only | Disabled |
A script is included to remove mp4 recordings older than 7 days:
chmod +x nvr-cleanup.sh
./nvr-cleanup.shEdit DAYS=7 at the top of the script to change the retention period.
Tested with:
- Reolink RLC-520A (hardware version
IPC_MS4NA45MP, firmware build2507241369)
The Reolink FTP implementation always attempts AUTH TLS before login. vsftpd must have SSL enabled to respond correctly — disabling SSL causes the camera firmware to get stuck in an infinite reconnect loop.
Cameras stream H.265 natively. Since Firefox does not support H.265 in WebRTC, go2rtc is configured to transcode to H.264 using hardware acceleration (VAAPI). The frontend explicitly requests the H.264 track:
/api/ws?src=CAMERA_NAME&video=h264&audio=opus
If you don't have a VAAPI-capable GPU, remove #hardware=vaapi from the ffmpeg lines in go2rtc.yaml to fall back to software encoding.
MIT

