Self-Host Moonfire NVR with Docker Compose

What Makes Moonfire NVR Different?

Most NVR software re-encodes video streams, chewing through CPU cycles. Moonfire NVR takes a fundamentally different approach — it records H.264-over-RTSP streams directly to disk without transcoding. Metadata goes into a SQLite3 database, and MP4 files are constructed on-demand when you want to view recordings. The result: six 1080p/30fps cameras running on a Raspberry Pi 2 at under 10% CPU usage.

SpecificationDetails
LicenseGPL v3
LanguageRust
DatabaseSQLite3
ProtocolH.264 over RTSP
TranscodingNone (passthrough recording)
Web UIBuilt-in, responsive
APIRESTful
ARM supportYes (Raspberry Pi compatible)
Latest versionv0.7.30

Prerequisites

  • A Linux server (Ubuntu 22.04+ recommended)
  • Docker and Docker Compose installed (guide)
  • IP cameras that output H.264 RTSP streams
  • 20+ GB of disk space (depends on camera count and retention)
  • 512 MB of RAM minimum
  • A domain name (optional, for remote access)

Docker Compose Configuration

Create a configuration file first. Moonfire NVR uses a TOML config file rather than environment variables for camera setup.

Create /etc/moonfire-nvr.toml:

[[binds]]
ipv4 = "0.0.0.0:8080"
allowUnauthenticatedPermissions = { viewVideo = true }

# Uncomment and configure trust_forward_hdrs if behind a reverse proxy
# trustForwardHeaders = true
# trustClientCertificates = false

Create docker-compose.yml:

services:
  moonfire-nvr:
    image: ghcr.io/scottlamb/moonfire-nvr:v0.7.30
    container_name: moonfire-nvr
    restart: unless-stopped
    user: "1000:1000"
    environment:
      - TZ=America/New_York
      - RUST_BACKTRACE=1
    volumes:
      - moonfire-data:/var/lib/moonfire-nvr
      - /etc/moonfire-nvr.toml:/etc/moonfire-nvr.toml:ro
      - /var/tmp:/var/tmp
      - /usr/share/zoneinfo:/usr/share/zoneinfo:ro
      - /media/nvr:/media/nvr
    ports:
      - "8080:8080"
    logging:
      driver: journald
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8080/ || exit 1"]
      interval: 60s
      timeout: 10s
      retries: 3
      start_period: 30s

volumes:
  moonfire-data:

Important: Replace 1000:1000 with the UID:GID of the user that should own the data. The /media/nvr mount is where recordings are stored — point this to a disk with enough space for your retention requirements.

Initialize the Database

Before starting Moonfire NVR for the first time, initialize the SQLite database:

docker compose run --rm moonfire-nvr init

Configure Cameras

Use the interactive configuration tool to add cameras and sample directories:

docker compose run --rm moonfire-nvr config 2>debug-log

This opens a text-based configuration interface where you add:

  • Sample directories: Where recordings are stored (e.g., /media/nvr/main)
  • Cameras: RTSP URL, username, password, stream type
  • Retention: How long to keep recordings per camera

Each camera can have two streams:

  • Main stream: Full resolution for archival (e.g., 1080p or 4K)
  • Sub stream: Lower resolution for live viewing (e.g., 480p)

Start the service:

docker compose up -d

Initial Setup

Access the web UI at http://your-server-ip:8080.

The interface shows a timeline view of all camera recordings. You can:

  • Click any point on the timeline to start playback
  • Select a time range to download a clip as MP4
  • View live streams from connected cameras
  • Switch between main and sub streams

Moonfire NVR creates MP4 files on-the-fly from the raw recorded data — there are no pre-built video files on disk. This means scrubbing through hours of footage is instant.

Configuration

Adding More Cameras

Stop the container, run the config tool, and restart:

docker compose stop
docker compose run --rm moonfire-nvr config 2>debug-log
docker compose up -d

Storage Planning

Calculate storage needs:

ResolutionBitratePer Camera/DayPer Camera/Month
1080p4 Mbps~43 GB~1.3 TB
1080p2 Mbps~21 GB~650 GB
4K8 Mbps~86 GB~2.6 TB
720p1 Mbps~10 GB~320 GB

Set retention limits per camera in the config tool to prevent disk exhaustion. Moonfire NVR automatically deletes the oldest recordings when the retention limit is reached.

Authentication

By default, the config above allows unauthenticated viewing. For production, remove allowUnauthenticatedPermissions from the TOML config and create user accounts:

docker compose run --rm moonfire-nvr config 2>debug-log

Navigate to the users section and create accounts with appropriate permissions.

Reverse Proxy

Put Moonfire NVR behind a reverse proxy for HTTPS. With Nginx Proxy Manager, point to http://moonfire-nvr:8080 and enable WebSocket support.

If using Caddy:

nvr.yourdomain.com {
    reverse_proxy localhost:8080
}

Enable trustForwardHeaders = true in /etc/moonfire-nvr.toml when behind a reverse proxy. See Reverse Proxy Setup.

Backup

The critical data is the SQLite database in the moonfire-data volume. Recordings on /media/nvr are expendable (they’re just camera footage within your retention window).

docker compose stop
# Back up the database
docker run --rm -v moonfire-data:/data -v $(pwd):/backup alpine \
  tar czf /backup/moonfire-db-$(date +%Y%m%d).tar.gz /data
docker compose start

See Backup Strategy for automated approaches.

Troubleshooting

Camera stream won’t connect

Symptom: Moonfire NVR shows no video for a configured camera.

Fix: Verify the RTSP URL works outside of Docker first:

ffprobe rtsp://user:pass@camera-ip:554/stream1

Common RTSP URL formats by manufacturer:

  • Hikvision: rtsp://user:pass@ip:554/Streaming/Channels/101
  • Dahua/Amcrest: rtsp://user:pass@ip:554/cam/realmonitor?channel=1&subtype=0
  • Reolink: rtsp://user:pass@ip:554/h264Preview_01_main
  • Generic ONVIF: rtsp://user:pass@ip:554/stream1

”Permission denied” errors on startup

Symptom: Container exits with permission errors.

Fix: The user: directive in docker-compose.yml must match the ownership of the data directories:

sudo chown -R 1000:1000 /media/nvr

Database corruption after power loss

Symptom: Moonfire NVR won’t start after an unexpected shutdown.

Fix: SQLite WAL mode is resilient, but if corruption occurs:

docker compose run --rm moonfire-nvr check

This verifies database integrity and attempts recovery.

High disk I/O

Symptom: Disk throughput is maxed out with many cameras.

Fix: Moonfire NVR writes sequentially, which spinning disks handle well. If you’re running many cameras (8+), consider:

  • Separate disks for the database (SSD) and recordings (HDD)
  • Reducing main stream bitrates
  • Using sub-streams for cameras that don’t need full-resolution archival

Resource Requirements

Resource1-2 cameras4-6 cameras8+ cameras
RAM128 MB256 MB512 MB+
CPUMinimal (<5%)Minimal (<10%)Low
Disk I/OLowModerateHigh (use HDD RAID)
Network5-10 Mbps15-30 Mbps40+ Mbps

Moonfire NVR’s passthrough approach means CPU usage stays near zero regardless of resolution — the bottleneck is always disk throughput and network bandwidth.

Verdict

Moonfire NVR is the most resource-efficient NVR you can self-host. If you want cameras recorded 24/7 with minimal hardware, nothing beats its passthrough recording approach. A Raspberry Pi can handle a handful of cameras that would bring ZoneMinder or Frigate to their knees.

The trade-off: no AI-based object detection, no motion alerts, no smart features. Moonfire NVR records everything and lets you scrub through it later. If you need person detection, vehicle detection, or smart notifications, look at Frigate instead. If you just want reliable, efficient 24/7 recording with a clean web UI, Moonfire NVR is excellent.

FAQ

Can Moonfire NVR do motion detection?

No. It records continuously and doesn’t analyze video content. For motion or object detection, use Frigate or Viseron.

Does it support H.265/HEVC cameras?

Not yet. Moonfire NVR only supports H.264 streams. If your cameras output H.265, you’ll need to switch them to H.264 in the camera settings or use a different NVR.

Can I view recordings remotely?

Yes. The web UI works in any browser. Put it behind a reverse proxy with HTTPS and you can access recordings from anywhere. There’s no dedicated mobile app — use the browser.